Recently I was asked to create an automated deployment from TeamCity for an Azure web role. I had done this before with Azure websites so I figured the process was going to be similar. For websites you can use msdeploy which is relatively easy to configure.
This is another reason why, if possible, you should try to use Azure websites instead of worker roles. They are just simpler. Troy Hunt wrote a great article about this which goes into a lot of depth and what the trade-offs are: http://www.troyhunt.com/2014/01/with-great-azure-vm-comes-great.html
However, web and worker roles didn’t seem to be that easy. There’s no such thing as msdeploy for web and worker roles. Apart from that, it had to be done in Powershell. I’m not the biggest expert in Powershell so I went looking on the mighty interwebs for tutorials and snippets, but I only found half scripts or scripts that were used with an older version of the Azure Powershell cmdlets that doesn’t exist anymore. That’s why I wanted to provide a tutorial for everyone in the same situation.
There are two steps to deploying a role:
- Creating a package file (with a corresponding config-file)
- Deploying the package to your cloud service
Creating a package
Creating the package is rather simple and you can do that with the MsBuild. The entire script is a oneliner, providing the correct arguments to MsBuild:
exec { msbuild
Deploying a package
To deploy our package we need the Azure Powershell cmdlets. These can be downloaded here: http://www.windowsazure.com/en-us/downloads/ You can find the cmdlets under “Command-line”
To deploy a package we need to execute the following steps:
- Locate the package, publish settings and other variables
- Import the Azure cmdlets module
- Import the publish profile and configure the subscription
- Upload the package to a container in Azure blob storage
- Check if a deployment exists
- If there is no deployment, create a new one with the uploaded package
- If a deployment exists, upgrade it with the uploaded package
- Check if the deployment has succeeded
Locate the package, publish settings and other variables
In order to deploy the package we will need some information. We will get these parameters from the command-line arguments:
Param([string]$publishsettings, [string]$storageaccount, [string]$subscription, [string]$service, [string]$containerName="mydeployments", [string]$config, [string]$package, [string]$slot="Staging")
- Publishsettings: this is the publish profile that you can download from Azure
- StorageAccount: in order to upload the package we need a storage account. If you don’t have one, you first need to create one
- Subscription: This identifies which subscription the cloud service belongs to
- Service: The name of the cloud service that we are deploying to
- ContainerName: The name of the container that we will upload the package to. If it doesn’t exist it will be created.
- Config: The location of the configuration file for the cloud service
- Package: The location of the .cspkg file that was created by the first step (creating a package)
- Slot: You can deploy into Staging or into Production. This will default to Staging
To give us the capability to run the script without any parameters, I added the following to the script so that uninitialized variables can be entered while the script is executing:
Function Get-File($filter){ [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null $fd = New-Object system.windows.forms.openfiledialog $fd.MultiSelect = $false $fd.Filter = $filter [void]$fd.showdialog() return $fd.FileName } if (!$subscription){ $subscription = Read-Host "Subscription (case-sensitive)" } if (!$storageaccount){ $storageaccount = Read-Host "Storage account name" } if (!$service){ $service = Read-Host "Cloud service name" } if (!$publishsettings){ $publishsettings = Get-File "Azure publish settings (.publishsettings)|.publishsettings" } if (!$package){ $package = Get-File "Azure package (.cspkg)|.cspkg" } if (!$config){ $config = Get-File "Azure config file (.cspkg)|.cscfg" }
This makes sure that all variables are initialized (with a nice file dialog for the file locations).
Importing the Azure cmdlets module
To make use of the cmdlets we first have to import them into the current environment. This can be done with the following command (adjust the path accordingly):
Import-Module "C:Program Files (x86)Microsoft SDKsWindows AzurePowerShellAzureAzure.psd1"
Import the publish profile and configure the subscription
Next, we need to make sure that we are authorized to deploy and that we are deploying into the right subscription. We need to import the publish settings file, configure the subscription with a storage account and select the correct subscription.
Function Set-AzureSettings($publishsettings, $subscription, $storageaccount){ Import-AzurePublishSettingsFile $publishsettings Set-AzureSubscription $subscription -CurrentStorageAccount $storageaccount Select-AzureSubscription $subscription }
Set-AzureSettings -publishsettings $publishsettings -subscription $subscription -storageaccount $storageaccount
The second step here is particularly important. If you do not configure the subscription with a storage account, you will get an error when creating or upgrading a deployment. Strangely enough, there is no error when you upload a package to the storage account. Furthermore, I found out the hard way that you can’t include the subscription as named parameter on that command (ref: http://stackoverflow.com/questions/21469110/deploying-web-role-in-azure-throws-exception-on-currentaccountstoragename)
Upload the package to blob storage
There are two ways to create a new deployment for a cloud service. Either by specifying a local package or by specifying the url for a package in blob storage. I tried several times using the local package, but mostly the request timed out and the method didn’t prove be very reliable. That’s why I first included a step to upload the package to blob storage and then run the deploy from there. This hasn’t failed (yet).
Function Upload-Package($package, $container){ $blob = "$service.package.$(get-date -f yyyy_MM_dd_hh_ss).cspkg" $containerState = Get-AzureStorageContainer -Name $container -ea 0 if ($containerState -eq $null) { New-AzureStorageContainer -Name $container | out-null } Set-AzureStorageBlobContent -File $package -Container $container -Blob $blob -Force| Out-Null $blobState = Get-AzureStorageBlob -blob $blob -Container $container $blobState.ICloudBlob.uri.AbsoluteUri } $package_url = Upload-Package -package $package -containerName $containerName
This function will first check whether the container exists and if not create it in the storage account. Then it will upload the package with a date stamp in the file name and return the destination URL.
Check if a deployment exists
Creating a new deployment and upgrading an existing one are two different commands in Azure. First we need to check whether there’s already a deployment:
$deployment = Get-AzureDeployment -ServiceName $service -Slot $slot -ErrorAction silentlycontinue
This connects with Azure and checks whether there’s a deployment for the given service in the given slot (Staging or Production). This command will actually throw an error if there is none, so silently continue and then do a check on the $deployment variable to see if there was one or not.
Create a new deployment
If the $deployment variable has no name, it means we weren’t able to retrieve a deployment and we must a create a new one:
Function Create-Deployment($package_url, $service, $slot, $config){ $stat = New-AzureDeployment -Slot $slot -Package $package_url -Configuration $config -ServiceName $service } if ($deployment.Name -eq $null) { Create-Deployment -package_url $package_url -service $service -slot $slot -config $config }
This creates a new deployment for the cloud service in the specified slot using a local configuration file and the URL of the package that we previously uploaded.
Upgrade an existing deployment
Upgrading an existing deployment is fairly similar, apart from the fact that it uses a different cmdlet:
Function Upgrade-Deployment($package_url, $service, $slot, $config){ $setdeployment = Set-AzureDeployment -Upgrade -Slot $slot -Package $package_url -Configuration $config -ServiceName $service -Force } if ($deployment.Name -eq $null) { ... } else { Upgrade-Deployment -package_url $package_url -service $service -slot $slot -config $config }
Check if the deployment has succeeded
Lastly, we want to check whether the deployment was successful. We can do this by getting the deployment for the given service and outputting the deployment id:
Function Check-Deployment($service, $slot){ $completeDeployment = Get-AzureDeployment -ServiceName $service -Slot $slot $completeDeployment.deploymentid } $deploymentid = Check-Deployment -service $service -slot $slot
I have kept the script completely without any messages to the output to not distract from the important parts. If you want to run this script on a build server, it would obviously be handy if you could at least see what the script has been doing. Apart from that, usually a build server looks at the exit code. An Azure error will not cause a non zero exit. That’s why I wrapped the entire script in a try-catch block which writes out the exception message and exits with code 1 in case there is a failure. For reference (and easy copy-paste) here is the full script:
Param([string]$publishsettings, [string]$storageaccount, [string]$subscription, [string]$service, [string]$containerName="mydeployments", [string]$config, [string]$package, [string]$slot="Staging") Function Get-File($filter){ [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null $fd = New-Object system.windows.forms.openfiledialog $fd.MultiSelect = $false $fd.Filter = $filter [void]$fd.showdialog() return $fd.FileName } Function Set-AzureSettings($publishsettings, $subscription, $storageaccount){ Import-AzurePublishSettingsFile $publishsettings Set-AzureSubscription $subscription -CurrentStorageAccount $storageaccount Select-AzureSubscription $subscription } Function Upload-Package($package, $containerName){ $blob = "$service.package.$(get-date -f yyyy_MM_dd_hh_ss).cspkg" $containerState = Get-AzureStorageContainer -Name $containerName -ea 0 if ($containerState -eq $null) { New-AzureStorageContainer -Name $containerName | out-null } Set-AzureStorageBlobContent -File $package -Container $containerName -Blob $blob -Force| Out-Null $blobState = Get-AzureStorageBlob -blob $blob -Container $containerName $blobState.ICloudBlob.uri.AbsoluteUri } Function Create-Deployment($package_url, $service, $slot, $config){ $opstat = New-AzureDeployment -Slot $slot -Package $package_url -Configuration $config -ServiceName $service } Function Upgrade-Deployment($package_url, $service, $slot, $config){ $setdeployment = Set-AzureDeployment -Upgrade -Slot $slot -Package $package_url -Configuration $config -ServiceName $service -Force } Function Check-Deployment($service, $slot){ $completeDeployment = Get-AzureDeployment -ServiceName $service -Slot $slot $completeDeployment.deploymentid } try{ Write-Host "Running Azure Imports" Import-Module "C:Program Files (x86)Microsoft SDKsWindows AzurePowerShellAzureAzure.psd1" Write-Host "Gathering information" if (!$subscription){ $subscription = Read-Host "Subscription (case-sensitive)"} if (!$storageaccount){ $storageaccount = Read-Host "Storage account name"} if (!$service){ $service = Read-Host "Cloud service name"} if (!$publishsettings){ $publishsettings = Get-File "Azure publish settings (.publishsettings)|.publishsettings"} if (!$package){ $package = Get-File "Azure package (.cspkg)|.cspkg"} if (!$config){ $config = Get-File "Azure config file (.cspkg)|.cscfg"} Write-Host "Importing publish profile and setting subscription" Set-AzureSettings -publishsettings $publishsettings -subscription $subscription -storageaccount $storageaccount "Upload the deployment package" $package_url = Upload-Package -package $package -containerName $containerName "Package uploaded to $package_url" $deployment = Get-AzureDeployment -ServiceName $service -Slot $slot -ErrorAction silentlycontinue if ($deployment.Name -eq $null) { Write-Host "No deployment is detected. Creating a new deployment. " Create-Deployment -package_url $package_url -service $service -slot $slot -config $config Write-Host "New Deployment created" } else { Write-Host "Deployment exists in $service. Upgrading deployment." Upgrade-Deployment -package_url $package_url -service $service -slot $slot -config $config Write-Host "Upgraded Deployment" } $deploymentid = Check-Deployment -service $service -slot $slot Write-Host "Deployed to $service with deployment id $deploymentid" exit 0 } catch [System.Exception] { Write-Host $_.Exception.ToString() exit 1 }