Terraform Complete Guide — Infrastructure as Code for Cloud Engineers
Advertisement
Introduction
Why This Matters
Terraform by HashiCorp is the most widely adopted Infrastructure as Code tool, supporting over 1,000 cloud providers through its plugin architecture. Writing infrastructure in HCL (HashiCorp Configuration Language) gives teams reproducible, version-controlled, peer-reviewed infrastructure that can be planned before application, preventing costly mistakes in production.
Core Workflow
# Initialize — downloads providers, sets up backend
terraform init
# Validate — checks HCL syntax and configuration
terraform validate
# Plan — preview what will change (never modifies resources)
terraform plan -out=tfplan
# Apply — execute the planned changes
terraform apply tfplan
# Destroy — tear down all managed resources
terraform destroyBasic Configuration
# versions.tf
terraform {
required_version = ">= 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-lock"
}
}
# main.tf
provider "aws" {
region = var.aws_region
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
tags = local.common_tags
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
tags = merge(local.common_tags, {
Name = "${var.project}-web"
})
}
# Data sources — read existing resources
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
}
}Variables and Outputs
# variables.tf
variable "aws_region" {
type = string
description = "AWS region for all resources"
default = "us-east-1"
}
variable "instance_type" {
type = string
description = "EC2 instance type"
default = "t3.micro"
validation {
condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
error_message = "Instance type must be t3.micro, t3.small, or t3.medium."
}
}
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "project" {
type = string
}
# locals.tf
locals {
common_tags = {
Project = var.project
Environment = terraform.workspace
ManagedBy = "terraform"
}
}
# outputs.tf
output "instance_public_ip" {
value = aws_instance.web.public_ip
description = "Public IP of web server"
}
output "vpc_id" {
value = aws_vpc.main.id
}Modules
# modules/vpc/main.tf
variable "cidr_block" {}
variable "environment" {}
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
output "vpc_id" {
value = aws_vpc.this.id
}
# Root main.tf — consuming the module
module "vpc" {
source = "./modules/vpc"
cidr_block = "10.0.0.0/16"
environment = "production"
}
# Using a public registry module
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = "my-cluster"
cluster_version = "1.29"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
}Workspaces for Environments
# Create and switch workspaces
terraform workspace new staging
terraform workspace new production
terraform workspace list
terraform workspace select production
# Reference workspace in config
resource "aws_instance" "web" {
instance_type = terraform.workspace == "production" ? "t3.medium" : "t3.micro"
}State Management
# Remote state with S3 + DynamoDB locking
# Create S3 bucket for state
aws s3api create-bucket --bucket my-terraform-state --region us-east-1
aws s3api put-bucket-versioning \
--bucket my-terraform-state \
--versioning-configuration Status=Enabled
# Create DynamoDB table for state locking
aws dynamodb create-table \
--table-name terraform-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
# Useful state commands
terraform state list # list all managed resources
terraform state show aws_instance.web # inspect resource state
terraform state mv old_resource new_resource # rename resource
terraform import aws_instance.web i-1234567890 # import existing resourceCommon Mistakes
- Committing
terraform.tfstateto Git — use remote backends (S3, Terraform Cloud) with locking - Not using
terraform plan -out=tfplanbeforeapply— without saving the plan,applyre-plans and may include unintended changes - Using
countinstead offor_eachfor resource collections — removing a middle element withcountcauses cascading resource recreation - Not pinning provider and module versions —
~> 5.0syntax pins major version while allowing minor updates - Running
terraform destroyin production without a detailed review of the plan output
Best Practices
- Structure code into
modules/,environments/, with separate state per environment - Use
terraform validateandterraform fmtin CI beforeplanruns - Tag every resource with project, environment, and managed-by metadata
- Use
prevent_destroy = truelifecycle rule on critical resources like databases - Store sensitive outputs with
sensitive = trueto prevent them from appearing in logs - Use
terraform graph | dot -Tpng > graph.pngto visualize resource dependencies
Key Takeaways
- Terraform manages cloud resources declaratively via HCL —
planpreviews changes beforeapplyexecutes them - Remote state (S3 + DynamoDB for AWS) enables team collaboration and prevents state conflicts via locking
- Modules encapsulate reusable infrastructure components — use public registry modules to avoid reinventing the wheel
- Workspaces allow a single Terraform configuration to manage multiple environments with isolated state
- Data sources read existing cloud resources without managing them — useful for referencing shared VPCs or AMIs
for_eachis preferred overcountfor collections because it uses map keys instead of positional indices- Version-pin providers and modules in
required_providersto prevent unexpected breaking changes - The
lifecycleblock (prevent_destroy,ignore_changes,create_before_destroy) controls how Terraform manages resource updates
Advertisement
Related reading
Terraform Infrastructure Guide 2026 — Infrastructure as Code for AWS, GCP, and Azure6 min readTerraform Modules at Scale — Reusable, Versioned Infrastructure Components8 min readAWS for Developers 2026 — EC2, S3, Lambda, RDS, and CloudFront Guide6 min readTesting Infrastructure as Code — Terratest, Policy-as-Code, and Shift-Left IaC8 min readPulumi With TypeScript — Infrastructure as Real Code, Not YAML7 min readTerraform at Scale — State Management, Module Versioning, and Team Workflows7 min read