Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool

Azure Resource Templates: JSON Best Practices

Azure Resource Manager (ARM) templates are a powerful way to define and deploy your infrastructure as code (IaC) on Azure. Written in JSON, they provide a declarative syntax to describe the resources you want to deploy. While it's easy to start writing templates, adopting best practices ensures your templates are readable, maintainable, reusable, and less prone to errors.

This article explores key best practices for writing JSON ARM templates, suitable for developers of all levels looking to improve their IaC skills on Azure.

Structure Your Template

A well-structured template is the foundation of good IaC. An ARM template JSON file has a specific top-level structure:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    // ... parameter definitions ...
  },
  "variables": {
    // ... variable definitions ...
  },
  "functions": [
    // ... user-defined function definitions (less common) ...
  ],
  "resources": [
    // ... resource definitions ...
  ],
  "outputs": {
    // ... output definitions ...
  }
}

Organizing your template into these logical sections makes it easy to understand the template's purpose, its inputs, calculated values, deployed resources, and resulting information.

Leverage Parameters Effectively

Parameters allow you to provide inputs at deployment time, making your templates reusable across different environments (e.g., dev, test, prod) or configurations.

Use Appropriate Data Types

Define explicit data types for your parameters (`string`, `int`, `bool`, `array`, `object`, `securestring`, `secureObject`). This ensures data integrity and provides better validation.

Provide Meaningful Descriptions

Use the description property to explain the purpose and expected value of each parameter. This is crucial documentation for anyone using your template.

Set Default Values Where Possible

Defaults simplify deployments when common values are acceptable. However, avoid defaults for sensitive parameters like passwords.

Use Allowed Values

For parameters with a limited set of valid inputs (e.g., SKUs, locations), use the allowedValues property to prevent errors during deployment.

Secure Sensitive Information

Always use `securestring` and `secureObject` types for passwords, keys, or other secrets. These values are not logged during deployment.

Parameter Example:

"parameters": {
  "storageAccountName": {
    "type": "string",
    "minLength": 3,
    "maxLength": 24,
    "metadata": {
      "description": "The name of the Azure Storage account."
    }
  },
  "location": {
    "type": "string",
    "defaultValue": "[resourceGroup().location]",
    "allowedValues": [
      "eastus",
      "westus",
      "centralus"
    ],
    "metadata": {
      "description": "The location for the resources."
    }
  },
  "adminPassword": {
    "type": "securestring",
    "metadata": {
      "description": "Administrator password for the VM."
    }
  }
}

Define Variables for Readability and Reusability

Variables hold values that are used multiple times in your template or values that are constructed from parameter inputs or function calls. They improve readability and consistency.

Simplify Complex Expressions

If you have a complex expression (e.g., concatenating multiple strings, performing calculations), define it as a variable to keep your resource definitions clean.

Centralize Values Used Multiple Times

Define resource names (often constructed from a base name and environment), tags, or common configuration settings as variables.

Variable Example:

"variables": {
  "baseName": "[parameters('resourceBaseName')]",
  "storageAccountName": "[concat(variables('baseName'), 'storage')]",
  "appServiceName": "[concat(variables('baseName'), 'app')]",
  "commonTags": {
    "environment": "[parameters('environment')]",
    "managedBy": "ARM Template"
  }
}

Be Mindful of Variable Scope

Variables are evaluated once at the beginning of the deployment. They cannot dynamically change based on resource properties created during the deployment (like resource IDs). For dynamic values, use resource properties or outputs.

Use Copy for Resource Iteration

When you need to deploy multiple instances of the same resource type (e.g., several VMs, multiple subnets in a VNet), use the copy property on the resource definition. This is far better than copying and pasting resource blocks.

Deploy Multiple Resources

Use copy on the resource itself to create an array of resources of the same type.

Deploy Multiple Properties

Use copy within a resource definition (e.g., on the properties of a VNet subnet array) to create multiple child items within a single resource.

Copy Example (Multiple Storage Accounts):

{
  "type": "Microsoft.Storage/storageAccounts",
  "apiVersion": "2021-09-01",
  "name": "[concat(parameters('storagePrefix'), copyIndex())]",
  "location": "[parameters('location')]",
  "sku": { "name": "Standard_LRS" },
  "kind": "StorageV2",
  "copy": {
    "name": "storageLoop",
    "count": "[parameters('numberOfStorageAccounts')]"
  }
}

Copy Example (Multiple Subnets in VNet):

{
  // ... VNet resource definition ...
  "properties": {
    // ... VNet properties ...
    "subnets": [
      {
        "name": "[concat('subnet-', copyIndex())]",
        "properties": {
          "addressPrefix": "[variables('subnetAddressPrefixes')[copyIndex()]]"
          // ... other subnet properties ...
        },
        "copy": {
          "name": "subnetLoop",
          "count": "[length(variables('subnetAddressPrefixes'))]"
        }
      }
    ]
  }
}

The copyIndex() function is essential here, providing a zero-based index within the loop.

Specify API Versions

Every resource definition must specify an apiVersion. This version corresponds to a REST API version for that resource type.

Use Recent, Stable API Versions

Use a recent, non-preview API version to ensure access to the latest features while maintaining stability. Avoid using the absolute latest preview version in production templates unless necessary.

Consistency is Key

While not strictly required across *all* resource types, using consistent API versions for related resources (e.g., networking components within a VNet deployment) can sometimes help avoid unexpected behavior due to differing feature sets.

Implement Resource Dependencies

ARM deploys resources in parallel by default. Use the dependsOn property to specify that a resource must be deployed only after other resources are successfully created. This is critical for resources that rely on others, like a Virtual Machine needing a Network Interface, or a Web App needing an App Service Plan.

Use Resource Names or Resource IDs

The dependsOn value is an array of resource names or resource IDs. Using resource IDs generated by functions like resourceId() is generally recommended for robustness, especially when dealing with resource types in different namespaces or nesting.

Implicit Dependencies

Many ARM functions (like reference() or resourceId()) create implicit dependencies. If Resource B uses reference(resourceA), ARM understands that Resource A must be deployed before Resource B. While implicit dependencies are often sufficient, explicit dependsOn adds clarity and can sometimes resolve complex ordering issues.

DependsOn Example:

{
  "type": "Microsoft.Compute/virtualMachines",
  // ... other properties ...
  "dependsOn": [
    "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]",
    "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
  ]
}

Utilize Built-in Template Functions

ARM templates provide a rich set of built-in functions (like concat, resourceId, reference, parameters, variables, union, length, etc.) to construct values dynamically.

Understand Their Purpose

Familiarize yourself with commonly used functions. For example, resourceId() is used to get the full ID of a resource, and reference() is used to get the runtime state (properties) of a deployed resource.

Case-Insensitivity

Template functions and their arguments are case-insensitive. [parameters('name')] is the same as [PARAMETERS('NAME')].

Function Examples:

"storageAccountName": "[concat(parameters('storagePrefix'), uniqueString(resourceGroup().id))]",
"tags": "[union(variables('commonTags'), parameters('resourceTags'))]",
"ipAddress": "[reference(variables('publicIpAddressName')).ipAddress]",
"subnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vNetName'), variables('subnetName'))]"

Implement Conditional Deployments

Use the condition property on a resource definition to specify whether that resource should be deployed or not based on a boolean expression. This is useful for deploying certain resources only in specific environments or based on parameter inputs.

Condition Example:

{
  "type": "Microsoft.Network/publicIPAddresses",
  // ... other properties ...
  "condition": "[equals(parameters('deployPublicIp'), true)]"
}

Use Linked/Nested Templates

For large or complex deployments, break down your main template into smaller, reusable linked or nested templates. This improves modularity and manageability.

Modularity and Reusability

Create separate templates for logical groups of resources (e.g., networking, compute, database) and link them from a main deployment template.

Template Specs

Consider using Template Specs to store and share your templates securely within Azure, making linked templates easier to reference.

Follow Naming Conventions

Establish and follow consistent naming conventions for your Azure resources. This makes it easier to identify and manage resources within the Azure portal and CLI.

Incorporate Parameters/Variables

Often, names are constructed using parameters (like environment or project code) and variables (for base names and suffixes) using the concat function.

Be Aware of Resource Naming Rules

Each Azure resource type has specific naming restrictions (length, allowed characters). Check the documentation for the resources you are deploying.

Define Template Outputs

Outputs allow you to return values from your deployment, such as connection strings, resource IDs, IP addresses, or generated keys. These are invaluable for scripting subsequent steps after the ARM deployment completes.

Retrieve Key Information

Use functions like reference() or resourceId() to get the properties of deployed resources and expose them as outputs.

Secure Sensitive Outputs

Mark sensitive outputs with the "type": "securestring" property. Their values will be masked in deployment logs.

Outputs Example:

"outputs": {
  "storageAccountEndpoint": {
    "type": "string",
    "value": "[reference(variables('storageAccountName')).primaryEndpoints.blob]"
  },
  "vmPrivateIp": {
    "type": "string",
    "value": "[reference(variables('networkInterfaceName')).ipConfigurations[0].properties.privateIPAddress]"
  },
  "sqlAdminPassword": {
    "type": "securestring",
    "value": "[parameters('adminPassword')]" // Outputting a parameter value
  }
}

Add Comments (JSON Limitations)

Standard JSON does not support comments. While the Azure portal editor and some tools might allow them temporarily, they are technically invalid JSON and can cause issues with parsers or automation.

Workarounds

Common workarounds include:

  • Using the metadata property within parameters, variables, resources, or outputs.
  • Adding a top-level object property like _comments or explanation (though this adds non-standard properties).
  • Maintaining separate documentation.

Using the metadata property is the recommended approach within the template JSON itself.

Validate and Test Your Templates

Before deploying, always validate and test your templates.

Use the What-If Operation

The What-If operation shows you what changes ARM will make to your environment without actually deploying. This is invaluable for catching unintended consequences.

az deployment group create --resource-group myResourceGroup --template-file mainTemplate.json --parameters @params.json --mode WhatIf

ARM Template Linter (VS Code Extension / CLI)

Use the ARM template linter (available as a VS Code extension or a separate tool) to check your template against recommended practices and syntax errors automatically.

Deploy to a Test Resource Group

Always test complex templates in a dedicated test resource group before deploying to production environments.

Conclusion

Writing effective ARM templates goes beyond simply defining resources. By following these JSON best practices – structuring your template, using parameters and variables correctly, leveraging functions, implementing dependencies and conditions, using copy loops, and validating your work – you can create infrastructure code that is reliable, maintainable, and scalable. Investing time in learning and applying these practices will significantly improve your Azure IaC development workflow.

Need help with your JSON?

Try our JSON Formatter tool to automatically identify and fix syntax errors in your JSON. JSON Formatter tool