Infrastructure as Code in Azure: ARM, Bicep, and Terraform

Infrastructure as Code (IaC) means defining your cloud resources in text files and applying them with tooling — instead of clicking through the Azure portal. Azure supports three main IaC approaches: ARM templates, Bicep, and Terraform. Each has a different syntax and different trade-offs, but all three give you the same core benefit: reproducible, version-controlled infrastructure.

Why Infrastructure as Code matters

When you create resources through the Azure portal, no one can tell you exactly what you clicked, in what order, or what settings you chose. Six months later, rebuilding that environment from memory is painful. With IaC, the configuration lives in a file you can read, review in a pull request, store in git, and re-apply whenever you need a new environment.

The practical benefits are tangible:

  • Repeatability. You can spin up a staging environment that is identical to production by running one command.
  • Auditability. Git history shows who changed what and why.
  • Disaster recovery. If a resource group is accidentally deleted, you can recreate everything from source.
  • Drift detection. Tools can compare what is deployed against what is in code and flag differences.

ARM templates: the original Azure IaC format

Azure Resource Manager (ARM) templates are JSON documents that describe the resources you want in a resource group. When you submit an ARM template, Azure’s Resource Manager API processes it and creates or updates resources to match the declared state.

Here is a minimal ARM template that creates a storage account:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "storageAccountName": {
      "type": "string",
      "metadata": {
        "description": "Globally unique storage account name"
      }
    },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]"
    }
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "apiVersion": "2023-01-01",
      "name": "[parameters('storageAccountName')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "Standard_LRS"
      },
      "kind": "StorageV2",
      "properties": {
        "accessTier": "Hot"
      }
    }
  ]
}

Deploying ARM templates with the Azure CLI

# Deploy an ARM template to a resource group
az deployment group create \
  --resource-group my-rg \
  --template-file storage.json \
  --parameters storageAccountName=myuniquestorage123

# Deploy at the subscription level (for resource groups, policies, etc.)
az deployment sub create \
  --location eastus \
  --template-file subscription-level.json

# What-if: preview changes before applying
az deployment group what-if \
  --resource-group my-rg \
  --template-file storage.json \
  --parameters storageAccountName=myuniquestorage123
Note

ARM template deployment is idempotent by default. Running the same template twice will not create duplicate resources — it will update the existing resource to match the template. However, the behavior of properties not specified in the template depends on the deployment mode (complete vs. incremental).

Bicep: ARM templates without the JSON pain

Bicep is a language designed by Microsoft specifically for deploying Azure resources. It compiles to ARM JSON, so it uses the same underlying API, but the syntax is dramatically cleaner. You get type checking, IDE support in VS Code, and a module system for reusable components.

The same storage account in Bicep looks like this:

@description('Globally unique storage account name')
param storageAccountName string

@description('Azure region for the storage account')
param location string = resourceGroup().location

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    accessTier: 'Hot'
  }
}

output storageAccountId string = storageAccount.id
# Deploy a Bicep file directly — az CLI compiles it automatically
az deployment group create \
  --resource-group my-rg \
  --template-file storage.bicep \
  --parameters storageAccountName=myuniquestorage123

# Compile Bicep to ARM JSON (for inspection or use in pipelines that require JSON)
az bicep build --file storage.bicep

# Decompile existing ARM JSON to Bicep (quality varies)
az bicep decompile --file storage.json

Bicep modules for reusable infrastructure

Bicep modules let you break large deployments into smaller, reusable files. A module is just a Bicep file that another Bicep file references. This is the Bicep equivalent of Terraform modules.

// main.bicep — calls a storage module
module storage './modules/storage.bicep' = {
  name: 'storageDeployment'
  params: {
    storageAccountName: 'myuniquestorage123'
    location: 'eastus'
  }
}

// Use the module output
output storageId string = storage.outputs.storageAccountId

Terraform: multi-cloud IaC for Azure

Terraform by HashiCorp is an open-source IaC tool that uses its own language called HCL (HashiCorp Configuration Language). The Azure provider for Terraform wraps the ARM API and lets you manage Azure resources using the same workflow you would use for AWS, GCP, or any other cloud.

Terraform maintains a state file that tracks what it has deployed. On each run, it compares the desired configuration (your HCL files) against the current state and makes only the necessary changes.

# main.tf — Azure storage account with Terraform
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.90"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "example" {
  name     = "example-rg"
  location = "East US"
}

resource "azurerm_storage_account" "example" {
  name                     = "myuniquestorage123"
  resource_group_name      = azurerm_resource_group.example.name
  location                 = azurerm_resource_group.example.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  account_kind             = "StorageV2"
}

output "storage_account_id" {
  value = azurerm_storage_account.example.id
}
# Initialize the working directory and download providers
terraform init

# Preview changes
terraform plan

# Apply changes
terraform apply

# Destroy all resources managed by this config
terraform destroy

Comparing ARM, Bicep, and Terraform

FeatureARM TemplatesBicepTerraform
LanguageJSONBicep DSLHCL
Azure-nativeYesYesNo (via provider)
Multi-cloudNoNoYes
State managementNone (ARM handles it)None (ARM handles it)Requires state backend
Module systemLinked templatesBicep modulesTerraform modules
Learning curveHighMediumMedium
Best forLegacy projectsAzure-only teamsMulti-cloud / large orgs

Choosing the right tool for your team

If you are starting fresh on an Azure-only project, Bicep is the recommended choice. Microsoft actively develops it, it integrates natively with Azure services, and the learning curve is lower than ARM JSON. The VS Code Bicep extension gives you IntelliSense, linting, and visualization for free.

If your organization already uses Terraform across AWS and GCP, adding the Azure provider to your existing Terraform workflow is almost certainly the right call. The consistency of using one tool across clouds is worth more than the native advantages of Bicep.

ARM templates are worth knowing because many Azure quickstart examples still use them, and some third-party tools export ARM JSON. But for new work, Bicep or Terraform are better choices.

Tip

You do not have to choose just one. Many teams use Terraform to manage foundational infrastructure (resource groups, virtual networks, Key Vault) and Bicep or ARM templates for application-layer resources that change more frequently. Mixing tools at the right layer is a valid architecture.

Common mistakes

  1. Hardcoding secrets in templates. Never put passwords, connection strings, or API keys directly in ARM, Bicep, or Terraform files. Reference them from Azure Key Vault using secure parameter types or Terraform’s Key Vault data sources.
  2. Not using what-if or plan before applying. Running az deployment group create or terraform apply without previewing changes first is risky in production. Always run --what-if or terraform plan and review the output before confirming.
  3. Storing Terraform state locally. The default terraform.tfstate file on your laptop will be out of sync with anyone else on the team. Always configure remote state in Azure Blob Storage from day one, even for small projects.

Frequently asked questions

What is the difference between ARM templates and Bicep?

Bicep is a domain-specific language that compiles down to ARM JSON. Bicep is much easier to read and write, supports modules, and has better tooling. ARM templates are verbose JSON files. Both deploy the same underlying Azure resources.

Should I use Bicep or Terraform for Azure?

Use Bicep if your team is Azure-only and wants tight integration with Azure features like deployment stacks. Use Terraform if you manage multi-cloud infrastructure, already have Terraform expertise, or want a large ecosystem of providers and modules.

Is Infrastructure as Code only for large teams?

No. Even solo developers benefit from IaC. It makes environments reproducible, prevents configuration drift, and lets you version-control your infrastructure alongside your application code.

Last verified: 19 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.