Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive · DevOps

Terraform: Infrastructure as Code Basics

Write, plan and apply AWS resources with Terraform 1.2: providers, variables, outputs, modules, the plan and apply workflow, drift, imports and safe practices.

4 min read

  • Terraform 1.2
  • AWS provider 4.x
  • Region ap-south-1

Terraform describes infrastructure in .tf files and creates it through a plan, then apply cycle. The files are reviewed in Git like any other code, so changes to networks, databases and permissions get the same scrutiny as application changes.

Versions in use on this date. Terraform 1.2 (released May 2022; 1.3 is expected in a few months) and the HashiCorp AWS provider 4.x. Version 4.0 (February 2022) split many S3 bucket settings, such as versioning, into separate resources, which is why older tutorials no longer apply as written.

How a run works

flowchart TB
  CODE[tf files in Git] --> INIT[terraform init<br/>download providers and modules, configure backend]
  INIT --> VAL[terraform fmt and validate]
  VAL --> PLAN[terraform plan<br/>compare code, state and real AWS]
  STATE[(State file<br/>remote backend)] --> PLAN
  AWSR[(AWS APIs read)] --> PLAN
  PLAN --> REVIEW{Human or CI review of the plan}
  REVIEW -->|approve| APPLY[terraform apply plan file]
  REVIEW -->|reject| CODE
  APPLY --> AWSW[(AWS resources created, changed, destroyed)]
  APPLY --> STATE

A small but real configuration

versions.tf:

terraform {
  required_version = ">= 1.2.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 4.0" }
  }
}

provider "aws" {
  region = var.region
  default_tags {
    tags = { Project = "shop", Env = var.env, ManagedBy = "terraform" }
  }
}

variables.tf and outputs.tf:

variable "region" { type = string, default = "ap-south-1" }
variable "env"    { type = string }
variable "bucket_name" {
  type        = string
  description = "Globally unique bucket name"
  validation {
    condition     = can(regex("^[a-z0-9.-]{3,63}$", var.bucket_name))
    error_message = "Use 3-63 lowercase letters, digits, dots or hyphens."
  }
}

output "bucket_arn" { value = aws_s3_bucket.assets.arn }

main.tf:

resource "aws_s3_bucket" "assets" {
  bucket = var.bucket_name
}

resource "aws_s3_bucket_versioning" "assets" {
  bucket = aws_s3_bucket.assets.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_public_access_block" "assets" {
  bucket                  = aws_s3_bucket.assets.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
  bucket = aws_s3_bucket.assets.id
  rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } }
}

Run it:

terraform init
terraform fmt -recursive && terraform validate
terraform plan -var env=dev -var bucket_name=acme-assets-dev-2022 -out tfplan
terraform apply tfplan
terraform output
terraform destroy -var env=dev -var bucket_name=acme-assets-dev-2022   # only for disposable environments

The plan output marks each resource with + create, ~ update in place, -/+ replace (destroy and create) and - destroy. Treat replace on databases, load balancers or anything stateful as a stop sign: read why (usually an immutable argument changed).

Concepts you will use every day

Concept Meaning
Provider Plugin that talks to an API (AWS, Kubernetes, GitHub)
Resource A thing Terraform creates and manages
Data source Read-only lookup of something that already exists
Variable / output Inputs and results of a configuration or module
Module Reusable folder of resources, called with module "vpc" { source = "..." }
State Terraform’s record mapping code to real resource IDs
Workspace / directory per environment Ways to separate dev, staging and prod

Use for_each over a map for several similar resources (stable addresses), and avoid count for things that may be reordered. Reference resources, do not hard-code IDs, so Terraform builds the dependency graph.

Drift, import and refactoring

  • Drift: someone changed a resource in the console. terraform plan shows the difference; either apply to fix it or update the code to match.
  • Import an existing resource into state: terraform import aws_s3_bucket.assets existing-bucket-name, then write matching code until the plan is clean.
  • Rename in code without destroying: terraform state mv aws_s3_bucket.old aws_s3_bucket.new.
  • Protect critical resources with lifecycle { prevent_destroy = true }, and use ignore_changes sparingly for attributes managed elsewhere.

Safe-practice checklist

  1. Remote state with locking (see the next post); never commit terraform.tfstate, which can hold secrets in plain text.
  2. Commit .terraform.lock.hcl so everyone uses the same provider builds.
  3. Run plan in pull requests and post the output; apply only from main through CI with a reviewed plan file.
  4. Mark secrets sensitive = true but remember they still land in state; better, keep secrets in Secrets Manager and reference them at runtime.
  5. Give the apply role least privilege and use short-lived credentials rather than a shared admin key.
  6. Pin module and provider versions and upgrade deliberately, reading changelogs.
  7. Tag everything (the default_tags block above) so costs and owners are visible.

Troubleshooting

  • Error acquiring the state lock: another run is active, or a crashed run left a lock (see remote state post).
  • BucketAlreadyExists: S3 names are global; add an account or environment suffix.
  • Provider upgrade changes plans: read the upgrade guide; test in dev first.
  • Slow plans on large stacks: split into smaller states by component and use -target only for emergencies.