13 min read A. Sulaiman

Terraform Infrastructure as Code Mastery

Complete guide to managing infrastructure with Terraform, from basics to advanced patterns and best practices

Terraform enables you to define and manage infrastructure declaratively. This comprehensive guide covers everything from basic concepts to production-ready patterns.

Terraform Workflow
Figure 1: Terraform workflow and state management

Why Terraform?

  • Multi-cloud - AWS, GCP, Azure, and 3000+ providers
  • Declarative - Define desired state, not procedures
  • Version Control - Infrastructure as code in Git
  • Reusable - Modules for common patterns
  • Plan Before Apply - Preview changes before execution

1. Basic Structure

# main.tf
terraform {
  required_version = ">= 1.0"

  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-locks"
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "Terraform"
      Project     = "MyApp"
    }
  }
}

2. Variables and Outputs

Variables

# variables.tf
variable "environment" {
  description = "Environment name"
  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 "vpc_cidr" {
  description = "VPC CIDR block"
  type        = string
  default     = "10.0.0.0/16"
}

variable "availability_zones" {
  description = "List of availability zones"
  type        = list(string)
  default     = ["us-east-1a", "us-east-1b"]
}

variable "tags" {
  description = "Common tags for resources"
  type        = map(string)
  default     = {}
}

Outputs

# outputs.tf
output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "public_subnet_ids" {
  description = "List of public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "database_endpoint" {
  description = "RDS endpoint"
  value       = aws_db_instance.main.endpoint
  sensitive   = true
}

3. Resource Examples

VPC and Networking

# vpc.tf
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(var.tags, {
    Name = "${var.environment}-vpc"
  })
}

resource "aws_subnet" "public" {
  count = length(var.availability_zones)

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone = var.availability_zones[count.index]

  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name = "${var.environment}-public-${count.index + 1}"
    Type = "Public"
  })
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = merge(var.tags, {
    Name = "${var.environment}-igw"
  })
}

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 = merge(var.tags, {
    Name = "${var.environment}-public-rt"
  })
}

EC2 with Auto Scaling

# compute.tf
resource "aws_launch_template" "app" {
  name_prefix   = "${var.environment}-app-"
  image_id      = data.aws_ami.amazon_linux_2.id
  instance_type = var.instance_type

  vpc_security_group_ids = [aws_security_group.app.id]

  user_data = base64encode(templatefile("${path.module}/user-data.sh", {
    environment = var.environment
  }))

  iam_instance_profile {
    name = aws_iam_instance_profile.app.name
  }

  monitoring {
    enabled = true
  }

  tag_specifications {
    resource_type = "instance"

    tags = merge(var.tags, {
      Name = "${var.environment}-app"
    })
  }
}

resource "aws_autoscaling_group" "app" {
  name = "${var.environment}-app-asg"

  vpc_zone_identifier = aws_subnet.private[*].id
  target_group_arns   = [aws_lb_target_group.app.arn]
  health_check_type   = "ELB"

  min_size         = 2
  max_size         = 10
  desired_capacity = 2

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  tag {
    key                 = "Name"
    value               = "${var.environment}-app"
    propagate_at_launch = true
  }
}

Terraform Module Structure
Figure 2: Terraform module organization

4. Modules

Module Structure

modules/
└── vpc/
    ├── main.tf
    ├── variables.tf
    ├── outputs.tf
    └── README.md

Using Modules

module "vpc" {
  source = "./modules/vpc"

  environment        = var.environment
  vpc_cidr          = "10.0.0.0/16"
  availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]

  tags = var.common_tags
}

module "rds" {
  source = "./modules/rds"

  environment    = var.environment
  vpc_id         = module.vpc.vpc_id
  subnet_ids     = module.vpc.private_subnet_ids
  instance_class = "db.t3.micro"

  database_name = "myapp"
  username      = var.db_username
  password      = var.db_password
}

5. State Management

Remote State

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

    # Prevent accidental deletion
    skip_region_validation      = false
    skip_credentials_validation = false
  }
}

State Commands

# View state
terraform state list
terraform state show aws_instance.app

# Move resources
terraform state mv aws_instance.old aws_instance.new

# Remove from state (keeps resource)
terraform state rm aws_instance.test

# Import existing resource
terraform import aws_instance.app i-1234567890abcdef0

# Refresh state
terraform refresh

6. Workspaces

# Create workspace
terraform workspace new staging

# Switch workspace
terraform workspace select prod

# List workspaces
terraform workspace list

# Using workspace in code
resource "aws_instance" "app" {
  instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"

  tags = {
    Workspace = terraform.workspace
  }
}

7. Data Sources

# Fetch 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 current AWS account
data "aws_caller_identity" "current" {}

# Fetch existing VPC
data "aws_vpc" "existing" {
  id = var.existing_vpc_id
}

# Use data in resources
resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = "t3.micro"

  tags = {
    Owner = data.aws_caller_identity.current.account_id
  }
}

8. Provisioners (Use Sparingly)

resource "aws_instance" "app" {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = "t3.micro"

  # File provisioner
  provisioner "file" {
    source      = "app.conf"
    destination = "/etc/app/app.conf"

    connection {
      type        = "ssh"
      user        = "ec2-user"
      private_key = file(var.private_key_path)
      host        = self.public_ip
    }
  }

  # Remote exec
  provisioner "remote-exec" {
    inline = [
      "sudo systemctl start app",
      "sudo systemctl enable app"
    ]
  }

  # Local exec
  provisioner "local-exec" {
    command = "echo ${self.private_ip} >> private_ips.txt"
  }
}

9. Best Practices

Use Locals

locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "Terraform"
    Project     = var.project_name
  }

  name_prefix = "${var.project_name}-${var.environment}"

  availability_zones = slice(
    data.aws_availability_zones.available.names,
    0,
    var.az_count
  )
}

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
  tags       = local.common_tags
}

Dynamic Blocks

resource "aws_security_group" "app" {
  name   = "${local.name_prefix}-app-sg"
  vpc_id = aws_vpc.main.id

  dynamic "ingress" {
    for_each = var.ingress_rules

    content {
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }
}

Conditional Resources

resource "aws_db_instance" "replica" {
  count = var.environment == "prod" ? 1 : 0

  replicate_source_db = aws_db_instance.main.id
  instance_class      = var.replica_instance_class
}

10. Testing and Validation

Terraform Validate

# Check syntax
terraform validate

# Format code
terraform fmt -recursive

# Security scan
tfsec .

# Cost estimation
infracost breakdown --path .

Terratest (Go)

package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestVPCModule(t *testing.T) {
    opts := &terraform.Options{
        TerraformDir: "../modules/vpc",
        Vars: map[string]interface{}{
            "environment": "test",
            "vpc_cidr":    "10.0.0.0/16",
        },
    }

    defer terraform.Destroy(t, opts)
    terraform.InitAndApply(t, opts)

    vpcID := terraform.Output(t, opts, "vpc_id")
    assert.NotEmpty(t, vpcID)
}

Terraform Workflow Checklist

  • [ ] Code formatted (terraform fmt)
  • [ ] Validated (terraform validate)
  • [ ] Security scanned (tfsec)
  • [ ] Plan reviewed (terraform plan)
  • [ ] State locked
  • [ ] Changes applied (terraform apply)
  • [ ] Outputs documented
  • [ ] State backed up

Common Patterns

Pattern Use Case
Modules Reusable infrastructure components
Workspaces Multi-environment management
Remote State Team collaboration
Data Sources Reference existing resources
Locals Computed values and DRY code

Conclusion

Terraform provides:
- ✅ Infrastructure versioning in Git
- ✅ Automated provisioning and updates
- ✅ Multi-cloud flexibility
- ✅ Team collaboration with remote state
- ✅ Predictable changes with plan/apply workflow

Start simple and gradually adopt advanced patterns as your infrastructure grows.


Need Terraform expertise? Contact us for infrastructure automation consulting.