Using XML Transformations when deploying to Azure App Service using VSTS
I recently changed some web apps that had originally used the Azure Cloud Service (Classic) to use the newer Azure App Service mainly to bring the deployment time down without having to sacrifice the app's performance. We already have a CI build and release process and I needed to make a couple of changes in order to get a new pipeline working.
- Changes to Build Definition
- Remove the task used to create *.cspkg file
- Make sure that project Zip file in /bin folder is included in the artifact
- Changes to Release Definition
- Remove the Azure Cloud Service Deployment
task - Add Azure App Service Deploy task
- Remove the Azure Cloud Service Deployment
When using the Azure Cloud Service Deployment task you had the option of using a CsCfg file to change these values. I was using this method to update connection strings and environment variables for Dev/Test/Prod environments.
I was working on this and trying to add some new functionality to the app during a sprint so I was pressed for time and didn't fully research updating the connection strings depending on environments. I did read about XML transformations but ran into some trouble trying to get it to work. I was running out of time and changed to use XML variable transformation.
For example, we replace the Environment variable in the Web.config file of the app depending on which environment the app is running.
<setting name="Environment" serializeAs="String">
<value>Dev-AM</value>
</setting>
In order to change this value at each stage of the release I need to have a variable with the same name on the release.
The Problem
The setup we had with the CsCfg files was a cleaner solution than this. We had a separate Azure Cloud project connected to our web app in Visual Studio so if you added a new setting to the Web.config file you could immediately add the same setting to the CsCfg files, check it in and then not worry about it in when it came to the release.
With this setup, if you make a change to the Web.config then you have to open the release definition and then add the variable and value for each environment. There are too many places to make changes and too many opportunities for mistakes.
My Solution
What I really wanted to do was to go back to a similar setup that we had in the past. This meant adding config files to the project in Visual Studio and add them to source control.
Once I had a plan I started to research the topic. The Microsoft documentation on File transforms and variable substitution reference provided a good introduction to the steps required to get this working. I also found these articles useful when getting started.
- How to web.config transform using VSTS Release Management
- Using web.config transforms and Release Manager – TFS 2017/Team Services edition
There are a couple of important notes at the bottom of the XML Transformation that are important to follow and I'll go through some of them in this article.
When I looked at the project we already had a couple of config files attached.
The documentation said that I could add a config file for each environment and this would be used in the Dev/Test/Prod environments during the release. I then added the files Web.Dev.config, Web.Test.config and Web.Prod.config.
A note from the documentation stated that
"By default, MSBuild applies the transformation as it generates the web package if the element is already present in the transform file in the *.csproj file. In such cases, the Azure App Service Deploy task will fail because there is no further transformation applied on the Web.config file. Therefore, it is recommended that the element is removed from all the transform files to disable any build-time configuration when using XML transformation."
I opened the project's .csproj file and found the following
<Content Include="Web.config">
<SubType>Designer</SubType>
</Content>
<Content Include="Web.Debug.config">
<DependentUpon>Web.config</DependentUpon>
</Content>
<Content Include="Web.Release.config">
<DependentUpon>Web.config</DependentUpon>
</Content>
I replaced this with the following
<Content Include="Web.config">
<SubType>Designer</SubType>
</Content>
<Content Include="Web.Debug.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Web.Release.config">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Web.Dev.config">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Web.Test.config">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Web.Prod.config">
<SubType>Designer</SubType>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
I added the line
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
because the documentation said
Set the Copy to Output Directory property for the configuration transform files to Copy If Newer.
This can be done in Visual Studio in the Copy to Output Directory in the Properties pane too.
Once I had this done I was ready to update the pipeline. I changed the file tranformation option in the taks from XML variable substitution to XML transformation
I checked the Zip file to be deployed and all the necessary config files were included, I was ready to test the new release defintion. Unfortunately, I started getting errors and when I checked the logs I saw this error.
2018-07-12T08:29:48.2012668Z ##[section]Starting: Deploy Azure App Service
2018-07-12T08:29:48.2022101Z ==============================================================================
2018-07-12T08:29:48.2022523Z Task : Azure App Service Deploy
2018-07-12T08:29:48.2022831Z Description : Update Azure WebApp Services On Windows, Web App On Linux with built-in images or docker containers, ASP.NET, .NET Core, PHP, Python or Node based Web applications, Function Apps, Mobile Apps, Api applications, Web Jobs using Web Deploy / Kudu REST APIs
2018-07-12T08:29:48.2023172Z Version : 3.3.49
2018-07-12T08:29:48.2023374Z Author : Microsoft Corporation
2018-07-12T08:29:48.2023564Z Help : [More Information](https://aka.ms/azurermwebdeployreadme)
2018-07-12T08:29:48.2023812Z ==============================================================================
2018-07-12T08:29:50.6279541Z Got connection details for Azure App Service:'appdev'
2018-07-12T08:29:56.9290420Z ConnectionString attributes in Web.config is parameterized by default. Note that the transformation has no effect on connectionString attributes as the value is overridden during deployment by 'Parameters.xml or 'SetParameters.xml' files. You can disable the auto-parameterization by setting /p:AutoParameterizationWebConfigConnectionStrings=False during MSBuild package generation.
2018-07-12T08:29:57.0519735Z [command]D:\a\_tasks\AzureRmWebAppDeployment\3.3.49\ctt\ctt.exe s:D:\a\r1\a\temp_web_package\Content\D_C\a\1\s\HTML5\obj\Release\Package\PackageTmp\Web.config t:D:\a\r1\a\temp_web_package\Content\D_C\a\1\s\HTML5\obj\Release\Package\PackageTmp\Web.Release.config d:D:\a\r1\a\temp_web_package\Content\D_C\a\1\s\HTML5\obj\Release\Package\PackageTmp\Web.config pw i
2018-07-12T08:29:57.8480778Z System.NullReferenceException: Object reference not set to an instance of an object.
2018-07-12T08:29:57.8481467Z at Microsoft.Web.XmlTransform.XmlTransformationLogger.ConvertUriToFileName(XmlDocument xmlDocument)
2018-07-12T08:29:57.8481900Z at Microsoft.Web.XmlTransform.XmlTransformationLogger.LogWarning(XmlNode referenceNode, String message, Object[] messageArgs)
2018-07-12T08:29:57.8482295Z at Microsoft.Web.XmlTransform.Transform.ApplyOnAllTargetNodes()
2018-07-12T08:29:57.8551132Z ##[error]Error: XML transformation error while transforming D:\a\r1\a\temp_web_package\Content\D_C\a\1\s\HTML5\obj\Release\Package\PackageTmp\Web.config using D:\a\r1\a\temp_web_package\Content\D_C\a\1\s\HTML5\obj\Release\Package\PackageTmp\Web.Release.config.
2018-07-12T08:29:59.2079869Z Successfully updated deployment History at https://appdev.scm.azurewebsites.net/api/deployments/575 ##[section]Finishing: Deploy Azure App Service
This is important
I wasted a lot of time getting errors when trying to run the release pipeline and I couldn't figure out why the errors were occurring. The files were in the correct location and everything seemed to be setup correctly. After Googling for a while I found this article with the solution. The issue was with double transformation when doing the release.
I was running the build with the BuildConfiguration variable set to 'release'. This means when the build is run it also does the XML transformation using the Web.Release.config file. When I looked inside the file I saw the line
<compilation xdt:Transform="RemoveAttributes(debug)" />
This removes all the debug attributes from the Web.config file.
When the release is updated to use XML transformation what happens is described in the info icon for the task.
The build ran Web.Release.config which removed all the debug attributes. When the release is run it also runs the transform for Web.Release.config and then the Web.<EnvironmentName>.config. Trying to remove the debug attributes twice causes the error. It's important to read the fine print.
There are 2 options to fix this
- Run the build in debug mode
- Update the config files
I decided to go with option 2 so I commented out the line in Web.Release.config containing RemoveAttributes(debug) and then added it to the other config files for each environment in the release pipeline. Either way should work but I chose method to reduce the changes I needed to make to the release definition.
Once I made the necessary changes to the config file, I ran the release which completed without any errors. I then used FTP to connect to the app service and I could see all the variables I needed had been updated.