Lina Brihoum
DevSecOps

Terraform vs Bicep

Terraform vs Bicep
9 min read
DevSecOps

Comparing Microsoft Bicep and HashiCorp Terraform: A Deep Dive

Introduction

The ability to deploy and manage infrastructure efficiently and reliably has never been more crucial, and Infrastructure as Code (IaC) is how teams get there: cloud environments defined in version-controlled files instead of console clicks. Microsoft Bicep and HashiCorp Terraform are two of the leading tools in this space, and most comparisons between them stop at "Bicep is Azure-only, Terraform is multi-cloud." That's true, and it's also the least interesting difference between them.

The interesting difference is architectural: these two tools have fundamentally different answers to the hardest question in infrastructure as code — how do you know what actually exists? Terraform answers it with a state file it maintains itself. Bicep answers it by delegating to Azure Resource Manager (ARM), the control plane that already knows. Almost every operational property of both tools — how they detect drift, how they handle renames, what breaks and how you fix it — follows from that one design decision. This post digs into both.

Overview of Microsoft Bicep

Bicep is a domain-specific language that deploys Azure resources declaratively. It is best understood as a language, not an engine: every Bicep file is transpiled into an ARM template (JSON), and the deployment itself is executed by Azure Resource Manager — the same control plane that processes every portal click and CLI command. Anything ARM can deploy, Bicep can deploy, on day one of the feature's release. That "day zero support" claim is structural, not marketing: there is no provider plugin waiting to be updated, because there is no provider layer at all.

If you have written raw ARM templates, Bicep's reason for existing is immediately visible — it replaces hundreds of lines of JSON with type-checked, IntelliSense-assisted syntax that reads like configuration rather than an escape room:

Bicep
main.bicep
param location string = resourceGroup().location
param appName string
 
resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: '${appName}-plan'
  location: location
  sku: { name: 'P1v3' }
}
 
resource appService 'Microsoft.Web/sites@2023-12-01' = {
  name: appName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
  }
}

Note the details that matter in practice: every resource pins an API version (@2023-12-01), which makes behavior explicit and upgrades deliberate; dependencies between resources are inferred from references (appServicePlan.id), so ordering is derived rather than hand-declared; and parameters with defaults make the same file reusable across environments.

No State File — Which Is Both the Feature and the Limitation

Bicep has no state file because ARM is the state — Azure's control plane knows what exists because it created it. This eliminates an entire category of operational pain that Terraform teams know well (state locking, state corruption, secrets in state, "who has the state bucket credentials"). Before deploying, what-if asks ARM to compute the delta between your template and reality — Bicep's answer to terraform plan.

But the delegation has sharp edges worth knowing precisely. ARM deployments default to incremental mode: resources in the template are created or updated, but resources that exist in the resource group and are absent from the template are left untouched. Delete a resource from your Bicep file, deploy, and the resource is still running — and still billing. Terraform, by contrast, treats absence as intent to destroy. This single difference is the biggest mental-model adjustment when moving between the tools, and unnoticed orphaned resources are its recurring cost. Microsoft's answer is deployment stacks, which track a deployment's resources as a managed set and can delete what falls out of the template — closing the gap by, in effect, reintroducing a lightweight notion of state on the Azure side.

Tooling and Ecosystem

Bicep's integration story is its strongest suit: first-class VS Code support (validation, completion, types for every resource provider), native support in Azure CLI and Azure DevOps/GitHub Actions tasks, bicep decompile for converting existing ARM JSON, and Azure Verified Modules — Microsoft-maintained reference modules for standard architecture patterns. Where ARM's type system lags a preview feature, the escape hatch is deploying that one resource by raw API contract while everything around it stays typed.

Overview of HashiCorp Terraform

Terraform is an IaC tool that defines cloud and on-prem resources in HCL (HashiCorp Configuration Language) and manages their full lifecycle through an explicit pipeline: terraform plan computes a diff between your configuration, its state file, and (via refresh) real-world resources; terraform apply executes exactly that plan. The provider model is the multiplier: thousands of providers make "Terraform" really mean "one workflow for AWS, Azure, GCP, Cloudflare, Datadog, GitHub, Kubernetes, and your DNS registrar" — infrastructure and the SaaS tooling around it, managed with one language, one review process, one audit trail.

Terraform
main.tf
resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
}
 
resource "aws_ebs_volume" "example" {
  availability_zone = "us-west-2a"
  size              = 1
}
 
resource "aws_volume_attachment" "ebs_att" {
  device_name = "/dev/sdh"
  volume_id   = aws_ebs_volume.example.id
  instance_id = aws_instance.example.id
}

State: The Power and the Tax

Terraform's state file maps every resource block to a real-world resource ID, and it is what makes Terraform's strongest behaviors possible: precise diffs without trusting the cloud's own reporting, deletion-on-absence, import for adopting existing infrastructure, and refactoring primitives (moved blocks to rename without destroy/recreate, removed blocks to forget without deleting). Mature teams lean on these constantly — the ability to restructure a two-year-old codebase without touching a single running resource is not a luxury at scale.

The tax is equally real, and anyone running Terraform in production learns its line items. State must live in a remote backend with locking (an S3 bucket with DynamoDB locking, an Azure storage account with blob leases) or two engineers applying at once will corrupt it. State contains secrets in plaintext — database passwords, generated keys — so the backend is a security boundary and must be treated as one. And state can diverge from reality: a resource deleted in the console leaves a dangling state entry; an emergency change made by hand becomes drift that the next plan proposes to revert, possibly at the worst moment. Operating Terraform well means operating its state deliberately — which is a genuine ongoing cost that Bicep users simply don't pay.

Modules and the Registry

Terraform's module system is the strongest in the IaC space: versioned, composable, publishable to a public registry with thousands of community modules and to private registries for organizational standards. The pattern that scales is a platform team publishing opinionated modules ("our VNet," "our AKS cluster," with security defaults baked in) and product teams consuming them — infrastructure governance delivered as a package manager rather than a review meeting.

Key Differences

State and Drift

  • Bicep delegates state to ARM: no file to manage, no locking, no secrets-in-state problem — but incremental deployments won't remove deleted resources (without deployment stacks), and what-if has known blind spots with some resource types where it reports noise or misses changes.
  • Terraform owns its state: exact diffs, deletion-on-absence, imports and refactoring — paid for with backend management, locking discipline, and drift reconciliation as an operational routine.

Language and Refactoring

  • Bicep is deliberately small and Azure-shaped: typed resource declarations, modules, user-defined types in recent versions. Renaming a resource symbol is free (ARM correlates by resource name in Azure, not by file symbol).
  • Terraform's HCL is a larger language — for_each, count, dynamic blocks, rich functions — powerful enough to build real abstractions and to build unreadable ones. Renaming a resource block is a destroy/recreate unless you write a moved block, because identity lives in the state's address, not in Azure's name.

Preview Fidelity

  • Bicep's what-if is good and improving, but its accuracy depends on each resource provider's implementation.
  • Terraform's plan is the gold standard of the discipline — a saved plan file can be reviewed, approved, and applied exactly as reviewed, which is the backbone of most infrastructure change-management processes built on it.

Scope of Control

  • Bicep manages what ARM manages. Your DNS provider, monitoring SaaS, and GitHub org are out of scope.
  • Terraform manages anything with a provider — which in practice means the whole estate around the cloud, not just the cloud.

Choosing Between Them

The honest decision tree is short. All-in on Azure, Microsoft-aligned team, low appetite for state operations → Bicep, without hesitation: day-zero resource support, zero state infrastructure, and Microsoft support across the whole chain. Anything multi-cloud, or an estate where DNS/monitoring/SaaS belong in code too → Terraform: the provider ecosystem and refactoring machinery are unmatched, and the state tax is a known, manageable cost. Large Azure-heavy enterprises frequently land on both deliberately — Terraform for the cross-cutting platform layer (identity, networking, policy across clouds and vendors), Bicep for Azure application workloads where its velocity shines. That split is not indecision; it is putting each tool where its architecture is the advantage.

One more option worth naming: teams that want Terraform's workflow with an open-source license have OpenTofu, the community fork created after HashiCorp's 2023 license change — drop-in compatible today, and increasingly a line item in this comparison.

Conclusion

Bicep and Terraform are both excellent, and the choice between them is really a choice about where the source of truth lives. Bicep trusts Azure Resource Manager to know what exists — eliminating state operations entirely, at the cost of Azure-only scope and the incremental-deployment orphan problem. Terraform maintains its own truth — buying exact plans, deletion-on-absence, imports, and refactoring across a thousand providers, at the cost of running state as a small production system of its own. Teams that understand that trade pick correctly on the first try; teams that compare feature checklists usually end up learning it the expensive way. Know where your truth lives, and the rest of the decision follows.