Infrastructure as Code
Provision infrastructure the way you write software
Stop clicking in consoles. Declare infrastructure in code, review it in a PR, and apply it reproducibly — with the state, module, policy and testing discipline that real teams need.
On this page · 6 lessons
Terraform fundamentals
Providers, resources, variables, state, and the plan/apply workflow that makes infrastructure reviewable.
Clicking around a cloud console doesn't scale and can't be reviewed or repeated. Infrastructure as Code (IaC) with Terraform lets you declare your infrastructure in files, version it in Git, and apply it reproducibly across environments.
You write HCL describing the resources you want:
resource "aws_instance" "web" {
ami = "ami-0abc123"
instance_type = "t3.micro"
tags = { Name = "web-server" }
}
The workflow is three commands: terraform init (download providers), terraform plan (preview exactly what will change — read this every time), and terraform apply (make it real). To tear it all down: terraform destroy.
Terraform tracks what it manages in a state file. On any real team you store this remotely (e.g. an S3 bucket with locking) so state is shared and two people can't apply at once. As your config grows, factor it into reusable modules — a network module, a database module — that you call with different inputs per environment.
Terraform at scale
Remote state with locking, reusable modules, per-environment state, drift, and reading the plan before it deletes prod.
Terraform on a real team looks nothing like the tutorial. Four things separate a toy config from production infrastructure.
Remote state with locking. The state file maps your code to real resources. Locally, it's a landmine — two engineers applying at once will corrupt it. Store it in an S3 bucket with DynamoDB locking (or Terraform Cloud), so state is shared, versioned, and only one apply runs at a time. State also contains secrets in plaintext — encrypt it and lock down access.
Modules. Stop copy-pasting. A module is a reusable component with inputs and outputs — a network module, a service module — called once per environment with different variables. This is the difference between 3,000 lines of duplication and 300 lines of intent.
Environment separation. Keep dev, staging, and prod in separate state files so a mistake in dev can never destroy prod. Do this with separate directories (clear, explicit — usually the better choice) or workspaces (one config, many states).
Drift and safety. Someone will click in the console. terraform plan detects that drift. Protect critical resources with prevent_destroy, and read every plan — the line that matters is Plan: 0 to add, 0 to change, 1 to destroy, and that destroy might be your database.
Configuration management with Ansible
Agentless config over SSH: inventories, playbooks, roles — and idempotency that makes re-running safe.
Terraform provisions infrastructure (create the servers); Ansible configures what's on it (install packages, edit config, deploy apps). They're complementary.
Ansible's big win is being agentless — it connects over plain SSH, so there's nothing to install on the targets. You describe the desired state in a playbook (YAML) and Ansible makes it so:
- hosts: webservers
become: true
tasks:
- name: Ensure nginx is installed
apt: { name: nginx, state: present }
- name: Ensure nginx is running
service: { name: nginx, state: started, enabled: true }
An inventory lists your hosts (grouped, e.g. webservers, dbservers), and roles package reusable sets of tasks so you can share and compose configuration.
The critical property is idempotency: running a playbook twice changes nothing the second time, because tasks declare a desired state ("nginx present"), not imperative steps ("run apt install"). That's what makes it safe to re-run against production.
Immutable infrastructure & Packer
Bake golden images and replace servers instead of patching them. Cattle, not pets.
There are two philosophies for managing servers. In the mutable model you patch and tweak running servers over time — which slowly produces "snowflake" servers no two of which are alike, and nobody remembers how they got that way.
The modern approach is immutable infrastructure: you never modify a running server. To change anything, you build a brand-new machine image and replace the old one wholesale. This kills configuration drift, makes rollbacks trivial (boot the previous image), and guarantees every server is identical.
Packer (by HashiCorp) is the tool for baking these "golden images." You define a template that starts from a base OS, runs your provisioning (often an Ansible playbook), and outputs a ready-to-boot image — an AWS AMI, a Docker image, etc. Your Terraform then launches instances from that image.
The pattern ties the whole stack together:
Packer (bake image) → Terraform (launch it) → replace, don't patch
OpenTofu, Pulumi, CDK & Crossplane
The IaC landscape and how to choose deliberately — real languages, cloud-native tools, and Kubernetes-driven infra.
Terraform is the default, not the only option. Knowing the landscape lets you choose deliberately — and read someone else's stack.
- OpenTofu — the open-source fork of Terraform, created after HashiCorp changed its licence. Drop-in compatible, and increasingly the neutral default.
- Pulumi — infrastructure in real programming languages (TypeScript, Python, Go). You get loops, conditionals, classes, and unit tests naturally, rather than fighting HCL's limits. The trade-off: full language power invites full language complexity.
- AWS CDK — define AWS infra in code that synthesises to CloudFormation. Excellent if you're all-in on AWS; useless anywhere else.
- CloudFormation / ARM / Bicep — the clouds' own native tools. Deeply integrated and always support new services on day one, but they lock you in and are typically more verbose.
- Crossplane — manages cloud infrastructure from inside Kubernetes, using CRDs. Provision a database with
kubectl apply. Powerful for platform teams already living in Kubernetes.
How to choose: declarative HCL (Terraform/OpenTofu) is the safe, hireable default — the biggest ecosystem and the most jobs. Reach for Pulumi when your infra logic is genuinely complex, CDK when you're AWS-only and your team are developers, and Crossplane when you're building a Kubernetes-native platform.
Testing & policy for infrastructure
tflint, checkov and tfsec on every PR; OPA/Conftest for rules that must never break; Terratest for real verification.
Infrastructure code can delete a database. It deserves the same rigour as application code — tested, linted, and governed before it applies.
Static analysis catches problems without touching the cloud:
terraform validateandtflint— syntax and provider-specific mistakes.checkov,tfsec, Trivy — security misconfigurations: a public S3 bucket, an unencrypted volume, a security group open to0.0.0.0/0. Run these on every PR.
Policy as code enforces the rules that must never be broken. OPA/Conftest (or Sentinel) let you write them as code and fail the pipeline:
"All resources must have an
ownertag." "No security group may allow 0.0.0.0/0 on port 22." "Production databases must have deletion protection enabled."
This turns tribal knowledge and code-review vigilance into an automated gate that never gets tired or distracted.
Testing goes further: Terratest (Go) actually provisions infrastructure in a sandbox account, asserts it behaves (the endpoint responds, the bucket is private), and destroys it. Slower, but it's the only way to verify a module genuinely works.
The pipeline shape: validate → lint → security scan → policy check → plan → human review → apply.
- $Provision your whole cloud environment in Terraform with remote locked state and reusable modules; destroy and rebuild it from scratch.
- $Add checkov + an OPA policy ('no 0.0.0.0/0 on port 22', 'all resources tagged') to CI and make a violating PR fail.