If you're managing Azure infrastructure today, you've almost certainly faced this question: Bicep or Terraform?
For years the answer was straightforward. Terraform was the multi-cloud heavyweight with a mature ecosystem. Bicep was Microsoft's young challenger — promising, but lacking real-world polish. Fast-forward to 2026, and the picture has shifted. Bicep has gone from promising upstart to genuinely production-hardened. Terraform has weathered licensing turbulence, the OpenTofu fork, and shifting community dynamics.
This article isn't a "which is better" flamewar. It's a practical guide covering where each tool excels, where it falls short, and — most importantly — a proven pattern for running both in the same organisation during migration periods.
The State of Play in 2026
Bicep (current 1.x-era CLI releases)
Bicep is Microsoft's DSL for Azure Resource Manager. It transpiles to ARM JSON but spares you writing that JSON by hand. What's changed recently:
- Azure Verified Modules (AVM) have reached critical mass for common Azure patterns. Microsoft lists 170+ published Bicep resource modules, with additional proposed modules in progress. AVM should be your default starting point for standard Azure resources, while niche services may still require direct resource declarations.
- Deployment remains Azure Resource Manager based. The supported deployment path is still Azure CLI or Azure PowerShell, for example
az deployment group createandaz deployment group what-if. The Bicep CLI focuses on build, decompile, publish, restore, lint, format, and newer local validation features such as snapshots. - Language improvements include user-defined types, compile-time loops, full ternary expressions, and module scoping that mirrors Azure resource hierarchies.
- CI/CD tooling is mature: GitHub Actions, Azure DevOps, and GitLab CI can all run Azure CLI/Bicep CLI workflows. Avoid relying on marketplace popularity claims; the important point is that Bicep deployments fit cleanly into standard pipeline runners.
Terraform / OpenTofu (current 1.x releases)
Terraform remains the undisputed leader for multi-cloud infrastructure. Despite the 2023 BSL licensing controversy:
- AzureRM provider v4.x is the current major line for Azure resource management. Treat provider upgrades as engineering work: read the v4 upgrade guide, pin versions, and test plans before production rollout.
- HCP Terraform is a common enterprise remote execution platform, offering policy-as-code, cost estimation, and managed state workflows.
- OpenTofu remains a viable alternative for teams that cannot accept BSL terms. The projects are diverging; for example, OpenTofu 1.8 introduced early evaluation and provider mocking, while Terraform continues to expand its enterprise and HCP workflow capabilities.
- Terraform Stacks have moved from beta to general availability in HashiCorp's documentation and are designed to coordinate infrastructure lifecycle at scale. They are useful for multi-component orchestration, but they should still be evaluated against your current HCP Terraform or Terraform Enterprise operating model.
- The module ecosystem remains enormous: the Terraform Registry exposes more than 20,000 modules overall and more than 2,000 AzureRM modules, with Azure-specific coverage that can still be stronger than AVM for niche services.
Practical Code: Deploy the Same Thing in Both
Let's compare concrete infrastructure. I'll deploy a standard pattern: a resource group, a storage account with network rules, and an App Service plan and app with a system-assigned managed identity.
Bicep
// main.bicep
param environment string = 'dev'
param location string = resourceGroup().location
var namePrefix = 'bicepdemo-${environment}'
var tags = {
environment: environment
managedBy: 'bicep'
project: 'demo-2026'
}
// AVM module for storage account
module storage 'br/public:avm/res/storage/storage-account:0.22.1' = {
name: '${namePrefix}-sa'
params: {
name: '${namePrefix}sa'
location: location
tags: tags
kind: 'StorageV2'
sku: 'Standard_GRS'
minimumTlsVersion: 'TLS1_2'
networkAcls: {
bypass: 'AzureServices'
defaultAction: 'Deny'
ipRules: []
virtualNetworkRules: []
}
blobProperties: {
deleteRetentionPolicy: { days: 7; enabled: true }
containerDeleteRetentionPolicy: { days: 7; enabled: true }
}
containers: [
{ name: 'appdata'; publicAccess: 'None' }
{ name: 'logs'; publicAccess: 'None' }
]
}
}
// AVM module for App Service
module appService 'br/public:avm/res/web/site:0.15.0' = {
name: '${namePrefix}-app'
params: {
name: '${namePrefix}-app'
location: location
tags: tags
kind: 'app'
serverFarmResourceId: appServicePlan.outputs.resourceId
siteConfig: {
alwaysOn: true
minTlsVersion: '1.2'
ftpsState: 'FtpsOnly'
appSettings: [
{ name: 'STORAGE_CONNECTION_STRING'; value: storage.outputs.connectionStrings[0] }
{ name: 'WEBSITE_RUN_FROM_PACKAGE'; value: '1' }
]
}
identity: { type: 'SystemAssigned' }
}
}
// Direct resource declaration when AVM doesn't have a module
resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: '${namePrefix}-plan'
location: location
tags: tags
sku: { name: 'P1v3'; tier: 'PremiumV3' }
}
Key observations about the Bicep approach:
- AVM modules are consumed via
br/public:avm/res/...from the public Bicep module registry, and module versions are pinned explicitly. Public AVM modules do not require private registry authentication. - The language is declarative but Azure-native — resource types use exact ARM API version strings (
Microsoft.Web/serverfarms@2023-12-01). This gives you perfect fidelity with the Azure control plane but zero portability. - No state management. There is no
terraform stateequivalent. Bicep relies entirely on Azure Resource Manager's resource tracking. This is both a blessing (no state file to secure) and a curse (noterraform destroymodelling outside of deployment scopes). - Azure deployment what-if is close to
terraform plan, but it is executed through ARM deployment commands such asaz deployment group what-ifand works per deployment scope rather than per Terraform state.
Terraform
# main.tf
terraform {
required_version = ">= 1.15"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {
storage { data_plane_available = true }
}
}
variable "environment" {
type = string
default = "dev"
}
locals {
name_prefix = "tfdemo-${var.environment}"
tags = {
environment = var.environment
managed_by = "terraform"
project = "demo-2026"
}
}
resource "azurerm_resource_group" "main" {
name = "${local.name_prefix}-rg"
location = "Southeast Asia"
tags = local.tags
}
resource "azurerm_storage_account" "main" {
name = "${local.name_prefix}sa"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "GRS"
min_tls_version = "TLS1_2"
tags = local.tags
network_rules {
default_action = "Deny"
bypass = ["AzureServices"]
}
}
resource "azurerm_storage_container" "appdata" {
name = "appdata"
storage_account_name = azurerm_storage_account.main.name
container_access_type = "private"
}
resource "azurerm_storage_container" "logs" {
name = "logs"
storage_account_name = azurerm_storage_account.main.name
container_access_type = "private"
}
resource "azurerm_service_plan" "main" {
name = "${local.name_prefix}-plan"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
os_type = "Linux"
sku_name = "P1v3"
tags = local.tags
}
resource "azurerm_linux_web_app" "main" {
name = "${local.name_prefix}-app"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
service_plan_id = azurerm_service_plan.main.id
tags = local.tags
site_config {
always_on = true
ftps_state = "FtpsOnly"
min_tls_version = "1.2"
application_stack { node_version = "18-lts" }
}
app_settings = {
"STORAGE_CONNECTION_STRING" = azurerm_storage_account.main.primary_connection_string
"WEBSITE_RUN_FROM_PACKAGE" = "1"
}
identity { type = "SystemAssigned" }
}
Key observations about the Terraform approach:
- Provider abstraction means the same HCL skills transfer to AWS, GCP, or on-prem. The Terraform lifecycle (
init → plan → apply) is identical across clouds. - Explicit state via
terraform.tfstateor a remote backend. State is the source of truth — Terraform reconciles your config against state, not directly against Azure. This is powerful (drift detection, partial updates, targeted destroys) but demands discipline around locking, backups, and access control. terraform plangives you a detailed diff of every resource attribute that will change, with no side effects — machine-readable and reviewable.- The module registry offers community modules that abstract entire stacks. AVM modules also exist for Terraform — AVM is provider-agnostic.
The Decision Matrix
Here is the framework I use with teams. Score honestly — the answer is rarely "always" for either side.
| Criterion | Bicep | Terraform / OpenTofu |
|---|---|---|
| Cloud scope | Azure only | Multi-cloud (AWS, GCP, OCI, on-prem) |
| Learning curve | Shallow for Azure devs | Moderate; HCL is its own language |
| State management | None (ARM manages state) | Required (state file/backend must be managed) |
| Drift detection | Azure deployment what-if at deployment scope | terraform plan from state diff; richer |
| Module ecosystem | AVM (mature, Microsoft-verified) | Terraform Registry (vast, community + verified) |
| CI/CD integration | Native in GitHub/Azure DevOps | Excellent everywhere via CLI |
| Team permissions | Uses RBAC directly; no extra infra | State backend needs its own RBAC |
| Policy enforcement | Azure Policy (built-in, deploy-time) | Sentinel/OPA (separate config) |
| Licensing | MIT (open source) | BSL 1.1 (IBM/HashiCorp); OpenTofu is MPL-2.0 |
| Maturity | 4+ years, production-ready | 10+ years, battle-tested |
| Complex logic | Limited (loops, conditions, ternary) | Rich (count, for_each, dynamic blocks, functions) |
| Resource lifecycle | ARM-native (no destroy tracking) | Full lifecycle (CRUD + destroy) |
When to Pick Bicep
- Azure-only shop. If your org is all-in on Azure with no near-term multi-cloud plans, Bicep is the simplest path. No state backend to manage, no extra RBAC for Terraform storage, and Microsoft's engineering team fixes bugs in the language when they're ARM bugs too.
- Azure Policy compliance is critical. Bicep deployments are natively evaluated against Azure Policy at deploy time. Terraform requires Sentinel (HCP Terraform) or a separate OPA layer.
- You hate managing state files. There is no
terraform state rm, no import gymnastics, no state migration drama. The mental overhead is meaningfully lower. - Small-to-medium Azure deployments. For a team managing 10–50 resource groups, Bicep is faster to write, faster to deploy, and easier to onboard new engineers onto.
When to Pick Terraform
- Multi-cloud or hybrid cloud. Azure + AWS, or Azure + GCP + on-prem. Terraform (or OpenTofu) is the only tool that gives you a unified configuration language across all of them.
- Complex dependency graphs. Terraform's
for_each,count,dynamicblocks, anddepends_onhandle conditional infrastructure in ways Bicep can't quite match. - You need
terraform destroy. In ephemeral environments (PR branches, preview environments, sandboxes), the ability to destroy an entire environment with one command is a killer feature. Bicep has no built-in destroy lifecycle. - Existing Terraform investment. 200+ Terraform modules, an established state backend, engineers who think in HCL — there is zero business case to migrate. Bicep gains are marginal; rip-and-replace cost is real.
Making Them Coexist: The Hybrid Pattern
Here's the pattern I've seen work successfully at several Malaysian enterprises. It acknowledges that most orgs don't have a greenfield choice — you have Bicep in some teams and Terraform in others, often from M&A inheritance, organic team preference, or a partial migration.
The Layer Cake Architecture
┌──────────────────────────────────────────┐
│ Terraform (multi-cloud orchestration) │
│ - Network connectivity / hub-spoke │
│ - DNS zones / global resources │
│ - Cross-cloud VPN / Private Link │
├──────────────────────────────────────────┤
│ Bicep + AVM (Azure workload owners) │
│ - App Service / AKS / Functions │
│ - Storage / SQL / Redis │
│ - Workload-specific networking │
├──────────────────────────────────────────┤
│ Terraform (shared services / landing) │
│ - Subscription vending / Mgmt groups │
│ - Policy / RBAC base │
│ - Log Analytics / Sentinel │
└──────────────────────────────────────────┘
How this works in practice:
The bottom Terraform layer provisions the Azure landing zone — management groups, subscription orchestration, base policies, and connectivity resources (hub VNet, Azure Firewall, ExpressRoute). This is a slow-moving layer updated quarterly.
The middle Bicep layer is owned by individual workload teams. They use AVM modules to deploy their application infrastructure. Because the landing zone is already in place, they just need resourceGroup().location and a param for their VNet ID. No state file coordination needed — Azure RBAC handles access control, and ARM sees each deployment as an independent transaction.
The top Terraform layer manages global connectivity. If the organisation also runs workloads in AWS, this layer manages cross-cloud transit gateway attachments, DNS resolution across clouds, and Private Link endpoints bridging Azure and on-prem.
What Makes This Work
Explicit boundaries. Each layer owns distinct Azure resources with no overlap. Landing zone Terraform never touches a storage account's retention policy — that's the workload team's job. Bicep layers never create a VNet or management group.
Azure Policy prevents overlap. Deny assignments at the management group level prevent accidental resource creation outside agreed boundaries. A workload team's Bicep pipeline literally cannot create a VNet because policy rejects it.
State isolation. Terraform manages its own state for layers 1 and 3. Bicep has no state. There is no shared state file, no remote state data source cross-contamination, and no accidental terraform destroy taking out the production app along with the network.
Migration Path: Terraform to Bicep
If you've decided to move workload modules from Terraform to Bicep (a choice accelerating in 2025–2026 for Azure-only teams):
- Start with stable resources — storage accounts, service plans. Avoid anything with active daily changes.
bicep decompileyour existing ARM templates (extract ARM JSON from a recent apply, runbicep decompile template.json). The output won't be pretty — expect hand editing — but it saves typing from scratch.- Validate against existing resources with
az deployment group what-ifor the equivalent subscription/management-group scope command. When clean, deploy withaz deployment group create. ARM does not simply skip existing resources; it reconciles them to the declared template, so review what-if output carefully before applying. - Retire Terraform state with
terraform state rmon the migrated resources. Do NOTterraform destroy— that deletes them. - Repeat incrementally. One resource type at a time. A weekend cut-over is a recipe for a Monday outage.
Pitfalls to Watch For
Bicep-Specific
"It's just ARM, so drift is invisible." Bicep has no persistent state, so manual changes in the portal are invisible until you run Azure deployment what-if again. Unlike Terraform, there's no terraform refresh. Solution: Use Azure Policy for guardrails and schedule az deployment group what-if or the equivalent scope command in CI to detect drift.
Module version management. AVM modules release frequently. Pinning to an old version misses bug fixes; using latest risks breaking changes. Solution: Use Dependabot or Renovate for automated dependency PRs, just like npm or NuGet packages.
Incremental vs. Complete mode. Azure deployments default to incremental mode. Complete mode can delete resources in scope that are not declared in the template, which surprises teams from Terraform's surgical model. Solution: Avoid Complete mode in production unless you have a tightly controlled deployment scope and explicit deletion intent.
Terraform-Specific
State file leaks. Terraform state contains resource IDs and can contain sensitive values, depending on the resources and providers used. A committed .tfstate in a public repo is a serious exposure. Solution: Use remote backends with encryption at rest, restrict state access, and add *.tfstate to .gitignore.
count vs. for_each pain. Switching a resource from count to for_each destroys and recreates all instances. Solution: Use for_each by default — map keys provide stable state addresses.
Provider version drift. terraform init -upgrade can pull a provider version that changes API schema and forces unwanted resource recreations. Solution: Pin provider versions with ~> X.Y and commit .terraform.lock.hcl.
Hybrid-Specific
Cross-layer dependencies. Terraform manages the VNet; Bicep deploys the app. A new subnet for Private Endpoints needs two PRs and two pipelines. Solution: Use Azure Policy to create an "allowed subnets" convention rather than hardcoded IDs. Or use Terraform data sources to export subnet IDs for Bicep parameters.
Two teams, one certificate. An App Gateway in Terraform needs its TLS cert from a Key Vault managed by Bicep — chicken-and-egg. Solution: Bootstrapping: Terraform creates the Key Vault in the landing zone, then Bicep populates the secrets. Track this formally in your architecture decision record.
Conclusion
The Bicep-versus-Terraform debate in 2026 is a false binary. Both tools are production-grade, both have thriving ecosystems, and both serve you well for Azure infrastructure. The right choice depends on your team's context — not GitHub stars.
For Azure-only teams tired of state file management, Bicep + AVM is now a complete solution. For multi-cloud organisations, Terraform (or OpenTofu) remains the clear winner. And for the messy middle where most of us live, the hybrid layer-cake pattern lets you have both without the operational debt of a forced migration.
The real skill isn't picking the "best" tool. It's understanding your boundaries well enough to let each tool do what it does best.
Key Takeaways
- Bicep in 2026 is production-ready for Azure-only workloads. AVM modules, Azure CLI/PowerShell deployment workflows, and no separate state backend make it a low-friction option for teams committed to Azure.
- Terraform remains essential for multi-cloud. No other tool gives you unified configuration language, lifecycle management, and state tracking across Azure, AWS, GCP, and on-prem.
- You don't have to choose one. The layer-cake pattern — Terraform for landing zones and cross-cloud connectivity, Bicep for workload teams — isolates state, responsibilities, and failure domains.
- Plan for state, whatever you pick. Bicep trades state management for drift invisibility; Terraform trades simplicity for state complexity. Neither is free.
- Start with a decision matrix, not a preference. Run your workloads through the criteria in this article. The answer will be different for a startup building on Azure-only vs. a mature enterprise spanning three cloud providers — and that's okay.
Law Wen Feng is a Principal Solution Architect based in Malaysia, specialising in cloud infrastructure, DevOps, and platform engineering. He writes about infrastructure-as-code, cloud architecture, and the engineering practices that make teams productive. Views are his own.
Got an IaC decision to make or a migration from Terraform to Bicep (or vice versa) on your roadmap? I help Malaysian enterprises design their infrastructure tooling strategy and review landing zone architectures. Connect on LinkedIn.