Azure ARM Template Development - Validate with WhatIf

A local development PowerShell script for making ARM template testing easier with the new WhatIf Operation

Hi, I’m back with yet another script I would like to introduce and pretty much document for my and other’s future reference.

Today’s script is named “Deploy-AzARMTemplate.ps1” and it’s geared towards one-click deploys from your local development environment to pre-configured cloud dev/test environment. Making use of the new What-If validation the script also allows us to pre-validate the deployment.

Purpose

So what does this script does? or what is it for? This script’s main purpose is to allow DevOps engineers to be able to make a full deployment to a pre-set environment for testing purposes. The script is easily modifiable and can work with multiple projects, given they are using similar naming structure.

The benefit of this portable and configurable script is to reduce the time spent on curating different AZResourceGroupDeployment cmdlets, such as Test-AzResourceGroupDeployment , Get-AzResourceGroupDeploymentWhatIfResult and obviously the real deal New-AzResourceGroupDeployment which generates the deployment.

I was aiming to set the parameters once and then just run the script by itself or with some easy common switches such as -Test , -WhatIf or -Force to be able to perform these functions.

This script is aimed to be used locally while developing/extending your ARM templates and allow for quick deployment to a Dev/Test environment. Hence keeping your Azure Environment information is not insecure as long as your repository is private and only people who you want access has access to it. Azure Subscription ID or Tenant IDs are not exactly secrets on their own, but it may expose you for some vulnerabilities if they are known together by a 3rd party.

Prerequisites

Make sure you are on Powershell AzModule version 4.2 or higher. The script already enforces this.
To learn more about the new WhatIf operation you can read more here: https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/template-deploy-what-if?tabs=azure-powershell

The Script

Without further do let’s get into it.

I’ll break down the Script into sections first.

First let set our parameters block and configure the path where our ARM template files resides and also Azure Environment Information

#Requires -Module @{ ModuleName = 'Az'; ModuleVersion = '4.2' }

[CmdletBinding()]
param (
    [Parameter()][switch]$WhatIf,
    [Parameter()][switch]$Test,
    [Parameter()][switch]$Force
)
#region Configuration
#Deployment Assets Configuration
# Go Up one level so you can pick a project folder.
# This is here in case you want to keep your script in different folder.
$OperationsPath = Split-Path $PSScriptRoot
# Target the folder which desired ARM Template and Parameter files are in.
$ArmArtifactsPath = "$OperationsPath\infrastructure"

#Azure Environment Configuration
$TenantId = '#{YOUR-AZUREAD-TENANT-ID}#'
$SubscriptionId = '#{YOUR-AZURE-SUBSCRIPTION-ID}#'
#endregion

Then we make sure we are in the correct Azure Context

#region Connect to Correct Tenant and Subscription
$CurrentContext = Get-AzContext
#If there is no AzContext found connect to desired Subscription and Tenant
if (!$CurrentContext) {
    Connect-AzAccount -Tenant $TenantId -Subscription $SubscriptionId -UseDeviceAuthentication
    $CurrentContext = Get-AzContext
}
#If subscription ID doesnt match, call the Set-AzContext with
#SubscriptionId and TenantId to allow switch between tenants as well.
if ($CurrentContext.Subscription.Id -ne $SubscriptionId) {
    $CurrentContext = Set-AzContext -Subscription $SubscriptionId -Tenant $TenantId
}
#endregion

Then let’s configure the service parameters. These sections are all separated so everyone can just configure that section.

For example, here use a ResourceGroup name structure where I just purely concatenate the components but you may be using a whole other, structure like having ‘-’ in between those parts.
The idea is, we have all the different identifier enums about our service so we can curate whatever string we want use.

#region Service Constants
$ProjectName = 'mert'
$Stage = 'dev'
$ServiceType = 'fnc'
$Region = "wus"
#endregion

#region Service Curated Parameters
$ResourceGroupName = $ProjectName + $Stage + $ServiceType + $Region + '01'
$TemplateFile = "$ArmArtifactsPath\template.json"
$TemplateParameterFile = "$ArmArtifactsPath\parameters.$Stage.$($Region)01.json"

$ARGS = @{
    ResourceGroupName     = $ResourceGroupName
    TemplateFile          = $TemplateFile
    TemplateParameterFile = $TemplateParameterFile
    Mode                  = 'Incremental'
    Verbose               = $true
    ErrorAction           = 'Stop'
}
#endregion

Deployment Helpers, to only run certain mode. They are both good for certain things. The Test-AzResourceGroupDeployment performs a very primitive check and it’s already covered in regular or what-if deployments but it is still somewhat useful. These can sometimes detect problems that VsCode ARM Extension cant, so it’s here to make the script complete but using this mode completely optional.

Get-AzResourceGroupDeploymentWhatIfResult is something I use a whole lot more nowadays as it attempts to simulate the real REST API response from the Azure side, assuming the deployment was made.

As you would see in Microsoft’s disclaimer as well that it’s still in preview mode so it may still generate some extra noise. So practicing with it now, when it’s out of preview, you will be ready to use it to it’s full extent!

#region Deployment Helpers
$ErrorActionPreference = "Stop"
if ($Test) {
    Test-AzResourceGroupDeployment @ARGS
    exit $LASTEXITCODE
}
if ($WhatIf) {
    $WhatIfResult = Get-AzResourceGroupDeploymentWhatIfResult @ARGS `
                                    -ResultFormat FullResourcePayloads
    $WhatIfResult
    exit $LASTEXITCODE
}
#endregion

Finally the part that performs a WhatIf first and deploys. This is pretty useful as you pretty much be able to see a Delta of your changes.

If -Force flag is passed to the script then, the -Confirm flag is set to false hence the deployment will just proceed without confirmation.

This is useful if you know you already know that your template is valid, healthy and deploys fine but you are just making changes to it and want fast deployments.

#region Deployment ("Validate & Deploy" or "Forced" Deployment)
try {
    $PromptForConfirmation = (($Force) ? $false : $true)
    Write-Host "Deploying $Region"
    $Deployment = New-AzResourceGroupDeployment @ARGS `
                                                -Name "$(New-Guid)" `
                                                -Confirm:$PromptForConfirmation `
                                                -WhatIfResultFormat FullResourcePayloads
}
catch {
    Write-Host $_.Exception.Message
    exit $LASTEXITCODE
}

# Tell me If my Deployment was Successfull
if ($Deployment.ProvisioningState -eq "Succeeded") {
    Write-Host "Deployed $Region Successfully"
}
# Or if there was an error during Deployment I want to know about it
elseif ($Deployment.ProvisioningState -ne "Succeeded" -and ($Deployment.CorrelationId)) {
    Write-Host "$(Get-AzLog -CorrelationId $Deployment.CorrelationId)"
}
# Else just let me know if there was no AzResourceGroupDeployment Object found
elseif (!($Deployment)) {
    Write-Warning "No AzResourceGroupDeployment Object Found"
}
#endregion

In this section, we pretty much use the -Confirm switch built-in to the New-AzResourceGroupDeployment cmdlet to achieve the prompt behaviour.

The rest of the code is for catching exceptions for broken/invalid template deployment(or WhatIf) attempts and print the error/exception message as needed.

What is Cool About WhatIf

The WhatIf check makes ARM templates look like terraform plan command that tells you what would change.

However, terraform works with a state file, here the state file is the actual state’s of the Azure Resources, being retrieved upon request via the REST API.

Terraform, populates the state file itself so when you do a deployment after the first time, the state file is compared to declared deployment. This is a very accurate difference as both are generated by Terraform.

You need to use the deployment mode “Complete” instead of the default “Incremental” if you wish this new WhatIf check in ARM Template deployments works closer to Terraform.

But because you are comparing the actual live resources to your ARM template hence it’s not easy to always 1:1 match the default generated properties to your ARM template, or there are some properties which are just metadata.

Hence currently there is noise in the output but that doesn’t make it any less of a tool to be used for Testing your ARM template if it will pass an actual deployment request made to the Azure ARM Rest API endpoint.

Or to simply visualize and see the impact printed out for you after you have made some changes, updating or extending your ARM template.

The full script file can be found at: https://github.com/MertSenel/devops-powershell/blob/main/scripts/local/arm-template-development-helper/Deploy-AzARMTemplate.ps1

Script in Action

Folder Structure

I use a folder structure like below, I’m sharing this to give the script some more context, in terms of how the files are named.

Sample Folder Structure

Test Mode

.\Deploy-AzARMTemplate.ps1 -Test

Test Mode

WhatIf Mode

.\Deploy-AzARMTemplate.ps1 -WhatIf

What If Mode 1
What If Mode 2

Deployment Mode

.\Deploy-AzARMTemplate.ps1

Performs WhatIf and presents the changes before proceeding with the deployment.

Deployment Mode - Pre-Deployment Confirmation

if “Y” is selected proceed with the deployment

Deployment Mode - Proceed with Deployment

if “N” is selected deployment is aborted.

Deployment Mode - Abort the Deployment

Forced Deployment Mode

.\Deploy-AzARMTemplate.ps1 -Force

It will do the same as above but, won’t ask for confirmation. Just runs WhatIf Validation and if no error thrown, proceeds with the deployment.

You can think of this like terraform apply -auto-approve command where it skips the interactive approval.

Conclusion

This is very handy script to move around with you in your different project and configure it once to your desired ARM Template project and you can always deploy it to your desired dev/test environment with ease.

This script can also be extend to accept some sensitive parameter values via adding a securestring parameter to the script and also extending the Hashtable. This is forward the secret value to your ARM Template deployment as input in a secure manner, so you dont have to save it in plain text.

You can use this to pass any type of secrets to an ARM template.

param (
    [Parameter()][switch]$WhatIf,
    [Parameter()][switch]$Test,
    [Parameter()][switch]$Force,
    [Parameter()][securestring]$mySecretValue
)

$ARGS = @{
    ResourceGroupName     = $ResourceGroupName
    TemplateFile          = $TemplateFile
    TemplateParameterFile = $TemplateParameterFile
    Mode                  = 'Incremental'
    Verbose               = $true
    ErrorAction           = 'Stop'
    mySecretParameterName = $mySecretValue
}

You can also convert any of the constant to an array and deploy to let say multiple regions with single loop and convert this script to make more than one deployments. It’s flexible after this point.

#region Service Constants
$ProjectName = 'mert'
$Stage = 'dev'
$ServiceType = 'fnc'
#$Region = "wus"
$Regions = "wus", "aue" ,"neu"

foreach($Region in $Regions) {
    # The rest of the Deployment code logic goes here.
}
#endregion

Another benefit is that, having this source controlled and not testing your deployments via your terminal will also give you more confidence in the configuration you are passing to your deployment.

Last benefit is, you can add breakpoints to your script to debug a certain value at any given time if you are having troubles getting your deployment working.

As always I hope this script helped you out and let me know if you have any feedback or criticism about it via comments section.

Mert Senel
Mert Senel
Principal Cloud DevOps Engineer - Microsoft Certified Azure Administrator & DevOps Expert

Related