Continuous Deployment of Cloud Services with VSTS
In my last blog post, I showed how you can use ASP.NET Core with an Azure Cloud Service Web Role. The next step is to enable CI/CD for it, since you really shouldn’t be using “Publish” within Visual Studio for deployment.
As part of this, I wanted to configure the Cloud Service settings per environment in VSTS and not have any configuration checked-in to source control. Cloud Services’ configuration mechanism makes this a bit challenging due to the way it stores configuration, but with a few extra steps, it’s possible to make it work.
What you’ll need
To follow along, you’ll need the following:
- Cloud Service the code can live in GitHub, VSTS, or many other locations. VSTS can build from any of them.
- Azure Key Vault we’ll use Azure Key Vault to store the secrets. Creating a Key Vault is easy and the standard tier will work.
- VSTS this guide is using Visual Studio Team Services, so you’ll need an account there. Those are free for up to five users and any number of users with MSDN licenses.
What we’re going to do
The gist here is that we’ll create a build definition that publishes the output of the Cloud Service project as an artifact. Then, we’ll create a release management process that takes the output of the build and deploys it to the cloud service in Azure. To handle the configuration, we’ll tokenize the checked-in configuration, then use a release management task to read configuration values stored in Key Vault and replace the matching tokenized values before the Azure deployment.
Moving the configuration into Key Vault
Create a new Key Vault to hold your configuration. You should have one Key Vault per environment that you intend to release to, since the secret names will directly translate to variables within VSTS. For each setting you need, create a secret with name like CustomSetting-Setting1
or CustomSetting-Setting2
and set their values. Next, in your ServiceConfiguration.Cloud.cscfg
, set the values to be __CustomSetting-Setting1__
and __CustomSetting-Setting2__
. The __
is the token start/end, and the value identifies which VSTS variable should be used to replace it.
One tip: If you have Password Encryption certificates or SSL endpoints configured, the .cscfg
will have the certificates’ SHA-1 thumbprint’s encoded in them. If you want to configure this per environment, then replace those with token values. The configuration checker will enforce that it looks like a thumbprint, so use values like:
ABCDEF01234567ABCDEF01234567ABCDEF012345
BACDEF01234567ABCDEF01234567ABCDEF012345
Those sentinel values will be replaced with tokens during the build process and those tokens can be replaced with variable values.
We’ll use these in the build task later on.
The build definition
- Start with a new
Empty
build definition. - On the process tab, choose the
Hosted VS2017
Agent queue and give your build definition a name. - Select
Get Sources
and point to your repository. This could be VSTS, GitHub or virtually any other location. - Add the tasks we’ll need:
Visual Studio Build
(three times),Publish Build Artifacts
(once). It should look something like this:
- For the first
Visual Studio Build
task, set the following values:
Setting Value Display name Restore solution Solution AspNetCoreCloudService.sln
Visual Studio Version Visual Studio 2017 MSBuild Arguments /t:restore
Platform $(BuildPlatform)
Configuration $(BuildConfiguration)
-
For the second
Visual Studio Build
task, use the following values:Setting Value Display name Build solution Solution AspNetCoreCloudService.sln
Visual Studio Version Visual Studio 2017 MSBuild Arguments Platform $(BuildPlatform)
Configuration $(BuildConfiguration)
-
And the third
Visual Studio Build
task should be set as:Setting Value Display name Publish Cloud Service Solution TheCloudService\TheCloudService.ccproj
Visual Studio Version Visual Studio 2017 MSBuild Arguments /t:Publish /p:OutputPath=$(Build.ArtifactStagingDirectory)\
Platform $(BuildPlatform)
Configuration $(BuildConfiguration)
-
If you are using sentinel certificate values, add a
PowerShell Task
. Configure the PowerShell task by selecting “Inline Script”, expand Advanced and set the working folder to the publish directory (like$(Build.ArtifactStagingDirectory)\app.publish
) and use the following script:$file = "ServiceConfiguration.Cloud.cscfg" # Read file $content = Get-Content -Path $file # substitute values $content = $content.Replace("ABCDEF01234567ABCDEF01234567ABCDEF012345", "__SslCertificateSha1__") $content = $content.Replace("BACDEF01234567ABCDEF01234567ABCDEF012345", "__PasswordEncryption__") # Save [System.IO.File]::WriteAllText($file, $content)
This replaces the fake SHA-1 thumbprints with tokens that release management will use. Be sure to define variables in release management that match the names you use.
-
Finally, set the
Publish Artifact
step to:Setting Value Display name Publish Artifact: Cloud Service Path to Publish $(Build.ArtifactStagingDirectory)\app.publish
Artifact Name TheCloudService
Artifact Type Server -
Go to the Variables tab and add two variables:
Name Value BuildConfiguration Release
BuildPlatform Any CPU
-
Hit
Save & Queue
to save the definition and start a new build. It should complete successfully. If you go to the build artifacts folder, you should seeTheCloudService
with the.cspkg
file in it.
Deploying the build to Azure
This release process depends on one external extension that handles the tokenization, the Release Management Utility Tasks. Install it from the marketplace into your VSTS account before starting this section.
- In VSTS, switch to the Releases tab and create a new release definition using the “Azure Cloud Service Deployment” template.
- Give the environment a name, like “Cloud Service – Prod”.
- Click the “Add artifact” box and select your build definition. Should look something like this:
If you want continuous deployment, click the “lightning bolt” icon and enable the CD trigger. - Click on the Tasks tab and specify an Azure subscription, storage account, service name and location. If you need to link your existing Azure subscription, click the “Manage” link. If you need a new storage account to hold the deployment artifacts, you can create that in the portal as well, just make sure to create a “Classic” storage account.
- Go to the Variables tab and select “Variable groups”, then “Manage variable groups.” Add a new variable group, give it a name like “AspNetCloudService Production Configuration”, select your subscription (click Manage to link one), and select the Key Vault we created earlier to hold the config. Press the Authorize button if prompted.
Finally, click Add to select which secrets from Key Vault should be added to this variable group.
It’s important to note that it does not copy the values at this point. The secret’s values are always read on use, so they’re always current. Save the variable group and return back to the Release Management definition. At this point, you can select “Link variable group” and link the one we just created. - Add a
Tokenize with XPath/Regular Expressions
task before theAzure Deployment
task. - In the Tokenizer task, browse to the
ServiceConfiguration.Cloud.cscfg
file, something like$(System.DefaultWorkingDirectory)/AspNetCoreCloudService-CI/TheCloudService/ServiceConfiguration.Cloud.cscfg
depending on what you call your artifacts. - Ensure that the Azure Deployment task is last, and you should be all set.
- Create a new release and it should deploy successfully. If you view your cloud service configuration on Azure Portal, you should see the real values, not the
__Tokenized__
values.
Summary
That’s it, you now have an ASP.NET Core Cloud Service deployed to Azure with CI/CD through VSTS. If you want to add additional environments, simply add an additional key vault and linked variable group for each environment, clone the existing environment configuration in the Release Management editor and set the appropriate environmental values. Variable groups are defined at the release definition level, so for multiple-environments you can use a suffix in your variable names and then update the PowerShell script in step 7 to append that per environment (__MyVariable-Prod__
), etc.