I was recently involved in a mid-scale deployment for a customer, who unfortunately didn’t have Azure DevOps. They already had their own ARM Templates and they wanted their 2nd/3rd Line infrastructure team to be able to deploy their infrastructure in one go and with little effort to different environments instead of individually, without using linked templates or modifying the original templates.
I began by firstly analyzing their ARM code and noticed that they had hundreds of ARM Templates and parameter files all over the shop. It was extremely confusing to figure out what was doing what, and there was a non-existent naming convention as well. The solution had to be simple, require little modifications to the script and also had to deploy what was requested.
Having a single Deployment Script (written in PowerShell) was the most efficient way possible, as this would call multiple ARM Templates sitting in blob storage and deploy them at will, creating each deployment in a function and then “hashing” out any resources not need.
I began modifying their ARM code, being sure to standardize each parameter carefully (e.g. VirtualNetworkName, instead of vnetname). For this to work efficiently and be seamless, all parameters in all templates had to rely on the same name.
Once that task was done, I then uploaded the templates into private-access blob storage.
I then started scripting the core deployment script which will deploy all the templates hosted in the secure blob. It first generates a SAS Token which is then passed into the core deployment script. Each ARM template has string-based parameters which I’m passing using the PowerShell Parameter Object file which uses a HashTable.
This has now dramatically reduced the number of Parameter Files to zero and the main parameters like “location, resourcegroupname virtualnetworkname etc” because we’re passing them in a single core deployment script.
To provide a real-life scenario, I’ve got a demo script below which will deploy a storage account to an existing resource group. Granted, deploying one of them is simple, but consolidating a full deployment into one script you can see the extreme benefits is more difficult without Azure DevOps.
First, we map out our parameters. They are pretty self-explanatory, so I won’t go into much detail around them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
[ CmdletBinding ()] Param ( [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $location = "West Europe" , [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $ResourceGroupName = "RG-Storage" , [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $storageaccounttype = "Standard_LRS" , [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $storageaccountname = "storsacccraig003" , [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $artifactsstoragename = "cloudshellcraig001" , [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $artifactscontainer = "artifacts" ) |
Next, we need to extrapolate the parameters from the main ARM template. Retrieving this requires PowerShell Core 6 (because it uses the switch “-AsHashtable”) or you could copy and paste them from the JSON file (but that’s tedious),
You need to locate your ARM template code and pop it into a variable, then include the Convert-From-Json cmdlet, which will do exactly what it says with that switch at the end “-AsHashTable” the properties which you need to retrieve are the .parameters.Keys
1
2
3
4
5
|
$TemplateFileText = [System.IO.File] ::ReadAllText( "C:\Users\repos\Storage\azuredeploy_storage.json" ) $TemplateObject = ConvertFrom-Json $TemplateFileText -AsHashtable $TemplateObject .parameters.Keys |
We then get back the output displayed below:
1
2
3
|
Location StorageAccountName StorageAccountType |
This now can be used in our deployment script, but we need to add some extra functions for this to be able to work with the ARM template, which will look something like this:
1
2
3
4
5
|
$storageparams =@{ location = $location StorageAccountName = $storageaccountname StorageAccountType = $storageaccounttype } |
The $storageparams variable is our -TemplateParameterObject switch, which will be defined when we run the New-AzResourceGroupDeployment cmdlet.
The extrapolated strings from the previous command can now be included in the body of the $storageparams equalling the parameter variables at the very top of our script $location $storageaccountname & $storageaccounttype variables are all in the parameters at the top.
We then create the main function, which will generate the SAS Token and deploy the Storage ARM Template.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
Function DeployStorage { $StorageAccountContext = ( Get-AzStorageAccount | Where-Object { $_ .StorageAccountName -eq $artifactsstoragename }).Context $StartTime = Get-Date $EndTime = ( Get-Date ).AddHours(2) $sas = New-AzStorageBlobSASToken -Container $artifactscontainer ` -Blob "azuredeploy_Storage.json" ` -Context $StorageAccountContext ` -Permission "r" ` -StartTime $StartTime ` -ExpiryTime $EndTime ` -FullUri Start-Sleep -Seconds 2 # This is so the SAS Token Generation has time to output correctly $DeploymentName = "DeployStorage01" $templateFileLoc = $sas New-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName -Name $DeploymentName -TemplateUri $templateFileLoc -TemplateParameterObject $storageparams -Verbose } |
You can see in the function that the variables are all being called in the parameters at the top of our script, and the switch -TemplateParameterObject is pointing to the $storageparams.
If you had a virtual machine with the same parameter values like “Virtual Network Name”, you can see it’s minimizing the amount of code and also removing the .parameters JSON file.
This method was a nice test case for me which is now live and rocking in the client’s environment. It would greatly improve, as well, if the client had Azure DevOps.
Thank you for reading and I hope you found this blog post helpful. You can find the full code below for your use:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
[ CmdletBinding ()] Param ( [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $location = "West Europe" , [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $ResourceGroupName = "RG-Storage" , [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $storageaccounttype = "Standard_LRS" , [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $storageaccountname = "storsacccraig003" , [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $artifactsstoragename = "cloudshellcraig001" , [ Parameter ( Mandatory = $false )] [ValidateNotNullOrEmpty()] [string] $artifactscontainer = "artifacts" ) ################# Storage Template Parameters ################################## $storageparams =@{ location = $location StorageAccountName = $storageaccountname StorageAccountType = $storagetype } ################# Storage Deployment ################################## Function DeployStorage { % { Write-Host " "} Write-Host " Generating SAS Token for Storage Template... " -ForegroundColor Yellow; % {Write-Host ""} $StorageAccountContext = (Get-AzStorageAccount | Where-Object{$_.StorageAccountName -eq $artifactsstoragename}).Context $StartTime = Get-Date $EndTime = (Get-Date).AddHours(2) $pwd = New-AzStorageBlobSASToken -Container $artifactscontainer ` -Blob " azuredeploy_Storage.json " ` -Context $StorageAccountContext ` -Permission " r " ` -StartTime $StartTime ` -ExpiryTime $EndTime ` -FullUri Start-Sleep -Seconds 2 % {Write-Host ""} Write-Host " Deploying Storage Account... " -ForegroundColor Yellow; % {Write-Host ""} $DeploymentName = " DeployStoragedeployment01" $templateFileLoc = $pwd New-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName -Name $DeploymentName -TemplateUri $templateFileLoc -TemplateParameterObject $storageparams -Verbose } DeployStorage |