Here’s the solution I settled on.
The first step is a “Deploy Java Archive” to a deployment target of “Offline Package Drop”, which saves the part I need, a .war file, inside some location on the local disk. I looked into setting up the Octopus Deploy local repository, but I couldn’t figure out how to get that to work with the setup I was attempting to accomplish because of various things that seemed to be getting in the way, such as:
- I couldn’t do any of the typical deployment targets, and I didn’t know about the “you don’t need a deployment target” setting until I had already found a solution.
- The development team I work with likes to do a lot of minor changes without increment the build number. So for example we may have a lot of builds on 1.2.7-SNAPSHOT that need to be redeployed as new versions, and Octopus Deploy seemed to want to assume that all artifacts named 1.2.7-SNAPSHOT in the repository were the same and so it didn’t need to get, and then upload, a fresh file.
So anyway, the “Deploy Java Archive” process step puts the whateverwhatever.war file somewhere on the local file system, and then I wrote a PS script to take it from there.
At the top of the script, the place where it chooses where to deploy is via the variable $AzureSlotName, and that’s an Octopus Deploy variable that is dependent on the environment Octopus Deploy is deploying to.
The PS script is a “Azure Powershell Script” process step, and it uses the Azure service principal for permissions. The first thing it does is get the ftp username/password/url from the Azure publishing profile and saves those to variables. Then it uses those credentials to delete the ROOT.war file in the wepapps folder on the Azure Web App. I found that if I tried to simply overwrite the ROOT.war file, the Azure kudu thing wouldn’t always unpack the ROOT.war file correctly and the app was sometimes unstable.
After deleting the ROOT.war file, the PS script waits around for kudu to delete the ROOT directory, then uploads the application.war file as ROOT.war into the Azure Web App webapps slot. I actually wrote a script to recursively FTP delete the ROOT directory, but it would sometimes clash with the Kudu. When I deleted the ROOT.war file before attempting to delete the ROOT directory, the recursive FTP delete script would scan folders then send FTP commands to delete files and occasionally the file would be removed by Kudu before the FTP command got to it, which would cause the FTP server to send back some 5xx error. The environment running the PS script would then crash, and it was too much of a headache to deal with the combination of the OD script running environment + recursion + PS + FTP 5xx errors.
The other problem I’d run into, if I ran the recursively FTP delete on the ROOT folder first, was that the Azure kudu tool would sometimes reconize that some files were missing in the ROOT folder and start unpacking the ROOT.war into it, which would down the road cause complete app instability. So what I settled on was removing the ROOT.war file, then waiting up to around 3 minutes for the Kudu to remove the ROOT folder, and if it didn’t then have the recursively delete script kick in. I ended up canning even that, however, because it was just too much of a headache to include that as well in the script, so I just assumed that Kudu would do its job and if it failed I’d expect a human to manually fix it.
So after the Kudu removes the ROOT folder, the remaining part of the script gets the .war file and uploads it via FTP to the webapps folder.
Here’s the full contents of the PS script. There’s 5 parts:
- Get FTP Credential info
- Scan the directory (I don’t even need this part in anymore, but I’m somewhat scared to take it out)
- Delete ROOT.war
- Wait around
- Deploy ROOT.war
#Azure version of "Delete, Wait, Upload - FTP"
######################################
###Connect and get connection details
######################################
$publishProfileStr = Get-AzureRmWebAppSlotPublishingProfile -Name $AzureAppName -ResourceGroupName $AzureResourceGroupName -Slot $AzureSlotName -OutputFile null
$xmlvar = New-Object -TypeName System.Xml.XmlDocument
$xmlvar.LoadXml($publishProfileStr)
$ftpUsername = $xmlvar.SelectNodes("//publishProfile[@publishMethod=`"FTP`"]/@userName").value
$ftpPassword = $xmlvar.SelectNodes("//publishProfile[@publishMethod=`"FTP`"]/@userPWD").value
$ftpUrl = $xmlvar.SelectNodes("//publishProfile[@publishMethod=`"FTP`"]/@publishUrl").value + "/webapps/"
$ftpCreds = New-Object System.Net.NetworkCredential($ftpUsername, $ftpPassword)
#####################
### Look at directory
#####################
# Get info about directory, using ftp creds
try
{
$listRequest = [Net.WebRequest]::Create($ftpUrl)
$listRequest.Method = [System.Net.WebRequestMethods+FTP]::ListDirectoryDetails
$listRequest.Credentials = $ftpCreds
$lines = New-Object System.Collections.ArrayList
$listResponse = $listRequest.GetResponse()
$listStream = $listResponse.GetResponseStream()
$listReader = New-Object System.IO.StreamReader($listStream)
while (!$listReader.EndOfStream -and $listReader -ne $null)
{
$line = $listReader.ReadLine()
$lines.Add($line) | Out-Null
}
}
catch
{
}
finally
{
if ($listReader -ne $null)
{
$listReader.Dispose()
}
if ($listStream -ne $null)
{
$listStream.Dispose()
}
if ($listResponse -ne $null)
{
$listResponse.Dispose()
}
}
$fileCount = $lines.Count
###################
### Delete ROOT.war
###################
foreach($line in $lines)
{
Write-Host $line
}
try
{
$deleteFileUrl = $ftpUrl + "ROOT.war"
$deleteRequest = [Net.WebRequest]::Create($deleteFileUrl)
$deleteRequest.Credentials = $ftpCreds
$deleteRequest.Method = [System.Net.WebRequestMethods+FTP]::DeleteFile
$deleteRequest.GetResponse() | Out-Null
}
catch
{
Write-Host "Couldn't find ROOT.war at location."
}
#############################################################################################
### Wait around for 2-5 minutes, occationally checking to see whether the ROOT folder is gone
#############################################################################################
$retries = 0
$fileCount = 1
while($fileCount -gt 0 -and $retries -lt 6)
{
Write-Host "Attempt: $retries"
##########################
### Checking the directory
##########################
try
{
$listRequest = [Net.WebRequest]::Create($ftpUrl)
$listRequest.Method = [System.Net.WebRequestMethods+FTP]::ListDirectoryDetails
$listRequest.Credentials = $ftpCreds
$lines = New-Object System.Collections.ArrayList
$listResponse = $listRequest.GetResponse()
$listStream = $listResponse.GetResponseStream()
$listReader = New-Object System.IO.StreamReader($listStream)
while (!$listReader.EndOfStream -and $listReader -ne $null)
{
$line = $listReader.ReadLine()
$lines.Add($line) | Out-Null
}
}
catch
{
}
finally
{
if ($listReader -ne $null)
{
$listReader.Dispose()
}
if ($listStream -ne $null)
{
$listStream.Dispose()
}
if ($listResponse -ne $null)
{
$listResponse.Dispose()
}
}
$fileCount = $lines.Count
Write-Host "Files Count: $fileCount"
$retries = $retries + 1
Start-Sleep -s 15
}
Write-Host "Done retrying, files counted: $fileCount"
############################
### Deploy the ROOT.war file
############################
$buildVersion = $OctopusParameters["Octopus.Release.Number"]
$packageRootPath = "C:\Octopus\Packages\TempTestDeploy\Dev\MyApplication\$buildVersion\Packages"
Write-Output "Package Root Path: $packageRootPath"
$likeFileName = "com.company.app.*.war"
$rootWarUrl = $ftpUrl + "ROOT.war"
$file = Get-ChildItem -Path $packageRootPath -File
Write-Output "File: $file"
$warPath = $file.FullName
Write-Output "war file path: $warPath"
try{
$webClient = New-Object -TypeName System.Net.WebClient
$webClient.Credentials = New-Object System.Net.NetworkCredential($ftpUsername, $ftpPassword)
$uri = New-Object System.Uri($rootWarUrl)
Write-Output "Uploading war file to $($uri.AbsoluteUri)"
$webClient.UploadFile($uri, $warPath)
}
catch{
Write-Error "Something went wrong: $_"
}
finally {
Write-Output "Done, disposing webClient."
$webClient.Dispose()
}
Note: the “preformatted text” button didn’t work at all. Three back-tick characters at beginning and end did.