Azure ARM template deployment scripts

After you finish reading this post and experimenting with the Azure sample template below, you may never again have to write a nested or linked template. And, believe me, that’s a good thing. (Feel free to leave a comment as thanks. 🙂 ) The topic of this post is the relatively new Azure Resource Manager type, Microsoft.Resources/deploymentScripts.

deploymentScripts — the ability to run a PowerShell or Azure CLI script in an ARM template — will revolutionize ARM template design and coding. It also makes it significantly easier to use ARM templates, making them more attractive to DevOps developers. It does this in several interesting ways we’ll explore here.

First, allow me to summarize Azure Resource Manager. ARM is the user-side API to all Azure services. It exposes a JSON interface that the actual resource providers, like databases and VMs, expose as schema in what are called “types”. DevOps developers simply declare to ARM what resources should be created without having to know the details of how a database or VM is actually created in Azure. ARM manages the interface between a user asking for a resource to be created and the actual back-end service that does that work. Second, it’s important to know that Azure resources are deployed into a resource group — a logical container which records the work ARM did for you and which holds the deployed resources.

ARM is more than just these two things, of course. But these two basic characteristics of ARM and resource groups are useful to understand the true breakthrough the new deploymentScripts type represents.

Because ARM templates are designed to deploy resources into a single resource group container, when you need to create resources in a second container things get complicated. It’s no easy task to switch from one resource group to another in a template at runtime. Until the arrival of deploymentScripts, a DevOps developer had two choices for this task: a nested template or a linked template. Both are, in a word, clumsy. For example, I tried for days and days to create a nested template that would do what the template in this post does but could never get it to work as cleanly as when I used deploymentScripts and PowerShell. So, one major advantage of using deployment scripts in ARM templates is the ease with which one can deploy the template to resources in multiple resource groups. It’s hard to put in words what a difference this can make, as embedding templates inside templates (as Microsoft.Resources/deployments requires) makes it nearly impossible to debug and maintain an ARM template, no matter how good you are at DevOps development.

The other major improvement that deploymentScripts offers is the addition of logic to ARM templates. This is no small thing. ARM templates have traditionally been declarative: you “tell” the Resource Manager what you want using JSON types and it creates it. There’s no logic. By adding scripting capability, particularly PowerShell, deploymentScripts permits, for the first time, real logic in template development and deployment. An added benefit is that PowerShell offers fabulous string manipulation capabilities, something a DevOps developer will appreciate every time she codes a concat template function.

With all that, let’s take a look at a sample deploymentScripts template. The objective of this template is 1) deploy a VM, 2) deploy a Log Analytics workspace in a different resource group from the VM and 3) connect the Microsoft Monitoring Agent (MMA) to the LA workspace. We may want, for example, to record VM heartbeat records from multiple VMs in a single LA workspace. That way, we can centralize LA workspace solutions that use this data. Unless we put all our VMs in a single resource group — and nobody would do this — we have a need to deploy an ARM template that can connect MMA to a workspace in a resource group other than the one in which we are deploying a new VM.

The first task (defining a VM in ARM) is pretty easy. And, the third isn’t hard either — just deploy the MMA extension as a child resource of Microsoft.Compute/virtualMachines. However, to connect the MMA extension in our VM to a LA workspace, we need two items of information contained in the LA workspace. We need a workspaceId and a workspaceKey from the LA workspace. The thing that makes this hard in a traditional template is when that LA workspace is in a different resource group. That case pretty much means we have to use either a nested or linked template to define the LA workspace.

But using a deploymentScripts PowerShell script, we can create the workspace in a different resource group and output the two variables we need to connect the VM very easily. Azure PowerShell cmdlets typically aren’t chained to a single resource group the way ARM templates are. So the PowerShell Az cmdlet New-AzOperationalInsightsWorkspace accepts a ResourceGroupName parameter. This means we can create the LA workspace in a different resource group without using a nested or linked template. The ARM template is still deployed to the VM’s resource group — but we use Azure PowerShell to deploy (or access) the LA workspace in a different resource group. That may not seem so major to you, but trust me, if you code ARM templates this is big, big news.

Let’s take a look at the PowerShell script that the sample deploymentScripts runs :

param( 
    [string]$workspaceName,
    [string]$workspaceResourceGroup,
    [string]$workspaceLocation
)
$DeploymentScriptOutputs = @{ }
$ws = New-AzOperationalInsightsWorkspace -Location $workspaceLocation -Name $workspaceName -Sku PerGb2018 -ResourceGroupName $workspaceResourceGroup -Force
$DeploymentScriptOutputs['customerId'] = ($ws.CustomerId).Guid
$DeploymentScriptOutputs['primarySharedKey'] = ($ws | Get-AzOperationalInsightsWorkspaceSharedKey).PrimarySharedKey",

This a very simple PowerShell script to create an LA workspace. Note our inputs and outputs. Input parameters are described in the deploymentScripts arugments property, as shown in the snippet below. We can see that these are coming directly from the parameters specified at template deployment time. Be sure to observe the PowerShell parameter conventions in this string, specifically the spaces between names and values.

"arguments": "[concat(' -workspaceName ', parameters('workspaceName'),' -workspaceResourceGroup ',parameters('workspaceResourceGroup'), ' -workspaceLocation ',parameters('workspaceLocation') )]"

Outputs are equally simple. We simply create a PowerShell hashtable ($DeploymentScriptsOutputs) in our script using a key for each output we wish. This hashtable can be conveniently referenced elsewhere in the ARM template using the reference template function as shown here:

"settings": {
    "workspaceId": "[reference('runPowerShellToCreateLaWorkspace').outputs.CustomerId]",
    "stopOnMultipleConnections": false
    },
    "protectedSettings": {
    "workspaceKey": "[reference('runPowerShellToCreateLaWorkspace').outputs.primarySharedKey]"
    }

Compare this to the level of effort required to obtain this key and id using a nested or linked template and your head will explode from joy. It’s simple, clean and neat.

There’s much more to say about deploymentScripts that I don’t have room for here, but I will make make one implementation recommendation and then show you some screenshots.

Since deploymentScripts run in a container, you need a user-assigned managed identity that has rights on either the resource group or the subscription. Save yourself some time and just create the identity with an RBAC contributor role at the subscription level. You’ll should also avoid system-generated names and name the resources that deploymentScripts creates using containerSettings. You pay for the container resources, so don’t leave them lying about. Use cleanupPreference to make sure you delete items you no longer need.

Once your template completes, you will see a new resource type in the portal:

ARM template deploymentScript type in Azure portal
ARM template deploymentScript type in Azure portal (click to enlarge)

Here’s a screenshot looking “inside” the deploymentScripts type in the portal. You can see both the substituted arguments and the script as downloaded into the container instance.

deploymentScripts arugments and script
deploymentScripts arugments and script (click to enlarge)

Finally, here’s a screenshot showing the outputs from $DeploymentScriptOutputs which can be accessed elsewhere in the template using the template reference function.

deploymentScripts output
deploymentScripts output (click to enlarge)

There is much more to talk about in the new deploymentScripts type but hopefully, this overview will give you enough information to get started and get you excited about never, ever again having to write nested or linked ARM templates. Here is the complete deploymentScripts template, followed by a sample parameters file.

{
// Copyright 2020 Air11 Technology LLC
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0
 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License. #>

    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "location": {
            "type": "string"
        },
        "userAssignedIdentityResourceId": {
            "type": "string"
        },
        "resourceTags": {
            "type": "object"
        },
        "workspaceName": {
            "type": "string"
        },
        "workspaceLocation": {
            "type": "string"
        },
        "workspaceResourceGroup": {
            "type": "string"
        },
        "networkInterfaceName": {
            "type": "string"
        },
        "privateIPAddress": {
            "type": "string"
        },
        "enableAcceleratedNetworking": {
            "type": "bool"
        },
        "subnetName": {
            "type": "string"
        },
        "virtualNetworkId": {
            "type": "string"
        },
        "virtualMachineName": {
            "type": "string"
        },
        "virtualMachineSku": {
            "type": "string"
        },
        "osDiskType": {
            "type": "string"
        },
        "virtualMachineSize": {
            "type": "string"
        },
        "adminUsername": {
            "type": "string"
        },
        "adminPassword": {
            "type": "secureString"
        },
       "utcValue": {
            "type": "string",
            "defaultValue": "[utcNow()]"
    }
    },
    "variables": {
        "vnetId": "[parameters('virtualNetworkId')]",
        "subnetRef": "[concat(variables('vnetId'), '/subnets/', parameters('subnetName'))]",
        "vmResourceId": "[resourceId('Microsoft.Compute/virtualMachines', parameters('virtualMachineName'))]"
    },
    "resources": [
        {
            "type": "Microsoft.Resources/deploymentScripts",
            "apiVersion": "2019-10-01-preview",
            "name": "runPowerShellToCreateLaWorkspace",
            "kind": "AzurePowerShell",
            "location": "[resourceGroup().location]",
            "identity": {
                "type": "userAssigned",
                "userAssignedIdentities": {
                    "[parameters('userAssignedIdentityResourceId')]": {
                    }
                }
            },
            "properties": {
                "forceUpdateTag": "[parameters('utcValue')]",
                "containerSettings": {
                    "containerGroupName": "[concat(toLower(parameters('workspaceName')),'-deploymentscripts')]"
                    },
                "azPowerShellVersion": "3.8",
                "scriptContent": "
param( [string]$workspaceName, [string]$workspaceResourceGroup, [string]$workspaceLocation)
$DeploymentScriptOutputs = @{ }
$ws = New-AzOperationalInsightsWorkspace -Location $workspaceLocation -Name $workspaceName -Sku PerGb2018 -ResourceGroupName $workspaceResourceGroup -Force
$DeploymentScriptOutputs['customerId'] = ($ws.CustomerId).Guid
$DeploymentScriptOutputs['primarySharedKey'] = ($ws | Get-AzOperationalInsightsWorkspaceSharedKey).PrimarySharedKey",
                "arguments": "[concat(' -workspaceName ', parameters('workspaceName'),' -workspaceResourceGroup ',parameters('workspaceResourceGroup'), ' -workspaceLocation ',parameters('workspaceLocation') )]",
                "timeout": "PT1H",
                "cleanupPreference": "OnExpiration",
                "retentionInterval": "P1D"
            }
        },
        {
            "name": "[parameters('networkInterfaceName')]",
            "type": "Microsoft.Network/networkInterfaces",
            "apiVersion": "2018-10-01",
            "location": "[parameters('location')]",
            "dependsOn": [
            ],
            "properties": {
                "ipConfigurations": [
                    {
                        "name": "ipconfig1",
                        "properties": {
                            "subnet": {
                                "id": "[variables('subnetRef')]"
                            },
                            "privateIPAllocationMethod": "Static",
                            "privateIPAddress": "[parameters('privateIPAddress')]"
                        }
                    }
                ],
                "enableAcceleratedNetworking": "[parameters('enableAcceleratedNetworking')]"
            },
            "tags": "[parameters('resourceTags')]"
        },
        {
            "name": "[parameters('virtualMachineName')]",
            "type": "Microsoft.Compute/virtualMachines",
            "apiVersion": "2019-03-01",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[concat('Microsoft.Network/networkInterfaces/', parameters('networkInterfaceName'))]"
            ],
            "properties": {
                "hardwareProfile": {
                    "vmSize": "[parameters('virtualMachineSize')]"
                },
                "storageProfile": {
                    "osDisk": {
                        "createOption": "fromImage",
                        "managedDisk": {
                            "storageAccountType": "[parameters('osDiskType')]"
                        }
                    },
                    "imageReference": {
                        "publisher": "MicrosoftWindowsServer",
                        "offer": "WindowsServer",
                        "sku": "[parameters('virtualMachineSku')]",
                        "version": "latest"
                    }
                },
                "networkProfile": {
                    "networkInterfaces": [
                        {
                            "id": "[resourceId('Microsoft.Network/networkInterfaces', parameters('networkInterfaceName'))]"
                        }
                    ]
                },
                "osProfile": {
                    "computerName": "[parameters('virtualMachineName')]",
                    "adminUsername": "[parameters('adminUsername')]",
                    "adminPassword": "[parameters('adminPassword')]",
                    "windowsConfiguration": {
                        "enableAutomaticUpdates": true,
                        "provisionVmAgent": true
                    }
                }
            },
            "identity": {
                "type": "systemAssigned"
            },
            "resources": [
                {
                    "type": "extensions",
                    "apiVersion": "2018-10-01",
                    "name": "InstallMonitoringAgent",
                    "location": "[parameters('location')]",
                    "dependsOn": [
                        "[variables('vmResourceId')]"
                    ],
                    "properties": {
                        "publisher": "Microsoft.EnterpriseCloud.Monitoring",
                        "type": "MicrosoftMonitoringAgent",
                        "typeHandlerVersion": "1.0",
                        "autoUpgradeMinorVersion": true,
                        "settings": {
                            "workspaceId": "[reference('runPowerShellToCreateLaWorkspace').outputs.CustomerId]",
                            "stopOnMultipleConnections": false
                        },
                        "protectedSettings": {
                            "workspaceKey": "[reference('runPowerShellToCreateLaWorkspace').outputs.primarySharedKey]"
                        }
                    }
                }
            ],
            "tags": "[parameters('resourceTags')]"
        }
    ],
    "outputs": {
        "adminUsername": {
            "type": "string",
            "value": "[parameters('adminUsername')]"
        },
        "customerId": {
            "type": "string",
            "value": "[reference('runPowerShellToCreateLaWorkspace').outputs.CustomerId]"
        },
        "workspaceKey":{"type": "string","value":"[reference('runPowerShellToCreateLaWorkspace').outputs.primarySharedKey]"}
    }
}

And here is the sample parameters file:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "resourceTags": {
            "value": {
                "CreatedBy": "Alex Neihaus",
                "DateCreated": "2020-05-18 14:22",
                "Application": "Azure deploymentScripts example"
            }
        },
        "location": {
            "value": "westus"
        },
        // Be sure to change the below to the resourceID of a managed identity with RBAC role Contributor at the subscription level
        "userAssignedIdentityResourceId":{
            "value": "/subscriptions/12345678-4444-0000-0000-0123456789ABC/resourcegroups/YourRg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/UserAssignedIdentityYourUserManagedIdentity"
        },
        "workspaceLocation": {
            "value": "westus"
        },
        "workspaceResourceGroup": {
            "value": "RgLaWorkspace"
        },
        "workspaceName": {
            "value": "DemoLogAnalysticsWorkspace"
        },
        "networkInterfaceName": {
            "value": "netintlademo01"
        },
        "privateIPAddress": {
            "value": "10.173.252.75"
        },
        "enableAcceleratedNetworking": {
            "value": false
        },
        "subnetName": {
            "value": "Subnet-10-173-252-64-26"
        },
        // Be sure to change the below to the resourceID of a valid virtual network
        "virtualNetworkId": {
            "value": "/subscriptions/12345678-4444-0000-0000-0123456789ABC/resourceGroups/YourRG/providers/Microsoft.Network/virtualNetworks/YourVnet"
        },
        "virtualMachineName": {
            "value": "VmLaDemo"
        },
        "virtualMachineSku": {
            "value": "2019-Datacenter-smalldisk"
        },
        "virtualMachineSize": {
            "value": "Standard_B4ms"
        },
        "osDiskType": {
            "value": "Premium_LRS"
        },
        "adminUsername": {
            "value": "AdminVMDemo"
        },
        // Better, use a key vault to retreive the VM's admin password
        "adminPassword": {
            "value": "UseAMuchStrongerPasswordThanThis2020!"
        }
    }
}

Posted

in

, ,

by

Comments

2 responses to “Azure ARM template deployment scripts”

  1. santhosh Avatar
    santhosh

    Hi Alex, Is it possible to run the deploymentscript without Identity, like as a logged in user?

    1. Alex Neihaus Avatar
      Alex Neihaus

      It depends. See this page. You might be able to Connect-AzAccount in the deploymentScript scriptContent but I wouldn’t do it that way.

Leave a Reply

Your email address will not be published. Required fields are marked *