5 min read
Azure Resource Manager Templates Best Practices
Azure Resource Manager (ARM) templates provide a declarative way to define and deploy Azure infrastructure. Following best practices ensures your templates are maintainable, reusable, and secure.
Template Structure
Basic Template
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"description": "Deploys a web application with SQL database",
"author": "Michael John Pena",
"version": "1.0.0"
},
"parameters": {},
"variables": {},
"functions": [],
"resources": [],
"outputs": {}
}
Parameters Best Practices
{
"parameters": {
"environment": {
"type": "string",
"allowedValues": ["dev", "staging", "prod"],
"metadata": {
"description": "Environment name for resource naming"
}
},
"location": {
"type": "string",
"defaultValue": "[resourceGroup().location]",
"metadata": {
"description": "Location for all resources"
}
},
"appServicePlanSku": {
"type": "object",
"defaultValue": {
"name": "S1",
"tier": "Standard",
"capacity": 1
},
"metadata": {
"description": "SKU configuration for App Service Plan"
}
},
"sqlAdminPassword": {
"type": "securestring",
"metadata": {
"description": "SQL Server admin password"
}
},
"tags": {
"type": "object",
"defaultValue": {
"environment": "[parameters('environment')]",
"managedBy": "ARM Template"
}
}
}
}
Variables for Naming Conventions
{
"variables": {
"prefix": "[concat('app', parameters('environment'))]",
"appServicePlanName": "[concat(variables('prefix'), '-plan')]",
"webAppName": "[concat(variables('prefix'), '-web-', uniqueString(resourceGroup().id))]",
"sqlServerName": "[concat(variables('prefix'), '-sql-', uniqueString(resourceGroup().id))]",
"sqlDatabaseName": "[concat(variables('prefix'), '-db')]",
"storageAccountName": "[concat(replace(variables('prefix'), '-', ''), uniqueString(resourceGroup().id))]",
"appInsightsName": "[concat(variables('prefix'), '-insights')]",
"keyVaultName": "[concat(variables('prefix'), '-kv-', uniqueString(resourceGroup().id))]"
}
}
Resource Dependencies
{
"resources": [
{
"type": "Microsoft.Web/serverfarms",
"apiVersion": "2021-02-01",
"name": "[variables('appServicePlanName')]",
"location": "[parameters('location')]",
"tags": "[parameters('tags')]",
"sku": "[parameters('appServicePlanSku')]",
"properties": {}
},
{
"type": "Microsoft.Web/sites",
"apiVersion": "2021-02-01",
"name": "[variables('webAppName')]",
"location": "[parameters('location')]",
"tags": "[parameters('tags')]",
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]",
"[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]"
],
"identity": {
"type": "SystemAssigned"
},
"properties": {
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]",
"siteConfig": {
"appSettings": [
{
"name": "APPINSIGHTS_INSTRUMENTATIONKEY",
"value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName'))).InstrumentationKey]"
},
{
"name": "APPLICATIONINSIGHTS_CONNECTION_STRING",
"value": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName'))).ConnectionString]"
},
{
"name": "KeyVaultUri",
"value": "[reference(resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))).vaultUri]"
}
],
"connectionStrings": [
{
"name": "DefaultConnection",
"connectionString": "[concat('Server=tcp:', reference(resourceId('Microsoft.Sql/servers', variables('sqlServerName'))).fullyQualifiedDomainName, ',1433;Database=', variables('sqlDatabaseName'), ';')]",
"type": "SQLAzure"
}
]
}
}
}
]
}
Linked Templates for Modularity
Main Template
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"templateBaseUrl": {
"type": "string",
"defaultValue": "https://raw.githubusercontent.com/myorg/templates/main/"
},
"environment": {
"type": "string"
}
},
"resources": [
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2021-04-01",
"name": "networkDeployment",
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "[concat(parameters('templateBaseUrl'), 'modules/network.json')]",
"contentVersion": "1.0.0.0"
},
"parameters": {
"environment": {"value": "[parameters('environment')]"}
}
}
},
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2021-04-01",
"name": "storageDeployment",
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "[concat(parameters('templateBaseUrl'), 'modules/storage.json')]"
},
"parameters": {
"environment": {"value": "[parameters('environment')]"},
"subnetId": {"value": "[reference('networkDeployment').outputs.storageSubnetId.value]"}
}
},
"dependsOn": ["networkDeployment"]
},
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2021-04-01",
"name": "webAppDeployment",
"properties": {
"mode": "Incremental",
"templateLink": {
"uri": "[concat(parameters('templateBaseUrl'), 'modules/webapp.json')]"
},
"parameters": {
"environment": {"value": "[parameters('environment')]"},
"subnetId": {"value": "[reference('networkDeployment').outputs.webSubnetId.value]"},
"storageAccountName": {"value": "[reference('storageDeployment').outputs.storageAccountName.value]"}
}
},
"dependsOn": ["networkDeployment", "storageDeployment"]
}
],
"outputs": {
"webAppUrl": {
"type": "string",
"value": "[reference('webAppDeployment').outputs.url.value]"
}
}
}
Conditional Deployments
{
"parameters": {
"deployDiagnostics": {
"type": "bool",
"defaultValue": true
},
"environment": {
"type": "string"
}
},
"variables": {
"isProduction": "[equals(parameters('environment'), 'prod')]"
},
"resources": [
{
"condition": "[parameters('deployDiagnostics')]",
"type": "Microsoft.Insights/diagnosticSettings",
"apiVersion": "2021-05-01-preview",
"name": "diagnostics",
"scope": "[resourceId('Microsoft.Web/sites', variables('webAppName'))]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', variables('webAppName'))]"
],
"properties": {
"workspaceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsName'))]",
"logs": [
{
"category": "AppServiceHTTPLogs",
"enabled": true
}
]
}
},
{
"condition": "[variables('isProduction')]",
"type": "Microsoft.Web/sites/slots",
"apiVersion": "2021-02-01",
"name": "[concat(variables('webAppName'), '/staging')]",
"location": "[parameters('location')]",
"dependsOn": [
"[resourceId('Microsoft.Web/sites', variables('webAppName'))]"
],
"properties": {
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
}
}
]
}
Copy Loops
{
"parameters": {
"storageAccounts": {
"type": "array",
"defaultValue": [
{"name": "data", "sku": "Standard_LRS"},
{"name": "logs", "sku": "Standard_GRS"},
{"name": "backup", "sku": "Standard_RAGRS"}
]
}
},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2021-06-01",
"name": "[concat(variables('prefix'), parameters('storageAccounts')[copyIndex()].name, uniqueString(resourceGroup().id))]",
"location": "[parameters('location')]",
"tags": "[parameters('tags')]",
"copy": {
"name": "storageCopy",
"count": "[length(parameters('storageAccounts'))]",
"mode": "Parallel"
},
"sku": {
"name": "[parameters('storageAccounts')[copyIndex()].sku]"
},
"kind": "StorageV2",
"properties": {
"minimumTlsVersion": "TLS1_2",
"supportsHttpsTrafficOnly": true,
"allowBlobPublicAccess": false
}
}
]
}
User-Defined Functions
{
"functions": [
{
"namespace": "naming",
"members": {
"resourceName": {
"parameters": [
{"name": "prefix", "type": "string"},
{"name": "resourceType", "type": "string"},
{"name": "environment", "type": "string"}
],
"output": {
"type": "string",
"value": "[toLower(concat(parameters('prefix'), '-', parameters('resourceType'), '-', parameters('environment')))]"
}
},
"uniqueName": {
"parameters": [
{"name": "prefix", "type": "string"},
{"name": "resourceType", "type": "string"}
],
"output": {
"type": "string",
"value": "[toLower(concat(parameters('prefix'), parameters('resourceType'), uniqueString(resourceGroup().id)))]"
}
}
}
}
],
"variables": {
"webAppName": "[naming.resourceName('app', 'web', parameters('environment'))]",
"storageAccountName": "[naming.uniqueName('st', 'app')]"
}
}
Outputs for Downstream Processes
{
"outputs": {
"webAppName": {
"type": "string",
"value": "[variables('webAppName')]"
},
"webAppUrl": {
"type": "string",
"value": "[concat('https://', reference(resourceId('Microsoft.Web/sites', variables('webAppName'))).defaultHostName)]"
},
"webAppIdentityPrincipalId": {
"type": "string",
"value": "[reference(resourceId('Microsoft.Web/sites', variables('webAppName')), '2021-02-01', 'Full').identity.principalId]"
},
"resourceIds": {
"type": "object",
"value": {
"webApp": "[resourceId('Microsoft.Web/sites', variables('webAppName'))]",
"appServicePlan": "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]",
"sqlServer": "[resourceId('Microsoft.Sql/servers', variables('sqlServerName'))]"
}
}
}
}
Deployment Scripts
{
"resources": [
{
"type": "Microsoft.Resources/deploymentScripts",
"apiVersion": "2020-10-01",
"name": "configureApp",
"location": "[parameters('location')]",
"kind": "AzurePowerShell",
"identity": {
"type": "UserAssigned",
"userAssignedIdentities": {
"[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('identityName'))]": {}
}
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', variables('webAppName'))]"
],
"properties": {
"azPowerShellVersion": "6.4",
"timeout": "PT30M",
"arguments": "[format('-WebAppName {0} -ResourceGroup {1}', variables('webAppName'), resourceGroup().name)]",
"scriptContent": "
param([string]$WebAppName, [string]$ResourceGroup)
# Configure web app settings
$settings = @{
'WEBSITE_RUN_FROM_PACKAGE' = '1'
'WEBSITE_ENABLE_SYNC_UPDATE_SITE' = 'true'
}
Set-AzWebApp -Name $WebAppName -ResourceGroupName $ResourceGroup -AppSettings $settings
$output = @{
'configured' = $true
'timestamp' = (Get-Date).ToString('o')
}
$DeploymentScriptOutputs = $output
",
"cleanupPreference": "OnSuccess",
"retentionInterval": "P1D"
}
}
]
}
Best Practices Summary
- Use consistent naming with variables and functions
- Externalize parameters for environment-specific values
- Use secure parameters for sensitive data
- Modularize with linked templates for reusability
- Always specify API versions explicitly
- Add metadata and comments for documentation
- Use conditions for optional resources
- Validate templates before deployment
- Use What-If to preview changes
- Consider migrating to Bicep for improved authoring experience
Conclusion
ARM templates remain a powerful tool for Infrastructure as Code on Azure. By following these best practices, you can create maintainable, reusable templates that scale with your organization’s needs.
For new projects, consider using Bicep, which compiles to ARM templates but offers a cleaner syntax and better tooling support.