Let’s imagine that you implement your infrastructure as code with ARM templates and you need to provision Private Virtual Network (further vnet) for different environments and you use different subnet configuration for each of them. Let’s see how we can solve this.
Define our infrastructure
Before we start, let’s define our infrastructure. We support 2 environments, dev
and prod
with one vnet
configured as follow:
Specifications for dev
VNet name: iac-dev-copy-poc-vnet
addressPrefix: 10.112.0.0/16
Subnets:
name: aks-net
addressPrefix: 10.112.0.0/20
name: agw-net
addressPrefix: 10.112.16.0/25
Specifications for prod
VNet name: iac-prod-copy-poc-vnet
addressPrefix: 10.111.0.0/16
Subnets:
name: aks-net
addressPrefix: 10.111.0.0/20
name: agw-net
addressPrefix: 10.111.16.0/25
name: AzureBastionSubnet
addressPrefix: 10.111.16.128/27
As you can see, in production there is one extra subnet to deploy Azure Bastion.
Create 2 resource groups
As always, we start by creating new resource group. Since we have 2 environments, we need to create 2 resource groups:
One for dev
az group create -n iac-dev-copy-poc-rg -l westeurope
and one for prod
az group create -n iac-prod-copy-poc-rg -l westeurope
First iteration
How can we implement ARM templates to support our requirements? One obvious solution will be to implement 2 ARM templates, one for each environment. We will use -dev
and -prod
suffixes for template files.
Create template file for dev
Let’s create template-dev.json
file with ARM template for vnet resource.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"resources": [
{
"name": "iac-dev-copy-poc-vnet",
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2019-11-01",
"location": "[resourceGroup().location]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"10.112.0.0/16"
]
},
"subnets": [
{
"name": "aks-net",
"properties": {
"addressPrefix": "10.112.0.0/20"
}
},
{
"name": "agw-net",
"properties": {
"addressPrefix": "10.112.16.0/25"
}
}
]
}
}
]
}
Create template file for prod
Let’s create template-prod.json
file with ARM template for vnet resource.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"resources": [
{
"name": "iac-prod-copy-poc-vnet",
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2019-11-01",
"location": "[resourceGroup().location]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"10.111.0.0/16"
]
},
"subnets": [
{
"name": "aks-net",
"properties": {
"addressPrefix": "10.111.0.0/20"
}
},
{
"name": "agw-net",
"properties": {
"addressPrefix": "10.111.16.0/25"
}
},
{
"name": "AzureBastionSubnet",
"properties": {
"addressPrefix": "10.111.16.128/25"
}
}
]
}
}
]
}
Our deployment script might look like this:
#!/usr/bin/env bash
# Usage:
# ./deploy.sh dev
# ./deploy.sh prod
environment=$1
az deployment group create -g iac-${environment}-copy-poc-rg --template-file template-${environment}.json
Let’s deploy our infrastructure to dev
./deploy.sh dev
and to prod
./deploy.sh prod
This is good and will work fine if we only have one resource in our template file, but what if our ARM template contains not only vnet resource, but some other resources? With “one template per environment” approach, we duplicate all recourses in each template file and that will be a maintenance nightmare.
What we actually want is, one ARM template and set of parameter files for each environment. So, how can we implement such a model?
As it turned out, we can do it if we use 2 ARM templates features:
According to the documentation
By adding the copy element to the properties section of a resource in your template, you can dynamically set the number of items for a property during deployment. You also avoid having to repeat template syntax.
Iteration two - refactoring
So, the idea is that we introduce subnets configuration as an array of objects, move it to the parameters file and use copy
element of ARM template to implement vnet resource.
Let’s also define some conventions:
- Template file will be called
template.json
- Parameters files will be called
parameters-dev.json
andparameters-prod.json
with the following parameters:
Parameter name | dev | prod |
---|---|---|
environment | dev | prod |
location | westeurope | westeurope |
vnetAddressPrefix | 10.112.0.0/16 | 10.111.0.0/16 |
subnetsConfiguration | array of objects | array of objects |
vnet
name will be composed in the template based onenvironment
parameter by using concat() function.
Let’s do this!
Add parameters to the template
At the template file, define parameters at the parameters section
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environment": {
"type": "string"
},
"location": {
"type": "string"
},
"vnetAddressPrefix": {
"type": "string"
},
"subnetsConfiguration": {
"type": "array"
}
},
"functions": [
],
"variables": {
},
"resources": [
...
]
}
Create parameters file for dev environment
Create new file called parameters-dev.json
and add parameters values for dev environment.
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environment": {
"value": "dev"
},
"location": {
"value": "westeurope"
},
"vnetAddressPrefix": {
"value": "10.112.0.0/16"
},
"subnetsConfiguration": {
"value": [
{
"name": "aks-net",
"addressPrefix": "10.112.0.0/20"
},
{
"name": "agw-net",
"addressPrefix": "10.112.16.0/25"
}
]
}
}
}
As you can see here, the subnetsConfiguration
parameter is the array of objects with 2 fields: subnet name
and subnet addressPrefix
.
Create parameters file for prod environment
Create new file called parameters-prod.json
and add parameters values for prod environment.
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environment": {
"value": "prod"
},
"location": {
"value": "westeurope"
},
"vnetAddressPrefix": {
"value": "10.111.0.0/16"
},
"subnetsConfiguration": {
"value": [
{
"name": "aks-net",
"addressPrefix": "10.111.0.0/20"
},
{
"name": "agw-net",
"addressPrefix": "10.111.16.0/25"
},
{
"name": "AzureBastionSubnet",
"addressPrefix": "10.111.16.128/25"
}
]
}
}
}
Refactor template file
Here is the refactored version of our template.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environment": {
"type": "string"
},
"location": {
"type": "string"
},
"vnetAddressPrefix": {
"type": "string"
},
"subnetsConfiguration": {
"type": "array"
}
},
"variables": {
"vnetName": "[concat('iac-', parameters('environment'), '-copy-poc-vnet')]"
},
"resources": [
{
"name": "[variables('vnetName')]",
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2019-11-01",
"location": "[parameters('location')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"[parameters('vnetAddressPrefix')]"
]
},
"copy": [
{
"name": "subnets",
"count": "[length(parameters('subnetsConfiguration'))]",
"input": {
"name": "[parameters('subnetsConfiguration')[copyIndex('subnets')].name]",
"properties": {
"addressPrefix": "[parameters('subnetsConfiguration')[copyIndex('subnets')].addressPrefix]"
}
}
}
]
}
}
]
}
Some comments about changes we did in the template file:
- We added variable
vnetName
and usedconcat
function to compose the vnet name based on environment - We replaced resource
location
with value fromlocation
parameter - We replaced hard coded value of
addressPrefixes
property with value fromvnetAddressPrefix
parameter - Finally, we used
copy
element of ARM template and implementedsubnets
property ofMicrosoft.Network/virtualNetworks
resource from thesubnetsConfiguration
array
In the context of our use-case, you can think of copy
element as a foreach
loop, where we iterate through each item of subnetsConfiguration
array and create new array under Microsoft.Network/virtualNetworks
resource properties called subnets
where each subnet item has the following structure
{
"name": "<>",
"properties": {
"addressPrefix": "<>"
}
}
copyIndex() function returns the current index in the subnet
array.
It’s not that easy to work with copy
element, especially it’s hard to debug it, but there is one trick you can use if you stack with copy
element. That is - you can use an output
section of ARM template to print the result of copy
element execution.
Let’s create an empty ARM template file called debug.json
with only one subnetsConfiguration
parameter and implement the same copy
element logic inside the output
section.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"subnetsConfiguration": {
"type": "array",
"defaultValue": [
{
"name": "aks-net",
"addressPrefix": "10.112.0.0/20"
},
{
"name": "agw-net",
"addressPrefix": "10.112.16.0/25"
}
]
}
},
"variables": {
},
"resources": [
],
"outputs": {
"copy-function-result": {
"type": "array",
"copy": {
"count": "[length(parameters('subnetsConfiguration'))]",
"input": {
"name": "[parameters('subnetsConfiguration')[copyIndex()].name]",
"properties": {
"addressPrefix": "[parameters('subnetsConfiguration')[copyIndex()].addressPrefix]"
}
}
}
}
}
}
Now let’s deploy it
az deployment group create -g iac-dev-copy-poc-rg --template-file debug.json
and check the result’s output section
{- Finished ..
...
"properties": {
...
"outputs": {
"copy-function-result": {
"type": "Array",
"value": [
{
"name": "aks-net",
"properties": {
"addressPrefix": "10.112.0.0/20"
}
},
{
"name": "agw-net",
"properties": {
"addressPrefix": "10.112.16.0/25"
}
}
]
}
},
...
"resourceGroup": "iac-dev-copy-poc-rg",
"type": "Microsoft.Resources/deployments"
}
It contains the copy
element’s execution result. This way you can “debug” it while you implementing your ARM templates.
Now, we have one template.json
file and 2 parameters files for each environments and we need to change our deploy.sh
file.
#!/usr/bin/env bash
# Usage:
# ./deploy.sh dev
# ./deploy.sh prod
environment=$1
az deployment group create -g iac-${environment}-copy-poc-rg --template-file template.json --parameters parameters-${environment}.json
Let’s re-deploy our infrastructure to dev
./deploy.sh dev
and to production
./deploy.sh prod
Summary
copy
element is quite powerful tool under the ARM templates tool-belt. It’s not very intuitive in the begging, but eventually you will find yourself comfortable and easy using it.
It can be used at the following scenarios:
- deploying multiple instances of the same resource
- creating multiple properties on single resource instance
- working with array parameters and variables
- returning array at the output section
I used the same technique to implement other type of resources like:
-
Network Security Group - here you can extract
securityRules
to the parameters files -
APIM custom domains - you can implement environment specific custom domains with certificates via
hostnameConfigurations
section
Clean up
Don’t forget to clean up your resources when you are done with the exercise.
Remove iac-dev-copy-poc-rg
resource group with all resources.
az group delete -n iac-dev-copy-poc-rg
Remove iac-prod-copy-poc-rg
resource group with all resources.
az group delete -n iac-prod-copy-poc-rg
If you have any issues/comments/suggestions related to this post, you can reach out to me at evgeny.borzenin@gmail.com.
With that - thanks for reading!