- Infrastructure as Code with ARM Templates and Bicep: Part 1 – ARM Template Fundamentals
- Infrastructure as Code with ARM Templates and Bicep: Part 2 – Building Production-Ready Infrastructure
- Infrastructure as Code with ARM Templates and Bicep: Part 3 – Advanced ARM Template Patterns
- Infrastructure as Code with ARM Templates and Bicep: Part 4 – Introduction to Bicep
- Infrastructure as Code with ARM Templates and Bicep: Part 5 – Advanced Bicep Patterns
- Infrastructure as Code with ARM Templates and Bicep: Part 6 – CI/CD Integration and Advanced Deployment
- Infrastructure as Code with ARM Templates and Bicep: Part 7 – Enterprise Governance and Compliance

As your infrastructure grows in complexity, you’ll need advanced ARM template patterns to maintain modularity, handle conditional deployments, and efficiently create multiple similar resources. This part covers nested templates, linked templates, conditional logic, and copy loops.
Welcome to Part 3 of our Infrastructure as Code series, where we explore the advanced patterns that make ARM templates truly powerful for enterprise scenarios.
What You’ll Learn
- Nested and linked template patterns
- Conditional resource deployment
- Copy loops for arrays and objects
- Template modularity and reusability
- Advanced parameter handling
Template Modularity with Linked Templates
Break down complex infrastructure into manageable, reusable modules:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"projectName": {
"type": "string"
},
"environment": {
"type": "string",
"allowedValues": ["dev", "staging", "prod"]
},
"deployDatabase": {
"type": "bool",
"defaultValue": true
},
"vmCount": {
"type": "int",
"defaultValue": 1,
"minValue": 1,
"maxValue": 5
},
"_artifactsLocation": {
"type": "string",
"defaultValue": "[deployment().properties.templateLink.uri]"
}
},
"variables": {
"templateBaseUri": "[uri(parameters('_artifactsLocation'), '.')]",
"networkTemplateUri": "[uri(variables('templateBaseUri'), 'modules/network.json')]",
"storageTemplateUri": "[uri(variables('templateBaseUri'), 'modules/storage.json')]",
"computeTemplateUri": "[uri(variables('templateBaseUri'), 'modules/compute.json')]",
"databaseTemplateUri": "[uri(variables('templateBaseUri'), 'modules/database.json')]"
},
"resources": [
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2021-04-01",
"name": "networkDeployment",
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "[variables('networkTemplateUri')]"
},
"parameters": {
"projectName": {
"value": "[parameters('projectName')]"
},
"environment": {
"value": "[parameters('environment')]"
}
}
}
},
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2021-04-01",
"name": "storageDeployment",
"dependsOn": [
"networkDeployment"
],
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "[variables('storageTemplateUri')]"
},
"parameters": {
"projectName": {
"value": "[parameters('projectName')]"
},
"environment": {
"value": "[parameters('environment')]"
},
"vnetResourceId": {
"value": "[reference('networkDeployment').outputs.vnetResourceId.value]"
}
}
}
},
{
"condition": "[parameters('deployDatabase')]",
"type": "Microsoft.Resources/deployments",
"apiVersion": "2021-04-01",
"name": "databaseDeployment",
"dependsOn": [
"networkDeployment"
],
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "[variables('databaseTemplateUri')]"
},
"parameters": {
"projectName": {
"value": "[parameters('projectName')]"
},
"environment": {
"value": "[parameters('environment')]"
},
"dataSubnetResourceId": {
"value": "[reference('networkDeployment').outputs.dataSubnetResourceId.value]"
}
}
}
}
],
"outputs": {
"deploymentSummary": {
"type": "object",
"value": {
"networkDeployed": true,
"storageDeployed": true,
"databaseDeployed": "[parameters('deployDatabase')]",
"vmCount": "[parameters('vmCount')]"
}
}
}
}
Copy Loops for Scaling Resources
Use copy loops to create multiple instances of resources efficiently:
{
"type": "Microsoft.Network/networkInterfaces",
"apiVersion": "2023-04-01",
"name": "[concat('nic-vm', copyIndex(1), '-', parameters('projectName'), '-', parameters('environment'))]",
"location": "[resourceGroup().location]",
"copy": {
"name": "nicLoop",
"count": "[parameters('vmCount')]"
},
"dependsOn": [
"[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]",
"[resourceId('Microsoft.Network/loadBalancers', variables('loadBalancerName'))]"
],
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[parameters('subnetResourceId')]"
},
"loadBalancerBackendAddressPools": "[if(greater(parameters('vmCount'), 1), createArray(createObject('id', concat(resourceId('Microsoft.Network/loadBalancers', variables('loadBalancerName')), '/backendAddressPools/BackendPool1'))), createArray())]"
}
}
],
"networkSecurityGroup": {
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]"
}
}
}
Advanced Variable Copy Patterns
Use variable copy for complex object construction:
"variables": {
"copy": [
{
"name": "virtualMachines",
"count": "[parameters('vmCount')]",
"input": {
"name": "[concat('vm', copyIndex('virtualMachines', 1))]",
"size": "[if(equals(parameters('environment'), 'prod'), 'Standard_D4s_v3', 'Standard_B2s')]",
"zone": "[string(add(mod(copyIndex('virtualMachines'), 3), 1))]",
"dataDisks": [
{
"lun": 0,
"diskSizeGB": "[if(equals(parameters('environment'), 'prod'), 1024, 128)]",
"createOption": "Empty"
}
]
}
}
]
}

Conditional Deployment Patterns
Deploy resources based on environment or feature flags:
{
"condition": "[or(equals(parameters('environment'), 'staging'), equals(parameters('environment'), 'prod'))]",
"type": "Microsoft.Network/applicationGateways",
"apiVersion": "2023-04-01",
"name": "[concat('agw-', parameters('projectName'), '-', parameters('environment'))]",
"location": "[parameters('location')]",
"properties": {
"sku": {
"name": "[if(equals(parameters('environment'), 'prod'), 'WAF_v2', 'Standard_v2')]",
"tier": "[if(equals(parameters('environment'), 'prod'), 'WAF_v2', 'Standard_v2')]",
"capacity": "[if(equals(parameters('environment'), 'prod'), 3, 1)]"
},
"webApplicationFirewallConfiguration": "[if(equals(parameters('environment'), 'prod'), createObject('enabled', true, 'firewallMode', 'Prevention', 'ruleSetType', 'OWASP', 'ruleSetVersion', '3.2'), null())]"
}
}
Database Module with Environment Logic
Create environment-specific database configurations:
{
"variables": {
"environmentConfig": {
"dev": {
"sqlServerSku": "Basic",
"databaseSku": {
"name": "S0",
"tier": "Standard"
},
"backupRetentionDays": 7,
"geoRedundantBackup": "Disabled"
},
"staging": {
"sqlServerSku": "Standard",
"databaseSku": {
"name": "S1",
"tier": "Standard"
},
"backupRetentionDays": 14,
"geoRedundantBackup": "Enabled"
},
"prod": {
"sqlServerSku": "Premium",
"databaseSku": {
"name": "P1",
"tier": "Premium"
},
"backupRetentionDays": 35,
"geoRedundantBackup": "Enabled"
}
},
"currentEnvironmentConfig": "[variables('environmentConfig')[parameters('environment')]]"
},
"resources": [
{
"type": "Microsoft.Sql/servers/databases",
"apiVersion": "2023-05-01-preview",
"name": "[concat(variables('sqlServerName'), '/', parameters('databases')[copyIndex()].name)]",
"location": "[parameters('location')]",
"copy": {
"name": "databaseLoop",
"count": "[length(parameters('databases'))]"
},
"dependsOn": [
"[resourceId('Microsoft.Sql/servers', variables('sqlServerName'))]"
],
"sku": "[variables('currentEnvironmentConfig').databaseSku]",
"properties": {
"collation": "[parameters('databases')[copyIndex()].collation]",
"maxSizeBytes": "[parameters('databases')[copyIndex()].maxSizeBytes]",
"catalogCollation": "SQL_Latin1_General_CP1_CI_AS",
"zoneRedundant": "[if(equals(parameters('environment'), 'prod'), true, false)]",
"readScale": "[if(equals(parameters('environment'), 'prod'), 'Enabled', 'Disabled')]",
"requestedBackupStorageRedundancy": "[variables('currentEnvironmentConfig').geoRedundantBackup]"
}
}
]
}
Nested Copy Loops
Implement complex scenarios with nested copy operations:
{
"type": "Microsoft.Compute/virtualMachines",
"apiVersion": "2023-03-01",
"name": "[variables('virtualMachines')[copyIndex()].name]",
"copy": {
"name": "vmLoop",
"count": "[length(variables('virtualMachines'))]"
},
"dependsOn": [
"nicLoop",
"[resourceId('Microsoft.Compute/availabilitySets', variables('availabilitySetName'))]"
],
"properties": {
"availabilitySet": "[if(greater(parameters('vmCount'), 1), createObject('id', resourceId('Microsoft.Compute/availabilitySets', variables('availabilitySetName'))), null())]",
"hardwareProfile": {
"vmSize": "[variables('virtualMachines')[copyIndex()].size]"
},
"storageProfile": {
"copy": [
{
"name": "dataDisks",
"count": "[length(variables('virtualMachines')[copyIndex()].dataDisks)]",
"input": {
"lun": "[variables('virtualMachines')[copyIndex()].dataDisks[copyIndex('dataDisks')].lun]",
"diskSizeGB": "[variables('virtualMachines')[copyIndex()].dataDisks[copyIndex('dataDisks')].diskSizeGB]",
"createOption": "[variables('virtualMachines')[copyIndex()].dataDisks[copyIndex('dataDisks')].createOption]",
"managedDisk": {
"storageAccountType": "Premium_LRS"
}
}
}
]
}
}
}
Template Deployment Script
Automate complex deployments with bash scripting:
#!/bin/bash
# Set variables
RESOURCE_GROUP="rg-webapp-${ENVIRONMENT}"
TEMPLATE_URI="https://raw.githubusercontent.com/your-repo/arm-templates/main/main-template.json"
PARAMETER_FILE="${ENVIRONMENT}.parameters.json"
# Create resource group if it doesn't exist
az group create --name $RESOURCE_GROUP --location $LOCATION
# Validate template
echo "Validating template..."
az deployment group validate \
--resource-group $RESOURCE_GROUP \
--template-uri $TEMPLATE_URI \
--parameters @$PARAMETER_FILE
# Deploy template
echo "Deploying template..."
az deployment group create \
--resource-group $RESOURCE_GROUP \
--template-uri $TEMPLATE_URI \
--parameters @$PARAMETER_FILE \
--name "deployment-$(date +%Y%m%d-%H%M%S)"
# Get outputs
echo "Deployment outputs:"
az deployment group show \
--resource-group $RESOURCE_GROUP \
--name "deployment-$(date +%Y%m%d-%H%M%S)" \
--query properties.outputs
Best Practices for Advanced Templates
- Modular Design: Break complex templates into smaller, focused modules
- Conditional Deployment: Use conditions to deploy resources based on environment or feature flags
- Copy Loops: Leverage copy loops for creating multiple similar resources efficiently
- Variable Calculations: Use variables with copy loops for complex object construction
- Environment Configuration: Create environment-specific configurations using variables
- Output Management: Provide comprehensive outputs for integration with other systems
Testing Complex Templates
# Test with different parameter combinations
for env in dev staging prod; do
echo "Testing $env environment..."
az deployment group validate \
--resource-group "rg-test-$env" \
--template-file main-template.json \
--parameters @$env.parameters.json
done
# Test with different VM counts
for count in 1 3 5; do
echo "Testing with $count VMs..."
az deployment group validate \
--resource-group "rg-test-scale" \
--template-file main-template.json \
--parameters @dev.parameters.json \
--parameters vmCount=$count
done
What’s Next?
In Part 4, we’ll transition from ARM templates to Bicep, exploring how Bicep simplifies the authoring experience while maintaining all the power of ARM templates. We’ll convert our complex template to Bicep and explore Bicep-specific features.
We’ll discover:
- Bicep syntax and structure
- Converting ARM templates to Bicep
- Bicep-specific features and improvements
- Tooling and development experience
- Migration strategies
This is Part 3 of our 7-part series on Infrastructure as Code with ARM Templates and Bicep. We’re mastering advanced patterns that enable enterprise-scale deployments.