Feature Matrix

  ARM Templates Bicep Terraform (azurerm) Terraform (azapi)
Language JSON Bicep DSL HCL HCL
Maintained by Microsoft Microsoft HashiCorp / OpenTofu Microsoft
License Free Free / MIT BSL (Terraform) / MPL (OpenTofu) MPL 2.0
Azure-native Yes Yes Yes (via azurerm provider) Yes (via azapi provider)
Multi-cloud No No Yes Azure only
State file No No Yes Yes
Day-0 coverage Full Full Provider lag possible Full — mirrors ARM/Bicep API
Field names ARM API names ARM API names Custom HCL-friendly names ARM API names (Bicep-aligned)
Plan preview --what-if --what-if terraform plan terraform plan
Loops copy element for expression for_each / count for_each / count
Conditions condition element if expression count = cond ? 1 : 0 count = cond ? 1 : 0
Modules / reuse Linked templates Modules + Registry Modules + Registry Modules + Registry
IDE support Good Excellent Excellent Good
Learning curve Steep Moderate Moderate Low for Bicep users
Community Large Medium (growing) Very large Small (growing)

Plans vs What-If

Both terraform plan and --what-if preview changes before applying, but they work differently:

  terraform plan az deployment ... --what-if
Diff source State file + live refresh Azure Resource Manager
Accuracy High Moderate — known noisy diffs
Saveable output Yes (-out=tfplan) No
Apply from saved plan Yes (terraform apply tfplan) No
Cross-resource impacts Shown via dependency graph Not shown

See also: Bicep What-If · Terraform Plan

Same Resource, Three Languages

Deploy a Storage Account (Standard LRS, StorageV2) in each tool:

ARM Template (~25 lines)

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "storageAccountName": { "type": "string" },
    "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": {
        "minimumTlsVersion": "TLS1_2",
        "allowBlobPublicAccess": false,
        "supportsHttpsTrafficOnly": true
      }
    }
  ]
}
az deployment group create --resource-group my-rg \
  --template-file storage.json --parameters storageAccountName=myaccount

Bicep (~13 lines)

param storageAccountName string
param location string = resourceGroup().location

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
    supportsHttpsTrafficOnly: true
  }
}
az deployment group create --resource-group my-rg \
  --template-file storage.bicep --parameters storageAccountName=myaccount

Terraform / azurerm (~17 lines)

variable "storage_account_name" { type = string }
variable "resource_group_name"  { type = string }
variable "location"             { type = string; default = "westeurope" }

resource "azurerm_storage_account" "main" {
  name                            = var.storage_account_name
  resource_group_name             = var.resource_group_name
  location                        = var.location
  account_tier                    = "Standard"
  account_replication_type        = "LRS"
  account_kind                    = "StorageV2"
  min_tls_version                 = "TLS1_2"
  allow_nested_items_to_be_public = false
  https_traffic_only_enabled      = true
}
terraform init && terraform apply -var="storage_account_name=myaccount"

Terraform / azapi (~18 lines)

variable "storage_account_name" { type = string }
variable "resource_group_id"    { type = string }
variable "location"             { type = string; default = "westeurope" }

resource "azapi_resource" "storage" {
  type      = "Microsoft.Storage/storageAccounts@2023-01-01"
  name      = var.storage_account_name
  location  = var.location
  parent_id = var.resource_group_id

  body = {
    sku        = { name = "Standard_LRS" }
    kind       = "StorageV2"
    properties = {
      minimumTlsVersion        = "TLS1_2"
      allowBlobPublicAccess    = false
      supportsHttpsTrafficOnly = true
    }
  }
}
terraform init && terraform apply -var="storage_account_name=myaccount"

Field names mirror the Bicep/ARM API exactly — minimumTlsVersion instead of min_tls_version, allowBlobPublicAccess instead of allow_nested_items_to_be_public. See the azapi page for more details.

Multiple files visibility

W.I.P.

Supported resources/providers

Support for new resources

New Azure features appear in ARM and Bicep on day 0 — the moment Microsoft ships the feature, the ARM API supports it. Terraform’s azurerm provider typically follows within days to weeks, depending on provider maintainer availability. The azapi provider (maintained by Microsoft) closes this gap entirely by calling the ARM REST API directly, providing the same day-0 coverage as ARM and Bicep — at the cost of higher verbosity and less abstraction. See the azapi page.

Support for many providers

Only Terraform / OpenTofu supports managing resources outside of Azure in the same workflow. Popular non-Azure providers include aws, google, kubernetes, helm, vault, datadog, and hundreds more.