Infrastructure as Code (IaC) defines infrastructure—VMs, networks, databases, load balancers, DNS, and clusters—in version-controlled machine-readable files. That makes intended changes reviewable and repeatable within the declared inputs, provider version, credentials, quotas, and external service state. It does not make development and production identical: regions, data, capacity, policy, and provider-side defaults can still differ.

Why IaC

  • Bounded reproducibility — the definition records intended resources and inputs; lock providers and modules, then verify plans because external APIs, defaults, quotas, and existing data still affect the result.
  • Version control — infrastructure changes go through Git: diffs, code review, blame, and rollback to a known-good state.
  • Surfaces and repairs drift — refresh and plan can reveal supported out-of-band changes; apply can reconcile declared fields, but ignored attributes, external systems, and provider limitations remain.
  • Disaster recovery and scale — definitions can recreate managed resources and repeated node shapes, provided data recovery, secrets, regional dependencies, and capacity are handled separately.
  • Documentation by definition — the code is the always-current source of truth for what exists.

Declarative vs Imperative

The central distinction in IaC tools:

  • Declarative (“what”) — you describe the desired end state and the tool computes create/update/delete operations to converge. Reapplying is intended to produce no change when inputs and observed external state are unchanged, but provider side effects, unknown values, and non-idempotent APIs can still violate that expectation. Terraform, Bicep, CloudFormation, Pulumi, and Kubernetes manifests use declarative models.
  • Imperative (“how”) — you write the sequence of commands to execute. An imperative program can be idempotent when it checks and converges state explicitly, but that behavior is the program author’s responsibility rather than a property of the syntax.

Declarative tools are a useful default because a computed diff makes intended actions inspectable. A plan reduces surprise; it does not make replacement, deletion, or provider behavior automatically safe.

# Terraform (declarative): describe the desired resource; Terraform computes the actions
resource "aws_s3_bucket" "assets" {
  bucket = "myapp-assets-prod"
  tags   = { Environment = "prod" }
}
// Bicep (declarative, Azure-native): same idea, transpiles to ARM JSON
resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: 'myappassetsprod'
  location: resourceGroup().location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
}

State and the Plan/Apply Loop

Terraform first refreshes its view of remote objects, builds a dependency graph from configuration and provider relationships, and produces a plan. Review that plan as an executable change set: replacements, deletes, and provider-version changes deserve explicit attention. Apply the saved plan, not a freshly recomputed one, when the approval must bind to exact operations.

Store state in a remote backend with encryption, access control, versioning, and locking. State is operational data and can contain secrets; it is not ordinary source code. If an apply is interrupted, inspect the real resource and state before retrying. Use import, state mv, or provider-specific recovery only after backing up state. Never “fix” drift by editing the state JSON by hand.

Declarative tools track what they’ve created in a state file that maps your code to real cloud resources. The workflow:

  1. Plan — diff desired config against current state → a preview of what will be created/changed/destroyed. The safety gate; review it like a PR.
  2. Apply — execute the plan; update state.

State is the crux of Terraform-style tools (Bicep/ARM and CloudFormation keep state on the provider side instead). Mismanaged state is the most common source of IaC pain:

  • Remote, locked state — store state in a shared backend with access control, versioning, and supported locking. For Terraform’s S3 backend, set use_lockfile = true; DynamoDB-based locking is deprecated. Never commit terraform.tfstate to Git because it contains resource details and can contain secrets.
  • Drift — if someone changes a resource in the console, real state diverges from the file; the next plan shows the drift so you can re-converge.

The Tool Landscape

ToolScopeLanguageNotes
Terraform / OpenTofuMulti-cloudHCL (declarative)Broad provider coverage; verify provider maturity for each required resource. OpenTofu is an independently governed fork.
BicepAzureDSL → ARMAzure-native, no state file to manage (state lives in Azure).
AWS CloudFormation / CDKAWSYAML / real languages (CDK)AWS-native; CDK lets you author in TypeScript/C#/Python.
PulumiMulti-cloudReal languages (C#, TS, Python, Go)Declarative model authored in general-purpose code.
AnsibleConfig mgmtYAMLMore config management than provisioning; often paired with the above.

NOTE

Provisioning vs configuration management. Terraform/Bicep provision infrastructure (create the VM, network, DB). Tools like Ansible/Chef/Puppet configure what’s inside (install packages, set files). They’re complementary — provision with one, configure with the other — though containers + Kubernetes increasingly fold configuration into immutable images.

Provisioning, Configuration, Orchestration, and GitOps

BoundaryDeclared resultTypical failureDecision rule
Image buildVersioned application plus runtime filesystemMutable or unscanned artifactBuild once and address by digest
ProvisioningNetworks, clusters, databases, identitiesDrift or destructive replacementRequire a reviewed plan
Machine configurationPackages, files, and services converge on hostsSnowflake hosts or non-idempotent runsPrefer immutable images when replacement is cheap
OrchestrationRuntime units are scheduled and reconciledReadiness, capacity, or lifecycle mismatchUse it only for long-running runtime state
Application configurationEnvironment-specific values reach the processSecret leakage or staging/production driftValidate schema and inject at runtime
GitOpsA controller continuously reconciles declared cluster stateBad Git state propagates automaticallyProtect the repository and define emergency reconciliation controls

Tools overlap, so classify the state they own before choosing one. Terraform can configure bootstrap data, Kubernetes can provision cloud resources through controllers, and Ansible can create resources, but overlapping ownership creates competing reconcilers. One resource should have one authoritative controller.

Pitfalls

  • Committing state or secrets — the state file can contain plaintext secrets and full resource maps; keep it in a locked remote backend, never in Git. Don’t hard-code secrets in IaC — reference a secret store.
  • Manual console changes (“ClickOps”) — editing resources by hand creates drift the code doesn’t know about; a later apply may revert or conflict. Make all changes through code.
  • No state locking — two engineers applying at once corrupt shared state. Use a backend with locking.
  • Giant monolithic stacks — one state file for everything makes every change slow and risky. Split by lifecycle/blast-radius (network vs app vs data) and compose with modules.
  • No plan review — applying without reading the plan can silently destroy/recreate a database. Treat the plan as a mandatory review gate, ideally in CI.
  • Hand-rolled credentials in CI — authenticate the pipeline to the cloud with OIDC/workload identity, not a long-lived stored key.

Tradeoffs

IaCManual / ClickOps
ReproducibilityHigh for declared inputs and supported resources; external state still mattersLow when changes are undocumented
Speed (first time)Slower (write definitions)Faster to click once
Speed (repeat/scale)Automated, but bounded by provider operations and dependenciesManual effort grows with copies
AuditabilityFull (Git history, plan)None
Learning curveReal (HCL, state, modules)None

Decision rule: use IaC for shared or long-lived infrastructure where review, repeatability, and recovery justify maintaining definitions and state. Choose the tool from provider/resource coverage, state and policy model, language, operator skill, and lifecycle ownership. Keep Terraform state remote and locked, review a saved plan when approval must bind to exact actions, and route emergency console changes back into code after the incident.

Questions

References