5 Writing advanced ARM templates

This chapter covers

  • Writing an ARM template that deploys to more than one scope
  • Structuring large IaC solutions into more than one template
  • Dealing with dependencies and ordering resources in a deployment
  • Advanced constructs like conditions, loops, and deployment scripts
  • Reverse engineering existing infrastructure into an ARM template

In chapter 3 we explored the different sections of an ARM template and learned how to use all of them. In that chapter, we also worked on the infrastructure for an API that serves as the backend for a web shop that was responsible for processing the orders that come in, among other things. There was an App Service for hosting the API itself, and other components like a Key Vault, Application Insights, SQL Server and database, and storage accounts. This chapter will continue with those components and use them to go into more advanced ARM topics.

The first few sections of this chapter deal with topics that you’ll run into when your templates become larger, such as how to deploy to more than one scope, how to split them up, and how to order the deployment of resources. Next up are more advanced techniques that do not necessarily relate to larger templates but are more complicated in themselves: conditions, loops, and deployment scripts. You can use these constructs to optionally deploy resources, deploy a series of similar resources, or interleave resource deployment with custom scripts. Let’s first explore how to deploy to multiple scopes from a single template.

5.1 Deploying to multiple scopes using nested templates

When your infrastructure gets more complicated, you might run into a situation where you need to deploy resources to different scopes from within a single deployment. That’s a challenge, since a template by default targets a single scope. For example, suppose you want to create a resource group and a storage account in that, within the same template. Since a resource group is deployed to the subscription but the storage account is deployed to the resource group, that requires a deployment to multiple scopes. Another example could be that your infrastructure needs a Key Vault, and you decide to deploy that into a resource group that you name shared and which is used for resources you share among applications. You then deploy a storage account for a particular application into another resource group. The account key for that storage account needs to be stored in the Key Vault, but that now lives in another resource group. Using a nested deployment allows you to make these cross-scope deployments.

As the name implies, a nested template is a complete template written within another template. The reason for doing this is that the nested template can target another deployment scope than the main deployment does, such as another resource group, a subscription, or a management group.

In figure 5.1 you can see a template file called template.json that contains a template like you’ve seen before. The greater part of that template, which we call the main template, is deployed to Resource Group A within Subscription A. You set the values for this resource group and subscription while starting a deployment, as you saw in chapter 4. A smaller part of this file, the nested template, is deployed into Resource Group B within Subscription B. You will shortly see how you can control the values for the resource group and subscription in a nested template. Listing 5.1 shows how you can place a nested template within a main template.

Figure 5.1 Nested templates can target a different deployment scope.

Listing 5.1 A template containing a nested template

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {},
    "resources": [
        {
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2019-10-01",
            "name": "nestedTemplate1",
            "properties": {
                "template": {
                    <nested-template-syntax>      
                }
            }
        }
    ],
    "outputs": {
    }
}

At this location you specify a nested template.

Within the resources section in the preceding template, where you normally define resources like a storage account or SQL Server, you’ll see a resource of type Microsoft .Resources/deployments. That type is being used for the nested deployments. Its most important property is the template property. That’s where you define another complete template, including things like schema, contentVersion, parameters, and, of course, at least one resource.

Let’s build an example that needs a nested deployment. We’ll start with the following snippet, which deploys an Azure App Configuration resource.

{
    "type": "Microsoft.AppConfiguration/configurationStores",
    "apiVersion": "2019-11-01-preview",
    "name": "[variables('appConfigurationName')]",
    "location": "[parameters('location')]",
    "sku": {
        "name": "free"
    }
}

Azure App Configuration is a service that allows you to manage application settings and feature flags for one or more applications and environments in a single place. Since this is typically a resource that you share among applications and environments, like test and production, you only deploy one of these into a resource group or subscription with shared resources.

In this chapter’s scenario, the API, running on the Azure App Service, will need to be able to load its settings from the Azure App Configuration service. The most secure way to access the App Configuration from the App Service is to use the App Service’s managed identity. For this to work, you need to assign a reader role on the App Configuration to the App Services identity. You would typically include this role assignment in the App Service deployment, as that is where you know the identity of the App Service. The challenge here is that the App Configuration resource is shared and is therefore placed in another resource group and maybe even in another subscription. That means that you cannot deploy both the App Service and the role assignment from the same template, since that would require two different deployment scopes.

To work around this challenge, you can use a nested template to assign the required role when deploying the App Service. The following listing shows how to write a nested template within the template that deploys the App Service.

Listing 5.2 A nested template within its main template

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {
        "appConfigurationReaderRoleId": 
             "516239f1-63e1-4d78-a4de-a74fb236a071"                  
    },
    "resources": [
        {
            "apiVersion": "2018-02-01",
            "name": "API",
            "type": "Microsoft.Web/sites",
            "location": "West Europe",
            "identity": {
                "type": "SystemAssigned"
            },
            "properties": {
                ...
            }
        },
        {
            "type": "Microsoft.Resources/deployments",                 
            "apiVersion": "2020-06-01",
            "name": "RoleAssignmentOnAppConfig",
            "resourceGroup": "<resourcegroup-appconfiguration>",       
            "subscriptionId": "<subscription-appconfiguration>",       
            "dependsOn": [
                "API"
            ],
            "properties": {
                "mode": "Incremental",
                "template": {
                    "$schema": "https:/ /schema.management.azure.com/schemas/
   2019-04-01/deploymentTemplate.json#",
                    "contentVersion": "1.0.0.0",
                    "resources": [
                        {
                            "type":
   "Microsoft.AppConfiguration/configurationStores/
   providers/roleAssignments",
                            "apiVersion": "2018-01-01-preview",
                            "name": "[concat('AppConfiguration', 
   '/Microsoft.Authorization/', 
   guid('API', variables('appConfigurationReaderRoleId')))]",
                            "properties": {
                                "roleDefinitionId":                    
   [concat('/providers/Microsoft.Authorization/roledefinitions/',
   variables('appConfigurationReaderRoleId'))]",    
                                "principalId":                         
   "[reference('API', '2019-08-01', 'full').identity.principalId]"
                            }
                        }
                    ]
                }
            }
        }
    ],
    "outputs": {
    }
}

RBAC role: App Configuration Data Reader

The nested template resource

The resource group that this nested template deploys to

The subscription that this nested template deploys to

roleDefinitionId contains the role you want to assign.

principalId contains a reference to the identity that’s allowed access.

This example deploys two resources: it first deploys the App Service that hosts the API and then deploys the role assignment using a nested template. In a nested template, you can define a different resourceGroup and subscriptionId than you are deploying the main template in, and thereby change the scope for this nested template. (You’ll see an example that targets another management group shortly.) In this example, the different resourceGroup and subscriptionId are the ones that contain the App Configuration. That means that the template defined in the template element is deployed to that scope.

The first property within the properties object of the nested template is the mode, which is set to incremental. This mode refers to deployment modes you read about in section 4.4. Setting this to incremental might suggest that it is also possible to set it to complete, but that is not the case. For nested templates, the deployment mode always depends on the deployment mode of the main template. The resources defined in the nested template are automatically included in the evaluation when deploying to the same resource group. Any resource that is not defined in either the main or nested template is deleted when the main template’s deployment mode is complete.

In this example, the nested template is used to deploy the role assignment. Deploying a role on a single resource is done by deploying a resource of type roleAssignments to the resource you want to assign it to. Here we deploy a role on the App Configuration resource, so the type becomes "Microsoft.AppConfiguration/configurationStores/ providers/roleAssignments". The properties section has two properties: roleDefinitionId and principalId. The roleDefinitionId property points to the role you want to assign. That could be a built-in or custom RBAC role. This example uses the built-in App Configuration Data Reader role. The GUID used in the appConfigurationReaderRoleId variable is a static and unique role identifier that is always the same for every Azure customer, and it can be found in Microsoft’s “Azure built-in roles” article (http://mng.bz/YGej). The principalId property points to the identity that you want to assign the role to. Here we retrieve the principalId of the API by using the reference() function and drilling into its result using .identity.principalId.

5.1.1 Nested templates on a management group

So far you have seen a nested template being used to deploy a resource to a different subscription or resource group. You can also use them to deploy a resource to a different management group as shown in the following listing.

Listing 5.3 A nested template on a management group

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-08-01/managementGroupDeploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "variables": {
        "rootManagementGroupName": "rootManagementGroup",
        "rootManagementGroupId": 
             "[tenantResourceId('microsoft.management/managementGroups', 
              variables('rootManagementGroupName'))]",
        "testManagementGroupName": "Test",
        "testManagementGroupScope": 
             "[format('/providers/Microsoft.Management/managementGroups/{0}',
              variables('testManagementGroupName'))]",
        "contributorRoleDefinitionId": 
             "b24988ac-6180-42a0-ab88-20f7382dd24c",
        "roleDefinitionId": 
             "[concat('/providers/Microsoft.Authorization/roledefinitions/
              'b24988ac-6180-42a0-ab88-20f7382dd24c', 
              variables('contributorRoleDefinitionId'))]",
        "adGroupPrincipalId": "d14b006f-f50c-4239-840c-0f0724e428f9"
    },
    "resources": [
        {
            "type": "Microsoft.Management/managementGroups",                     
            "apiVersion": "2020-05-01",
            "scope": "/",
            "name": "Test",
            "properties": {
                "details": {
                    "parent": {
                        "id": "[variables('rootManagementGroupId')]"              
                    }
                }
            }
        },
        {
            "type": "Microsoft.Resources/deployments",
            "apiVersion": "2019-10-01",
            "name": "ContributorRoleOnAzurePlatformEngineer",
            "scope": "[concat('Microsoft.Management/managementGroups/',           
                         variables('testManagementGroupName'))]",
            "location": "westeurope",
            "properties": {
                "mode": "Incremental",
                "template": {
                    "$schema":
                         "https:/ /schema.management.azure.com/schemas/
                          2019-04-01/deploymentTemplate.json#",
                    "contentVersion": "1.0.0.0",
                    "resources": [
                        {
                            "type": 
 "Microsoft.Authorization/roleAssignments",                                     
                            "apiVersion": "2020-04-01-preview",
                            "name": 
                   "[guid('/', variables('adGroupPrincipalId'),
                    variables('contributorRoleDefinitionId'))]",
                            "properties": {
                                "scope": 
                                    "[variables('testManagementGroupScope')]",  
                                "principalId": "[variables('adGroupPrincipalId')]",
                                "roleDefinitionId":
                                   "[variables('roleDefinitionId')]"
                            }
                        }
                    ]
                }
            }
        }
    ]
}

Creating a new management group

Specifying a management group’s parent

Setting the scope of the nested template

The resource type used to deploy a role assignment

Setting the scope of the role assignment

The preceding listing contains two resources. It will first create a new management group using the type Microsoft.Management/managementGroups. This new management group, named Test, will be deployed within the root management group. That is done by specifying its parent id in the properties section (properties .details.parent.id). The second resource deployed in this template will assign the contributor role to an AAD group on that management group. This role assignment type, namely Microsoft.Authorization/roleAssignments, is different than the one used in the previous example where you assigned a role on a specific resource. Since the scope is no longer present in that type, you need to specify it in the scope property. For that to work, however, the scope defined there must match the deployment scope. Since this template is deployed at a higher scope, the rootManagement group, you need the nested template. In the nested template, you can set the current deployment scope by using the scope property to point to the new Test management group.

This template can be deployed at the root management group scope using the following Azure CLI command:

az deployment mg create 
  --location WestEurope 
  --management-group-id rootManagementGroup 
  --template-uri "azuredeploy.json"

Here you use the az deployment mg create command to deploy the template to the rootManagementGroup management group specified using the --management-group-id switch.

5.1.2 Evaluation scope

When deploying a nested template, you get to specify what scope to use when evaluating an expression. This scope determines how parameters, variables, and functions like resourceGroup() and subscription() are resolved. You can choose between the scope of the main template or the scope of the nested template. The expressionEvaluationOptions property allows you to specify which one by using either the value inner or outer, which refer to the nested and the main template respectively. The default value is outer.

In listing 5.2, you saw a subscriptionId defined in a nested template that was different from the subscriptionId that the template file was deployed to. Therefore, it was deployed into another subscription than the main template. Suppose you’re using the subscription() function within the nested template. When you set the scope to outer, that will return the subscription of the main template. When set to inner, it will return the subscription defined in the subscriptionId property in the nested template.

The example in figure 5.2 defines a variable named subscriptionId twice—both in the main template and in the nested template. Depending on the value of the scope property, the result is different.

Figure 5.2 Function evaluation scope is determined by expressionEvaluationOptions.scope.

If you specify inner as the scope, you can no longer reference parameters or variables defined in the main template. The consequence of this is that if your nested template requires input, you would have to specify parameters and pass values from the main template, as shown in the following listing.

Listing 5.4 A nested template that requires parameters

{
    "name": "templateWithParameters",
    "type": "Microsoft.Resources/deployments",
    "apiVersion": "2019-10-01",
    "properties": {
        "expressionEvaluationOptions": {
            "scope": "inner"
        },
        "mode": "Incremental",
        "parameters": {                               
            "vmName": {
                "value": "TestVirtualMachine"
            }              
        },
        "template": {
            "$schema": "https:/ /schema.management.azure.com/schemas/
                 2019-04-01/deploymentTemplate.json#",
            "contentVersion": "1.0.0.0",
            "parameters": {                           
                "vmName": {
                    "type": "string"
                }
            },
            "resources": [
                {
                    "type": "Microsoft.Compute/virtualMachines",
                    "apiVersion": "2020-06-01",
                    "name": "[parameters('vmName')]",
                    "properties": {
                        ...
                    }
                }
            ]
        }
    }
}

Specifying a value for the parameter defined within the template

Specifying parameters within the nested template

In the preceding listing, you again see a nested template, this time with the scope set to inner. To pass the VM’s name in, the template now has a parameter defined: vmName. Before the nested template’s definition, a value for that parameter is also specified in the main template. This is very similar to how you would specify a parameter on a normal template and then supply a variable using a parameter file.

When thinking about evaluation scopes, we recommend using inner wherever possible. An evaluation scope of inner makes sure that every nested template has a clear, defined scope and cannot accidentally reach parameters or variables from the main template.

5.1.3 Outputs

Just as with standalone templates, you can also return values from a nested template using outputs. You define them as follows:

{
    "type": "Microsoft.Resources/deployments",
    "apiVersion": "2019-10-01",
    "name": "nestedTemplateOutputExample",
    "properties": {
        "mode": "Incremental",
        "template": {
            ...
            "outputs": {
                "sampleName": {
                    "type": "string",
                    "value": "sampleValue"
                }
            }
        }
    }
}

The preceding snippet defines an output, in the outputs section, named sampleName within a nested template. You could use that output in the main template as shown in the following expression:

"[reference('nestedTemplateOutputExample').outputs.sampleName.value]"

When using nested templates to reach other deployment scopes, outputs are often the easiest way to pass generated values back to the main template.

When you start working with nested templates, you’ll quickly notice that your templates become larger and larger. Now it’s time to discuss how you can structure larger ARM template solutions to make them more manageable.

5.2 How to structure solutions

Small to medium solutions are best off with a single template containing all resources. It makes the solution easier to understand and maintain. For larger, more advanced solutions, you can use linked templates to break down the solution into smaller, more understandable, reusable solutions. Before diving into the details of linked templates, let’s first look at two high-level approaches to structuring ARM template solutions.

5.2.1 Small to medium solutions

As said previously, small to medium solutions work best in a single file. For example, you could create one template that contains all the resources for a particular application to run. For a second application, you could then create another template. When you do so, it’s good to combine resources that have the same lifecycle and that are changed together. If you also follow the advice to group resources with the same lifecycles in a resource group, you will get one ARM template per resource group. The following snippet shows an example folder structure that follows this approach.

+-- API
|   +-- api.json
|   +-- api.parameters.test.json
|   +-- api.parameters.prod.json
|   +-- azure-pipelines.yml
+-- Configuration
|   +-- configuration.json
|   +-- configuration.parameters.test.json
|   +-- configuration.parameters.prod.json
|   +-- azure-pipelines.yml
+-- ...

This example contains two root folders that relate to different application components and resource groups. Each folder represents a group of resources that is intended to be deployed independently from the others. The first one, API, contains resources to run the API. Think of the App Service plan, the App Service, and the storage accounts. The second folder, Configuration, could contain the Key Vault and App Configuration. You’ll often share these resource types across multiple applications, so they have a different lifecycle. Within each folder, there is a separate template, separate parameter files, and a separate deployment pipeline (in this case, the azure-pipelines.yml files).

5.2.2 Large solutions

For larger solutions, it is highly recommended that you break down your templates into smaller templates. Thais allows for better readability, maintainability, and reuse of templates. One way to do this is by separating templates into resource templates and composing templates. Resource templates are the building blocks for the solution. Each of those templates creates one particular resource. There might be a resource template that creates a Key Vault, another that creates a storage account, and another that creates the App Service. Composing templates are the glue that combines the resource templates, the building blocks, into a useful infrastructure. Figure 5.3 illustrates this.

Figure 5.3 Composing templates and resource templates

You can build up a tree structure in large solutions using composing and resource templates. It all starts with the main template, which is a special version of a composing template. The main template is the template that you reference when deploying the whole tree, and it includes all other templates, both composing templates and resource templates. Nothing prevents you from having composing templates that use other composing templates.

Using this approach, you can build a large infrastructure while still keeping the individual templates small and readable. The folder structure for your solution could look like this:

+-- Composing
|   +-- API
|      +-- Main.json
|      +-- Main.parameters.test.json
|      +-- Main.parameters.prod.json
|      +-- api.json
|      +-- storage.json
|      +-- azure-pipelines.yml
|   ...
+-- Resources
|   +-- SQL
|      +-- Server.json
|      +-- Database.json
|   +-- Web
|      +-- AppService.json
|      +-- AppServicePlan.json
+-- ...

The preceding example contains a few resource templates in the Resources folder—those create one particular resource. For the names of the subfolders, you could use the namespace of the resource. The Composing folder contains an API folder that contains templates to deploy the infrastructure for the API. The deployment starts with the Main template, which also has two parameter files. That Main template then includes the storage.json and api.json templates, which in turn call the Resources templates. Figure 5.4 illustrates this. Now that you’ve seen how to structure a large solution that spans multiple files, it is time to learn how to write linked templates.

Figure 5.4 A visual representation of how the templates use one another

5.3 Modularizing templates with linked templates

The linkedTemplate resource type works much like the nested template you saw earlier. The difference is that instead of embedding a template within the resource, you reference another file to use as the template. Everything you learned in section 5.1 about nested templates, such as working with the evaluation scope, setting another resource group or subscription ID, parameters, or output, is identical for linked templates.

Let’s look at an example:

{
    "type": "Microsoft.Resources/deployments",
    "apiVersion": "2019-10-01",
    "name": "linkedTemplate",
    "properties": {
        "mode": "Incremental",
        "templateLink": {
            "uri": "link/to/newStorageAccount.json",
            "contentVersion": "1.0.0.0"
        }
    }
}

Instead of the template object you saw in the nested template, a linked template has a templateLink object. This example uses the uri field in the templateLink object to specify the URI for another template file. If you choose to specify a contentVersion (optional), ARM checks if the contentVersion here matches the contentVersion specified in the linked template. If not, the deployment is aborted.

There are three ways to reference another template from a templateLink object:

  • Using a URI

  • Using a relative path

  • Using a template spec

The last option, using a template spec, is discussed in chapter 10. Let’s take a look at the other two options.

5.3.1 Using a URI

As you saw in the preceding example, the uri field allows you to call another template while deploying. The value in that field needs to be accessible over the internet using HTTP or HTTPS so that the Azure Resource Manager can use it. Therefore, you can’t point to your local machine, your local network, or a private repository on GitHub or in Azure DevOps. But that does not mean that the template needs to be publicly available to everyone.

A common way of making a template available over the internet but still keeping the contents secure is by storing the template in an Azure storage account. You can then create a shared access signature (SAS) token to enable access during deployment.

"templateLink": {
    "uri": "[concat(variables('baseUri'),
         '/Resources/WebApp/WebApp.json')]",
    "queryString": "[parameters('containerSasToken')]",
    "contentVersion": "1.0.0.0"
}

The uri field in the preceding example is used to reference another template. It gets a value by concatenating a baseUri, defined in a variable for reuse, with the path to the template you want to use. That would, for example, result in the following URI:

https:/ /booktemplates.blob.core.windows.net/templates/
    Resources/WebApp/WebApp.json

The queryString property is then used to add the SAS token to it. You can, for example, generate that using the Azure CLI:

az storage blob generate-sas 
    --account-name <storage-account> 
    --container-name <container> 
    --permissions acdrw 
    --expiry <date-time> 

The preceding command will return a SAS token that looks similar to this one:

?st=2021-03-05T09%3A16%3A56Z&se=2021-03-06T09%3A16%3A56Z
     &sp=racwdl&sv=2018-03-28&sr=c
     &sig=wMp3vAaG1ThAhZ4L5wTRASFWcI6ttTH4r0z6%2FRQwfb0%3D

The complete URI that the linked template will use, combining the uri and queryString properties, will look like this:

https:/ /booktemplates.blob.core.windows.net/templates/
    Resources/WebApp/WebApp.json?st=2021-03-05T09%3A16%3A56Z
    &se=2021-03-06T09%3A16%3A56Z&sp=racwdl
    &sv=2018-03-28&sr=c
    &sig=wMp3vAaG1ThAhZ4L5wTRASFWcI6ttTH4r0z6%2FRQwfb0%3D

As mentioned before, you can pass in parameters the same way as with nested templates. However, one additional method on a linked template is to use the parametersLink property to reference a separate parameters file:

"templateLink": {
    "uri": "https:/ /link/to/newStorageAccount.json",
    "contentVersion": "1.0.0.0"
},
"parametersLink": {
    "uri": "https:/ /link/to/newStorageAccount.parameters.json",
    "contentVersion": "1.0.0.0"
}

The preceding example shows how to specify a parameter file as input for the linked template using the uri field in the parametersLink object. The benefit of using this approach over the separate parameters is that it becomes more manageable to group and use the values. As with the template file itself, you also need to provide the SAS token while referencing the parameter file.

Linked templates and CI/CD

As you read in chapter 4, you can deploy your ARM templates using a CI/CD pipeline. That becomes a bit harder with linked templates, as they have to be accessible over the internet, so you would need to upload your templates on every change.

What you can do is copy all your templates to a new folder (container) in your storage account within the build stage in your CI/CD system. The name of the container could be the ID of the current build, for example. An example of a URL would then be something like this:

https:/ /boektemplates.blob.core.windows.net/
     <BUILD-ID>/Resources/WebApp/WebApp.json

That would lock this specific version of the templates with this specific run of your CI/CD pipeline, since it gets its own unique name. Later on in the pipeline, during the deployment, you would point to the templates in this specific container. That ensures that you would use these versions of the templates when later running your test or production stage, even if another new pipeline run comes in between, since that would create a new container and not overwrite the existing files.

Here is a part of an example pipeline used in Azure DevOps that contains this process:

stages:
- stage: 'PublishTemplates'
  displayName: 'Publish Templates'
  jobs: 
    - job: 'PublishTemplates'
      steps:
      - task: AzureCLI@2                                               
        displayName: "Create container $(Build.BuildId)"
        inputs:
          azureSubscription: 
               ${{ variables.azureResourceManagerConnection }}
          scriptType: 'bash'
          scriptLocation: 'inlineScript'
          inlineScript: 'az storage container create --connection-string 
               "${{ variables.storageAccountConnectionString }}" 
               -n $(Build.BuildId) 
               --account-name ${{ variables.storageAccountName }}'
      - task: AzureCLI@2                                               
        inputs:
          azureSubscription: 
               ${{ variables.azureResourceManagerConnection }}
          scriptType: 'bash'
          scriptLocation: 'inlineScript'
          inlineScript: 'az storage blob upload-batch --account-name 
               ${{ variables.storageAccountName }} -d $(Build.BuildId) 
                -s $(build.artifactstagingdirectory)/templates 
               --connection-string 
               "${{ variables.storageAccountConnectionString }}"'

An Azure CLI task that creates a new container on the storage account

An Azure CLI task that uploads the templates

The template contains two tasks. The first uses the Azure CLI to create a new folder (container) within the storage account. It uses the ID of the build as its name, using $(Build.BuildId). This $(Build.BuildId) is a predefined variable in Azure DevOps; more on that can be found in Microsoft’s “Use predefined variables” article (http://mng.bz/GE4A). The second task uses the Azure CLI to upload all templates to this new container.

The next stage in the pipeline could then deploy the templates to Azure:

- stage: 'DeployTemplates'
  displayName: 'Deploy Templates'
  jobs: 
    - job: 'DeployTemplates'
      steps:
 
      - task: AzureResourceManagerTemplateDeployment@3
        inputs:
          deploymentScope: 'Resource Group'
          azureResourceManagerConnection: 
              ${{ variables.azureResourceManagerConnection }}
          subscriptionId: ${{ variables.subscriptionId }}
          action: 'Create Or Update Resource Group'
          resourceGroupName: 'arm-template-demo'
          location: 'West Europe'
          templateLocation: 'Linked artifact'
          csmFile: '<storageaccountUrl>/$(Build.BuildId)/
               Composing/Main.json'
          csmParametersFile: '<storageaccountUrl>/$(Build.BuildId)/
               Composing/Main.parameters.test.json'
          deploymentMode: 'Incremental'

The preceding snippet shows a new stage that contains one task. This task will deploy the template using the csmFile property pointing to the storage account. Note the use of $(Build.BuildId) again here to point to the same container created in the previous stage. The entire template can be found on GitHub here: http://mng .bz/z4l6.

5.3.2 Using a relative path

As the heading suggests, you can also address another template using a relative path instead of a full URI. You do this by specifying a relative path to the linked template:

{
    "type": "Microsoft.Resources/deployments",
    "apiVersion": "2020-10-01",
    "name": "childLinked",
    "properties": {
        "mode": "Incremental",
        "templateLink": {
            "relativePath": "storage/storageAccount.json",
            "queryString": "[parameters('containerSasToken')]"
        }
    }
}

Here, the relativePath property is used in the templateLink object. The relative path should point to a linked template, relative to the location of the template currently deploying. To make this work, the folder structure where the JSON is hosted during the deployment should look like this:

+-- Templates
|   +-- mainTemplate.json
|   +-- storage
|      +-- storageAccount.json

Remember that this relates to the storage where the files should be available over the internet during deployment. The relativePath option does not refer to the location on your local computer.

In several of the examples you have seen in this book, there is a relationship between the resources. Therefore, they must be deployed in a particular order. Let’s examine how you can enforce the order of deployment.

5.4 Deploying resources in order

Sometimes you’ll have a pair of resources that have to deploy in a specific order. For example, the web shop presented in the scenario in chapter 3 needs an App Service to run an API. This App Service needs an App Service plan to run on, which needs to be deployed first.

Defining order in ARM templates can be done in two ways:

  • Explicitly—Using the dependsOn element

  • Implicitly—Using a reference to the output of another resource

5.4.1 Explicit deployment ordering

In the following listing, you’ll see the creation of two resources, the App Service plan and the App Service.

Listing 5.5 Explicit ordering between resources

"resources": [
    {
        "name": "appServicePlan",
        "type": "Microsoft.Web/serverfarms",
        "apiVersion": "2020-06-01",
        "location": "[resourceGroup().location]",
        "sku": {
            "name": "F1",
            "capacity": 1
        }
    },
    {
        "name": "webApp",
        "type": "Microsoft.Web/sites",
        "apiVersion": "2018-11-01",
        "location": "[resourceGroup().location]",
        "dependsOn": [                              
            "appServicePlan"
        ],
        "properties": {
            "serverFarmId": "[resourceId('Microsoft.Web/serverfarms',
                 'appServicePlan')]"
        }
    }
]

Specifying the dependency between resources using dependsOn

On the second resource, the App Service, you’ll find the dependsOn element. That is an array that can hold one or more references to other resources. By adding this dependsOn element, the ARM runtime can build up a dependency tree and deploy resources in the correct order. In this example, you make sure that the App Service plan is deployed first, because you specified its name in the App Service’s dependsOn element.

There are three ways in which you can define the resource to wait for in the dependsOn element:

  • [resourceId('Microsoft.Sql/servers/', parameters('serverName'))]—You can use the resourceId() function if the resource to wait for has been deployed outside of the current template, such as in a linked template or as part of another deployment.

  • [parameters('serverName')]—You can use an expression that points to the name of another resource within the same template.

  • Constant value, such as 'appServicePlan'—You can simply specify the name of another resource deployment within the same template.

The third option is by far the most readable, but it has the downside of being static. If you change the name of the deployment, you’ll need to remember to change this value as well. The second option solves that problem by reusing a parameter that specifies the name of a deployment. The first option does the same but for resources that are not defined in your current template. Since that option is less readable, you should only use it when the resource is not defined in the current template.

It’s important to know that even when you define a resource as a child resource, you still need to specify its dependency. Let’s look at an example in the following listing.

Listing 5.6 Mandatory dependency on a parent resource

"resources": [
    {
        "name": "appServicePlan",
        "type": "Microsoft.Web/serverfarms",
        "apiVersion": "2018-02-01",
        "location": "[resourceGroup().location]",
        "sku": {
            "name": "F1",
            "capacity": 1
        },
        "resources": [
            {
                "name": "webApp",
                "type": "sites",
                "apiVersion": "2018-11-01",
                "location": "[resourceGroup().location]",
                "dependsOn": [
                    "appServicePlan"
                ],
                "properties": {
                    "serverFarmId": 
                         "[resourceId('Microsoft.Web/serverfarms',
                          'appServicePlan')]"
                }
            }
        ]
    }
]

The preceding listing again deploys an App Service plan and an App Service. The App Service is defined as a child resource but still has the dependsOn element. If you left that out, the deployment would fail because ARM wouldn’t automatically add the dependsOn element for you.

5.4.2 Implicit deployment ordering

Another way to enforce order while deploying a template is by using an output from a linked or nested template, as you saw in the previous section. Suppose your App Service deployment outputs the name of the App Service.

"outputs": {
    "webappName": {
        "type": "string",
        "value": "[variables('webappname')]"
    }
}

You can now use that value in another template as follows:

"[reference('<name-of-resource>').outputs.webappName.value]"

By doing that, you have implicitly specified a deployment order. The ARM runtime waits for the reference resource to be deployed, since it needs the output.

Using the explicit way of describing ordering is better in terms of understandability and the templates’ readability. It also makes sure that the order is still in place when you stop using the output. So although you could use the implicit method, the general advice is not to rely on it.

Sometimes you’ll want to control whether a resource in your template should be deployed or not, depending on an expression. This is what conditions allow you to do.

5.5 Conditionally deploying resources

After some hard work, the web shop you have been creating resources for throughout chapter three and this chapter becomes a huge success. The business expands into other regions, and traffic increases month after month. However, after a couple of outages, you decide that it’s time to make the API more resilient. Remember, the API runs on an Azure App Service, and the way to make an App Service geo-redundant and more resilient is to deploy it into more than one region. Besides West Europe, you decide to also deploy the API into North Europe. But now that you have the app running in multiple regions, you need an Azure Traffic Manager to distribute traffic over the two deployments.

To save on costs, you decide that deploying to a second region is a bit too much for your test environment and that you don’t need a Traffic Manager there. A condition allows you to dictate just that. Examine the following example.

Listing 5.7 Adding a condition on a resource

"resources": [
    {
        "condition": "[equals(parameters('environment'), 'production')]"   
        "type": "Microsoft.Network/trafficManagerProfiles",
        "apiVersion": "2018-04-01",
        "name": "[parameters('trafficManagerName')]",
        "location": "global",
        "properties": {
            "profileStatus": "Enabled",
            "trafficRoutingMethod": "Geographic",
            "monitorConfig": {
                "protocol": "HTTPS",
                "port": 443,
                "path": "[parameters('path')]",
                "intervalInSeconds": 10,
                "toleratedNumberOfFailures": 2,
                "timeoutInSeconds": 5,
                "expectedStatusCodeRanges":
                     "[parameters('expectedStatusCodeRanges')]"
            },
            "endpoints": "[parameters('endpoints')]",
            "trafficViewEnrollmentStatus": "Disabled"
        }
    }
]

Using a condition to only deploy this resource to your production environment

This example deploys the Azure Traffic Manager. The first line on the resource is the condition element. It accepts an expression that needs to result in a Boolean value. In this example, the condition verifies that the environment you deploy to is the production environment. Only if that condition evaluates to true is the Traffic Manager deployed.

Another interesting element in listing 5.7 is the location, which is set to global. A Traffic Manager is one of the few resources you don’t deploy to a specific region but globally. For each user request, it determines the best endpoint to route the request to. The remainder of the configuration is specific to the Traffic Manager service and out of scope for this book.

One thing to keep in mind is that, although a condition might evaluate to false, the resource is still in the template and will be validated during the deployment. That means that every function you use within this resource’s definition still needs to work, and inputs to those functions need to be valid. Consider the following snippet that deploys a storage account:

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
        2019-08-01/managementGroupDeploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "storageAccount": {
            "type": "object",
            "defaultValue": {
                "enabled": true,
                "name": "storageaccount",
                "sku": {
                    "name": "Standard_GRS"
                }
            }
        }
    },
    "resources": [
        {
            "condition": "[parameters('storageAccount').enabled]",
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2019-04-01",
            "name": "[parameters('storageAccount').name]",
            "sku": {
                "name": "[parameters('storageAccount').sku.name]"
            },
            "kind": "StorageV2",
            "location": "West Europe"
        }
    ]
}

This example uses a condition to check whether the storage account should be deployed or not. Even when enabled is set to false, you would still need to provide the full object in the storageAccount parameter; you could not just drop the name and sku properties because references to the name and sku properties would throw errors while the template is being validated.

A workaround here could be to use the if() function. Instead of writing

"name": "[parameters('storageAccount').name]"

you could write

"name": "[if(parameters('storageAccount').enabled,
     parameters('storageAccount').name, '')]"

That if() statement checks the enabled property and uses the name property when enabled is set to true; it returns an empty string when it’s set to false.

Another thing to note is that the name of every resource still needs to be unique. If you are using a condition to deploy one resource or the other, both resources’ names need to be unique, even though only one will be deployed.

You can use dependsOn with a conditional resource without any issues. When the deployment condition evaluates to false, the resource is also deleted from all dependsOns by the Resource Manager.

5.5.1 Applying conditions to output

Conditions can also be applied to outputs. Actually, this is required when an output refers to a conditional resource. If you don’t apply the condition to the output, an error will be thrown if the condition is not met, as the resource and output won’t exist.

"outputs": {
    "resourceID": {
        "condition": "[equals(parameters('environment'), 'production')]",
        "type": "string",
        "value": "[resourceId('Microsoft.Network/trafficManagerProfiles',
             parameters('trafficManagerName'))]"
    }
}

This example shows how to use the condition element within an output. The template returns the resourceId of the Traffic Manager created. The condition element contains the same expression that’s used in the resource. Therefore, the resourceId() function in the value property is only evaluated when the condition is true.

There are situations in which you’ll want to create multiple instances of one particular resource type. You could copy the entire resource in the template, but better yet, you can use the copy element to implement loops.

5.6 Using loops to create multiple resources

In the web shop architecture, you saw two storage accounts. One is used to store the PDF files that the API generates, and the other is used to store logs. These types of data have different usage characteristics, like being publicly available or not and the level of storage redundancy they might need. You could store all the data in one storage account, but considering how the different types are used, it is better to separate the two.

To create multiple instances of the same resource type without copying and pasting the resource template, you can use the copy element to iterate over an array. The copy element has the following syntax:

"copy": {
    "name": "<name-of-loop>",
    "count": <number-of-iterations>,
    "mode": "serial" <or> "parallel",
    "batchSize": <number-to-deploy-serially>
}

The name of the copy is its unique identifier, like for any other resource. The count property specifies the number of resources to create. You’ll often assign the length of the array you are looping over to count.

The mode lets you specify whether you want to create your resources in a serial or parallel way. The default here is parallel. In a parallel copy, all resources are created in parallel, and the order in which they are created is not guaranteed. But sometimes, you will need a specific order. In these cases, you can use the serial option. Serial mode deploys the resources one by one. This also helps with resources for which you cannot deploy multiple instances at the same time.

In between serial and parallel, it is also possible to have resources created in batches. For this option, choose the serial mode by setting the batchSize property to a value higher than 1 (which is the default).

Let’s look at an example of how you could use the copy element to create multiple resources:

"variables": {
    "storageAccountNames": [
        "datastorage",
        "logstorage"
    ]
},
"resources": [
    {
        "type": "Microsoft.Storage/storageAccounts",
        "name": "[variables('storageAccountNames')[copyIndex()]]",
        "copy": {
            "name": "storagecopy",
            "count": "[length(variables('storageAccountNames'))]"
        },
        "sku": {
            "name": "Standard_GRS"
        },
        "kind": "StorageV2",
        "apiVersion": "2019-04-01",
        "location": "West Europe"
    }
]

The preceding example creates two storage accounts using the copy element. The names of the storage accounts are defined in an array in a variable called storageAccountNames. Within the count property of the copy element, you can see the length() function being used, with the array as input, to determine the number of iterations.

Resources that are generated using copy need to have unique names—just like any other resources. As the resource definition is copied multiple times, the only way to do that is to generate a name through an expression. In this case, the name is read from the storageAccountNames array. You can use the copyIndex() function to get the current position in the loop and retrieve an item from a particular index in the array.

One thing to be careful of is handling empty arrays. You might use a parameter of type array, which is specified to be optional and can thus be empty. If you use that in a copy element, you will receive the following error while deploying:

"The template resource '[concat( 'some-prefix-', parameters( 'nameParamArray' 
)[copyIndex()] )]' at line 'XX' and column 'YY' is not valid: The language 
expression property array index '0' is out of bounds."

That happens because the resource on which you are using the copy element is still being evaluated by the runtime, even when the array is empty. The copyIndex() function returns 0, which is not a valid index on an empty array. A workaround to that issue is to use the following syntax on the name property in the previous example:

"name": "[if(greater(length(variables( 'storageAccountNames' )), 0),
    variables('storageAccountNames')[copyIndex()], 'emptyArray')]",

This snippet checks if the array’s length is greater than 0 using a combination of the greater() and length() functions. If that is true, the array’s value for position 0 is returned. Otherwise, a dummy value, emptyArray, is returned. As an alternative solution, you could also specify a condition on the resource to have no resources deployed at all.

In the preceding example, the copy element is used on a resource. It can also be used on the following other elements:

  • Variables

  • Properties

  • Output

Let’s address each of them in turn.

5.6.1 Using copy on variables

The copy element can be used within the variables element to create an array.

"parameters": {
    "itemCount": {
        "type": "int",
        "defaultValue": 3
    }
},
"variables": {
    "storageAccounts": {
        "copy": [
            {
                "name": "names",
                "count": "[parameters('itemCount')]",
                "input": "[concat('myStorageAccount-', copyIndex())]"
            }
        ]
    }
}

This variables element contains an object called storageAccounts. The copy element in it has a name, count, and input defined. Again, the name is the loop identifier and is used as the name for the resulting array on the storageAccounts object. The count again specifies the number of iterations. The input is used to specify the value for each element in the resulting array, which in this case, is a string that is a concatenation of a string and the loops index. The outcome of this copy element is as follows:

"storageAccounts": {
    "names": [
        "myStorageAccount-0",
        "myStorageAccount-1",
        "myStorageAccount-2"
    ]
}

The outcome is a variable with the name storageAccounts. That variable holds an array with three items, which results from three times the input on the copy element.

5.6.2 Using copy on properties

You can also use the copy element to create properties on a resource. Let’s examine the following example, in which a virtual network with its subnets is created:

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "virtualNetworkName": {
            "type": "string"
        },
        "subnets": {
            "type": "array",
            "defaultValue": [
                {
                    "name": "firstSubnet",
                    "addressPrefix": "10.0.0.1/25"
                }
            ]
        }
    },
    "resources": [
        {
            "apiVersion": "2019-09-01",
            "name": "[parameters('virtualNetworkName')]",
            "type": "Microsoft.Network/virtualNetworks",
            "location": "West Europe",
            "properties": {
                "addressSpace": {
                    "addressPrefixes": [ "10.0.0.1/24" ]
                },
                "copy": [
                    {
                        "name": "subnets",
                        "count": "[length(parameters('subnets'))]",
                        "input": {
                            "name": 
     "[parameters('subnets')[copyIndex('subnets')].name]",
                            "properties": {
                                "addressPrefix": 
     "[parameters('subnets')[copyIndex('subnets')].addressPrefix]",
                                "privateEndpointNetworkPolicies": 
                                     "Disabled"
                            }
                        }
                    }
                ]
            }
        }
    ]
}

The resource created here is a virtual network. That network is further divided into chunks of IP addresses, called subnets. You define those subnets as an array in the properties element of the virtual network. Since that is an array, you can use the copy element to fill it. Again, the copy’s name becomes the resulting array’s name, the count specifies the number of iterations, and the input specifies each item’s format in the result.

In this example, each iteration returns an object. The end result of the copy element follows:

"subnets": [
    {
        "name": "firstSubnet",
        "properties": {
            "addressPrefix": "10.0.0.1/25",
            "privateEndpointNetworkPolicies": "Disabled"
        }
    },
    ...
]

5.6.3 Using copy on output

When you create multiple instances of a resource out of an array, it is sometimes necessary to return a property value from each created instance. Maybe you’re creating a list of storage accounts and then you need to return a list of all primary BLOB endpoints. Since that could be a dynamic list, you can use the copy element in the outputs section to create a dynamic output in the form of an array:

"outputs": {
    "storageEndpoints": {
        "type": "array",
        "copy": {
            "count": "[length(parameters('storageAccountArray'))]",
            "input": "[reference(concat(copyIndex(),
                 variables('baseName'))).primaryEndpoints.blob]"
        }
    }
}

The preceding copy element loops over each created storage account. In each iteration, the reference() function is used to retrieve the resource details and return the BLOB endpoint. This results in one primary BLOB endpoint for each storage account in the list.

5.6.4 Waiting for a loop to finish, using dependsOn

As you saw earlier, you can use the dependsOn element to dictate deployment order. To wait for a loop to finish, you could use the name of the copy element in the dependsOn element of another resource, as shown here:

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {
        "storageAccountNames": [
            "datastorage",
            "logstorage"
        ]
    },
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "name": "[variables('storageAccountNames')[copyIndex()]]",
            "copy": {
                "name": "storageIterator",
                "count": "[length(variables('storageAccountNames'))]"
            },
            "sku": {
                "name": "Standard_GRS"
            },
            "kind": "StorageV2",
            "apiVersion": "2019-04-01",
            "location": "West Europe"
        },
        {
            "type": "another-resource",
            "dependsOn": [ "storageIterator" ]     
        }
    ]
}

Using dependsOn to wait for a loop to finish

The preceding example would make the deployment of the second resource wait for a copy element called StorageIterator to finish.

Not everything in Azure is managed using ARM templates. Sometimes it is necessary to use PowerShell or the Azure CLI for automating a process. One way of doing that is to include a script in your deployment process, but another way of running that script is using a deploymentScript resource in an ARM template.

5.7 Deployment scripts

Deployment scripts are a way of executing scripts as part of an ARM deployment. This allows you to interleave script execution with resource creation, which is particularly useful for automating tasks that do not relate directly to Azure resources. For example, these are some tasks that cannot be achieved with ARM templates:

  • Performing data plane operations, such as copying BLOBs or seeding databases

  • Creating a self-signed certificate

  • Creating an object in Azure AD

  • Looking up a value from a custom system

Deployment scripts allow you to run a script within your ARM template deployment. It’s a resource like all the other examples you’ve seen so far, and its type is Microsoft .Resources/deploymentScripts. Deployment scripts support Azure PowerShell and Azure CLI as scripting languages.

While deploying the templates, the deployment script launches a Linux container on Azure Container Instances, uploads your script to a storage account (1), and runs the script within the container (2, 3). Any result of the script will be pushed back and be available in your ARM template (4). Figure 5.5 illustrates this.

Figure 5.5 A visual representation of the deployment script process

The fact that your script runs on Linux is important, because it sometimes limits what you can do with PowerShell. For example, you cannot do as much with PowerShell in relation to Active Directory as you could in Windows, simply because the commands do not exist in the Linux version of PowerShell Core.

Let’s look at an example of how to use a deployment script resource. In chapter 3, you created a SQL Server and used various methods to provide the administrator password. Although the secureString type and the Key Vault reference work well, the best way to authenticate with a SQL Server is to use personal accounts stored in Active Directory instead of a shared administrator. To make that possible, you need to assign an AD identity as the administrator on the SQL Server.

Doing this involves the following steps:

  1. Assign an identity to your SQL Server. This identity is used for authenticating sign-in requests with AD.

  2. Assign Directory Reader permissions to the identity in the previous step.

  3. Make an identity administrator on SQL. This second identity is used for connecting to the SQL server. The validity of this identity can be verified using the first identity.

The first step can be done using ARM templates, but the other two cannot, since they involve reading from and writing to Active Directory. Instead, a deployment script can be used to automate the task as part of an ARM deployment.

The first two steps are out of scope for this book, but step three makes for a great example of deployment scripts, so let’s implement that step. The end goal here is to assign a user or group from Active Directory as the administrator on the SQL Server. That, at first, seems a simple task that can be done using the following ARM template.

Listing 5.8 A deployment script resource

"resources": [
    {
        "type": "Microsoft.Sql/servers/administrators",
        "name": "[concat(parameters('serverName'), '/activeDirectory')]",
        "apiVersion": "2019-06-01-preview",
        "location": "[parameters('location')]",
        "properties": {
            "administratorType": "ActiveDirectory",
            "login": "[parameters('adminLogin')]",
            "sid": "[parameters('adminObjectID')]",
            "tenantId": "[subscription().tenantId]"
        }
    }
]

This template deploys a resource of type administrators on your existing SQL Server. The administratorType in the properties object specifies that you are going to use Active Directory. The login and sid specify the user or group to use as the administrators. The hard part in this snippet is the sid. That is the object ID from your user or group in AD, which cannot be retrieved using an ARM template. To get the sid, you will have to use a scripting language like PowerShell or the Azure CLI. This makes it an ideal candidate to see how deployment scripts work. The flow is illustrated in figure 5.6.

Figure 5.6 A visual representation of interacting with Azure AD using a deployment script

In figure 5.6, step 1 is where you assign the administrator to the SQL Server. As mentioned earlier, that requires you to get a value for the sid. Step 2 in the figure defines a deployment script resource in your ARM template. That runs a PowerShell script, step 3, that reaches out to Active Directory and returns the requested sid value.

Using a deployment script means creating two things: the deploymentScripts resource in an ARM template and a script file called from the deployment script. Let’s first look at listing 5.9 for the deploymentScript resource (DeploymentScripts/ deploymentscript.json); the PowerShell script will follow shortly.

Listing 5.9 A deployment script resource

"resources": [
    {
        "type": "Microsoft.Resources/deploymentScripts",
        "apiVersion": "2020-10-01",
        "name": "GetADGroupId",
        "location": "[resourceGroup().location]",
        "kind": "AzurePowerShell",                                          
        "properties": {
            "forceUpdateTag": "[parameters('refresh')]",                    
            "azPowerShellVersion": "7.0",
            "primaryScriptUri": "path/to/GetGroupObjectId.ps1",             
            "supportingScriptUris": [],
            "environmentVariables": [                                       
                {
                    "name": "clientId",
                    "secureValue": "[parameters('clientId')]"
                },
                {
                    "name": "clientSecret",
                    "secureValue": "[parameters('clientSecret')]"
                }
            ],
            "arguments": "[concat('-groupName ''', parameters('groupName'), 
                 ''' -tenantId ', subscription().tenantId)]", 
            "timeout": "PT10M",
            "cleanupPreference": "OnSuccess",
            "retentionInterval": "P1D"
        }
    }
],
"outputs": {
    "AadGroupId": {
        "value": "[reference('GetADGroupId').outputs.groupId]",
        "type": "string"
    }
}

The kind specifies what scripting language to use.

The forceUpdateTag is used to control whether to rerun the script.

The primaryScriptUri specifies the script to run on start.

Secrets are best passed in using environment variables.

Values can be passed in using arguments.

Like any resource in an ARM template, this one starts with the required properties you have seen before. The first new property is the kind, which specifies the scripting language you are using (AzurePowerShell in this case). The azPowerShellVersion property specifies the PowerShell version to use. The other option is, as mentioned before, the Azure CLI.

The remainder of the properties are defined in the properties object. First, forceUpdateTag allows you to trigger a new run of the script. When this value is different from the value during the previous deployment, the deployment script is executed. If the value remains the same, it is not. The second property, primaryScriptUri, defines the script to run, and supportingScriptUris allows you to define helper scripts. All scripts are downloaded on deployment, but only the primaryScriptUri is executed.

There is also the option to specify a script inline instead of in a separate file or files. That might work for minimal scripts, but it’s not ideal for larger scripts. It becomes hard to read because the editor treats it as a string instead of a script.

There are two ways to provide input to the script: arguments or environment variables. Environments variables are a good fit when you’re passing along secrets because environment variables have the secureValue option. Arguments do not have such an option and are therefore exposed in logs, which is not good for secrets. The script in this example uses a clientId and clientSecret for a service principal to log in to Active Directory. Those are secrets and are thus passed along using environment variables. The other input the script needs, the Active Directory group’s name, is not a secret and is passed using an argument.

As mentioned before, the script is run within a container on Azure Container Instances. That resource is created for you when the deployment starts, but you can specify a name to use in the template. If you don’t, a name is generated, which always ends with “azscripts”. Next to the container instance, a storage account is created for you. That is used to store your scripts and logs. You can also assign an existing storage account if you want to.

Let’s now take a look at the script itself (DeploymentScripts/script.ps1). In the example in listing 5.10, the script uses the Graph API to fetch the object ID of the group you want to use as the SQL Server administrator and return that as the groupId. Most of the script details are not important here, but in short, it does the following: it grabs secrets from the environment variables, gets an authentication token from the Graph API, and queries the API to get the object ID.

Listing 5.10 A deployment script, to be called from a deployment script resource

param([string] $groupName, $tenantId)
 
$ErrorActionPreference = 'Stop'
 
$clientId = $env:clientId                                              
$clientSecret = $env:clientSecret
 
$Body = @{
    'tenant' = $tenantId
    'client_id' = $clientId
    'scope' = 'https:/ /graph.microsoft.com/.default'
    'client_secret' = $clientSecret
    'grant_type' = 'client_credentials'
}
 
$url = "https:/ /login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
 
$Params = @{
    'Uri' = $url
    'Method' = 'Post'
    'Body' = $Body
    'ContentType' = 'application/x-www-form-urlencoded'
}
$AuthResponse = Invoke-RestMethod @Params                              
 
$Headers = @{
    'Authorization' = "Bearer $($AuthResponse.access_token)"
}
$groupUrl = "https:/ /graph.microsoft.com/v1.0/groups?
     `$filter=displayname eq '$groupName'"
$groupResult = Invoke-RestMethod -Uri $groupUrl -Headers $Headers      
 
$groupId = $groupResult.value[0].id
 
$DeploymentScriptOutputs = @{}
$DeploymentScriptOutputs['groupId'] = $groupId                         
 
Write-Host "Finished!"

Get the input secrets using the environment variables.

Get an authentication token.

Invoke a request to query Active Directory for the group.

Set a return value in the $DeploymentScriptOutputs variable.

In the last two lines, you see how you can return a value from the script into the ARM environment. Returning outputs is done by assigning a value to a property in the DeploymentScriptOutputs object—in this case, by assigning a value to the groupId property. If you now look back at the deployment script example in listing 5.9, you’ll see that you can reference script values in ARM templates in the same way as referencing outputs from nested deployments. First, you grab a reference to the resource, and second, you call the outputs property. This script’s output can then be used as input to the template that deploys the administrator on the SQL Server (listing 5.8).

The script is, by default, run using the identity that runs the deployment. This means that if the script needs to perform an Azure resource action, that identity would need permissions to do that. The identity is also used to create the underlying infrastructure needed to run the script, so it needs permissions to create the storage account, container instance, and deployment script resources. You might want to separate these responsibilities and use another identity to perform these actions. The identity property on the deploymentScript resource allows you to set that, as shown in this example:

"resources": [
    {
        "type": "Microsoft.Resources/deploymentScripts",
        "apiVersion": "2020-10-01",
        "name": "GetADGroupId",
        "identity": {                   
            "type": "userAssigned",
            "userAssignedIdentities": {
                "/subscriptions/01234567-89AB-CDEF-0123-456789ABCDEF/
 resourceGroups/myResourceGroup/providers/
 Microsoft.ManagedIdentity/userAssignedIdentities/myID": {}
            }
        },
        ...
    }
]

Set an identity on a deployment script.

The preceding snippet shows how you can set a user-defined identity on the deploymentScript resource.

The GitHub repository accompanying this book contains a PowerShell script that you can use to prepare your environment to run the preceding deployment script (http://mng.bz/067E). It will create a storage account and upload the script to run within the deploymentScript. It also creates a service principal and sets the correct permissions so it can query Azure Active Directory.

You can get details on the execution of the script in multiple places. First, logs are written to the storage account connected to the deploymentScript. There is a file share on the storage account that contains the execution results and the stdout file. The Azure portal can also be used to find details on the deployment. Simply navigate to the Deployment Script resource in the portal after deployment.

The overview page in figure 5.7 displays information like the provisioning state. You can also find the connected storage account and container instance. Below that, you’ll find the logs. From the left menu, you can view the deployment script content, the arguments passed to the script, and the output.

Figure 5.7 Deployment script details

In addition to the previous two options, you could also use PowerShell or the Azure CLI to get the logs. Here’s an example using PowerShell:

Get-AzDeploymentScriptLog -Name MyDeploymentScript 
     -ResourceGroupName DS-TestRg

The preceding command would get you the execution logs from a deployment script resource named MyDeploymentScript.

You can start with an empty template file when writing new ARM templates, and the extension in VS Code will help you quite a bit. However, there are a few ways to get a kickstart.

5.8 Reverse engineering a template

There are a few tools you can use to reverse engineer a template. The benefit of such an approach is that you don’t have to start from scratch, but can instead copy a great deal of configuration from an existing resource. Let’s explore a few options in the Azure portal.

5.8.1 Exporting templates

You’ll find an option to export ARM templates for every existing resource or resource group. This option is always there, even if you did not create the resource with a template but, for example, by using the portal. The page you are looking for is found in the menu on the left when viewing any resource or resource group in the Azure portal. It is under the Automation subsection and the menu item is called Export Template. Clicking that menu item shows you a new blade with the ARM template (figure 5.8).

Figure 5.8 Exporting an ARM template

This feature provides you with a complete, deployable template. It contains the resources and may contain parameters if you left the checkbox for them enabled. Exporting is not yet supported for some resources, and when that happens, a warning is shown—you can see an example of this in figure 5.8.

The generated templates contain all the properties on a resource, even those that use default values, which means the templates are lengthy. You might need to trim them down a bit to keep them readable. Depending on the number of resources in a resource group, it might take a while to generate the template.

As you can see in figure 5.8, this page also contains a Deploy button. That feature allows you to deploy the template that’s shown directly from the Azure portal. You could use that to quickly edit the template and check the results.

5.8.2 Using Resource Explorer

Another way to find a template for an existing resource is to use the Azure Resource Explorer. You can find it in the portal by selecting All Services from the menu and then searching for Resource Explorer. It will show you a tree view with two nodes, providers, and subscriptions. Within Providers, you can find information on the definitions of all Azure resource types (figure 5.9). You could use that if you need detailed resource information, but the online documentation is probably easier to use. Within Subscriptions, you can find the ARM templates and all of your existing resources.

Figure 5.9 Using Resource Explorer

In this view, you can drill down into your resources by first clicking a specific subscription. You can then find your resources either by the provider or by the resource group that contains them. Resource Explorer won’t give you a complete template, but it will give you the details of a specific resource.

5.8.3 Using the JSON view

When you navigate to an existing resource in the Azure portal, you’ll see a JSON View button in the top-right corner, as in figure 5.10. Clicking that button will show the full JSON definition for the specific resource displayed. It does not provide a complete template like some of the other options—it only shows this resource and misses parameters, for example.

Figure 5.10 JSON View button on the overview page of a resource

5.8.4 For a new resource

The previous options are only useful on existing resources, but luckily there is also a way to generate a template for new resources using the Azure portal. When you create a new resource, the last step in that process shows you a Download a Template for Automation button. Figure 5.11 shows the final step in the Resource Creation wizard. This specific example shows a storage account, but the Download a Template for Automation button will be there for any resource type.

Figure 5.11 Generating a template from a Resource Creation wizard

The output when you click this button is similar to exporting a template, which we discussed earlier. You again are presented with a complete template, with parameters for all the variables you specified in previous steps of the Resource Creation wizard. This option comes in handy when you want a quick start on a completely new resource.

Summary

  • You can deploy to more than one deployment scope from a single ARM template by using nested deployments.

  • More extensive ARM solutions can be structured by splitting them into composing and resource templates. That allows for better readability and the reuse of the templates. Medium-sized and smaller solutions can group resources by application component.

  • Advanced constructs like dependsOn, conditions, and loops are used for building more complex templates that support use cases with repetition and optional resources.

  • Templates can be tough to write, even using the proper tools. By exporting templates, using Resource Explorer, or downloading a template from the Resource Creation wizard, you can save yourself some time by copying template content from existing resources.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.179.139