Azure Bicep vs Terraform in 2026: Choosing Your Azure IaC Tool


Executive Snapshot

Bicep template(.bicep)Terraform config(.tf + azurerm)Azure Resource Manager(Deployment engine)Azure Resources(VMs, Storage, etc.)compiles to ARM JSONazurerm provider callsprovision / update
Both Bicep and Terraform deploy through Azure Resource Manager to resources.
Category Azure Bicep Terraform (AzureRM / AzAPI)
Language Bicep DSL (Azure-native) HCL (HashiCorp Configuration Language)
State management Stateless (ARM deployment history) State file (local or remote backend)
Multi-cloud Azure only AWS, GCP, Azure, Kubernetes, and 3,000+ providers
Day-1 resource support Same day as ARM API GA Depends on provider release cadence
Module registry Bicep Registry (Azure Verified Modules) Terraform Registry (public + private)
Licensing MIT / Free BSL 1.1 (Terraform ≥ 1.6); OpenTofu is MPL-2.0
Learning curve Low for Azure-only teams Moderate; transferable across clouds
Best for Azure-focused teams, Microsoft shops Multi-cloud, polyglot platform teams

TL;DR

flowchart TD
    A[Choosing an IaC tool] --> B{Azure-only estate?}
    B -->|Yes| C{Want native ARM integration?}
    B -->|No, multi-cloud| D[Terraform - provider ecosystem]
    C -->|Yes| E[Bicep - first-party, no state file]
    C -->|Prefer mature tooling| F{Existing Terraform skills?}
    F -->|Yes| D
    F -->|No| E
    D --> G[Manage remote state backend]
    E --> H[ARM handles deployment state]
    classDef bicep fill:#1f4f7a,stroke:#4db5ff,color:#fff
    classDef tf fill:#4a2b67,stroke:#a06fdc,color:#fff
    class E,H bicep
    class D,G tf
Decision flow choosing Bicep or Terraform by cloud scope and skills.
  • Bicep is the right default for teams that deploy exclusively to Azure and want frictionless Day-1 resource support without managing state files.
  • Terraform (or OpenTofu) wins when you need to orchestrate resources across multiple cloud providers, SaaS APIs, or Kubernetes clusters from a single workflow.
  • The AzAPI provider largely closes Bicep's "new-resource gap" for Terraform users, but adds complexity compared to Bicep's native parity.
  • Licensing changed the equation: HashiCorp's BSL 1.1 shift means enterprises should evaluate OpenTofu as a drop-in alternative before committing to new Terraform workflows.
  • Both tools can coexist — many organisations use Bicep for Azure resource provisioning and Terraform for higher-level platform orchestration.

Introduction

Infrastructure-as-Code on Azure has never had more capable tooling — or more room for bikeshedding. The azure bicep vs terraform debate resurfaces every time a new project kicks off or a platform team expands beyond a single cloud. In 2026, both ecosystems have matured considerably: Bicep ships Azure Verified Modules (AVM) with opinionated defaults, Terraform's AzureRM provider is on v4.x with dramatically improved coverage, and the fork of Terraform — OpenTofu — reached 1.9 with feature parity for most production workloads.

Choosing the wrong tool doesn't doom a project, but it creates friction: orphaned state files, day-two operational gaps, or modules that only one person on the team understands. This article gives you an honest, hands-on comparison so you can make an evidence-based decision rather than defaulting to whatever your last job used.


Prerequisites

  • Familiarity with Azure Resource Manager (ARM) concepts (resource groups, subscriptions, management groups)
  • Azure CLI ≥ 2.60 or Azure PowerShell ≥ 12.x installed and authenticated
  • Terraform ≥ 1.9 (or OpenTofu ≥ 1.9) installed, if evaluating Terraform examples
  • Basic understanding of declarative IaC principles
  • Contributor or Owner rights on an Azure subscription for running examples

1. Language and Authoring Experience

Bicep: Purpose-Built for ARM

Bicep compiles directly to ARM JSON, meaning everything Azure supports is expressible in Bicep with zero abstraction layer. The syntax is clean, concise, and statically typed.

// main.bicep — deploy a Storage Account with blob versioning enabled
@description('Azure region for all resources')
param location string = resourceGroup().location

@description('Storage account name — must be globally unique')
@minLength(3)
@maxLength(24)
param storageAccountName string

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

output storageAccountId string = storageAccount.id
output primaryEndpoint string = storageAccount.properties.primaryEndpoints.blob

Key authoring benefits:

  • IntelliSense in VS Code (Bicep extension) surfaces every valid property with documentation inline
  • bicep linter catches common mistakes before deployment
  • The targetScope keyword supports resource group, subscription, management group, and tenant deployments natively
  • Parameters can be stored in .bicepparam files, solving the "how do I pass secrets cleanly?" problem

Terraform: Expressive and Provider-Agnostic

Terraform's HCL is readable and well-documented, but its abstraction over ARM means you're one provider-release away from being blocked on a new Azure feature.

# main.tf — equivalent Storage Account in Terraform AzureRM v4
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  subscription_id = var.subscription_id
}

variable "location" {
  description = "Azure region for all resources"
  type        = string
  default     = "eastus2"
}

variable "storage_account_name" {
  description = "Storage account name — must be globally unique"
  type        = string

  validation {
    condition     = length(var.storage_account_name) >= 3 && length(var.storage_account_name) <= 24
    error_message = "Storage account name must be between 3 and 24 characters."
  }
}

resource "azurerm_resource_group" "main" {
  name     = "rg-storage-demo"
  location = var.location
}

resource "azurerm_storage_account" "main" {
  name                     = var.storage_account_name
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  account_kind             = "StorageV2"

  blob_properties {
    versioning_enabled = true
  }
}

output "storage_account_id" {
  value = azurerm_storage_account.main.id
}

output "primary_blob_endpoint" {
  value = azurerm_storage_account.main.primary_blob_endpoint
}

Both snippets achieve the same result. The HCL version is slightly more verbose and requires explicit resource group management, but gains portability.


2. State Management: The Biggest Architectural Difference

This is where the two tools diverge most significantly, and where many teams underestimate the operational burden.

Bicep: No State File

Bicep leverages ARM deployment history as its source of truth. Run az deployment group create and ARM calculates the diff server-side. There is no .tfstate file to corrupt, lock, or secure.

# Deploy Bicep template to a resource group
az deployment group create \
  --resource-group rg-storage-demo \
  --template-file main.bicep \
  --parameters storageAccountName=mystorageacct2026 \
  --confirm-with-what-if

# Review deployment history (Bicep's implicit "state")
az deployment group list \
  --resource-group rg-storage-demo \
  --query "[].{Name:name, State:properties.provisioningState, Timestamp:properties.timestamp}" \
  --output table

The downside: ARM deployment history is capped at 800 deployments per resource group and doesn't track out-of-band changes with the same granularity as a Terraform plan.

Terraform: Explicit State with Remote Backend

Terraform's state file is powerful and dangerous. It tracks every resource's current attributes and enables precise terraform plan diffs. The operational requirement is a robust remote backend.

# backend.tf — Azure Storage Account remote backend (recommended for Azure teams)
terraform {
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "tfstate2026prod"
    container_name       = "tfstate"
    key                  = "storage-demo/terraform.tfstate"
    # Use Entra ID (AAD) authentication — avoid storage keys
    use_azuread_auth     = true
  }
}

State operational checklist:

  • Enable soft delete and versioning on the state storage account
  • Use Azure Blob lease-based locking (built into the AzureRM backend) to prevent concurrent runs
  • Never store the state file in Git
  • Rotate the state account's access controls with Azure RBAC, not shared keys

3. Architecture: How Deployments Flow

flowchart TD
    subgraph Bicep["Bicep Deployment Flow"]
        B1([Developer writes .bicep]) --> B2[bicep build\ncompiles to ARM JSON]
        B2 --> B3[az deployment group create\nor az deployment sub create]
        B3 --> B4[(Azure Resource Manager\nServer-side diff engine)]
        B4 --> B5[Resources provisioned\nin Azure]
        B4 --> B6[(ARM Deployment History\nimplicit state)]
    end

    subgraph Terraform["Terraform Deployment Flow"]
        T1([Developer writes .tf]) --> T2[terraform init\ndownloads providers]
        T2 --> T3[terraform plan\nlocal diff vs state]
        T3 --> T4{Approved?}
        T4 -- Yes --> T5[terraform apply]
        T4 -- No --> T1
        T5 --> T6[(Remote State Backend\nAzure Blob Storage)]
        T5 --> T7[AzureRM / AzAPI\nProvider calls ARM REST API]
        T7 --> T8[Resources provisioned\nin Azure]
    end

    style Bicep fill:#0078d4,color:#fff,stroke:#005a9e
    style Terraform fill:#7B42BC,color:#fff,stroke:#5a2d8e

4. Day-1 Resource Support and the AzAPI Escape Hatch

Azure releases dozens of new resource types and properties each month. Bicep, as a thin wrapper over ARM, picks these up as soon as the ARM API is GA — sometimes even in preview.

Terraform's AzureRM provider has historically lagged 2–6 weeks behind. The AzAPI provider solves this but at the cost of losing HCL-native resource schema validation.

# Using AzAPI to deploy a resource not yet in AzureRM
# Example: hypothetical Azure AI Foundry workspace (preview resource type)
resource "azapi_resource" "ai_foundry_workspace" {
  type      = "Microsoft.AIFoundry/workspaces@2026-03-01-preview"
  name      = "aifw-prod-001"
  location  = var.location
  parent_id = azurerm_resource_group.main.id

  body = {
    properties = {
      sku = {
        name = "Standard"
      }
      publicNetworkAccess = "Disabled"
    }
  }
}

Verdict: If your team regularly deploys cutting-edge Azure PaaS services (Azure AI Foundry, Azure Operator Nexus, new Fabric capacities), Bicep's Day-1 parity is a genuine productivity advantage.


5. Modules and Reusability

Azure Verified Modules (AVM) — Bicep

Microsoft now publishes and maintains Azure Verified Modules through the AVM programme. These are opinionated, security-hardened modules with consistent interfaces.

// Consuming an AVM module for Azure Container Registry
module acr 'br/public:avm/res/container-registry/registry:0.6.0' = {
  name: 'acrDeployment'
  params: {
    name: 'acrprodeastus001'
    location: location
    acrSku: 'Premium'
    zoneRedundancy: 'Enabled'
    exportPolicyStatus: 'disabled'
    quarantinePolicyStatus: 'enabled'
    retentionPolicyDays: 30
    retentionPolicyStatus: 'enabled'
  }
}

Terraform Registry

Terraform's public registry has a larger community module ecosystem, but quality varies significantly. Microsoft publishes AzureRM-backed AVM modules at Azure/ on the registry.

# Equivalent ACR deployment via Terraform AVM module
module "acr" {
  source  = "Azure/avm-res-containerregistry-registry/azurerm"
  version = "~> 0.4"

  name                = "acrprodeastus001"
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location
  sku                 = "Premium"

  zone_redundancy_enabled = true

  retention_policy = {
    days    = 30
    enabled = true
  }
}

Module ecosystem comparison:

Factor Bicep AVM Terraform Registry
Microsoft-maintained modules ✅ AVM programme ✅ Azure/ namespace on registry
Community modules Growing Very large (variable quality)
Private registry Azure Container Registry Terraform Cloud / self-hosted
Versioning OCI artifact tags Semantic versioning in registry

6. CI/CD Integration

Both tools integrate cleanly with GitHub Actions and Azure DevOps. Bicep has a slight edge for teams already invested in Azure DevOps, while Terraform has broader ecosystem support.

Bicep in GitHub Actions

# .github/workflows/deploy-bicep.yml
name: Deploy Bicep Infrastructure

on:
  push:
    branches: [main]
    paths: ['infra/**']

permissions:
  id-token: write   # Required for OIDC authentication
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Azure Login (OIDC — no secrets stored)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Bicep What-If (drift detection)
        uses: azure/arm-deploy@v2
        with:
          resourceGroupName: rg-storage-demo
          template: ./infra/main.bicep
          parameters: ./infra/main.bicepparam
          deploymentMode: Incremental
          additionalArguments: --what-if

      - name: Bicep Deploy
        uses: azure/arm-deploy@v2
        with:
          resourceGroupName: rg-storage-demo
          template: ./infra/main.bicep
          parameters: ./infra/main.bicepparam
          deploymentMode: Incremental

Terraform in GitHub Actions

# .github/workflows/deploy-terraform.yml
name: Deploy Terraform Infrastructure

on:
  pull_request:
    paths: ['infra/**']
  push:
    branches: [main]
    paths: ['infra/**']

permissions:
  id-token: write
  contents: read
  pull-requests: write  # For plan comments on PRs

jobs:
  plan:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4

      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "~1.9"

      - name: Terraform Init
        run: terraform init
        working-directory: ./infra
        env:
          ARM_USE_OIDC: true
          ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Terraform Plan
        run: terraform plan -out=tfplan
        working-directory: ./infra
        env:
          ARM_USE_OIDC: true
          ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

7. The OpenTofu Factor

HashiCorp's August 2023 relicensing of Terraform to BSL 1.1 created a fork — OpenTofu — under the Linux Foundation. By mid-2026, OpenTofu 1.9 supports:

  • All AzureRM and AzAPI provider features (providers are MPL-2.0 licensed, unaffected by the fork)
  • tofu test framework with full parity to terraform test
  • Native end-to-end encryption for state files (ahead of upstream Terraform)

Practical guidance: If your organisation has legal concerns about BSL 1.1, or if you're building an internal platform that redistributes Terraform modules commercially, evaluate OpenTofu. The migration is a sed away for most workloads.

# Check OpenTofu compatibility with existing Terraform configs
# OpenTofu is a drop-in: rename binary, run init
tofu init -upgrade
tofu plan

8. Pitfalls to Avoid

Bicep-specific:

  1. Exceeding the 800-deployment limit per resource group. ARM silently prunes old deployments but this can mask history. Use --name flags on deployments and establish a naming convention.
  2. Overusing existing resource references without understanding they do not validate that the resource exists at plan time — failures surface only at deploy time.
  3. Nesting modules more than 3 levels deep. Bicep module composition is powerful but deeply nested hierarchies become difficult to debug; keep orchestration flat.
  4. Ignoring @secure() decorator on sensitive parameters. Without it, parameter values appear in ARM deployment history in plaintext.

Terraform-specific:

  1. Storing state in local filesystem in CI/CD. A pipeline agent that terminates mid-apply with local state leaves resources in an unknown state. Always use a remote backend.
  2. Using terraform destroy in production without a prevent_destroy = true lifecycle block on critical resources. Add it to your storage accounts, databases, and key vaults on day one.
  3. Committing .tfvars files with secrets to source control. Use environment variables (TF_VAR_*) or a secrets manager integration instead.
  4. Skipping terraform plan review in PR workflows. Automate plan output as a PR comment so reviewers see infrastructure changes alongside code changes.
  5. Ignoring provider version pinning. A ~> 4.0 constraint is acceptable; version = ">= 3.0" in a shared module will eventually break consumers on a major version boundary.

9. Decision Framework: Which Tool Should You Choose?

flowchart TD
    A([Start: New IaC project on Azure]) --> B{Do you manage\nnon-Azure resources\nin the same workflow?}
    B -- Yes --> C{AWS, GCP, Kubernetes,\nSaaS APIs?}
    C -- Yes --> D[Terraform / OpenTofu\nwith AzureRM + relevant providers]
    C -- No, just Azure\nplus minor SaaS --> E{BSL 1.1 a legal\nconcern?}
    E -- Yes --> F[OpenTofu with AzureRM provider]
    E -- No --> G[Terraform with AzureRM provider]
    B -- No, Azure only --> H{Team already has\nTerraform expertise?}
    H -- Yes, significant investment --> I[Continue Terraform\nor migrate to OpenTofu]
    H -- No / Greenfield --> J{Need Day-1 preview\nresource support?}
    J -- Yes --> K[Bicep ✅ Recommended default]
    J -- No --> L{Prefer no state\nfile management?}
    L -- Yes --> K
    L -- No / Want plan diffs --> M[Terraform / OpenTofu\nwith AzureRM]

    style K fill:#107c10,color:#fff,stroke:#0a5c0a
    style D fill:#7B42BC,color:#fff,stroke:#5a2d8e
    style F fill:#0078d4,color:#fff,stroke:#005a9e
    style G fill:#7B42BC,color:#fff,stroke:#5a2d8e

Troubleshooting

Bicep

Error Cause Fix
BCP034: The enclosing array expected an item of type... Type mismatch in array property Check the ARM API reference for the expected type; use bicep decompile on a working ARM template to inspect correct structure
Deployment limit reached (800) Resource group deployment history full Run az deployment group delete in bulk or set auto-delete policy; establish a deployment naming convention to prune predictably
BCP057: The name "X" does not exist in the current context Module output referenced before module completes Ensure dependsOn is set, or reference the module output correctly with moduleName.outputs.outputName
What-If shows unexpected changes on redeployment ARM resource defaults being written back into state Use @description and review ignore patterns; some ARM defaults (like encryption settings) are written server-side

Terraform

Error Cause Fix
Error: A resource with the ID already exists Resource created out-of-band; state doesn't know about it Run terraform import to bring it under management before applying
Error acquiring the state lock Abandoned lock from a failed pipeline run Use terraform force-unlock <LOCK_ID> with caution after confirming no concurrent run is active
Provider produced inconsistent result after apply AzureRM provider bug or API eventual consistency Pin provider to a known-good version; add depends_on to force ordering; check GitHub issues for the AzureRM provider
Backend configuration changed Backend block modified without re-initialisation Run terraform init -reconfigure or terraform init -migrate-state
Plan shows resource replacement for cosmetic change Terraform treating update-in-place property as ForceNew Check provider release notes; upgrade provider version; use lifecycle { ignore_changes = [...] } as a last resort

Key Takeaways

  • Bicep is Azure's first-party IaC language and is the lowest-friction choice for Azure-only teams — Day-1 resource support, no state file, native ARM integration.
  • Terraform's superpower is provider breadth: if you're orchestrating Azure + AWS + Datadog + Kubernetes from one codebase, HCL pays for itself.
  • The BSL 1.1 licence change is material for commercial platform teams; OpenTofu is a production-ready, drop-in alternative that deserves evaluation before new Terraform commitments.
  • State management is Terraform's biggest operational overhead — design your remote backend, locking, and RBAC strategy before your first terraform apply in production.
  • AVM modules raise the quality floor for both tools: prefer AVM-published modules over community modules of unknown quality for production Azure deployments.
  • Both tools support OIDC-based authentication — there is no reason to store service principal secrets in CI/CD pipelines in 2026.
  • They are not mutually exclusive: using Bicep for Azure resource provisioning and Terraform for platform-level orchestration (DNS, CDN origins, monitoring alerts) is a legitimate, well-proven pattern.

Next Steps

  1. Install the Bicep VS Code extension and run az bicep install to get the latest compiler — then decompile an existing ARM template with az bicep decompile to see how your existing resources would look in Bicep.
  2. Audit your Terraform licence exposure — run a review of your internal modules and distribution patterns against the BSL 1.1 terms; engage legal if you embed Terraform in a commercial product.
  3. Implement Azure Verified Modules for your highest-risk resource types (Key Vault, Storage, SQL) to inherit Microsoft's security baseline and reduce your own module maintenance burden.
  4. Set up a branch protection rule requiring terraform plan or az deployment group create --what-if output as a required PR check in your platform team's repository.
  5. Evaluate OpenTofu 1.9 in a non-production workload — the migration is low-risk and builds institutional knowledge before a potential forced migration.

Related Articles

  • [Azure Landing Zones with Bicep: Enterprise-Scale Deployment Guide]/azure/landing-zones-bicep-enterprise-scale/
  • [Terraform Remote State on Azure: Secure Backend Patterns for Platform Teams]/azure/terraform-remote-state-azure-secure-patterns/
  • [OpenTofu vs Terraform in 2026: Migration Guide and Feature Comparison]/devops/opentofu-vs-terraform-migration-guide/
Thorsteinn Halldorsson Senior Cloud Engineer

Senior Cloud Engineer with 25+ years of hands-on experience across the datacenter-to-cloud stack: fiber SAN and disk storage, IBM/Lenovo blade and Dell/HP/Lenovo servers, Hyper-V and VMware clusters, and SQL and Remote Desktop Services (RDS) clusters. Deep in the Microsoft platform — Active Directory, PKI/certificate services, SQL, Power BI, Dynamics 365 Business Central (NAV) and AX (Axapta), Microsoft 365, Entra, and Intune — with a focus on Azure operations, FinOps, and applying AI tools like GitHub Copilot and Claude in real workflows. Writes practical, no-nonsense guides for IT professionals who need to ship real solutions.

Leave a Reply

Your email address will not be published. Required fields are marked *