Skip to content

Infrastructure as Code with Terraform

Infrastructure as Code (IaC) means describing your servers, networks, and cloud resources in version-controlled files instead of clicking through consoles. Terraform is the most widely used IaC tool: you declare the desired end state, and Terraform figures out the create/update/delete actions to reach it. The result is reproducible, reviewable, and auditable infrastructure.

  • Providers — plugins that talk to a platform’s API (AWS, Cloudflare, Proxmox, etc.).
  • Resources — the things you declare (resource "cloudflare_record" "www" { ... }).
  • Data sources — read existing infrastructure you don’t manage.
  • State — Terraform’s record of what it has created, mapping config to real resources.

The loop is write → plan → apply:

Terminal window
terraform init # download providers, configure the backend
terraform plan # show what will change — review this like a diff
terraform apply # make it so (after confirmation)
terraform destroy # tear it all down
terraform {
required_providers {
cloudflare = { source = "cloudflare/cloudflare", version = "~> 4" }
}
}
resource "cloudflare_record" "www" {
zone_id = var.zone_id
name = "www"
type = "CNAME"
content = "example.pages.dev"
proxied = true
}

State is the most dangerous part of Terraform. Local state (terraform.tfstate on one laptop) breaks the moment a second person runs apply — concurrent runs corrupt state and drift from reality. Use a remote backend with locking:

terraform {
backend "s3" {
bucket = "my-tf-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "tf-locks" # state locking — prevents concurrent applies
encrypt = true
}
}

State often contains secrets (generated passwords, keys) in plaintext — encrypt the backend, restrict access, and never commit *.tfstate to git.

A module is a reusable, parameterized group of resources — inputs (variable), outputs (output), and the resources between them. Write a module when you’d otherwise copy-paste a block of resources; pull common ones from the Terraform Registry.

module "site" {
source = "./modules/static-site"
zone_id = var.zone_id
domain = "example.com"
}

Keep dev/staging/prod from stepping on each other:

ApproachHowWhen
Workspacesterraform workspace new staging — one config, separate state per workspaceSmall differences, same shape
Separate state per envA directory + backend key per environmentReal isolation; the safer default for prod
TerragruntWrapper that keeps backend/provider blocks DRY across many envsMany environments/accounts

Workspaces are tempting but share one config and backend, so a mistake can hit prod. For anything where blast radius matters, prefer separate state.

Bring click-ops resources under management with terraform import (or import blocks in modern Terraform), and refactor safely with terraform state mv. Expect friction — import populates state but not config, so you still write the matching HCL.

Drift is when reality diverges from state (someone changed a resource by hand). terraform plan is drift detection — run it on a schedule (CI cron) and alert on any non-empty plan so out-of-band changes surface before they cause an outage.

After HashiCorp moved Terraform to the BSL license in 2023, the community forked it as OpenTofu (Linux Foundation, MPL-licensed). It is a drop-in-compatible alternative for teams that want an open-source license; tofu mirrors the terraform CLI and reads the same configs.