You are viewing a preview of this lesson. Sign in to start learning
Back to Mastering AWS

Terraform on AWS

State management, drift detection, modules, and real-world Terraform patterns

Terraform on AWS: Infrastructure as Code Made Simple

Master infrastructure automation on AWS with free flashcards and hands-on Terraform practice. This lesson covers Terraform providers, resource definitions, state management, and AWS-specific best practicesโ€”essential skills for modern cloud engineers building scalable, repeatable infrastructure.


๐Ÿ’ป Welcome to Terraform on AWS

Terraform has revolutionized how we provision and manage cloud infrastructure. Instead of clicking through AWS console screens or writing complex scripts, you declare your desired infrastructure state in configuration files, and Terraform makes it happen. Think of it as "infrastructure blueprints" that are version-controlled, testable, and repeatable.

Why Terraform + AWS?

  • ๐Ÿ”„ Declarative syntax: Describe what you want, not how to create it
  • ๐Ÿ“‹ Version control: Track infrastructure changes like code
  • ๐Ÿ” Idempotent: Run the same config 100 times, get the same result
  • ๐ŸŒ Multi-region/multi-account: Manage complex AWS architectures
  • ๐Ÿค Team collaboration: Share modules, review changes via pull requests

๐Ÿ’ก Did you know? Terraform was created by HashiCorp in 2014 and now manages infrastructure across 300+ cloud providers, but AWS remains the most popular integration.


๐Ÿ—๏ธ Core Concepts

1. Providers: Connecting to AWS

The provider block tells Terraform how to authenticate and interact with AWS. It's your gateway to the AWS API.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
  profile = "default"  # Uses AWS CLI credentials
}

Key elements:

  • source: Where to download the provider (HashiCorp registry)
  • version: Constraint to prevent breaking changes (use ~> for minor updates)
  • region: Which AWS region to deploy resources
  • profile: AWS credential profile from ~/.aws/credentials

โš ๏ธ Authentication methods (in order of preference):

  1. IAM roles (for EC2/ECS/Lambda)
  2. AWS profiles (for local development)
  3. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
  4. โŒ Never hardcode credentials in Terraform files!

2. Resources: Building Blocks of Infrastructure

A resource is any AWS component you want to create: EC2 instances, S3 buckets, VPCs, security groups, etc.

Syntax pattern:

resource "<provider>_<type>" "<local_name>" {
  argument1 = "value1"
  argument2 = "value2"
}

Example: Creating an S3 bucket

resource "aws_s3_bucket" "data_lake" {
  bucket = "my-company-data-lake-2024"
  
  tags = {
    Environment = "Production"
    ManagedBy   = "Terraform"
  }
}

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

๐Ÿ” Notice:

  • Resource type: aws_s3_bucket (format: <provider>_<service>)
  • Local name: data_lake (used to reference in other resources)
  • Reference syntax: aws_s3_bucket.data_lake.id (access attributes)
  • Implicit dependency: Versioning resource depends on bucket

๐Ÿ’ก Naming convention: Use descriptive local names that reflect purpose, not AWS names. Think web_server_sg not sg_12345.


3. State: Terraform's Memory

Terraform tracks what it created in a state file (terraform.tfstate). This is how it knows what exists, what changed, and what to update.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚         TERRAFORM STATE WORKFLOW            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                             โ”‚
โ”‚  ๐Ÿ“ .tf files  โ†’  ๐Ÿงฎ Plan  โ†’  โ˜๏ธ AWS       โ”‚
โ”‚     (desired)      (diff)      (actual)     โ”‚
โ”‚                      โ†•                      โ”‚
โ”‚                 ๐Ÿ“Š State File               โ”‚
โ”‚            (tracks what exists)             โ”‚
โ”‚                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

State file contains:

  • Resource IDs (e.g., i-0abc123, sg-xyz789)
  • Current attribute values
  • Dependencies between resources
  • Metadata (provider versions, timestamps)

โš ๏ธ CRITICAL STATE MANAGEMENT RULES:

โŒ Never Do This โœ… Always Do This
Edit state file manually Use terraform state commands
Commit state to Git Use remote backend (S3 + DynamoDB)
Share state file via Dropbox Enable state locking
Delete state file Keep backups, use versioning

Remote backend configuration (production standard):

terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "prod/vpc/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

๐Ÿง  Mnemonic: STATE = Store, Track, Avoid conflicts, Track changes, Enable collaboration


4. Variables and Outputs: Reusability

Variables make configurations flexible and reusable across environments.

variables.tf:

variable "environment" {
  description = "Deployment environment"
  type        = string
  default     = "dev"
  
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "instance_count" {
  description = "Number of EC2 instances"
  type        = number
  default     = 1
}

variable "allowed_cidr_blocks" {
  description = "CIDR blocks for security group"
  type        = list(string)
  default     = ["10.0.0.0/16"]
}

Using variables:

resource "aws_instance" "app_server" {
  count         = var.instance_count
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
  
  tags = {
    Name        = "app-server-${var.environment}-${count.index}"
    Environment = var.environment
  }
}

Outputs expose values for other modules or external use.

outputs.tf:

output "instance_public_ips" {
  description = "Public IP addresses of app servers"
  value       = aws_instance.app_server[*].public_ip
}

output "load_balancer_dns" {
  description = "DNS name of load balancer"
  value       = aws_lb.main.dns_name
}

๐Ÿ’ก Variable precedence (highest to lowest):

  1. Command line: -var="instance_count=3"
  2. .tfvars file: terraform apply -var-file="prod.tfvars"
  3. Environment variables: TF_VAR_instance_count=3
  4. Default values in variable definition

5. Data Sources: Query Existing Resources

Data sources let you fetch information about resources Terraform didn't create.

## Get the latest Amazon Linux 2 AMI
data "aws_ami" "amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]
  
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

## Get default VPC
data "aws_vpc" "default" {
  default = true
}

## Use data source in resource
resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = "t3.micro"
  subnet_id     = data.aws_vpc.default.id
}

When to use data sources:

  • โœ… Reference existing VPCs, subnets, security groups
  • โœ… Look up latest AMI IDs dynamically
  • โœ… Get account ID, region, availability zones
  • โœ… Query resources managed by other teams

๐ŸŽฏ Example 1: Complete VPC Setup

Let's build a production-ready VPC with public/private subnets, NAT gateway, and internet gateway.

main.tf:

## VPC
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  tags = {
    Name = "main-vpc"
  }
}

## Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  
  tags = {
    Name = "main-igw"
  }
}

## Public Subnet
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-east-1a"
  map_public_ip_on_launch = true
  
  tags = {
    Name = "public-subnet-1a"
    Type = "Public"
  }
}

## Private Subnet
resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = "us-east-1a"
  
  tags = {
    Name = "private-subnet-1a"
    Type = "Private"
  }
}

## Elastic IP for NAT Gateway
resource "aws_eip" "nat" {
  domain = "vpc"
  
  tags = {
    Name = "nat-gateway-eip"
  }
}

## NAT Gateway
resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public.id
  
  tags = {
    Name = "main-nat-gateway"
  }
  
  depends_on = [aws_internet_gateway.main]
}

## Route Table for Public Subnet
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
  
  tags = {
    Name = "public-route-table"
  }
}

## Route Table for Private Subnet
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id
  
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main.id
  }
  
  tags = {
    Name = "private-route-table"
  }
}

## Associate Route Tables
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private" {
  subnet_id      = aws_subnet.private.id
  route_table_id = aws_route_table.private.id
}

Network flow diagram:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   VPC 10.0.0.0/16                   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚ Public Subnet    โ”‚      โ”‚ Private Subnet   โ”‚   โ”‚
โ”‚  โ”‚  10.0.1.0/24     โ”‚      โ”‚  10.0.2.0/24     โ”‚   โ”‚
โ”‚  โ”‚                  โ”‚      โ”‚                  โ”‚   โ”‚
โ”‚  โ”‚  ๐ŸŒ EC2 (Web)    โ”‚      โ”‚  ๐Ÿ’พ RDS          โ”‚   โ”‚
โ”‚  โ”‚  ๐Ÿ”“ NAT Gateway  โ”‚      โ”‚  ๐Ÿ”’ App Server   โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚           โ”‚                         โ”‚             โ”‚
โ”‚           โ†“                         โ†“             โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚    โ”‚ IGW Route   โ”‚          โ”‚ NAT Route   โ”‚      โ”‚
โ”‚    โ”‚ 0.0.0.0/0   โ”‚          โ”‚ 0.0.0.0/0   โ”‚      โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚
โ”‚           โ”‚                        โ”‚             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
            โ”‚                        โ”‚
            โ†“                        โ†“
      ๐ŸŒ Internet โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ NAT Gateway

Explanation:

  • Public subnet: Resources get public IPs, route to Internet Gateway
  • Private subnet: No public IPs, route through NAT Gateway for outbound
  • NAT Gateway: Allows private resources to reach internet (patches, updates)
  • Dependencies: NAT needs EIP, route tables need gateways created first

๐ŸŽฏ Example 2: EC2 with Security Groups

Let's deploy a web server with proper security configuration.

## Security Group for Web Server
resource "aws_security_group" "web_server" {
  name        = "web-server-sg"
  description = "Allow HTTP/HTTPS inbound, all outbound"
  vpc_id      = aws_vpc.main.id
  
  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  ingress {
    description = "HTTPS from anywhere"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  ingress {
    description = "SSH from office"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["203.0.113.0/24"]  # Your office IP range
  }
  
  egress {
    description = "Allow all outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  tags = {
    Name = "web-server-sg"
  }
}

## EC2 Instance
resource "aws_instance" "web_server" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web_server.id]
  key_name               = "my-ssh-key"
  
  user_data = <<-EOF
              #!/bin/bash
              yum update -y
              yum install -y httpd
              systemctl start httpd
              systemctl enable httpd
              echo "<h1>Hello from Terraform!</h1>" > /var/www/html/index.html
              EOF
  
  root_block_device {
    volume_size = 20
    volume_type = "gp3"
    encrypted   = true
  }
  
  tags = {
    Name        = "web-server"
    ManagedBy   = "Terraform"
    Environment = "Production"
  }
}

## Elastic IP (optional - for static IP)
resource "aws_eip" "web_server" {
  instance = aws_instance.web_server.id
  domain   = "vpc"
  
  tags = {
    Name = "web-server-eip"
  }
}

## Output the public URL
output "web_server_url" {
  description = "URL to access web server"
  value       = "http://${aws_eip.web_server.public_ip}"
}

Key features:

  • Security groups: Firewall rules at instance level
  • User data: Bootstrap script runs on first boot
  • Encrypted EBS: Root volume encryption enabled
  • Elastic IP: Persistent public IP (survives instance stop/start)

๐Ÿ’ก Security best practice: Never open SSH (port 22) to 0.0.0.0/0. Use VPN, bastion hosts, or AWS Systems Manager Session Manager instead.


๐ŸŽฏ Example 3: S3 + CloudFront Static Website

Host a static website with S3 backend and CloudFront CDN.

## S3 Bucket for Website Content
resource "aws_s3_bucket" "website" {
  bucket = "my-awesome-website-2024"
  
  tags = {
    Name = "Website Bucket"
  }
}

## Bucket Website Configuration
resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id
  
  index_document {
    suffix = "index.html"
  }
  
  error_document {
    key = "error.html"
  }
}

## Block Public Access (CloudFront will access via OAC)
resource "aws_s3_bucket_public_access_block" "website" {
  bucket = aws_s3_bucket.website.id
  
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

## CloudFront Origin Access Control
resource "aws_cloudfront_origin_access_control" "website" {
  name                              = "s3-website-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

## CloudFront Distribution
resource "aws_cloudfront_distribution" "website" {
  enabled             = true
  default_root_object = "index.html"
  price_class         = "PriceClass_100"  # US, Canada, Europe
  
  origin {
    domain_name              = aws_s3_bucket.website.bucket_regional_domain_name
    origin_id                = "S3-Website"
    origin_access_control_id = aws_cloudfront_origin_access_control.website.id
  }
  
  default_cache_behavior {
    target_origin_id       = "S3-Website"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD", "OPTIONS"]
    cached_methods         = ["GET", "HEAD"]
    compress               = true
    
    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }
    
    min_ttl     = 0
    default_ttl = 3600
    max_ttl     = 86400
  }
  
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
  
  viewer_certificate {
    cloudfront_default_certificate = true
  }
  
  tags = {
    Name = "Website CDN"
  }
}

## S3 Bucket Policy (allow CloudFront access)
resource "aws_s3_bucket_policy" "website" {
  bucket = aws_s3_bucket.website.id
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowCloudFrontAccess"
        Effect = "Allow"
        Principal = {
          Service = "cloudfront.amazonaws.com"
        }
        Action   = "s3:GetObject"
        Resource = "${aws_s3_bucket.website.arn}/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.website.arn
          }
        }
      }
    ]
  })
}

output "cloudfront_url" {
  description = "CloudFront distribution URL"
  value       = "https://${aws_cloudfront_distribution.website.domain_name}"
}

Architecture flow:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     HTTPS      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      S3 API     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  User    โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ โ”‚ CloudFront  โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ โ”‚ S3 Bucketโ”‚
โ”‚ Browser  โ”‚                 โ”‚     CDN     โ”‚    (private)    โ”‚ (private)โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                 โ”‚ (150+ PoPs) โ”‚                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                    โ”‚
                                    โ†“
                             ๐Ÿ”’ Origin Access
                                Control (OAC)

Benefits:

  • โšก Low latency: Content cached at edge locations worldwide
  • ๐Ÿ”’ Secure: S3 bucket private, CloudFront serves via HTTPS
  • ๐Ÿ’ฐ Cost-effective: S3 storage + CloudFront cheaper than EC2
  • ๐Ÿ“ˆ Scalable: Handles traffic spikes automatically

๐ŸŽฏ Example 4: RDS Database with Multi-AZ

Deploy a production PostgreSQL database with high availability.

## DB Subnet Group (spans multiple AZs)
resource "aws_db_subnet_group" "main" {
  name       = "main-db-subnet-group"
  subnet_ids = [aws_subnet.private_1a.id, aws_subnet.private_1b.id]
  
  tags = {
    Name = "Main DB Subnet Group"
  }
}

## Security Group for RDS
resource "aws_security_group" "rds" {
  name        = "rds-sg"
  description = "Allow PostgreSQL from app servers"
  vpc_id      = aws_vpc.main.id
  
  ingress {
    description     = "PostgreSQL from app tier"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app_server.id]
  }
  
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  tags = {
    Name = "rds-sg"
  }
}

## RDS PostgreSQL Instance
resource "aws_db_instance" "main" {
  identifier           = "main-postgres-db"
  engine               = "postgres"
  engine_version       = "15.3"
  instance_class       = "db.t3.medium"
  allocated_storage    = 100
  storage_type         = "gp3"
  storage_encrypted    = true
  
  db_name  = "appdb"
  username = "dbadmin"
  password = var.db_password  # โš ๏ธ Use Secrets Manager in production!
  
  multi_az               = true
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]
  
  backup_retention_period = 7
  backup_window          = "03:00-04:00"
  maintenance_window     = "sun:04:00-sun:05:00"
  
  enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
  
  skip_final_snapshot       = false
  final_snapshot_identifier = "main-postgres-final-snapshot"
  
  tags = {
    Name        = "Main PostgreSQL DB"
    Environment = "Production"
  }
}

## Outputs
output "rds_endpoint" {
  description = "RDS endpoint for application connection"
  value       = aws_db_instance.main.endpoint
  sensitive   = true
}

output "rds_arn" {
  description = "RDS ARN"
  value       = aws_db_instance.main.arn
}

Production considerations:

  • โœ… Multi-AZ: Automatic failover to standby in another AZ
  • โœ… Encrypted storage: Data-at-rest encryption
  • โœ… Backup retention: 7 days (adjust based on compliance needs)
  • โœ… CloudWatch logs: Monitor database performance
  • โš ๏ธ Password management: Use AWS Secrets Manager, not variables!

๐Ÿ”ง Try this: Connect password to Secrets Manager:

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/db/password"
}

resource "aws_db_instance" "main" {
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
  # ... rest of config
}

โš ๏ธ Common Mistakes

1. Not Using Remote State

โŒ Wrong:

## No backend config - state stays local
terraform {
  required_version = ">= 1.0"
}

โœ… Right:

terraform {
  backend "s3" {
    bucket         = "company-terraform-state"
    key            = "prod/infrastructure.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Why it matters: Local state files get lost, corrupted, or cause conflicts when multiple people work on the same infrastructure.


2. Hardcoding Sensitive Values

โŒ Wrong:

resource "aws_db_instance" "main" {
  username = "admin"
  password = "SuperSecret123!"  # โš ๏ธ NEVER DO THIS!
}

โœ… Right:

variable "db_password" {
  description = "Database password"
  type        = string
  sensitive   = true
}

resource "aws_db_instance" "main" {
  username = "admin"
  password = var.db_password
}

Pass via environment variable: export TF_VAR_db_password="..." or use Secrets Manager.


3. Ignoring Resource Dependencies

โŒ Wrong:

resource "aws_eip" "nat" {
  domain = "vpc"
}

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public.id
  # Missing: depends_on for IGW!
}

โœ… Right:

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public.id
  
  depends_on = [aws_internet_gateway.main]
}

NAT Gateway needs IGW to exist first, but Terraform can't infer this automatically.


4. Not Using Modules for Reusability

โŒ Wrong: Copy-paste same VPC config across 10 projects

โœ… Right: Create reusable module

modules/vpc/main.tf:

variable "environment" {
  type = string
}

variable "cidr_block" {
  type = string
}

resource "aws_vpc" "main" {
  cidr_block = var.cidr_block
  tags = {
    Environment = var.environment
  }
}

output "vpc_id" {
  value = aws_vpc.main.id
}

Use module:

module "vpc" {
  source      = "./modules/vpc"
  environment = "production"
  cidr_block  = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id = module.vpc.vpc_id
  # ...
}

5. Not Planning Before Applying

โŒ Wrong:

terraform apply  # Directly applies without review!

โœ… Right:

terraform plan -out=tfplan  # Review changes
## ... review output carefully ...
terraform apply tfplan      # Apply approved plan

๐Ÿ’ก In CI/CD: Always require terraform plan approval before apply.


6. Using Latest Provider Versions in Production

โŒ Wrong:

provider "aws" {
  # No version constraint - gets latest!
}

โœ… Right:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"  # Allow 5.x updates, not 6.0
    }
  }
}

Version constraint operators:

  • ~> 5.0: Allows 5.1, 5.2, but not 6.0 (recommended)
  • >= 5.0: Any version 5.0 or higher (risky)
  • = 5.0.0: Exact version only (too strict)

๐Ÿ”‘ Key Takeaways

๐Ÿ“‹ Quick Reference Card

Concept Key Points
Provider Connects to AWS, specifies region and authentication
Resource Infrastructure component you create (EC2, S3, VPC, etc.)
Data Source Query existing resources you didn't create
State Tracks what Terraform manages; use remote backend + locking
Variables Make configs reusable; pass via CLI, files, or env vars
Outputs Expose values for other modules or external use
Modules Reusable Terraform code packages

Essential Commands:

Command Purpose
terraform init Initialize working directory, download providers
terraform plan Preview changes before applying
terraform apply Create/update infrastructure
terraform destroy Delete all managed infrastructure
terraform fmt Format code to standard style
terraform validate Check syntax and configuration
terraform state list List resources in state

AWS Resource Naming Patterns:

Service Terraform Resource Example
EC2 aws_instance Virtual machines
S3 aws_s3_bucket Object storage
VPC aws_vpc Virtual network
RDS aws_db_instance Managed database
Security Group aws_security_group Firewall rules
IAM Role aws_iam_role Identity permissions
CloudFront aws_cloudfront_distribution CDN
ALB aws_lb Load balancer

๐Ÿง  Memory Device: TERRAFORM Workflow

Type your configuration (.tf files)
Execute init to download providers
Review with plan before changes
Run apply to create resources
Audit state to track what exists
Format code with fmt
Outputs show important values
Remote backend stores state
Modules enable reuse


๐Ÿค” Did You Know?

  • Terraform Cloud: HashiCorp's SaaS offering provides remote state, collaboration features, and policy-as-code for free (up to 5 users)
  • Import existing resources: Use terraform import to bring manually-created AWS resources under Terraform management
  • Cost estimation: terraform plan integrations with Infracost can show projected AWS costs before deployment
  • Graph visualization: Run terraform graph | dot -Tpng > graph.png to see resource dependency diagrams
  • Terraform started as a side project by Mitchell Hashimoto during a flight in 2014. Now it's used by 70% of Fortune 500 companies!

๐Ÿ“š Further Study

  1. Terraform AWS Provider Documentation - https://registry.terraform.io/providers/hashicorp/aws/latest/docs
    Complete reference for all AWS resources and data sources

  2. HashiCorp Learn: Terraform on AWS - https://developer.hashicorp.com/terraform/tutorials/aws-get-started
    Official interactive tutorials from beginner to advanced

  3. AWS Well-Architected Framework - https://aws.amazon.com/architecture/well-architected/
    Best practices for building secure, reliable infrastructure on AWS


๐ŸŽ‰ Congratulations! You now understand Terraform's core concepts on AWS. Start small with a single EC2 instance or S3 bucket, then gradually build complex architectures. Remember: infrastructure as code isn't just about automationโ€”it's about creating reproducible, auditable, and collaborative infrastructure management.

Next steps:

  • ๐Ÿ”ง Build a personal project with VPC + EC2 + RDS
  • ๐Ÿ“ฆ Create your first reusable module
  • ๐Ÿ”„ Set up CI/CD pipeline with GitHub Actions or GitLab CI
  • ๐Ÿข Explore advanced patterns: workspaces, remote state sharing, policy enforcement

Happy Terraforming! ๐Ÿš€