You've successfully subscribed to Florin Loghiade
Great! Next, complete checkout for full access to Florin Loghiade
Welcome back! You've successfully signed in
Success! Your account is fully activated, you now have access to all content.
Deployment Scripts in ARM Templates

Deployment Scripts in ARM Templates

in

I’ve been authoring ARM templates since Azure moved from the Service Manager model to the Resource Manager model, and I never regretted it.

We are starting with a bit of a history lesson. For some of us, this might seem like ancient history when counting in cloud years but bear with me.

When Azure first launched back in 2008, the underlying operating system was called Red Dog or RD in short, and it was Platform as a Service only. Yeah, your read right, only PaaS, no IaaS, or any other services you have right now; and it was very much agreeable. New, obscure, but interesting.

A while later, Microsoft introduced IaaS with its first SKUs of virtual machines, which allowed you to host services as you would do for many years in AWS. Everything was managed in the Windows Azure portal, as you would handle everything in the Azure portal today. So far, so good; if you wanted the clicky-clicky way of doing things, doing things at scale was a big no-no.
You see, you could only do one operation at a time, and if you would do multiples, then the first action would be superseded by the second one. So you can imagine that automating things in Windows Azure was a certifiable nightmare. I’ve even written a couple of PowerShell scripts to automate simple creation mechanisms for multiple virtual machines.

So as you can imagine, scripting was not as pleasurable as it would be today.

Some years pass. After a few notable changes, Windows Azure became Azure. Azure Service Manager got an update to Azure Resource Manager, which introduced parallelization via the portal, scripts, and most notably, ARM templates that bring us to today’s topic.

ARM Templates provide a way of deploying infrastructure in a declarative way. Much like PowerShell Desired State Configuration, if you’re familiar with it.


To implement infrastructure as code for your Azure solutions, use Azure Resource Manager templates (ARM templates). The template is a JavaScript Object Notation (JSON) file that defines the infrastructure and configuration for your project. The template uses declarative syntax, which lets you state what you intend to deploy without having to write the sequence of programming commands to create it. In the template, you specify the resources to deploy and the properties for those resources.

Templates overview – Azure Resource Manager | Microsoft Docs

So ARM templates provide me a way of deploying resources in Azure with one or more templates, and we’re happy. Write once and be done with it, right?

Yes and no. You see, one of the problems with ARM Templates is that they don’t get to configure inside the system you’re deploying. You can use PowerShell scripts, PowerShell DSC scripts, or Shell scripts inside ARM and launch them inside the VM, but you’re locked into configuring the OS instances you’re deploying.

What happens if you want to do some configuration on a PaaS SQL DB? Or you want to have a turn-key AKS solution? Well, if you asked me a couple of years ago, I would have said, “it’s complicated,” but nowadays, you have another solution, and that’s Deployment Scripts.

The “old way” was to use a VM extension to reference a link to a script file (ps1 or .sh) under Microsoft.Compute/virtualMachines/extensions while the new way, Deployment Scripts, has a new resource Microsoft resource.Resources/deploymentScripts

VM Extension Example

        {
            "type": "Microsoft.Compute/virtualMachines/extensions",
            "name": "[concat(concat(variables('vmWeb0xName'), copyIndex()),'/BashScript')]",
            "apiVersion": "[variables('computeApiVersion')]",
            "location": "[variables('location')]",
            "copy": {
                "name": "vmCopy",
                "count": "[parameters('vmWebCount')]"
            },
            "properties": {
                "publisher": "Microsoft.Azure.Extensions",
                "type": "CustomScript",
                "typeHandlerVersion": "2.0",
                "autoUpgradeMinorVersion": true,
                "settings": {
                    "fileUris": "[split(parameters('fileUris'), ' ')]"
                },
                "protectedSettings": {
                    "commandToExecute": "[parameters('commandToExecute')]"
                }
            },
            "dependsOn": [
                "[resourceId('Microsoft.Compute/virtualMachines/', concat(variables('vmWeb0xName'), copyIndex()))]"
            ],
        },

Deploment Script Example with MSI

    {
      "type": "Microsoft.Resources/deploymentScripts",
      "apiVersion": "2020-10-01",
      "name": "createAddCertificate",
      "location": "[resourceGroup().location]",
      "identity": {
        "type": "UserAssigned",
        "userAssignedIdentities": {
          "[parameters('identityId')]": {
          }
        }
      },
      "kind": "AzurePowerShell",
      "properties": {
        "forceUpdateTag": "[parameters('utcValue')]",
        "azPowerShellVersion": "5.0",
        "timeout": "PT30M",
        "primaryScriptUri": "<URLTOSCRIPT>"
        "cleanupPreference": "OnSuccess",
        "retentionInterval": "P1D"
      }
    }

So which should we use? Well, it depends on what you want to achieve. Deployment Scripts have the possibility of running scripts inside the Azure plane. This happens by spinning up an Azure Container Instance where your code runs. This means that you can do:

  1. Create SQL schemas
  2. Copy files between storage accounts
  3. Deploy specialized code in Web Apps
  4. Configure certificates inside a Key Vault
  5. Deploy very complex microservice systems inside AKS

Basically, the sky is the limit in this context. While with the VM Extensions, you had to cook your way to Azure, with Deployment Scripts, you only need to provide it with a Managed Identity, and it will do it directly.

For operations inside the Azure plane, you need to provide a User-Managed Identity, but you don’t need to create it as a prerequisite if it doesn’t exist already. You can just add it to the ARM Template or reference the ID inside the deployment script zone.

Deployment Scripts schema

Microsoft.Resources/deploymentScripts – ARM template reference | Microsoft Docs

Let’s get started with an example. Let’s assume that you want to do it from one hit.

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
        "utcValue": {
      "type": "string",
      "defaultValue": "[utcNow()]"
    },
    "identityName": {
      "type": "string",
      "metadata": {
        "description": "Specifies the name of the user-assigned managed identity."
      }
    }
  },
  "variables": {
    "roleAssignmentId": "[guid(concat(resourceGroup().id, 'contributor'))]",
    "contributorRoleDefinitionId": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]"
  },
  "resources": [
    {
      "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
      "apiVersion": "2018-11-30",
      "name": "[parameters('identityName')]",
      "location": "[resourceGroup().location]"
    },
    {
      "type": "Microsoft.Authorization/roleAssignments",
      "apiVersion": "2018-09-01-preview",
      "name": "[variables('roleAssignmentId')]",
      "dependsOn": [
        "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('identityName'))]"
      ],
      "properties": {
        "roleDefinitionId": "[variables('contributorRoleDefinitionId')]",
        "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('identityName')), '2018-11-30').principalId]",
        "scope": "[resourceGroup().id]",
        "principalType": "ServicePrincipal"
      }
    },
    {
      "type": "Microsoft.Resources/deploymentScripts",
      "apiVersion": "2020-10-01",
      "name": "demoSample",
      "location": "[resourceGroup().location]",
      "identity": {
        "type": "UserAssigned",
        "userAssignedIdentities": {
          "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('identityName'))]": {
          }
        }
      },
      "kind": "AzurePowerShell",
      "properties": {
        "forceUpdateTag": "[parameters('utcValue')]",
        "azPowerShellVersion": "5.0",
        "timeout": "PT30M",
        "scriptContent": "
          Write-Host 'Hello World'
        ",
        "cleanupPreference": "OnSuccess",
        "retentionInterval": "P1D"
      }
    }
  ]
}

The ARM template from above does a simple thing; It creates a User-Managed Identity, assigns it the role of Contributor, and outputs a Hello World text inside a Deployment Script block.

You might say that’s redundant, but the fun part already starts. We basically covered 90% of the work in one go. All you need to do is write PowerShell or Shell script code inside the scriptContent block or change it with primaryScriptUri and use a blob file from an Azure Storage account.

If I would like to run commands inside an AKS cluster, all I would need to do is adjust the script to use shellcode and just let it rip. e.g.

Deployment Script block only:

{
      "type": "Microsoft.Resources/deploymentScripts",
      "apiVersion": "2020-10-01",
      "name": "demoSample",
      "location": "[resourceGroup().location]",
      "identity": {
        "type": "UserAssigned",
        "userAssignedIdentities": {
          "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('identityName'))]": {
          }
        }
      },
      "kind": "AzureCli",
      "properties": {
        "forceUpdateTag": "[parameters('utcValue')]",
        "azCliVersion": "2.9.1",
        "timeout": "PT30M",
        "scriptContent": "
          az aks get-credentials --name aksCluster --resource-group resourceGroup
          helm repo add nginx-stable https://helm.nginx.com/stable
          helm repo update
          helm install nginx nginx-stable/nginx-ingress
        ",
        "cleanupPreference": "OnSuccess",
        "retentionInterval": "P1D"
      }
    }

The significant differences from the first example are that I changed from AzurePowerShell to AzureCli and from AzurePowerShellVersion to AzCliVersion.

The last things to mention before we signoff here is about the “forceUpdateTag”: “[parameters(‘utcValue’)]”, and the parameter utcValue with the value “[utcNow()]”

This means that the script will rerun if you rerun the ARM template again, which in most cases might mean a massive no-no because if the hand did.

That being said, have fun playing with it, and I as always have a good one!