Terraform: A Practical Introduction
Terraform turns infrastructure into version-controlled code: a declarative description of what your cloud should look like, plus a tool that figures out what to create, modify, or destroy. The mental model is simple. The pitfalls are around state and drift, and they trip everyone at least once.
The four concepts
Provider: a plugin that talks to one API (AWS, GCP, DigitalOcean, Cloudflare). Resource: a thing the provider manages (an EC2 instance, a DNS record). State: a JSON file mapping your code to the real-world objects Terraform created. Module: a reusable bundle of resources.
A minimal example
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" {
region = "eu-west-1"
}
resource "aws_s3_bucket" "logs" {
bucket = "myorg-logs-eu-west-1"
tags = {
Environment = "production"
Managed = "terraform"
}
}
resource "aws_s3_bucket_versioning" "logs" {
bucket = aws_s3_bucket.logs.id
versioning_configuration { status = "Enabled" }
}
terraform init # download providers, initialize state
terraform plan # show what will change
terraform apply # do it
terraform destroy # tear it all down
State, and why it matters
The state file (terraform.tfstate) is how Terraform knows what it owns. Lose it and Terraform thinks nothing exists, so the next apply tries to create everything again. Two non-negotiable practices for any non-toy use:
- Use a remote backend. S3 + DynamoDB locking, Terraform Cloud, GCS, or your provider's equivalent. Never commit
terraform.tfstateto git. - Lock the state during operations. Two people running
applysimultaneously can corrupt the file.
terraform {
backend "s3" {
bucket = "myorg-tfstate"
key = "prod/network.tfstate"
region = "eu-west-1"
dynamodb_table = "tfstate-locks"
encrypt = true
}
}
Variables and outputs
variable "environment" {
description = "Deployment environment"
type = string
default = "staging"
validation {
condition = contains(["staging", "production"], var.environment)
error_message = "environment must be staging or production"
}
}
output "bucket_name" {
value = aws_s3_bucket.logs.bucket
}
Modules
Modules are directories you call. Inputs are variables; outputs are outputs. A module for a VPC, called from your environment-specific code:
module "vpc" {
source = "./modules/vpc"
name = "production"
cidr = "10.0.0.0/16"
azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
}
Drift
"Drift" is when reality diverges from state — someone changed a security group in the AWS console, or a Lambda was modified by a deploy pipeline that doesn't go through Terraform. terraform plan will show the diff. Two strategies: import the drift (terraform import) so Terraform manages it, or change the cloud back to match the code.
Use terraform state subcommands (mv, rm, list, show). Manual JSON edits are how teams end up with corrupt state.