Azure ARM Template Development - Validate with WhatIf
Tags: devops, azure, powershell, arm
Published: 2020-08-16 06:24:36

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

```powershell
#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

```powershell
#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 seperated 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.

```powershell
#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!

```powershell
#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.

```powershell
#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.

<!-- Image: Sample Folder Structure-->

<!-- Dev.To Format -->
![Sample Folder Structure](https://dev-to-uploads.s3.amazonaws.com/i/vmonkmcy184vn5pzmsoy.png)
<figcaption>Sample Folder Structure</figcaption>

#### Test Mode

.\Deploy-AzARMTemplate.ps1 -Test

<!-- Image: Test Mode-->

<!-- Dev.To Format -->
![Sample Folder Structure](https://dev-to-uploads.s3.amazonaws.com/i/bwq5hzxgnxxwgtqwpe6x.png)
<figcaption>Test Mode</figcaption>

#### WhatIf Mode

.\Deploy-AzARMTemplate.ps1 -WhatIf

<!-- Image: What If Mode 1-->
<!-- Dev.To Format -->
![What If Mode 1](https://dev-to-uploads.s3.amazonaws.com/i/ia98u1ydrfpubo7t629i.png)
<figcaption>What If Mode 1</figcaption>

<!-- Image: What If Mode 2-->
<!-- Dev.To Format -->
![What If Mode 2](https://dev-to-uploads.s3.amazonaws.com/i/ji3gqbocddesyiblr1a7.png)
<figcaption>What If Mode 2</figcaption>

#### Deployment Mode

.\Deploy-AzARMTemplate.ps1 

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

<!-- Image: Deployment Mode - Pre-Deployment Confirmation -->
<!-- Dev.To Format -->
![Deployment Mode - Pre-Deployment Confirmation](https://dev-to-uploads.s3.amazonaws.com/i/38jl8tsdukfpb5nldjy9.png)
<figcaption>Deployment Mode - Pre-Deployment Confirmation</figcaption>

if "Y" is selected proceed with the deployment

<!-- Image: Deployment Mode - Proceed with Deployment-->
<!-- Dev.To Format -->
![Deployment Mode - Proceed with Deployment](https://dev-to-uploads.s3.amazonaws.com/i/bpayu2mbvov938961t1g.png)
<figcaption>Deployment Mode - Proceed with Deployment</figcaption>

if "N" is selected deployment is aborted.

<!-- Image: Deployment Mode - Abort the Deployment -->
<!-- Dev.To Format -->
![Deployment Mode - Abort the Deployment](https://dev-to-uploads.s3.amazonaws.com/i/kyv0f5f9v4zo2lmfbkt5.png)
<figcaption>Deployment Mode - Abort the Deployment</figcaption>

#### 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.

```powershell
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.

```powershell
#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.