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.
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.
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:
name
—Every Azure resource must have a name, and the naming rules are different for every type of resource. The name for a storage account, for example, is limited in length (3–24 characters) and can only include lowercase letters and numbers. The name for a SQL Server is permitted to be 63 characters long and may contain hyphens in addition to letters and numbers. A full list of all resources and their naming limitations is available in Microsoft’s “Naming rules and restrictions for Azure resources” article (http://mng.bz/GEWR). For child resources, the format of the name is different.
type
—This property specifies the type of resource you want to create. The type is a combination of the resource provider’s namespace and the resource type. An example of that is Microsoft.Sql/servers
, in which Microsoft.Sql
is the namespace and servers
the resource type. For child resources, the type format is a little different—more on that in a bit.
apiVersion
—This property specifies which version of the resource type you want to use. It comes in the format of a date, such as 2019-06-01
. The version sometimes has a postfix to let you know that the version is in preview and might change. An example of that would be 2019-06-01-preview
. Because this property is required, Microsoft can change a resource type’s properties without breaking your existing ARM templates. When you use the Visual Studio Code extension and have specified a recent schema version, the editor shows you the available versions.
location
—Using this property, you can dictate in which Azure region your resource should be deployed. Examples are West Europe
or East US
. You would usually place resources that interact with each other in the same region, as that gives the best performance and lowest costs. In chapter 2 you learned that resources are often deployed into a resource group. When you create a group, you also assign a location. Although the group you deploy a resource in will already have a location defined, you still need to specify that on the resource as well. It can be the same location as on the resource group, but it may be another one.
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.
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.
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.
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
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.
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.
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.
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.
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
.
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.
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.
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.
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.
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.
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.
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.
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:
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
.
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).
"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.
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>)
.
Resources describe what should be deployed in Azure. They have mandatory and (often) optional properties and can either be a child or parent resource.
Parameters in ARM templates allow for user input during deployment and thereby make a template more generic and reusable.
Variables in ARM templates help you repeat expressions throughout a template, making a template more readable and maintainable.
During deployment, template parameter values are provided inline, using parameter files, or they can be referenced from a Key Vault.
Outputs are used to return values to the user deploying the ARM template so you can chain other tools after the template deployment.
Functions in templates are used to, for example, reference resources and retrieve property values, perform logical operations, and make decisions based on input parameters.
When needed, a custom function can be written to augment the built-in functionality.