Terraform Complete Guide — Infrastructure as Code for Cloud Engineers

Sanjeev SharmaSanjeev Sharma
5 min read

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 destroy

Basic 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 resource

Common Mistakes

  • Committing terraform.tfstate to Git — use remote backends (S3, Terraform Cloud) with locking
  • Not using terraform plan -out=tfplan before apply — without saving the plan, apply re-plans and may include unintended changes
  • Using count instead of for_each for resource collections — removing a middle element with count causes cascading resource recreation
  • Not pinning provider and module versions — ~> 5.0 syntax pins major version while allowing minor updates
  • Running terraform destroy in production without a detailed review of the plan output

Best Practices

  • Structure code into modules/, environments/, with separate state per environment
  • Use terraform validate and terraform fmt in CI before plan runs
  • Tag every resource with project, environment, and managed-by metadata
  • Use prevent_destroy = true lifecycle rule on critical resources like databases
  • Store sensitive outputs with sensitive = true to prevent them from appearing in logs
  • Use terraform graph | dot -Tpng > graph.png to visualize resource dependencies

Key Takeaways

  • Terraform manages cloud resources declaratively via HCL — plan previews changes before apply executes 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_each is preferred over count for collections because it uses map keys instead of positional indices
  • Version-pin providers and modules in required_providers to prevent unexpected breaking changes
  • The lifecycle block (prevent_destroy, ignore_changes, create_before_destroy) controls how Terraform manages resource updates

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading