What You'll Learn
A complete Terraform guide from zero to production — providers, resources, variables, state management, modules, workspaces, and deploying a full AWS infrastructure stack.
What is Terraform?
Terraform is an open-source Infrastructure as Code (IaC) tool by HashiCorp that lets you define cloud and on-premises infrastructure in human-readable configuration files — then provision it consistently and repeatably.
Declarative
Describe what you want, not how to get there
Idempotent
Run the same config multiple times — same result
Multi-Cloud
Same language for AWS, Azure, GCP, and 1000+ providers
The Terraform Workflow
# 1. INIT — Download providers and initialize backend
terraform init
# 2. PLAN — Preview changes without applying
terraform plan
terraform plan -out=tfplan # Save plan to file
terraform plan -var="env=staging" # Pass variable
# 3. APPLY — Create/update infrastructure
terraform apply
terraform apply tfplan # Apply saved plan (no prompt)
terraform apply -auto-approve # Skip confirmation (CI/CD use only!)
terraform apply -target=aws_instance.web # Apply specific resource only
# 4. DESTROY — Tear down infrastructure
terraform destroy
terraform destroy -target=aws_instance.web
# Other useful commands
terraform validate # Validate configuration syntax
terraform fmt # Format .tf files
terraform state list # List all resources in state
terraform state show aws_vpc.main # Show specific resource state
terraform import aws_s3_bucket.backup my-bucket # Import existing resource
terraform output # Show output values
terraform graph | dot -Tsvg > graph.svg # Visualize dependencies
Complete AWS Infrastructure Example
# ─────────────── Provider Configuration ───────────────
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.30"
}
}
# Remote State Backend (recommended for teams)
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock" # State locking
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = "MyApp"
Environment = var.environment
ManagedBy = "Terraform"
}
}
}
# ─────────────── VPC ───────────────
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = "${var.environment}-vpc" }
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = { Name = "${var.environment}-igw" }
}
# Public subnets across 2 AZs
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = { Name = "${var.environment}-public-${count.index + 1}" }
}
# Private subnets for database
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 10}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = { Name = "${var.environment}-private-${count.index + 1}" }
}
# ─────────────── Security Groups ───────────────
resource "aws_security_group" "web" {
name = "${var.environment}-web-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# ─────────────── EC2 Instance ───────────────
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = aws_subnet.public[0].id
vpc_security_group_ids = [aws_security_group.web.id]
iam_instance_profile = aws_iam_instance_profile.web.name
key_name = var.key_pair_name
user_data = base64encode(templatefile("${path.module}/scripts/userdata.sh.tpl", {
app_name = var.app_name
env = var.environment
}))
root_block_device {
volume_type = "gp3"
volume_size = 20
encrypted = true
}
lifecycle {
create_before_destroy = true
}
tags = { Name = "${var.environment}-web-server" }
}
# ─────────────── RDS Database ───────────────
resource "aws_db_instance" "main" {
identifier = "${var.environment}-database"
engine = "postgres"
engine_version = "16.1"
instance_class = "db.t3.micro"
allocated_storage = 20
max_allocated_storage = 100 # Auto-scaling storage
storage_encrypted = true
db_name = "myapp"
username = var.db_username
password = random_password.db_password.result
vpc_security_group_ids = [aws_security_group.rds.id]
db_subnet_group_name = aws_db_subnet_group.main.name
backup_retention_period = 7
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.environment}-final-snapshot"
tags = { Name = "${var.environment}-db" }
}
Variables and Outputs
### variables.tf ###
variable "aws_region" {
description = "AWS region to deploy into"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "db_username" {
description = "Database master username"
type = string
sensitive = true # Will be masked in plan output
}
### terraform.tfvars ###
aws_region = "us-east-1"
environment = "prod"
instance_type = "t3.medium"
db_username = "dbadmin"
### outputs.tf ###
output "web_public_ip" {
description = "Public IP of the web server"
value = aws_instance.web.public_ip
}
output "database_endpoint" {
description = "RDS connection endpoint"
value = aws_db_instance.main.endpoint
sensitive = true # Won't print in console output
}
output "vpc_id" {
value = aws_vpc.main.id
}
Terraform Modules — Reusable Infrastructure
# Using public registry modules
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "${var.environment}-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = true # Use one NAT GW (cost savings in dev)
}
# Using a local module
module "web_server" {
source = "./modules/ec2-webserver"
environment = var.environment
vpc_id = module.vpc.vpc_id
subnet_id = module.vpc.public_subnets[0]
instance_type = var.instance_type
}
State Management — Critical!
- Never commit state files to Git — They contain sensitive data (passwords, keys).
- Always use remote state — S3 + DynamoDB for AWS. Enables state locking.
- Use workspaces for environments —
terraform workspace new stagingcreates isolated state. - Be careful with terraform destroy — There's no undo. Use
deletion_protection = trueon databases. - Import existing resources — Don't delete and recreate; use
terraform importto bring them under management.