3 Writing ARM templates

This chapter covers

In chapter 2 you were introduced to a simple ARM template, and you saw how to deploy it. This chapter goes into much more depth on ARM templates, exploring various techniques and features. While chapter 2 showed how to create your first template and briefly introduced an ARM template layout, this chapter covers declaring resources, parameters, variables, functions, and output sections in depth.

To help illustrate these topics, you’ll create an ARM template over the course of the chapter. This infrastructure hosts an API, which is the backend for a web shop. It can accept, process, and store orders. The API saves all orders in a SQL database and generates a PDF file, which is sent to the customer via email. These PDF files are also stored in an Azure storage account for later use. The API itself runs on an Azure App Service and uses Key Vault as a secret store. Finally, we’ll throw Application Insights into the mix, which allows for monitoring the API. Figure 3.1 provides a visualization of this infrastructure.

Figure 3.1 A simple application architecture

While reading this chapter, you’ll create a template with these components one by one. As ARM templates can become quite lengthy, the ever-growing template will not be repeated throughout the chapter, and some examples will not be shown in their full length.

For your reference, the complete template and templates for each section are available on GitHub at https://github.com/AzureIaCBook/book-templates/tree/main/Chapter_03. The discussion of each code listing will identify the name of the file in source control, so you can easily find it. Not all code snippets in this chapter will be in the complete template. Sometimes we’ll discuss more than one way to complete a particular task, and only one of those options will be used in the final template. The complete template is named template.json, and it can be found here: https://github.com/AzureIaCBook/book-templates/blob/main/Chapter_03/template.json. When you deploy this template, or any of the others, be sure to change the names of the Azure resources. Most of them need to be globally unique.

3.1 Resources

Without a doubt, the most important part of an ARM template is the resources section. It’s an array that allows you to define one or more resources. Three elements must be declared for every resource: name, type, and apiVersion. A fourth property, location, is required on most resources. There also are a few non-required elements that you can use on every resource. Some of the more commonly used ones are properties, dependsOn, condition, tags, and copy—chapter 5 covers these in detail. Besides the properties that are valid on every resource, most resources also have additional required or optional elements, which are different for each resource type.

The required fields were briefly introduced in chapter 2, but let’s explore them in more detail now:

The following listing (03-01-00.json in the book’s Chapter_03 GitHub folder) shows how you can combine all this information to create your first resource in the infrastructure described in the chapter introduction, the SQL Server, using a template.

Listing 3.1 A template deploying a SQL Server

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
          2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "resources": [
        {
            "type": "Microsoft.Sql/servers",
            "name": "MyFirstSQLServer",
            "apiVersion": "2019-06-01-preview",
            "location": "West Europe",
            "properties": {
                "administratorLogin": "administrator",
                "administratorLoginPassword": "mysecretpassword"
            }
        }
    ]
}

In the preceding listing, you can see the four mandatory fields, type, name, apiVersion, and location. It also contains a non-required property named properties. The value of this property is of type object. In turn, this object has two properties that specify the administratorLogin and administratorLoginPassword for this server. In this example, the password was added in plain text. You would not do that in a real-world scenario, and you will learn how to address this later on.

3.1.1 Child resources

Some resources cannot exist without the context of another resource. The SQL database you’re going to deploy on the SQL Server created in the previous template is such a resource: it cannot exist outside of a SQL Server. This is called a parent-child relationship between resources. Child resources are defined in one of two ways:

Depending on how you want to structure your templates, you can choose one approach over the other. It can be useful to define a child within the parent to group the resources and collapse them as a whole. You can’t do that, however, if the parent is not defined in the same template. Beyond this limitation, which alternative you choose is a matter of personal preference. This choice will also influence how you name child resources, as is shown in the following examples. The following listing (03-01-01-child-outside-parent.json) shows an example of deploying a child resource outside of the parent.

Listing 3.2 Deploying a child outside of its parent’s scope

"resources": [
    {
        "type": "Microsoft.Sql/servers",
        "name": "MyFirstSQLServer",
        ...
    },
    {
        "type": "Microsoft.Sql/servers/databases",
        "name": "MyFirstSQLServer/SQLDatabase",
        ...
    }
]

The preceding listing shows the creation of a SQL Server and a database deployed on that server. This relationship can be seen in the type and name of the child resource, which follow this format:

"type": "{resource-provider-namespace}/{parent-resource-type}/
     {child-resource-type}",
"name": "{parent-resource-name}/{child-resource-name}"

In this example, the child’s name is a combination of its own name and the name of the server, the parent, it resides on: MyFirstSQLServer/SQLDatabase. Its type is defined as Microsoft.Sql/servers/databases.

By contrast, the next listing (03-01-01-child-inside-parent.json) shows an example of how to define a child resource within the scope of a parent using the resources array on the parent.

Listing 3.3 Deploying a child within its parent’s scope

"resources": [
    {
        "type": "Microsoft.Sql/servers",
        "name": "MyFirstSQLServer",
        ...
        "resources": [
              "type": "databases",
              "name": "SQLDatabase",
              ...
        ]
    }
]

Like listing 3.2, this listing shows how to create a SQL Server and a database deployed on that server. Since the resources property is an array, you can add more than one resource. The name and type of the child resource follow this format:

"type": "{child-resource-type}",
"name": "{child-resource-name}"

Although you get to specify databases as the child’s type, the full type is Microsoft .Sql/servers/databases. You don’t have to provide Microsoft.Sql/servers because it’s assumed from the parent resource.

You probably noticed a few hardcoded property values in the preceding examples. In a real-world deployment, you would likely want to change these values for each environment you deploy the template to. For example, you should never use the same SQL Server password on test and production. Parameters in ARM templates allow you to get those values as input while deploying the template.

3.2 Parameters

Parameters are used to make a single template applicable to more than one situation. For example, suppose you have a good template that is ready for reuse between test and production environments, but there are some differences between these environments, like resource names. To capture the differences between these environments, you can use parameters and then provide values for them when you deploy your template.

Let’s first discuss the various types of parameters and how you can use them in your templates. Then you will learn how to use them while deploying your templates.

3.2.1 Parameter types

The following example shows the simplest way to define a parameter:

"parameters": {
    "sqlServerName": {
        "type": "string"
    }
}

This snippet defines the parameter sqlServerName for the name of a SQL Server, and it has string set as its data type. You can add up to 256 parameters to a template.

When defining a parameter, you always must specify its data type. Within ARM templates, the following types are available:

The following parameters section contains an example of the first three types and introduces the defaultValue property.

"parameters": {
    "stringParameter": {
        "type": "string",
        "defaultValue": "option 1"
    },
    "intParameter": {
        "type": "int",
        "defaultValue": 1
    },
    "boolParameter": {
        "type": "bool",
        "defaultValue": true
    } 
}

This snippet shows you the string, int, and bool types, which are the most straightforward data types. They allow you to provide text, a number, or a true or false value. Also new in this example is the defaultValue property, which allows you to make a parameter optional and to specify a value to be used when no value is provided at the time of deployment. If you don’t add a default value and don’t provide a value at deploy time, you will receive an error. You will shortly see how to provide values for the parameters while deploying your template.

You can use a parameter when declaring a resource by using the parameters() function. It’s one of the many functions you’ll use while writing templates—section 3.5 of this chapter is dedicated to functions. For now, know that the parameters() function accepts the name of a parameter as input.

Let’s put all you have learned so far into a full ARM template (03-02-00.json) as shown in the following listing.

Listing 3.4 Using parameters in a template

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {                               
        "name": {
            "type": "string"
        },
        "sqlServerUsername": {
            "type": "string"
        },
        "sqlServerPassword": {
            "type": "securestring"
        }
    },
    "resources": [
        {
            "type": "Microsoft.Sql/servers",
            "name": "[parameters('name')]",       
            "apiVersion": "2019-06-01-preview",
            "location": "[resourceGroup().location]",
            "properties": {
                "administratorLogin": "[parameters('sqlServerUsername')]",
                "administratorLoginPassword": 
                     "[parameters('sqlServerPassword')]"
            }
        }
    ]
}

Parameters defined in the parameters section of a template

A parameter is used via the parameters() function.

In this example, all the hardcoded values are replaced either with a parameter or, in the case of the location, with a function call. Two parameters of type string were added: the name of the SQL Server and the username. The third parameter, of type secureString, is used for the SQL Server password. The secureString type is a specialized version of the string type. As the name hints, it is used for setting secrets like passwords or tokens. For secure data types, the Azure Resource Manager makes sure that the value is not stored and is not retrievable from tools, logs, or the Azure portal.

Within the resources section of this template, the SQL Server is defined and parameters are used to fill its properties. For the location property, the resourceGroup() function is used. Remember that a resource group also has a location defined, and this is a way to reuse that value on your resource instead of having to declare it again. You will learn more about functions in a bit.

Parameters of type array

Parameters of type array allow you to specify not one, but a list of values. A list of values can be useful when you want to create more than one resource of the same type. If you recall from figure 3.1, running the backend API involves two storage accounts: one to store application data, like the generated PDF files, and the other to store application logs. You could create an array parameter to fetch the names for those storage accounts.

"parameters": {
    "storageAccountNames": {
        "type": "array",
        "defaultValue": [
            "datastorage",
            "logstorage"
        ]
    }
}

This parameters section shows the array type parameter, and it has names for both storage accounts defined in the defaultValue property. An important thing to note here is that the type of the values in the array is not specified. In theory, you can mix different types in the array, but this is never done in practice.

You can address an array using the [] notation, which you might know from other languages or tools. You can also find an example in the following listing (03-02-01.json).

Listing 3.5 Using the array parameter type

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "storageAccountNames": {
            "type": "array",
            "defaultValue": [
                "datastorage",
                "logstorage"
            ]
        }
    },
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "name": "[parameters('storageAccountNames')[0]]",
            "sku": {
                "name": "Standard_GRS"
            },
            "kind": "StorageV2",
            "apiVersion": "2019-04-01",
            "location": "West Europe"
        }
    ]
}

This snippet defines a storage account and uses the array’s first element, [0], as the resource’s name. In real-world scenarios, arrays are often combined with loops. Chapter 5 will discuss in detail how to loop over arrays.

Parameters of type object

The object data type allows for more complex input. An array allows you to specify a list of values, but an object allows you to specify a list of key/value pairs. You can use these to pass a partial configuration as a parameter, such as for a storage account.

For the SQL database in our example, you’ll need to define more properties on it than just the mandatory properties. A SQL database has a mandatory sku. The sku’s name defines what type of compute and features you get, and the capacity property specifies the performance of the database. A parameter of type object allows the template user to specify these properties as a single value. The following snippet shows how you could do this.

"parameters": {
    "SQLDatabase": {
        "type": "object",
        "defaultValue": {
            "name": "SQLDatabase",
            "sku": {
                "name": "Basic",
                "capacity": 5
            }
        }
    }
}

This example shows how parameters of type object are declared, together with a defaultValue that shows an example value. It has a top-level property name and a top-level property sku. The sku property is another object that contains two properties: the name and capacity. Using parameters of type object allows for more complex inputs in templates, instead of having a separate parameter for each necessary value.

Addressing an item in the object is done using the dot operator, shown in bold in the following listing (03-02-02.json).

Listing 3.6 Using the object parameter type

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "SQLDatabase": {
            "type": "object",
            "defaultValue": {
                "name": "SQLDatabase",
                "sku": {
                    "name": "Basic",
                    "capacity": 5
                }
            }
        }
    },
    "resources": [
        {
            "type": "Microsoft.Sql/servers/databases",
            "apiVersion": "2017-10-01-preview",
            "name": "[concat('MyFirstSQLServer/',
                 parameters('SQLDatabase').name)]",
            "location": "West Europe",
            "sku": {
                "name": "[parameters('SQLDatabase').sku.name]",
                "capacity": "[parameters('SQLDatabase').sku.capacity]"
            },
            "properties": {
                "collation": "SQL_Latin1_General_CP1_CI_AS",
                "maxSizeBytes": 2147483648
            }
        }
    ]
}

This template shows the creation of a SQL database. It uses a string parameter for the name and the sku is an object parameter. In the name property for the database, you see the concat() function being used. Remember from section 3.1.1 on child resources that the name of a child resource, when deployed outside the scope of the parent, should include the name of the parent. In this example, that name is created by concatenating the name of the parent with that of this child. You will learn more about functions in section 3.5. The name in the sku shows how you can address a nested property in an object.

Looking closely, you can also see that the sku object’s names (in the parameters value) are identical to the ones expected by the template. Knowing this, you could also write the same template as follows.

"resources": [
    {
        "type": "Microsoft.Sql/servers/databases",
        "apiVersion": "2017-10-01-preview",
        "name": "[concat('MyFirstSQLServer/', 
             parameters('SQLDatabase').name)]",
        "location": "West Europe",
        "sku": "[parameters('SQLDatabase').sku]",
        "properties": {
            "collation": "SQL_Latin1_General_CP1_CI_AS",
            "maxSizeBytes": 2147483648
        }
    }
]

Using this approach, you get to assign a parameter of type object (or a subset of that) to an object property, making the template smaller and therefore more readable and easier to maintain.

Just as there is a secureString data type to match the string type, there is also a secureObject data type to match the object type. As with the secureString type, the value of any secureObject is hidden in any logs and output.

3.2.2 Limiting and describing parameter values

Using the allowedValues property, you can limit the values that are valid for a parameter. For example, a SQL Server resource type can specify whether it should be reachable over its public IP address. You can specify this using the publicNetworkAccess property, which only accepts two possible values. This is a perfect example of a property with a fixed set of allowed values.

When a user tries to deploy a template and specify a value that is not in the allowedValues list, they will encounter an error. When working with tools like Visual Studio Code, they will see the allowed values to help them choose a valid value while addressing the template.

The following snippet shows how to use allowedValues.

"parameters": {
    "publicNetworkAccess": {
        "type": "string",
        "allowedValues": [
            "Enabled",
            "Disabled"
        ],
        "defaultValue": "Enabled"
    }
}

This snippet shows you a string parameter that only allows two specific values as its input, as specified in the allowedValues property: Enabled or Disabled.

Another useful property for defining parameters is the description property in the metadata object.

"parameters": {
    "storageSKU": {
        "type": "string",
        "metadata": {
            "description": "The type of 
                 replication to use for the storage account."
        }
    }
}

As the name implies, the description property allows you to describe your parameter. The value for this property is used in tooling to help the user deploy the template.

3.2.3 Specifying parameter values

When you deploy your template, you’ll need to specify values for each parameter that does not have a default value. There are a few ways to do that:

The next three subsections will discuss these options.

Using inline parameters

To pass inline parameters, provide the names of the parameter while using the New-AzResourceGroupDeployment command.

Imagine you have the following template to create a storage account (03-02-04.json).

Listing 3.7 An ARM template that requires a parameter

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "storageSKU": {
            "type": "string",
            "allowedValues": [
                "Standard_LRS",
                "Standard_GRS"
            ]
        }
    },
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2021-04-01",
            "name": "MyStorageAccount",
            "kind": "StorageV2",
            "location": "West Europe",
            "sku": {
                "name": "[parameters('storageSKU')]"
            }
        }
    ]
}

This template has one parameter called storageSKU. A deployment using PowerShell that specifies this parameter is shown in the following example. (Section 4.2.2 will go deeper into different deployment methods like PowerShell, the Azure CLI, and others.)

New-AzResourceGroupDeployment -Name ExampleDeployment `
  -ResourceGroupName ExampleResourceGroup `
  -TemplateFile 03-02-04.json `
  -storageSKU "Standard_LRS"

This example uses the New-AzResourceGroupDeployment PowerShell cmdlet to deploy the template. You get to specify a name and resource group for the deployment, to identify it uniquely and to specify its destination. Using the -TemplateFile, you specify which template to deploy. For each parameter you want to pass in, you add another parameter to the command, like -storageSKU "Standard_LRS" in this example.

Specifying parameter values in a parameter file

Often you’ll need to use parameters to specify values at runtime for different environments, like test and production. But specifying all the values every time you deploy isn’t very convenient, and this is why parameter files exist. Parameter files allow you to specify and store parameters in a file, such as one for each environment.

Let’s look again at the storage account template example from the previous section. You could create the following test and production parameter files (03-02-05 .parameters.test.json and 03-02-05.parameters.prod.json) to specify the parameters for each environment.

Listing 3.8 Example parameters.test.json file

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "storageSKU": {
            "value": "Standard_LRS"
        }
    }
}

For the test environment, using Standard_LRS for the SKU is good enough. Standard locally redundant storage (Standard_LRS) is the cheapest level of redundancy in Azure, and it ensures that all storage account data is copied three times within the same Azure region. As this is for a test environment, cost is the main driver for our choices here.

A similar file, but with a different value, is created for production as shown in the following listing.

Listing 3.9 Example parameters.production.json file

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "storageSKU": {
            "value": "Standard_GRS"
        }
    }
}

In the production environment, you want to be a little more resilient, so you can choose to use Standard_GRS as its SKU. With Standard geo-redundant storage (Standard_GRS), the storage account data is copied six times: three times in one region and three times in another region.

When deploying the template, you can specify which parameter file to use. If you’re using PowerShell, the command would be as follows:

New-AzResourceGroupDeployment -Name ExampleDeployment `
  -ResourceGroupName ExampleResourceGroup `
  -TemplateFile 03-02-04.json `
  -TemplateParameterFile parameters.test.json

This example uses the New-AzResourceGroupDeployment PowerShell cmdlet to deploy the template. By using the -TemplateFile and -TemplateParameterFile parameters, you specify which template and parameter file to deploy.

You can also combine a parameter file and the inline parameters option in a single deployment. That would allow you to override a parameter specified in a parameters file. Take a look at the following example:

New-AzResourceGroupDeployment -Name ExampleDeployment `
  -ResourceGroupName ExampleResourceGroup `
  -TemplateFile 03-02-04.json `
  -TemplateParameterFile parameters.test.json `
  -storageSKU "Standard_GRS"

This example uses the same parameters file while deploying the storage account, but a value is also passed in for the storageSKU parameter. That takes precedence over the value in the parameters file, so the actual value in this example will be Standard_GRS instead of Standard_LRS as specified in the parameters file.

The next chapter will go into much more detail on deploying ARM templates and parameter files.

Fetching parameters from a Key Vault

In the first example in this chapter, where a SQL Server was created, a password for the server administrator was hardcoded. You would never do that in the real world. One way to use a parameter in that situation would be to use the secureString data type and set the value when deploying the template. In this chapter’s scenario, however, you could also use Key Vault.

Azure Key Vault is a service that securely stores and accesses certificates, connection strings, passwords, and other secrets. Within a parameter file, you can reference secrets in that vault for use during ARM template deployment. That way, you don’t have to provide secrets while deploying your templates—they are resolved from Key Vault at runtime. The following snippet shows how to do this.

"parameters": {
    "adminPassword": {
        "reference": {
            "keyVault": {
                "id": "/subscriptions/<subscription-id>/resourceGroups/
                     <rg-name>/providers/Microsoft.KeyVault/vaults/<vault-name>"
            },
            "secretName": "ExamplePassword"
        }
    }
}

Referencing a secret from Key Vault in a parameter file is done by specifying a reference property on the parameter, and not the value property you have seen before. The value for reference is another object that specifies the resource ID of the vault and the name of the secret in the vault using secretName.

Using functions in a parameter file

When referencing Key Vault from a parameter file, you cannot use functions like resourceId(). Not being able to use functions means that you must always specify the entire resource ID and cannot resolve it at runtime. That is not ideal, since it makes the template hard to reuse, and some might consider the resource ID of the Key Vault a secret itself. Specifically for a SQL Server, it would be better to use Azure Active Directory authentication, as discussed in chapter 5.

Just referencing secrets in a Key Vault and a secret in your parameter file, like the preceding one is not enough. You also need to make sure that the Azure Resource Manager can access the secret. There are two things you must set up for this to work.

First, you need to allow ARM to access the secrets while deploying. The following snippet shows how to create the Key Vault in a template that has enabledForTemplateDeployment set to true.

"resources": [
    {
      "type": "Microsoft.KeyVault/vaults",
      "apiVersion": "2019-09-01",
      "name": "[parameters('keyVaultName')]",
      "location": "[resourceGroup().location]",
      "properties": {
          "sku": {
              "family": "A",
              "name": "standard"
          },
          "tenantId": "[subscription().tenantId]",
          "enabledForTemplateDeployment": true
        }
    }
]

In this snippet, you can find the enabledForTemplateDeployment property in the Key Vault properties object. Setting it to true allows you to access this vault during an ARM template deployment.

Second, the identity that is deploying the template needs to have access to the Key Vault that is referenced. This identity is your account when you are deploying from your local computer. When you’re using another tool to deploy your template, the identity used with that tool needs these permissions. In chapter 8, for example, you will learn how to use Azure DevOps to deploy your templates. You will then also learn how to create and properly configure an identity for Azure DevOps.

Parameters allow you to get input from the outside world and make your templates more reusable, but you’ll also need a way to store repetitive values or complex expressions. Variables are the tool for this job, so let’s discuss those next.

3.3 Variables

Within ARM templates, variables define a value once and allow you to reuse it multiple times throughout the template. They are different from parameters in that parameter values can be set when starting a deployment, whereas variable values cannot be changed without editing the template. Parameters are for deploy-time input, whereas variables allow you to store values that you use multiple times within your template.

Just as with parameters, you define variables in a separate section of a template. The Azure Resource Manager resolves the variables and replaces each usage before the deployment starts. The value of a variable must match one of the data types we mentioned when discussing parameters. You don’t have to specify the data type yourself; it is automatically inferred.

The following snippet shows the definition of a variable:

"variables": {
    "storageName": "myStorageAccount"
}

This example shows a variable of type string being used as the name of a storage account.

Using a variable in your templates is like using a parameter, as shown in the following listing (03-03-00.json).

Listing 3.10 Using a variable in your template

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "variables": {
        "storageName": "myStorageAccount"
    },
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "name": "[variables('storageName')]",
            "sku": {
                "name": "Standard_GRS"
            },
            "kind": "StorageV2",
            "apiVersion": "2019-04-01",
            "location": "West Europe"
        }
    ]
}

In this example, the storageName variable’s value is used as the storage account’s name by fetching it using the variables() function.

Just as with parameters, you can handle more complex scenarios using objects and arrays. In the section on parameters, you saw how to use parameter files to set specific values for a test or production environment. You can do something similar using variables as shown in the following template.

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "environmentName": {
            "type": "string",
            "allowedValues": [
                "test",
                "prod"
            ]
        }
    },
    "variables": {
        "storageSettings": {
            "test": {
                "sku": {
                    "name": "Standard_LRS"
                }
            },
            "prod": {
                "sku": {
                    "name": "Standard_GRS"
                }
            }
        }
    }
}

In the preceding example, a parameter is defined for the name of the environment the template is currently deploying to. Then a variable is defined to hold both the test and production settings used for creating a storage account. These can now be used in the remainder of the template by using the following expression:

"[variables('storageSettings')[parameters('environmentName')].sku]"

The variable used in the preceding expression is an array of objects. The parameters() function is used to point to a specific index in that array and fetch an environment setting. Since the returned value is an object, it is possible to use dot notation to get to the sku object (or to fetch its name using sku.name if you wanted to).

Now that you know how to use parameters and variables, it’s time to see how you can return values from ARM template deployments using outputs.

3.4 Outputs

No matter what tool you use to deploy your template, you may find yourself wanting to retrieve a value from the deployment afterward. Maybe you need the name of a resource you just created or the fully qualified domain name (FQDN) to SQL Server in the next step of your deployment pipeline. The outputs section in the template is where you get to define such outputs. This section itself is of type object. The following snippet shows an example.

"outputs": {
    "sqlServerFQDN": {
        "type": "string",
        "value": "[reference('SqlServer').fullyQualifiedDomainName]"
    }
}

This example returns the FQDN for a SQL Server. The name for the returned value is specified as sqlServerFQDN and it’s of type string, as specified in the type property. Its value is determined by using the reference() function, which accepts the name of a resource as input and returns its properties. In this case, the returned value is an object, and you can use dot notation to get to the needed property.

The following listing (03-04-00.json) shows a complete template that contains the same example output.

Listing 3.11 An ARM template with an output

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "resources": [
        {
            "type": "Microsoft.Sql/servers",
            "kind": "v12.0",
            "name": "bookexampledatabase",
            "apiVersion": "2015-05-01-preview",
            "location": "West Europe",
            "properties": {
                "administratorLogin": "bookexampledatabase",
                "administratorLoginPassword": "1234@demo22",
                "version": "12.0"
            }
        }
    ],
    "outputs": {
        "sqlServerFQDN": {
            "type": "string",
            "value": 
                 "[reference('bookexampledatabase').fullyQualifiedDomainName]"
        }
    }
}

The preceding listing deploys a SQL Server, as you have seen before. What’s new here is that it also returns its FQDN as output. You can deploy this template using, for example, the Azure CLI:

az deployment group create \             
  --name ExampleDeploymentDatabase \
  --resource-group BookExampleGroup \
  --template-file SQLServer.json \ 
  --query properties.outputs.sqlServerFQDN.value

Most names used for switches in this statement, like --name or --resource-group, are slightly different in naming but identical in usage to the ones you saw in the previous PowerShell example.

The preceding command includes the --query switch, which allows you to grab one specific property from the template deployment’s output. It does that by executing what is called a JMESPath query. You can find more about that in Microsoft’s article titled “How to query Azure CLI command output using a JMESPath query”: (http://mng.bz/z4MX). In this example, the output is

"bookexampledatabase.database.windows.net"

You can always grab output from previous deployments using the az deployment group show command, as shown next.

az deployment group show \
  -n ExampleDeploymentDatabase \
  -g BookExampleGroup \
  --query properties.outputs.sqlServerFQDN.value

Executing the preceding snippet produces output similar to the first example. Notice that you can also use the short notation for switches like resource group and name, as in this example. For example, --name is the same as -n, and --resource-group is the same as -g. You can find all the available switches and their short notations in the Microsoft documentation for the Azure CLI (http://mng.bz/2nza). Which you use is often a personal preference. The shorter version might be easier to read at first, since the command is shorter, but it might be more confusing when you come back to the code after a while.

Don’t use secureString or secureObject as output

While it is technically possible to output values of type secureString or secureObject, it is not useful. The values for these types are removed from logs and deployment results, which means that the return values are always empty.

Throughout the examples so far, you have seen the use of some functions like parameters(), reference(), and resourceGroup().location. Now it is time to explore functions in more depth.

3.5 Functions

To help build templates more quickly and make them more expressive and reusable, many built-in functions are at your disposal. And even if there isn’t a built-in function for your specific scenario, you can always write one of your own. The use of functions introduces logic into your templates and they are used in expressions.

3.5.1 Expressions

By using functions, either built-in or user-defined ones, you can create expressions to extend the default JSON language in ARM templates. Expressions start with an opening square bracket, [, end with a closing square bracket, ], and can return a string, int, bool, array, or object. You’ve already seen quite a few examples in the previous code snippets, but here is another example that deploys a storage account.

"resources": [
    {
        "type": "Microsoft.Storage/storageAccounts",
        "name": "[parameters('storageName')]",         
            ...
        "location": "[resourceGroup().location]"       
    }
]

An expression using the parameters() function

An expression using the resourceGroup() function, returning an object

As with almost any other programming language, function calls are formatted as functionName(arg1,arg2,arg3). In this example, the parameters() function is used with storageName as the argument to retrieve the value for that parameter. As you can see, string values are passed in using single quotes. In the location property, you can see another expression. Here the resourceGroup() function returns an object, and you can use dot notation to get the value of any of its properties.

Sometimes you’ll find yourself wanting to assign a null value to a property based on a condition, and nothing otherwise. Assigning a null makes sure that ARM ignores the property while deploying your template. By using the json() function, you can assign null values. Consider the following example, which creates a virtual network subnet that may or may not get a routing table assigned, depending on whether it has been specified in a parameter.

Listing 3.12 Assigning null values inline

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
         2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "subnet": {
            "type": "object"
        }
    },
    "resources": [
        {
            "type": "Microsoft.Network/virtualNetworks/subnets",
            "location": "West Europe",
            "apiVersion": "2020-05-01",
            "name": "[parameters('subnet').name]",
            "properties": {
                "addressPrefix": "[parameters('subnet').addressPrefix]",
                "routeTable": 
                     "[if(contains(parameters('subnet'), 'routeTable'),
                     json(concat('{\"id\": \"',
                     resourceId('Microsoft.Network/routeTables',
                     parameters('subnet').routeTable.name), '\"}')),
                     json('null'))]"     
            }
        }
    ]
}

Assigning the null value using json('null')

The first thing to notice in the expression on the routeTable property is the contains() function invocation on an object. This shows that you can use the contains() function to check whether an object has a particular property defined or not. In this example, if the routeTable property exists, the json() function is used to create the object and assign it. If it does not exist, json('null') is used to ignore it.

Instead of using the json() function to create the object in place, you could also create a variable to hold it, and then reference that, as shown in the following example (03-05-01.json).

Listing 3.13 Assigning null values using a variable

{
    "$schema": "https:/ /schema.management.azure.com/schemas/
          2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "subnet": {
            "type": "object"
        }
    },
    "variables": {
        "routeTable": {
            "id": "[resourceId('Microsoft.Network/routeTables',
             parameters('subnet').routeTable.name)]"
        }
    },
    "resources": [
        {
            "type": "Microsoft.Network/virtualNetworks/subnets",
            "location": "West Europe",
            "apiVersion": "2020-05-01",
            "name": "[parameters('subnet').name]",
            "properties": {
                "addressPrefix": "[parameters('subnet').addressPrefix]",
                 "routeTable": "[if(contains(parameters('subnet'), 
                 'routeTable'), variables('routeTable'), json('null'))]"  
            }
        }
    ]
}

Assigning the null value using json('null')

Here you see the route table defined as a variable and used in the routeTable property in the properties object using the variables() function. The benefit of this is that your template is more readable, especially when the object you create in the json() function gets larger.

As you saw, the syntax for assigning null to an object is json('null'). You can use json('[]') when you need to do the same for an array type, or just use null on a string type.

3.5.2 Built-in functions

The list of built-in functions is quite long and is available in Microsoft’s “ARM template functions” article (http://mng.bz/pOg2). This book does not cover all of them, but we’ll show you the most frequently used functions. Functions are grouped into the following groups:

The following sections explore reference and logic functions in detail. Chapter 5 describes array functions in detail when discussing loops. The other types of functions are less concerned with writing an ARM template and more with finding values for specific properties. You can look up the workings of these functions in Microsoft’s documentation.

Scope functions

First up are scope functions. They allow you to get information on existing resources in Azure. Let’s start with an example that deploys an Azure Key Vault (03-05-02-scope.json).

Listing 3.14 Using scope functions

"resources": [
    {
      "type": "Microsoft.KeyVault/vaults",
      "apiVersion": "2019-09-01",
      "name": "[parameters('keyVaultName')]",
      "location": "[resourceGroup().location]",
      "properties": {
          "sku": {
              "family": "A",
              "name": "standard"
          },
          "accessPolicies": "[parameters('accessPolicies').list]",
          "tenantId": "[subscription().tenantId]",
          "enabledForTemplateDeployment": true
        }
    }
]

This example uses two scope functions. First, it uses the resourceGroup() function to get information on the resource group that the template is deployed into. One of the returned properties is its location. Second, you can see the use of the subscription() function. That function returns information on the subscription you are deploying to, and one of its properties is the tenantId. That is the ID of the Azure Active Directory tenant used for authenticating requests to the Key Vault.

With the Key Vault in place, it is time to store secrets in it. Storing secrets is possible using the Azure portal, but this is a book on ARM templates, so let’s practice using them instead. In the coming examples, you’ll go through the following steps:

  1. Create an Azure storage account.

  2. Store the Management Key for that storage account in Azure Key Vault.

  3. Make sure the API can read the secret from the Key Vault.

Let’s start by creating the storage account itself (03-05-02-scope.json).

Listing 3.15 Creating a storage account

"resources": [
    {
        "type": "Microsoft.Storage/storageAccounts",
        "apiVersion": "2019-04-01",
        "name": "myStorageAccount",
        "location": "South Central US",
        "sku": {
            "name": "Standard_LRS"
        },
        "kind": "StorageV2"
    }
]

Before you store the key, you need to retrieve it using the listKeys() function:

listKeys(resourceId('Microsoft.Storage/storageAccounts',
     parameters('storageAccountName')), 2019-04-01').key1

The listKeys() function accepts a reference to a resource as its first input. Here the resourceId() function is used to get that. The identifier returned has the following format:

/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}
     /providers/{resourceProviderNamespace}/{resourceType}/{resourceName}

The second input parameter to listKeys is the API version of the resource whose keys you are getting. Once the listKeys() function returns, you can use the key1 or key2 property to get one of the access keys to the storage account.

Now that you have a working snippet for fetching the storage account key, you can use the following snippet to store this key in the Key Vault (03-05-02-scope.json).

Listing 3.16 Storing a secret in a Key Vault

"resources": [
    {
        "type": "Microsoft.KeyVault/vaults/secrets",
        "apiVersion": "2018-02-14",
        "name": "[concat(parameters('keyVaultName'), '/', 
             parameters('secretName'))]",
        "location": "[parameters('location')]",
        "properties": {
            "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts',
                 parameters('storageAccountName')), '2019-04-01').key1]"
        }
    }
]

This snippet adds a secret with the name secretName, as defined in the template’s name property. For the value of the secretName parameter, the key of the storage account is retrieved using the listKeys() function and assigned to the value property in the template’s properties object.

The final step in this flow is to make sure that the API can also access the Key Vault to retrieve this secret at runtime. Your API will run on an Azure App Service. Just like the SQL database, the Azure App Service is also a child resource that always needs to run on an App Service Plan. Both are created in the following snippet (03-05-02-web-scope.json). Notice that the App Service Plan is called serverfarms in this example. That’s an old name for the same thing.

Listing 3.17 Creating an App Service

"resources": [
    {
        "apiVersion": "2015-08-01",
        "name": "[parameters('serverFarmName')]",       
        "type": "Microsoft.Web/serverfarms",
        "location": "[resourceGroup().location]",
        "sku": {
            "name": "S1",
            "capacity": 1
        },
        "properties": {
            "name": "[parameters('serverFarmName')]"
        }
    },
    {
        "apiVersion": "2018-02-01",
        "name": "[parameters('appServiceName')]",    
        "type": "Microsoft.Web/sites",
        "location": "[resourceGroup().location]",
        "dependsOn": [                                  
            "[resourceId('Microsoft.Web/serverfarms', 
                 parameters('serverFarmName'))]"
        ],
        "identity": {
            "type": "SystemAssigned"                    
        },
        "properties": {
            "serverFarmId": "[resourceId('Microsoft.Web/serverfarms',
                 providers('serverFarmName'))]"
        }
    }
]

The serverfarm is the parent resource.

The App Service Plan is the child resource, so you need the dependsOn.

The App Service gets an identity assigned.

The only new concept in the preceding listing is the assignment of the identity on the App Service. You could see this identity as a user and your application will run using that user. This identity can then be used from the application to authenticate with other resources easily and securely, like a Key Vault. More on the concept of identities can be found in Microsoft’s “How to use managed identities for App Service and Azure Functions” article (http://mng.bz/Oo2o).

The final step is to give this identity permissions on the Key Vault. That is done by deploying the following role assignment (03-05-02-web-scope.json).

Listing 3.18 Granting App Service permissions on the Key Vault

"variables": {
    "keyVaultSecretReaderRoleId": "4633458b-17de-408a-b874-0445c86b69e6" 
         // RBAC Role: Key Vault Secrets User
},
"resources: [
    {
        "type": "Microsoft.KeyVault/vaults/providers/roleAssignments",
        "apiVersion": "2018-01-01-preview",
        "name": "[concat(parameters('keyVaultName'),                         
             '/Microsoft.Authorization/',
             guid(parameters('appServiceName'),
             variables('keyVaultSecretReaderRoleId')))]",
        "dependsOn": [
            "[parameters('appServiceName')]"
        ],
        "properties": {
            "roleDefinitionId":                                              
                 "[concat('/providers/Microsoft.Authorization/roledefinitions/', 
                 variables('keyVaultSecretReaderRoleId'))]",    
            "principalId": "[reference(resourceId('Microsoft.Web/sites',     
                 parameters('appServiceName')), '2019-08-01', 
                 'full').identity.principalId]"   
        }
    }
 
]

The name for roleAssignments needs to be globally unique.

roleDefinitionId defines the role to be assigned.

principalId defines who should get the role.

As you can see, roleAssignments in the preceding listing is a child resource of the Key Vault, so the role is assigned on that scope. The role that should be applied is set using a GUID in the roleDefinitionId property. These GUIDs are predefined values that are the same in every Azure environment. A full list of them can be found in Microsoft’s “Azure built-in roles” article (http://mng.bz/YGPK). In this example, that value is stored in a variable so you can give it a descriptive name. Finally, the principalId property specifies to whom the role should be assigned. Here you see that a reference() function is used to grab the principalId from the identity on the App Service that was assigned in the previous snippet.

Another resource we promised to discuss in this chapter is Application Insights. Application Insights is a service used to monitor the API. You can use it to find the API’s performance metrics, for example, or store custom metrics and report on them. Since the API in our scenario processes orders, we’ll create a metric that holds each processed order’s value. An Application Insights instance stores its data in a service called a Log Analytics workspace. The following snippet creates both these resources (03-05-02-app-insights-scope.json).

Listing 3.19 Creating an Application Insights instance

"resources": [
    {
        "name": "[variables('logAnalyticsWorkspaceName')]",
        "type": "Microsoft.OperationalInsights/workspaces",
        "apiVersion": "2020-08-01",
        "location": "[parameters('location')]",
        "properties": {
            "sku": {
                "name": "pergb2018"
            },
            "retentionInDays": 30,
            "workspaceCapping": {
                "dailyQuotaGb": -1
            }
        }
    },
    {
        "apiVersion": "2020-02-02-preview",
        "name": "[variables('applicationInsightsName')]",
        "type": "Microsoft.Insights/components",
        "kind": "web",
        "location": "[parameters('location')]",
        "dependsOn": [
            "[variables('logAnalyticsWorkspaceName')]"
        ],
        "properties": {
            "applicationId": "[variables('applicationInsightsName')]",
            "WorkspaceResourceId": 
                  "[resourceId('Microsoft.OperationalInsights/workspaces', 
                  variables('logAnalyticsWorkspaceName'))]",
            "Application_Type": "web"
 
        }
    }
]

Here you see the creation of an Application Insights resource that uses a parameter named applicationInsightsName for the resource’s name.

To connect the API to that instance of Application Insights, you need its InstrumentationKey. One way to pass that to the API is to set an application setting on the Azure App Service that runs the API. The following listing shows how to do that (03-05-02-app-insights-scope.json).

Listing 3.20 Setting the InstrumentationKey

"resources": [
    {
        "name": "[concat(parameters('webappname'), '/', 'appsettings')]",
        "type": "Microsoft.Web/sites/config",
        "apiVersion": "2018-02-01",
        "location": "[resourceGroup().location]",
        "properties": {
            "APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(parameters(
                 'applicationInsightsName'), '2018-05-01-preview')
                 .InstrumentationKey]"
        }
    }
]

This snippet deploys a resource called appsettings within the Azure web app. Within the properties, the actual configuration—a setting called APPINSIGHTS_ INSTRUMENTATIONKEY—is added. At runtime this setting is used by the Application Insights SDK in the API to connect to the correct Application Insights instance.

It’s important here that you see that the setting gets its value by referencing the just-created Application Insights resource using the reference() function, in a way similar to the resourceId() function. In contrast with the resourceId() function, the reference() function does not specify the entire resourceId, but only the name of the resource that is being referenced. This shorter and more readable notation is only usable when the resource you are referencing is defined within the same template. In all other cases, you must use the resourceId() function. In both cases, the returned object gives you information about the Application Insights instance, and one of those properties is the InstrumentationKey.

Logical Functions

Other often-used functions come from the group of logical functions. Imagine that you deploy virtual machines for your internal customers, and some customers demand that you deploy those machines in an availability set, to ensure high availability. When you deploy two or more virtual machines in an availability set, Microsoft will ensure that they, for example, will never reboot the underlying hardware off of all machines at the same time.

However, you only need this availability set for the production resources. To reuse a single resource declaration that allows for both variations, you can use the following code (03-05-02-logical.json).

Listing 3.21 The if function

"variables": {
    "deployAvailabilitySet": "[and(parameters('availabilitySet'), 
         equals(parameters('environment'), 'production'))]",
    "availabilitySetIdentifier": {
        "id": "[resourceId('Microsoft.Compute/availabilitySets',
             parameters('availabilitySetName'))]"
    }
},
"resources": [
    {
        "name": "[parameters('vmName')]",
        "type": "Microsoft.Compute/virtualMachines",
        "location": "[resourceGroup().location]",
        "apiVersion": "2017-03-30",
        "properties": {
            "availabilitySet": "[if(variables('deployAvailabilitySet'),
                 variables(availabilitySetIdentifier), json('null'))]",
            ...
        }
    }
]

In this example, the equals() function is used inside another logical function, the and() function. The first input value to the and() function is a Boolean parameter to check if the use of availability sets is required for this customer. The second input value is the outcome of the equals() function, which checks whether you are currently deploying to your production environment.

The outcome of the and() function is saved in the deployAvailabilitySet variable. This variable is used in the template while setting the availabilitySet property of the virtual machine. Only when this parameter’s value evaluates to true is the availability set defined in the variable assigned. In all other cases, the json() function assigns null.

In more extensive templates, the if() function is often part of a condition. A condition allows you to define whether a resource should be deployed at all. You will find more on that in chapter 6.

ARM templates support most of the logic functions that anyone with a programming or scripting background would expect, including not(), and(), or(), equals(), if(), and many more.

Most of these built-in functions are usable at any of the scopes to which you can deploy templates: the tenant, management group, subscription, and resource group. You might not be familiar with these scopes, but they will be introduced in the next chapter. For now, it’s enough to know that there are different scopes in Azure, and that there is a hierarchy between them; a resource group is always defined within a subscription, a subscription within a management group, and a management group within a tenant. Although most functions can be deployed at any scope, there are exceptions, and they are well documented. As an example, take the subscription() function, which gives you information on the subscription you are deploying to. This function is not available when you deploy a template on the management group level, as there is no subscription involved. Subscriptions always reside within a management group and therefore at a lower level. Deploying to different scopes, including management groups, is discussed in chapter 4.

3.5.3 User-defined functions

Although the list of built-in functions is quite long, you may still find that you miss out on some functionality. In that case, a user-defined function can be what you need.

User-defined functions especially shine when you have complex expressions that you repeatedly reuse in a template. In the following example, you’ll see a user-defined function in the functions section for capitalizing a string (03-05-03.json).

Listing 3.22 Creating a user-defined function

"functions": [
    {
     "namespace": "demofunction",
     "members": {
        "capitalize": {
            "parameters": [
                {
                    "name": "input",
                    "type": "string"
                }
            ],
            "output": {
                "type": "string",
                "value": "[concat(toUpper(first(parameters('input'))),
                     toLower(skip(parameters('input'), 1)))]"
                }
            }
        }
    }
]

The functions section is an array where you can define more than one custom function. The first property you specify is its namespace, to make sure you don’t clash with any of the built-in functions. Then, in the members array, you define one or more functions. A function definition always starts with its name, in this case, capitalize. The parameters array is used for defining input parameters, precisely the same as for a template. One limitation is that you cannot give parameters a default value. Within a user-defined function, you can only use the parameters defined in the function, not the template’s parameters. The output section is where you define the actual logic of your function. You specify the output type and its value.

User-defined functions come with a few limitations. First, the functions cannot access variables, so you must use parameters to pass those values along. Functions cannot call other user-defined functions and cannot use the reference(), resourceId(), or list() functions. A workaround for these two limitations is to use those functions in the parameters while using the function (03-05-03.json) as shown in the following listing.

Listing 3.23 Using a user-defined function

"resources": [
    {
        "type": "Microsoft.Sql/servers",
        "name": "[demofunction.capitalize(parameters('sqlServerName'))]",   
        "apiVersion": "2019-06-01-preview",
        "location": "[parameters('location')]",
        "properties": {
            "administratorLogin": "[parameters('sqlServerUsername')]",
            "administratorLoginPassword": 
                 "[parameters('sqlServerPassword')]"
        }
    }
]

The user-defined function used to create the name of the SQL Server

In the preceding example, the user-defined function is used to create a value for the SQL Server’s name property. The function is used as demofunction.capitalize() and the parameter sqlServerName is used as input. So, the format to use a user-defined function is <namespace>.<functionsname>(<parameterinput>).

Summary