ARM Templates for IaC
Complete the full lesson to earn 25 points
Work through each section, then tap “Mark as Complete” on the last one.
Module: Infrastructure as Code (IaC)
Lesson: Azure Resource Manager (ARM) Templates
Introduction: The Shift to Declarative Infrastructure
In the early days of cloud computing, engineers often provisioned resources manually through web-based consoles. While intuitive for a single virtual machine, this "click-ops" approach becomes a liability as environments grow. When you manage dozens or hundreds of resources, manual configuration leads to "configuration drift," where the actual state of your infrastructure diverges from what you intended, making it nearly impossible to replicate environments consistently across development, testing, and production.
Infrastructure as Code (IaC) is the practice of managing and provisioning your infrastructure using machine-readable definition files rather than manual interaction. ARM Templates are the native way to implement IaC within the Microsoft Azure ecosystem. An ARM template is a JavaScript Object Notation (JSON) file that defines the infrastructure and configuration for your project. By using these templates, you move from an imperative model—where you tell the system how to build something step-by-step—to a declarative model, where you simply define the desired end state.
Understanding ARM templates is critical for any modern cloud engineer because it allows for version control, automated testing, and repeatable deployments. When your infrastructure is defined in code, you can treat your environment exactly like your application code: you can branch it, review it in pull requests, and deploy it through automated pipelines. This lesson will guide you through the anatomy of an ARM template, the deployment process, and the best practices required to manage your cloud resources effectively.
The Anatomy of an ARM Template
An ARM template is essentially a blueprint. When you submit this blueprint to Azure, the Azure Resource Manager engine parses the file and performs the necessary operations to bring your environment into compliance with that blueprint. If a resource already exists and matches the configuration in the template, Azure leaves it alone. If it doesn't exist, Azure creates it. If the configuration differs, Azure updates the resource.
Every ARM template follows a specific structure composed of several top-level sections. Understanding these sections is the first step toward writing your own.
- $schema: This defines the location of the JSON schema file that describes the version of the template language. This helps your editor provide IntelliSense and validation while you type.
- contentVersion: A version number you define to track changes to your templates.
- parameters: These are values you provide during deployment. They allow you to reuse the same template for different environments (e.g., using different VM sizes for dev versus production).
- variables: These are values that simplify template logic. They are constructed within the template and cannot be changed during deployment.
- resources: The core of the file. This is where you list the actual Azure resources to be deployed, such as virtual machines, storage accounts, or virtual networks.
- outputs: These are values returned after the deployment finishes, such as the public IP address of a newly created VM or the connection string for a database.
Callout: Declarative vs. Imperative In an imperative approach, you write a script that says "Create a VM, then add a disk, then open a port." If the script fails halfway, you have to write complex logic to handle the cleanup or partial state. In the declarative ARM template model, you simply state, "I want a VM with this disk and this port configuration." Azure handles the "how" and ensures the final state is achieved, regardless of the starting point.
Practical Example: Deploying a Storage Account
To understand how these sections work together, let's look at a simple template that deploys an Azure Storage Account. This is the "Hello World" of ARM templates.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageAccountName": {
"type": "string",
"metadata": {
"description": "The name of the storage account"
}
}
},
"variables": {},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2021-04-01",
"name": "[parameters('storageAccountName')]",
"location": "[resourceGroup().location]",
"sku": {
"name": "Standard_LRS"
},
"kind": "StorageV2",
"properties": {}
}
]
}
Breakdown of the Example
- Parameters: We define
storageAccountNameas a parameter. This makes the template flexible; you can pass different names depending on whether you are deploying to a dev or production resource group. - Resources: We define the type
Microsoft.Storage/storageAccounts. TheapiVersionis crucial because it tells Azure which version of the resource provider API to use. - Functions: Notice the use of
[parameters('storageAccountName')]and[resourceGroup().location]. These are ARM template functions. They allow the template to be dynamic, pulling information from the environment rather than hardcoding values.
Note: Always ensure your
apiVersionis current. Using outdated API versions can lead to missing features or unexpected behavior during deployment. Microsoft regularly updates these, so check the Azure documentation for the latest versions.
Advanced Concepts: Dependencies and Logic
Real-world infrastructure is rarely composed of a single resource. Usually, resources have dependencies. For example, a Virtual Machine depends on a Virtual Network, which in turn depends on a Resource Group. ARM templates handle these dependencies in two ways: implicit and explicit.
Implicit Dependencies
Azure Resource Manager often detects dependencies automatically. If you reference a resource's ID within another resource's property, Azure understands that the first resource must be created before the second one.
Explicit Dependencies
Sometimes, the relationship isn't obvious to the engine. In these cases, you use the dependsOn property. This forces the template to wait until the specified resource is successfully deployed before moving on to the next.
{
"type": "Microsoft.Network/virtualNetworks",
"name": "myVNet",
"apiVersion": "2021-02-01",
"location": "eastus",
"properties": { ... }
},
{
"type": "Microsoft.Network/publicIPAddresses",
"name": "myPublicIP",
"apiVersion": "2021-02-01",
"dependsOn": [
"[resourceId('Microsoft.Network/virtualNetworks', 'myVNet')]"
],
"properties": { ... }
}
In this snippet, the publicIPAddresses resource will not begin deployment until the virtualNetworks resource is finished. This is vital for avoiding errors where a resource tries to attach to a parent that does not yet exist.
Step-by-Step: Deploying Your First Template
Now that we have the theory, let's walk through how to actually deploy this template using the Azure CLI.
- Save your template: Save the JSON code provided above into a file named
template.json. - Create a Resource Group: Before you can deploy, you need a container for your resources.
az group create --name MyResourceGroup --location eastus - Validate the template: It is best practice to validate your template before attempting a full deployment to catch syntax errors.
az deployment group validate --resource-group MyResourceGroup --template-file template.json --parameters storageAccountName=mystorage123 - Execute the deployment: Once validated, run the deployment command.
az deployment group create --resource-group MyResourceGroup --template-file template.json --parameters storageAccountName=mystorage123 - Verify: Log into the Azure Portal or use
az storage account list --resource-group MyResourceGroupto confirm your storage account is active.
Best Practices for ARM Template Development
Managing large-scale infrastructure requires discipline. If you treat your ARM templates like "write-once-and-forget" files, you will eventually encounter technical debt. Follow these industry-standard practices to maintain a healthy IaC codebase.
1. Parameterize Sensibly
Avoid hardcoding values inside your resources. If a value might change, such as a SKU, a location, or a capacity limit, make it a parameter. However, don't over-parameterize. If a value is constant across all your environments, it is perfectly acceptable to hardcode it to keep the template clean.
2. Use Template Linking
As your projects grow, your JSON files will become massive and unreadable. Break your infrastructure into smaller, modular templates. You can use a "Main" template that calls "Linked" templates. This allows you to manage network resources in one file, compute resources in another, and storage in a third.
3. Implement Version Control
Store your ARM templates in a Git repository. This provides an audit trail of who changed what and when. It also allows you to roll back to a previous version if a deployment causes an unexpected issue.
4. Leverage Functions
ARM templates include a powerful library of functions. Use resourceId() to dynamically resolve resource identifiers, concat() to build strings, and if() for conditional logic. Using these keeps your templates dry and prevents manual errors.
Tip: Use the VS Code extension "Azure Resource Manager Tools." It provides excellent IntelliSense, autocompletion, and real-time validation, which significantly reduces the time spent debugging JSON syntax errors.
Common Pitfalls and How to Avoid Them
Even experienced engineers run into issues with ARM templates. Recognizing these common traps will save you hours of troubleshooting.
The "Deployment Mode" Confusion
ARM supports two deployment modes: Incremental and Complete.
- Incremental: This is the default. Azure adds or modifies resources but leaves existing resources in the resource group that are not mentioned in the template untouched.
- Complete: Azure ensures that the resource group contains only the resources defined in the template. If a resource exists in the group but is not in the template, Azure deletes it.
- Warning: Never use "Complete" mode unless you have a deep understanding of your environment. It is a common cause of accidental production outages where entire databases or networks are wiped out because they weren't included in a template update.
Circular Dependencies
A circular dependency occurs when Resource A depends on Resource B, and Resource B depends on Resource A. The deployment will fail immediately. This often happens when you try to nest resources too deeply or when you mistakenly add a dependsOn property that isn't actually required. Always map out your dependencies visually on paper or a whiteboard before writing the code.
Scope Mismatch
Resources exist at different scopes: Subscription, Resource Group, or Management Group. If you try to deploy a resource that belongs at the subscription level (like a Policy Assignment) using a command intended for a Resource Group, the deployment will fail. Ensure your deployment command matches the scope of the resources in your template.
Comparison: ARM vs. Bicep
While this lesson focuses on ARM templates, it is important to mention Bicep. Bicep is a domain-specific language (DSL) that compiles down to ARM templates. It was created specifically to solve the verbosity and complexity of JSON.
| Feature | ARM Templates (JSON) | Bicep |
|---|---|---|
| Syntax | Verbose JSON | Clean, concise, readable |
| Comments | Not natively supported (needs hacks) | Native support |
| Modularity | Difficult to manage | Built-in module support |
| Tooling | Standard text editors | Rich VS Code integration |
| Underlying Tech | Direct to ARM API | Compiles to ARM JSON |
Callout: Why learn ARM if Bicep exists? Bicep is a wrapper around ARM. When you deploy a Bicep file, it is converted into an ARM template before being sent to the Azure Resource Manager. Understanding the underlying ARM JSON is essential for debugging, performance tuning, and understanding exactly what the Azure platform is doing under the hood.
Advanced Logic: Copy Loops and Conditions
One of the most powerful features of ARM templates is the ability to use loops and conditions. This allows you to provision multiple instances of a resource or conditionally deploy a resource based on a parameter.
Copy Loops
If you need to deploy five instances of a virtual machine, you don't need to write the resource definition five times. You use a copy property.
{
"type": "Microsoft.Compute/virtualMachines",
"apiVersion": "2020-06-01",
"name": "[concat('vm-', copyIndex())]",
"copy": {
"name": "vmLoop",
"count": 5
},
"properties": { ... }
}
The copyIndex() function tracks the current iteration, allowing you to name your resources uniquely (e.g., vm-0, vm-1, etc.).
Conditions
Conditions allow you to deploy a resource only if a specific requirement is met. For instance, you might want to deploy an expensive "Premium" storage account only in production, while using "Standard" storage in development.
{
"condition": "[equals(parameters('environment'), 'prod')]",
"type": "Microsoft.Storage/storageAccounts",
"name": "premiumStorage",
"sku": { "name": "Premium_LRS" },
...
}
This logic keeps your templates lean and prevents you from having to maintain separate files for different environments.
Handling Secrets and Sensitive Data
One of the most critical aspects of IaC is security. You should never store plain-text secrets like passwords or API keys inside your ARM templates. If you commit these to source control, your security is compromised.
Instead, use Azure Key Vault. You can reference secrets stored in Key Vault directly within your ARM template. By using the reference function, your template fetches the secret at runtime during the deployment.
- Create a Key Vault and store your secret.
- Enable the "Template Deployment" permission on the Key Vault.
- Reference the secret in your ARM template:
"adminPassword": { "reference": { "keyVault": { "id": "/subscriptions/.../providers/Microsoft.KeyVault/vaults/myVault" }, "secretName": "mySecret" } }
This ensures that the secret is never exposed in your template code or the deployment logs.
Testing and Validation Strategies
Before deploying to production, you should implement a testing strategy. Infrastructure testing ensures that your templates are not only syntactically correct but also logically sound.
- Static Analysis: Use tools like
arm-ttk(ARM Template Toolkit). This is a set of tests that validate your templates against best practices (e.g., ensuring you aren't using hardcoded locations, checking for missing tags, or verifying that all resources have a defined type). - What-If Analysis: Before running a deployment, use the
what-ifcommand. This tells you exactly what changes will occur—which resources will be created, modified, or deleted—without actually applying them.az deployment group what-if --resource-group MyResourceGroup --template-file template.json - Integration Testing: Deploy your template to a temporary "sandbox" resource group in your Azure subscription. Run tests against the deployed resources to ensure they behave as expected, then delete the resource group.
Managing Large Deployments: The "Main-Child" Pattern
As your infrastructure grows, you should adopt the "Main-Child" or "Nested" template pattern. In this approach, a main template acts as an orchestrator. It defines parameters and then calls child templates for specific workloads.
- Main Template: Contains the high-level parameters and
Microsoft.Resources/deploymentsresources. - Child Templates: Contain the actual resource definitions (e.g.,
network.json,compute.json,database.json).
This pattern is highly recommended because it allows different teams to own different parts of the infrastructure. The network team can maintain the network.json file, while the database team manages database.json. They can work in parallel without causing merge conflicts in a massive, singular JSON file.
Monitoring and Troubleshooting Deployments
When a deployment fails, the Azure Portal provides a "Deployment" view under your Resource Group. This is your first stop for troubleshooting.
- Check the Error Message: Azure usually provides a specific error code (e.g.,
Conflict,Forbidden,ResourceNotFound). - View the Operation Details: Click on the failed resource to see the specific input and output parameters that caused the failure.
- Review the Activity Log: If the error is generic, the Activity Log often contains more granular details about why a resource provider rejected your request.
- Use the Correlation ID: Every deployment has a unique ID. If you need to open a support ticket with Microsoft, this ID is the most important piece of information you can provide.
Warning: Avoid manual changes to resources that are managed by ARM templates. If you manually change a setting in the portal, the next time you deploy your ARM template, it will overwrite your manual change. This is the definition of configuration drift, and it is the primary reason why IaC requires a "code-first" culture.
Key Takeaways
- Declarative vs. Imperative: ARM templates allow you to define the desired state of your infrastructure, letting Azure handle the complexities of reaching that state.
- Modularity is Essential: Use the main-child pattern to break down large, complex infrastructure into manageable, reusable templates that can be maintained by different teams.
- Security First: Never store sensitive data in plain text. Always use Azure Key Vault to reference secrets securely during the deployment process.
- Test Before Deploying: Always use the
what-ifcommand and static analysis tools likearm-ttkto catch errors and potential configuration drift before they impact production. - Avoid Manual Changes: Once you adopt ARM templates, the portal should only be used for monitoring, not for making changes. All infrastructure changes must originate from the template code to ensure consistency.
- Understand Dependencies: Properly manage resource relationships using
dependsOnor by utilizing implicit dependencies to ensure resources are created in the correct sequence. - Version Control: Treat your templates as application code by storing them in Git, using pull requests for reviews, and automating deployments through CI/CD pipelines.
By mastering ARM templates, you transition from being a manual administrator to an automated infrastructure engineer. This skill set is the foundation for building resilient, scalable, and secure cloud environments that can evolve alongside your business requirements.
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles your daily points limit so you progress twice as fast, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× daily points limit
- ✓ Distraction-free lessons