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
orexplanation
(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