Transform configs on Azure

It can be a powerful tool to give your release process the ability to manipulate configuration files on the server that aren’t directly controlled by your source code.

The primary use cases i’ve used this for is to at deployment time re-wire connection strings on the fly or to have the stock Sitecore web.config be deployed stock and managed by the release process through remote transforms. Whatever your devops constraints are this technique could come in handy.

The technique is fairly simple:

  • Connect to Azure, here we’re using a service principal
  • Generate kudu credentials from publishsettings
  • Download xml file
  • Transform file using XDTs and optionally tokens
  • Upload xml file back to app service
  • Note: This requires the loading of the Microsoft.Web.XmlTransform.dll DLL, so make sure that’s available.

        [string]$SlotName = "",
    function Get-AzureRmWebAppPublishingCredentials($resourceGroupName, $webAppName, $slotName = $null){
    	if ([string]::IsNullOrWhiteSpace($slotName)){
    		$resourceType = "Microsoft.Web/sites/config"
    		$resourceName = "$webAppName/publishingcredentials"
    		$resourceType = "Microsoft.Web/sites/slots/config"
    		$resourceName = "$webAppName/$slotName/publishingcredentials"
    	$publishingCredentials = Invoke-AzureRmResourceAction -ResourceGroupName $resourceGroupName -ResourceType $resourceType -ResourceName $resourceName -Action list -ApiVersion 2015-08-01 -Force
        	return $publishingCredentials
    function Get-KuduApiAuthorisationHeaderValue($resourceGroupName, $webAppName, $slotName = $null){
        $publishingCredentials = Get-AzureRmWebAppPublishingCredentials $resourceGroupName $webAppName $slotName
        $ret = @{}
        $ret.header = ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword))))
        $ret.url = $publishingCredentials.Properties.scmUri
        return $ret
    function Get-FileFromWebApp($resourceGroupName, $webAppName, $slotName = "", $kuduPath){
        $KuduAuth = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName
        $kuduApiAuthorisationToken = $KuduAuth.header
        $kuduApiUrl = $KuduAuth.url + "/api/vfs/site/wwwroot/$kuduPath"
        Write-Host " Downloading File from WebApp. Source: '$kuduApiUrl'." -ForegroundColor DarkGray
        $tmpPath = "$($env:TEMP)\$([guid]::NewGuid()).xml"
        $null = Invoke-RestMethod -Uri $kuduApiUrl `
                            -Headers @{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} `
                            -Method GET `
                            -ContentType "multipart/form-data" `
                            -OutFile $tmpPath
        $ret = Get-Content $tmpPath | Out-String
        Remove-Item $tmpPath -Force
        return $ret
    function Write-FileToWebApp($resourceGroupName, $webAppName, $slotName = "", $fileContent, $kuduPath){
        $KuduAuth = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName
        $kuduApiAuthorisationToken = $KuduAuth.header
        $kuduApiUrl = $KuduAuth.url + "/api/vfs/site/wwwroot/$kuduPath"
        Write-Host " Writing File to WebApp. Destination: '$kuduApiUrl'." -ForegroundColor DarkGray
        Invoke-RestMethod -Uri $kuduApiUrl `
                            -Headers @{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} `
                            -Method Put `
                            -ContentType "multipart/form-data"`
                            -Body $fileContent
    function Update-XmlDocTransform($xml, $xdt, $tokens)
        Add-Type -LiteralPath "$PSScriptRoot\Microsoft.Web.XmlTransform.dll"
        $xmldoc = New-Object Microsoft.Web.XmlTransform.XmlTransformableDocument;
        $xmldoc.PreserveWhitespace = $true
        $useTokens = $false
        if ($tokens -ne $null -and $tokens.Count -gt 0){
            $useTokens = $true
            $sb = [System.Text.StringBuilder]::new((Get-Content -Path $xdt))
            $tmpPath = "$($env:TEMP)\$([guid]::NewGuid()).xdt"
            $tokens.Keys | ForEach-Object{
                $null = $sb.Replace($_, $tokens[$_])
            Set-Content -Path $tmpPath -Value $sb.ToString()
            $xdt = $tmpPath
        $transf = New-Object Microsoft.Web.XmlTransform.XmlTransformation($xdt);
        if ($transf.Apply($xmldoc) -eq $false)
            throw "Transformation failed."
        if ($useTokens){
            Remove-Item -Path $xdt -Force
        return $xmldoc.OuterXml
    $Credential = New-Object -TypeName PSCredential($ServicePrincipalID, (ConvertTo-SecureString -String $ServicePrincipalKey -AsPlainText -Force))
    # Connect to Azure using SP
    $connectParameters = @{
        Credential     = $Credential
        TenantId       = $TenantId
        SubscriptionId = $SubscriptionId
    Write-Host 'Connecting to Azure.'
    $null = Add-AzureRmAccount @connectParameters -ServicePrincipal
    $contents = Get-FileFromWebApp `
        -resourceGroupName $ResourceGroupName `
        -webAppName $WebAppServiceName `
        -slotName $SlotName `
        -kuduPath $KuduPath
    $XDTs | Foreach-Object{
        $contents = Update-XmlDocTransform -xml $contents -xdt "$PSScriptRoot\XDT\$_.xdt" -tokens $Tokens
    Write-FileToWebApp `
        -resourceGroupName $ResourceGroupName `
        -webAppName $WebAppServiceName `
        -slotName $SlotName `
        -kuduPath $KuduPath `
        -fileContent $contents

    Additionally with some simple adaptations of this code you can use Kudu to perform whatever kind of file manipulations you want using a similar technique.