loading...

ARM Template deployment, the what-if option for your Azure Deployments

omiossec profile image Olivier Miossec ・10 min read

Abstract

In this post, I will show you how to use the new what-if option in the PowerShell cmdlet and how to use it in an Azure DevOps to validate and build a work item in the Board

One of the biggest issues you can have when you choose to deploy your Azure resources with ARM Template (and it's the same for any kind of Infrastructure as Code tools) is the Quick Fix Issue.
Normally when you choose to use an Infrastructure as Code tool, you always try to use it to deploy any change in your solutions.
But, if sometimes you cannot, because it's too complex or you do not have time, this portion of your infrastructure will not be covered by the IaC tools, and you want to avoid conflicts between the manual method and IaC.

But imagine a situation, the production is down, and to address the problem you need to change something. It could be a route, a load balancer, or anything else, the change is easy either by the portal, PowerShell, or CLI but there is no time to change the IaC source code. The change is made, but the next time you run your template the same error came back or worse and the production is going offline again.

This kind of situation is one of the intends of the What-If option. Introduced as a private preview at the MS Ignite 2019, the new option is in public preview now. This option uses a template file and its parameters as input, if there are resources that already exist it will calculate the difference between what you all ready have in Azure and what you will deploy and output created, update and deleted resources.

The what-if option is a PowerShell module, you can install it by using the Install-module cmdlet like this:

Install-Module Az.Resources -RequiredVersion 1.12.1-preview -AllowPrerelease

How does it work? Take this simple arm template file.
Assume you have this template file. It creates a Network Security group and a VNET and assigns the NSG to the first Subnet.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "nsgName":  {                 
            "type": "string",
            "defaultValue": "DefaultNSG"
        },
        "vnetName":  {                 
            "type": "string",
            "defaultValue": "firstVnet"
        },     
        "vnetPrefix":  {                 
            "type": "string",
            "defaultValue": "192.168.10.0/24"
        },
        "subnetName": {
            "type": "string",
            "defaultValue": "firstSubnet"
        },
        "subnetPrefix": {
            "type": "string",
            "defaultValue": "192.168.10.0/25"
        }
    },
    "variables": {},
    "resources": [
        {
            "name": "[parameters('nsgName')]",
            "type": "Microsoft.Network/networkSecurityGroups",
            "apiVersion": "2019-11-01",
            "location": "[resourceGroup().location]",
            "properties": {
                "securityRules": [
                ]
            }
        },
        {
            "name": "[parameters('vnetName')]",
            "type": "Microsoft.Network/virtualNetworks",
            "apiVersion": "2019-11-01",
            "location": "[resourceGroup().location]",
            "dependsOn": [
                "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('nsgName'))]"
            ],
            "properties": {
                "addressSpace": {
                    "addressPrefixes": [
                        "[parameters('vnetPrefix')]"
                    ]
                },
                "subnets": [
                    {
                        "name": "[parameters('subnetName')]",
                        "properties": {
                            "addressPrefix": "[parameters('subnetPrefix')]",
                            "networkSecurityGroup": {
                                "ID": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('nsgName'))]"
                            }
                        }
                    }
                ]
            }
        }
    ],
    "outputs": {}
}

To use the new What-if option, open a new shell, and import the module with the required version.

Import-Module Az.Resources -RequiredVersion 1.12.1

Then log in to Azure using Connect-AzAccount

To test the template, you just need to use the new-AzResourceGroupDeployment cmdlet with the -whatif option

New-AzResourceGroupDeployment -ResourceGroupName demo-whatif -TemplateFile ./demo-arm.json -TemplateParameterFile ./demo-arm.params.json -WhatIf

if it is a new deployment, the what-if option will display the two created resources, the network security group, and the virtual network.
You should have something similar to this
Alt Text

The green represents created resources. You can deploy the template and see what happens when you re-run the what-if option.
You may find some modifications about private endpoints and private link services but nothing about the resources in the template.

The output is very rich, but you may want to have less detail you can use the - WhatIfResultFormat parameter with ResourceIdOnly to limit the output

New-AzResourceGroupDeployment -ResourceGroupName demo-whatif -TemplateFile ./demo-arm.json -TemplateParameterFile ./demo-arm.params.json -WhatIf  WhatIfResultFormat ResourceIdOnly

Alt Text

You will only see the impacted Resource ID.

But it may not be enough, with PowerShell you may want to parse the result. You need to get results into a variable to manipulate it later.
You can do that by using the Get-AzResourceGroupDeploymentWhatIfResult cmdlet using the same parameters.

$TemplateDeploymentResult = Get-AzResourceGroupDeploymentWhatIfResult -TemplateFile ./demo-arm.json -TemplateParameterFile ./demo-arm.params.json -ResourceGroupName demo-whatif -ResultFormat FullResourcePayloads

You will get a PSWhatIfOperationResult object. You can use the error and status properties to see if the result was successful or not.
The changes properties return a List1 object

$TemplateDeploymentResult.Changes

Scope                    : /subscriptions/xxxx/resourceGroups/demo-whatif
RelativeResourceId       : Microsoft.Network/networkSecurityGroups/nsg3
FullyQualifiedResourceId : /subscriptions/xxx/resourceGroups/demo-whatif/providers/Microsoft.Network/networkSecurityGroups/nsg3
ChangeType               : Create
ApiVersion               : 2019-11-01
Before                   :
After                    : {apiVersion, id, location, name}
Delta                    :

Scope                    : /subscriptions/xxxx/resourceGroups/demo-whatif
RelativeResourceId       : Microsoft.Network/virtualNetworks/vnet3
FullyQualifiedResourceId : /subscriptions/xxxx/resourceGroups/demo-whatif/providers/Microsoft.Network/virtualNetworks/vnet3
ChangeType               : Create
ApiVersion               : 2019-11-01
Before                   :
After                    : {apiVersion, id, location, name}
Delta                    :

The “ChangeType" property gives you, for each resource, what will happen in the target resource group. ChangeType values can be Create, the resource will be created, Modify, the resource will be modified, Delete, the resource will be deleted, NoChange, the resource in the resource group is the same as in the template and ignore, when resources in the resource group are not affected by the template.
The Delta field will give you an array of the change that may occur to the resource.
Now let see what happens if you change the subnet prefix.

Alt Text

But let see the result if you modify the Network Security Group from the portal and not from the template. Just add a rule from the portal (the result will be the same if you use PowerShell or Azure CLI).

Alt Text

As you see, the what-if options show you that the security rule you created in the portal will be deleted if the template is deployed.

The main problem with this process, it's a manual one. It’s great if you deploy resources a few times a month. But it may not be useful if you want to deploy several times a week. You may want to automate it.
But you may face a problem, deleted or modified resources can be a desired outcome. After all the next desired state you design in your template can imply deletions and modifications.

It’s a tricky situation, there is no easy answer to how to identify a wanted change from an unwanted one?
The only solution I see, using manual approvals.

In Azure DevOps you can set up an approval process before deploying anything to Azure. One way to achieve that is to use a gate in the release pipeline.

You need to create a pipeline for the build process, using a YAML or the classic editor and a release pipeline.

This build process can include steps to tests the templates or any other tasks, but it must also include a step to execute a what-if test and display the result in a reading format.
It means you need to provide a connection to your subscription and the resource group where to deploy the template and also access to the Azure DevOps API to create an issue.

For the connection, you will need a service principal

$ResourceGroupName = "omc-demo-whatif"
$ServicePrincipal = New-AzADServicePrincipal -DisplayName "whatif-demo-sp" -SkipAssignment 
New-AzRoleAssignment -RoleDefinitionName Contributor -ServicePrincipalName $ServicePrincipal.ApplicationId -ResourceGroup $ResourceGroupName

The service principal is created, and a role is assigned to the target resource group so it will be able to test and deploy resources defined in templates.

You will need to get the AppId, the tenant ID, and the SP secret.

$SecureStringBinary =[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($servicePrincipal.Secret)
[System.Runtime.InteropServices.Marshal]::PtrToStringAuto($ SecureStringBinary)

To create the service connection, Open Project settings, go to Pipelines, and click on Service connections. Select service principal and subscription as scope. Enter your Subscription ID, the name, the AppID, the secret key, and the tenant ID.

Alt Text

To access the Azure DevOps API you will need a Personal Access Token or PAT in Azure DevOps with the privilege to create work items.
To create a PAT you will need to use the User settings and use the Personal Access Tokens menu and select the new token.
You will be asked to provide a name, an expiration date, and a scope. For the scope, the tool should only add work items, select read/write option, and click on the create button.

Alt Text

Copy the password and save it. There is no way to retrieve it later.

Now that we have a service connection and the PAT, you can build the build pipeline. Simply create an azure-pipeline.yml file at the root of your repository.

The first step is to choose the trigger and the repository, the image you will use (here I use ubuntu, but you can choose a Windows-based image), and declare some variables.

trigger:
- master

resources:
  - repo: self

variables:
  resourceGroupName: 'demo-whatif'
  templateFolder: "arm"
  templateFile: "demo-arm.json"
  templateParameterFile: "demo-arm.params.json"
  organisationName: "AZ-OMDEMO" 
  projectName: "what-if"

pool:
  vmImage: 'ubuntu-latest'

To perform the what-if action we need first to install the AZ.resources module in preview.

steps:
- task: PowerShell@2
  displayName: 'Update Az Resources Module'
  inputs:
    targetType: 'inline'
    script: |
      Install-Module Az.Resources -RequiredVersion 1.12.1-preview -AllowPrerelease -Force -Scope CurrentUser

We need to run a PowerShell script to test the template and create a new task in the dashboard if any update or delete is found in the test.
How to handle the access token in the script? You cannot put it directly in the script, you need to use a secret variable.

A secret variable is, almost, identical to any other variable in a pipeline. The variable is encrypted, and the process makes it available on the agent when needed. The variable is obfuscated in any output.

- task: AzurePowerShell@5
  displayName: 'Get Az Resources What-if Result'
  env:
    PATSECRET: $(patSecret)
  inputs:
    azureSubscription: AzureDemo
    scriptType: filePath
    scriptPath: $(Build.SourcesDirectory)/testwhatif.ps1
    azurePowerShellVersion: 'LatestVersion'

Also, instead of a standard variable, a secret variable is not mapped to the environment variables on the agent. You need to specify it as an environment variable.

To use the what-if option you need a connection to Azure. For that, you need to use an AzurePowerShell task with the connection you create earlier.
You also need to provide the token for Azure DevOps by using an environment variable.

The PowerShell script is here

$ErrorView = 'NormalView'

$SourceFolder = [Environment]::GetEnvironmentVariable('BUILD_SOURCESDIRECTORY')
$TargetResourceGroupName=[Environment]::GetEnvironmentVariable('RESOURCEGROUPNAME')
$ArmTemplateFolder=[Environment]::GetEnvironmentVariable('TEMPLATEFOLDER')
$ArmTemplateFile=[Environment]::GetEnvironmentVariable('TEMPLATEFILE')
$ArmTemplateParametersFile=[Environment]::GetEnvironmentVariable('TEMPLATEPARAMETERFILE')
$AzureDevOpsOrganisation = [Environment]::GetEnvironmentVariable('ORGANISATIONNAME')
$AzureDevOpsProjedtName = [Environment]::GetEnvironmentVariable('PROJECTNAME') 
$PipelineBuildID = [Environment]::GetEnvironmentVariable('BUILD_BUILDID')  
$AzureDevOpsPAT = [Environment]::GetEnvironmentVariable('PATSECRET')  
$ArmTemplateFilePath = Join-Path -Path $ArmTemplateFolder -ChildPath $ArmTemplateFile
$ArmTemplateParametersFilePath = Join-Path -Path $ArmTemplateFolder -ChildPath $ArmTemplateParametersFile
$TaskText = ""
$UriPost = "https://dev.azure.com/$($AzureDevOpsOrganisation)/$($AzureDevOpsProjedtName)/_apis/wit/workitems/`$task?api-version=5.1" 

    if (!(test-path -Path $ArmTemplateFolder -ErrorAction SilentlyContinue)) {
        Throw "Template folder doesn't exist"
        Exit 1
    }
    if (!(test-path -Path $ArmTemplateFilePath -ErrorAction SilentlyContinue)) {
        Throw "Template file doesn't exist"
        Exit 1
    }
    if (!(test-path -Path $ArmTemplateParametersFilePath -ErrorAction SilentlyContinue)) {
        Throw "Template parameter file doesn't exist"
        Exit 1
    }

    $whatifResultObject = Get-AzResourceGroupDeploymentWhatIfResult -TemplateFile $ArmTemplateFilePath -TemplateParameterFile $ArmTemplateParametersFilePath -ResourceGroupName $TargetResourceGroupName -ResultFormat FullResourcePayloads

    foreach ($change in $whatifResultObject.Changes) {
                switch ($change.ChangeType) {
                    "NoChange" { 
                        $TextColor = "Green"
                        $Text = "Resource all ready deployed in the same was as the template"
                     }
                     "Modify" { 
                        $TextColor = "Magenta"
                        $Text = "Resource will be Updated by the template"
                     }
                     "Create" { 
                        $TextColor = "Cyan"
                        $Text = "Resource does not exist in the resource group and will be deployed"
                     }
                     "Ignore" { 
                        $TextColor = "Green"
                        $Text = "Resource exist in Azure but not in the template, no change"
                     }
                     "Delete" { 
                        $TextColor = "Red"
                        $Text = "Resource exist in Azure but not in the template, Resource will be deleted"
                     }
                }

                write-host "$($change.ChangeType) $($change.RelativeResourceId)" -ForegroundColor $TextColor
                write-host $Text -ForegroundColor $TextColor
                write-host " `r`n"

                if (($change.ChangeType -eq "Modify") -OR ($change.ChangeType -eq "Delete")) {
                    $TaskText += $text 
                    $TaskText += "`r`n"
                    $TaskText += "$($change.ChangeType) $($change.RelativeResourceId)"
                    $TaskText += "`r`n"
                }
        }

if (($whatifResultObject.Changes.ChangeType -contains "Modify") -OR ($whatifResultObject.Changes.ChangeType -contains "Delete")) {

    $taskSubject = "ARM Template need a review in Build $($PipelineBuildID)"

    $Payload = @(
        @{
            "op" = "add"
            "path" =  "/fields/System.Title"
            "from" = $null
            "value" = $taskSubject
        }
        @{
            "op" = "add"
            "path" =  "/fields/System.Description"
            "from" = $null
            "value" = $TaskText
        }
    )

    $JsonPayLoad = $Payload | ConvertTo-Json -Depth 3

    $AzureDevOpsAuthenicationHeader=@{
        "Authorization" = ('Basic {0}' -f [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($AzureDevOpsPAT)")))
    } 


    Invoke-RestMethod-Uri$UriPost -MethodPATCH-Headers$AzureDevOpsAuthenicationHeader -Body$JsonPayLoad -ContentType "application/json-patch+json"
}

The first section extract variables from the environment variables, the second part test if the required files are present, the third section get the result from the what-if option and parse the PSWhatIfOperationResult to display the result and if a Delete or a Modify action is detected it creates a new task in the Azure Board.

To create the task, you need to create the authorization header using the Personal Access Token. You will need to create the data in the JSON format. You need to define at least two fields, Path.System.Title and Path.System.Description.

Now that you have made the test, you need to define the gate. But before that, you need to copy arm files into the artifact.

- task: CopyFiles@2
  inputs:
    contents: 'arm/**'
    targetFolder: $(Build.ArtifactStagingDirectory)

- task: PublishBuildArtifacts@1
  inputs:
    pathToPublish: $(Build.ArtifactStagingDirectory)
    artifactName: ArmFiles

With the artifact, you can create a release pipeline. In Azure DevOps go to the Pipelines section and then to Releases. Click on New Pipeline and select Empty Job.
On the left select the artifact from the build pipeline. On the left of the first stage click on the pre-deployment condition. Enable Gates and Pre-Deployment approvals and add a user.

Now select Stage, add an ARM Template deployment task. In the task select the connection created earlier and your subscription, select the resource group, and your location. For template and template parameter files use the (. . .) button and use the JSON files in the artifact location (normally $(System.DefaultWorkingDirectory)/_what-if/ArmFiles/arm/).

Your release pipeline is ready. It can include some tests (thinks about Azure Resource Manager Tool Kit) an evaluation about what will happen if you apply the template, the what-if option, a gate to be sure to have an evaluation before the deployment, and a deployment step.

The what-if option is still in preview and the output is not always perfect. Sometime you will see a property in a resource as deleted when nothing will happen. In this case, if you want the team behind the module, fill an issue by using GitHub

Posted on by:

omiossec profile

Olivier Miossec

@omiossec

Microsoft Azure MVP, Passionate about Cloud and DevOps. Co-organizers of the French PowerShell UG and Paris PowerShell & WinOps UG. I live in Paris.

Discussion

markdown guide
 

Great post! I'm planning to do a lab myself that runs the what if result cmdlet but observe the results in pester tests so, I can publish results in unit test format. This cmdlet really added a good tool to the TDD toolkit for IaC with ARM templates.

 

Great Idea, you can also transform the result int nunit format