ARM Templates for Azure
Complete the full lesson to earn 25 points
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks and see fewer ads — read each lesson on a single page with Pro
Infrastructure as Code: Mastering ARM Templates for Azure
Introduction: The Shift from Manual to Automated Infrastructure
In the early days of cloud computing, many engineers provisioned resources through the Azure Portal by clicking buttons, selecting options from dropdown menus, and manually configuring settings. While this approach is intuitive for learning, it becomes a significant liability as environments grow in complexity. When you manually configure a virtual network or a database, you create "snowflake" environments—unique, undocumented, and difficult to reproduce. If that environment is accidentally deleted or needs to be replicated in a different region, the manual process is prone to human error, inconsistency, and extreme time inefficiency.
This is where Infrastructure as Code (IaC) changes the game. Infrastructure as Code is the practice of managing and provisioning your computer data center through machine-readable definition files, rather than physical hardware configuration or interactive configuration tools. For Microsoft Azure, the native implementation of this concept is the Azure Resource Manager (ARM) template. An ARM template is a JSON file that defines the infrastructure and configuration for your project. By using these templates, you treat your infrastructure exactly like your application code: you can version control it, test it, and deploy it consistently every single time.
Understanding ARM templates is critical for any cloud professional because it provides the foundation for DevOps practices. When you define your infrastructure in code, you gain the ability to create repeatable deployments, enforce security standards, and document your environment automatically. This lesson will guide you through the anatomy of an ARM template, how to write them, how to deploy them, and how to structure your projects for long-term maintainability.
The Anatomy of an ARM Template
An ARM template is essentially a JSON document that tells Azure what resources you want to create and how they should be configured. Because it is JSON, it is hierarchical and structured, making it easy for machines to parse and for humans to read—provided the structure is well-organized.
Every ARM template follows a standard schema. When you open an ARM template file, you will notice several top-level elements that are required for the Azure Resource Manager to process the request correctly. Understanding these components is the first step toward writing your own templates.
Key Components of an ARM Template
- $schema: This defines the version of the template language. It tells Azure which set of rules to use when validating the template.
- contentVersion: This is a user-defined version number for your template. You can use it to track changes to your own infrastructure definitions, similar to versioning your application code.
- parameters: These are values you provide during deployment that allow you to make your template flexible. For example, you might have a parameter for the environment name (e.g., "dev", "prod") or the virtual machine size.
- variables: These are values that you construct inside the template itself. They are useful for concatenating strings or performing logic that you don't want to expose as a user input.
- resources: This is the heart of the template. It is an array where you list the actual Azure resources you want to create, such as storage accounts, virtual networks, or virtual machines.
- outputs: These are values returned by the deployment. For example, if you create a public IP address, you might want the template to return the IP string so that your subsequent application deployment can use it.
Callout: ARM Templates vs. Bicep You may have heard of Bicep, which is a domain-specific language for deploying Azure resources. It is important to understand that Bicep is an abstraction over ARM templates. When you deploy a Bicep file, it is automatically "transpiled" into an ARM JSON template before being submitted to the Azure Resource Manager. Learning ARM templates directly is still valuable because it helps you understand the underlying structure that Bicep generates, which is essential for troubleshooting complex deployments.
Writing Your First ARM Template: A Practical Example
Let’s look at a simple example: creating a standard Azure Storage Account. A storage account requires a name, a location, and a SKU (the performance tier).
{
"$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": {}
}
]
}
Breaking Down the Code
- The Resource Type:
Microsoft.Storage/storageAccountsidentifies the service being provisioned. Every resource in Azure has a specific provider namespace and type. - The API Version: This is critical. Azure updates its resource providers frequently. The
apiVersiontells the Azure Resource Manager which version of the REST API to use for this specific resource. - Expressions: Notice the square brackets
[...]. This indicates an ARM template expression.[parameters('storageAccountName')]tells the engine to look for the value provided by the user, while[resourceGroup().location]is a built-in function that fetches the location of the resource group you are deploying into.
Tip: Managing API Versions Always check the Azure documentation to ensure you are using the latest stable
apiVersion. Using an outdated version can sometimes lead to deployment failures if the resource provider has deprecated certain features or configuration options.
Advanced Template Features: Variables and Logic
As your infrastructure grows, hardcoding values becomes unsustainable. This is where variables and template functions become your best friends. Variables allow you to centralize values that might be used in multiple places, such as naming conventions.
Using Variables for Naming Conventions
Imagine you need to create a storage account and a virtual network, and both need to follow a naming convention like [Prefix]-[Environment]-[Region]-Resource. Instead of manually typing this in every resource definition, you can build it in the variables section.
"variables": {
"storageName": "[concat('st', parameters('environment'), uniqueString(resourceGroup().id))]"
}
In this example, we use the concat function to build a string and uniqueString to ensure the name is globally unique—a strict requirement for Azure storage accounts. By using resourceGroup().id as the seed for the unique string, we ensure that the generated name remains consistent within that specific resource group.
Conditional Deployment
Sometimes you only want to deploy a resource under certain conditions. Perhaps you want to deploy a premium storage account in production but a standard one in development. You can use the condition property for this.
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"condition": "[equals(parameters('environment'), 'prod')]",
"name": "premiumstorage",
"sku": { "name": "Premium_LRS" },
...
}
]
This logic adds a layer of intelligence to your infrastructure. Instead of having two separate templates for different environments, you have one template that adapts its behavior based on the parameters passed to it.
Deployment Strategies: How to Execute Your Templates
Once you have written your template, you need to deploy it. There are three primary ways to deploy ARM templates: the Azure CLI, Azure PowerShell, and the Azure Portal. In a professional environment, you should favor the CLI or PowerShell as these allow you to integrate your deployments into CI/CD pipelines.
Deploying with Azure CLI
The Azure CLI is highly efficient for scripting. You can deploy a template with a single command:
az deployment group create \
--name MyDeployment \
--resource-group MyResourceGroup \
--template-file azuredeploy.json \
--parameters storageAccountName=mystorage123
Deploying with Azure PowerShell
PowerShell is equally powerful and often preferred by Windows-centric sysadmins.
New-AzResourceGroupDeployment -ResourceGroupName MyResourceGroup `
-TemplateFile .\azuredeploy.json `
-storageAccountName mystorage123
The Role of Parameter Files
As you add more parameters, passing them as individual flags in the command line becomes cumbersome. The industry standard is to use Parameter Files. A parameter file is a separate JSON document that contains the values for your template.
parameters.json:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storageAccountName": {
"value": "mystorage123"
}
}
}
By using parameter files, you can maintain different files for different environments (e.g., dev.parameters.json, prod.parameters.json). This keeps your deployment commands clean and makes your configuration management much more transparent.
Best Practices for ARM Template Development
Developing ARM templates is not just about getting the code to work; it is about writing code that can be maintained by a team over several years. Here are the industry-standard best practices.
1. Modularity and Linked Templates
Do not try to put your entire enterprise infrastructure into a single, massive JSON file. That becomes a "monolith" that is impossible to debug. Instead, use Linked Templates. You can create a "main" template that calls out to smaller, specialized templates (e.g., one for networking, one for compute, one for storage). This allows you to reuse the "networking" template across multiple projects.
2. Use Tags for Cost Tracking
Always include tags in your resource definitions. Tags are key-value pairs that help you organize your Azure resources. A common strategy is to use tags like Environment, Project, and Owner.
"tags": {
"Environment": "[parameters('environment')]",
"Project": "CloudMigration"
}
3. Avoid Hardcoding
Never hardcode values that might change. If you need a virtual network name, pass it as a parameter. If you need a location, use [resourceGroup().location]. Hardcoding creates technical debt that will inevitably cause deployment failures when you attempt to deploy to a new region or a different subscription.
4. Implement What-If Analysis
Before deploying, always run the "What-If" operation. This command compares your template against the current state of the resource group and tells you exactly what will be created, modified, or deleted.
az deployment group what-if \
--resource-group MyResourceGroup \
--template-file azuredeploy.json \
--parameters @parameters.json
This is the single most important step for avoiding accidental deletions or unexpected configuration drift in production environments.
Common Pitfalls and How to Avoid Them
Even experienced developers encounter issues when working with ARM templates. Being aware of these common traps will save you hours of debugging.
The "Circular Dependency" Trap
A circular dependency occurs when Resource A depends on Resource B, but Resource B also depends on Resource A. For example, a Virtual Machine might depend on a Network Interface (NIC), but the NIC might be defined as depending on the VM. Azure will return an error stating that the deployment cannot be completed.
- The Fix: Use the
dependsOnproperty carefully. Only define dependencies when absolutely necessary. Often, the Azure Resource Manager can infer dependencies without you needing to explicitly define them.
Namespace Mismatches
Each Azure resource has a provider namespace (e.g., Microsoft.Compute, Microsoft.Network). If you mistype a namespace or use an invalid resource type, the deployment will fail immediately.
- The Fix: Use the VS Code extension for ARM Tools. It provides IntelliSense, which helps you select the correct resource type and API version as you type, drastically reducing syntax errors.
The "Deployment Mode" Confusion
Azure supports two deployment modes: Incremental and Complete.
- Incremental: This is the default. It adds new resources and updates existing ones but leaves resources in the resource group that are not mentioned in your template alone.
- Complete: This is dangerous. In this mode, Azure deletes any resource in the resource group that is not defined in your template.
- The Fix: Stick to Incremental mode unless you have a very specific requirement to enforce an "exact state" for a resource group. Always double-check your mode before running a deployment.
Warning: The Dangers of 'Complete' Mode Using "Complete" mode is a common cause of accidental production outages. If you accidentally leave a database or a critical networking component out of your template and run a deployment in Complete mode, Azure will delete that resource to ensure the environment matches your template exactly. Always use Incremental mode unless you are 100% certain of the state of your resource group.
Comparison Table: Deployment Methods
| Feature | Azure Portal | Azure CLI / PowerShell | CI/CD Pipeline (GitHub Actions/ADO) |
|---|---|---|---|
| Repeatability | Low | High | Very High |
| Version Control | None | Possible | Native |
| Skill Level | Beginner | Intermediate | Advanced |
| Audit Trail | Manual | Good | Excellent |
| Best For | Prototyping | Ad-hoc tasks | Production environments |
Structuring Your Infrastructure for Success
When you move from a single template to a multi-environment deployment, you need a strategy for organizing your files. A common structure used by DevOps teams looks like this:
- /templates: This folder contains the raw ARM templates for individual resources (e.g.,
storage.json,vnet.json). - /parameters: This folder contains environment-specific files (e.g.,
dev.json,prod.json). - /scripts: This folder contains helper scripts, such as validation scripts or cleanup tasks.
- azure-pipelines.yaml: The configuration file for your CI/CD tool (e.g., Azure DevOps or GitHub Actions) that ties everything together.
By keeping your logic (templates) separate from your configuration (parameters), you make it significantly easier to manage the lifecycle of your infrastructure. If you need to upgrade a virtual machine size, you simply change it in your prod.json parameter file. The template itself—which defines the logic of the VM—remains untouched, reducing the risk of introducing bugs into the core architecture.
Integrating ARM Templates into CI/CD
The ultimate goal of learning ARM templates is to automate them. When you check your code into a repository like GitHub or Azure DevOps, you can trigger a deployment automatically. This ensures that your infrastructure is always in sync with your source code.
A Typical CI/CD Workflow
- Code Commit: A developer updates a template or parameter file and pushes the change to a repository.
- Validation: A CI (Continuous Integration) pipeline runs a "validate" command. This checks the JSON syntax and ensures the parameters are valid without actually deploying anything.
- What-If: The pipeline runs the
what-ifcommand to show the team what changes will occur. - Approval: A lead engineer reviews the
what-ifoutput and approves the change. - Deployment: The CD (Continuous Deployment) pipeline executes the ARM template deployment against the target subscription.
This flow eliminates the "it worked on my machine" problem. Because the pipeline is the only thing that changes production, you have a perfect audit trail of who changed what, when they changed it, and why.
Troubleshooting ARM Templates
Inevitably, you will encounter a deployment error. The Azure Resource Manager returns error codes that are usually quite descriptive. Here is how to handle the most common ones:
- DeploymentQuotaExceeded: You have reached the limit of deployments in your resource group. You can clear these out using the Azure CLI or by setting a retention policy.
- PropertyConflict: You are trying to set a property that is read-only or conflicts with another setting. Review the documentation for that specific resource.
- ResourceNotFound: You are referencing a resource (like a subnet) that doesn't exist. Check your parameter values to ensure they match the actual names of existing resources.
If you are stuck, the best resource is the Deployment History in the Azure Portal. Go to the Resource Group, click on "Deployments," and select the failed deployment. Click on the "Error Details" tab. It will often provide the exact JSON path where the error occurred, which allows you to pinpoint the mistake in your template immediately.
Frequently Asked Questions
1. Is it better to use ARM templates or Terraform?
Both are excellent tools. ARM templates are native to Azure and have zero-day support for new features. Terraform is cloud-agnostic and uses a different language (HCL). Choose ARM templates if you are fully committed to the Azure ecosystem. Choose Terraform if you have a multi-cloud strategy and want a unified workflow across AWS, Azure, and GCP.
2. Can I use ARM templates to manage resources I created manually?
Yes, you can "export" a template from an existing resource group. In the Azure Portal, go to the resource group and select "Export template." This provides a great starting point, though the generated code is often messy and contains unnecessary details. Use it as a reference, but clean it up before using it in production.
3. How do I handle secrets like passwords?
Never store passwords in your ARM templates. Use Azure Key Vault. Store the secret in the Key Vault, and pass the secret URI as a parameter to your ARM template. The ARM template will then reference the Key Vault to retrieve the value at runtime.
4. How do I learn more about the latest resources?
The Azure Resource Manager documentation is the definitive source. It includes a comprehensive reference for every resource provider, including all available properties and API versions.
Key Takeaways
- IaC is Essential: Moving from manual configuration to Infrastructure as Code is the foundational step for any scalable, professional cloud operation. It eliminates manual errors and enables environment consistency.
- Structure Matters: Use a modular approach with linked templates. Do not create massive, monolithic files. Keep your infrastructure logic separate from your environment configurations.
- Safety First: Always use "What-If" analysis before deploying. This prevents accidental deletions and helps you understand the impact of your changes before they hit production.
- Version Control Everything: Your ARM templates and parameter files should live in a source control system (like Git). This allows for team collaboration, code reviews, and a clear history of infrastructure changes.
- Use Modern Tools: Take advantage of VS Code extensions for ARM templates. They provide syntax highlighting, validation, and IntelliSense that significantly speed up development and reduce errors.
- Security by Design: Never hardcode sensitive information. Always use Azure Key Vault to manage secrets, passwords, and certificates, and reference them in your templates.
- Automation is the Goal: Treat your infrastructure like your application code. Integrate your templates into CI/CD pipelines to ensure that every deployment is tested, approved, and repeatable.
By mastering ARM templates, you are not just learning a tool—you are adopting a mindset of precision and automation. As you continue your journey, remember that the best infrastructure is the one that is documented, version-controlled, and easily reproducible. Happy coding!
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