Infrastructure as Code with ARM Templates and Bicep: Part 3 – Advanced ARM Template Patterns

Infrastructure as Code with ARM Templates and Bicep: Part 3 – Advanced ARM Template Patterns

This entry is part 3 of 7 in the series Infrastructure as Code templates using ARM and Bicep

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

  1. Modular Design: Break complex templates into smaller, focused modules
  2. Conditional Deployment: Use conditions to deploy resources based on environment or feature flags
  3. Copy Loops: Leverage copy loops for creating multiple similar resources efficiently
  4. Variable Calculations: Use variables with copy loops for complex object construction
  5. Environment Configuration: Create environment-specific configurations using variables
  6. 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.

Navigate<< Infrastructure as Code with ARM Templates and Bicep: Part 2 – Building Production-Ready InfrastructureInfrastructure as Code with ARM Templates and Bicep: Part 4 – Introduction to Bicep >>

Written by:

265 Posts

View All Posts
Follow Me :
How to whitelist website on AdBlocker?

How to whitelist website on AdBlocker?

  1. 1 Click on the AdBlock Plus icon on the top right corner of your browser
  2. 2 Click on "Enabled on this site" from the AdBlock Plus option
  3. 3 Refresh the page and start browsing the site