Terraform — Infrastructure as Code

Provision and manage cloud infrastructure declaratively using HashiCorp Configuration Language (HCL).

What is Terraform?

Beginner

Terraform is an open-source Infrastructure as Code (IaC) tool by HashiCorp. It lets you define cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share.

Declarative vs Imperative

Terraform is declarative — you describe the desired end state and Terraform figures out how to achieve it. This is different from imperative tools (scripts) where you describe the steps to take.

Why Terraform?

Installation & Setup

Beginner
# Install Terraform on Ubuntu/Debian
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

# Verify
terraform version

# Enable tab completion
terraform -install-autocomplete

HCL Basics

Beginner

HashiCorp Configuration Language (HCL) is Terraform's configuration language.

# Block types in Terraform

# Provider block — configures a cloud provider
provider "aws" {
  region = "us-east-1"
}

# Resource block — defines infrastructure
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  tags = {
    Name = "WebServer"
  }
}

# Variable block — parameterizes configuration
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

# Output block — exports values
output "public_ip" {
  value = aws_instance.web.public_ip
}

Your First Terraform Configuration

Beginner
# main.tf — Create an EC2 instance with a security group
terraform {
  required_version = ">= 1.7"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_security_group" "web_sg" {
  name_prefix = "web-sg-"
  description = "Allow HTTP and SSH"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    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"]
  }
}

resource "aws_instance" "web" {
  ami                    = "ami-0c55b159cbfafe1f0"
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.web_sg.id]

  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

  tags = {
    Name        = "WebServer"
    Environment = "dev"
    ManagedBy   = "terraform"
  }
}

output "web_url" {
  value = "http://${aws_instance.web.public_ip}"
}

Core Commands

Beginner
# Initialize — download providers and modules
terraform init

# Format — auto-format your .tf files
terraform fmt -recursive

# Validate — check syntax and configuration
terraform validate

# Plan — preview changes (dry run)
terraform plan

# Apply — create/update infrastructure
terraform apply

# Apply with auto-approve (CI/CD)
terraform apply -auto-approve

# Show current state
terraform show

# List resources in state
terraform state list

# Destroy all resources
terraform destroy

Variables & Outputs

Intermediate
# variables.tf
variable "environment" {
  description = "Deployment environment"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "production"], var.environment)
    error_message = "Must be dev, staging, or production."
  }
}

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

variable "allowed_cidrs" {
  description = "List of allowed CIDRs"
  type        = list(string)
  default     = ["10.0.0.0/16"]
}

variable "tags" {
  description = "Resource tags"
  type        = map(string)
  default = {
    ManagedBy = "terraform"
    Project   = "devops-mastery"
  }
}

# Using variables
resource "aws_instance" "web" {
  count         = var.instance_count
  instance_type = var.environment == "production" ? "t3.medium" : "t3.micro"
  tags          = merge(var.tags, { Name = "web-${count.index + 1}" })
}
# terraform.tfvars (environment-specific values)
environment    = "production"
instance_count = 3
allowed_cidrs  = ["10.0.0.0/16", "172.16.0.0/12"]

# Or use -var flag
terraform apply -var="environment=staging"

# Or environment variables
export TF_VAR_environment="production"

State Management

Intermediate

Terraform state maps your config to real-world resources. It's stored in terraform.tfstate by default.

Critical

Never edit the state file manually. Never commit terraform.tfstate to version control — it may contain secrets. Always use remote state in team environments.

# State commands
terraform state list                          # List all resources
terraform state show aws_instance.web         # Show resource details
terraform state mv aws_instance.web aws_instance.app  # Rename resource
terraform state rm aws_instance.web           # Remove from state (doesn't destroy)
terraform state pull                          # Download remote state
terraform import aws_instance.web i-12345     # Import existing resource

Modules

Intermediate

Modules are reusable packages of Terraform configuration — the building blocks of your infrastructure.

# Module structure:
# modules/vpc/
# ├── main.tf
# ├── variables.tf
# ├── outputs.tf
# └── README.md

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

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

resource "aws_subnet" "public" {
  count                   = length(var.public_subnet_cidrs)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = var.azs[count.index]
  map_public_ip_on_launch = true

  tags = { Name = "${var.project}-public-${count.index + 1}" }
}

# modules/vpc/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}
output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}
# Using the module in root config
module "vpc" {
  source = "./modules/vpc"

  cidr_block          = "10.0.0.0/16"
  public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
  azs                 = ["us-east-1a", "us-east-1b"]
  project             = "devops-mastery"
  tags                = var.tags
}

# Reference module outputs
resource "aws_instance" "web" {
  subnet_id = module.vpc.public_subnet_ids[0]
}

Data Sources

Intermediate

Data sources let you query existing infrastructure that wasn't created by your Terraform config.

# Look up the latest Amazon Linux 2 AMI
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

# Get current AWS account info
data "aws_caller_identity" "current" {}

# Look up an existing VPC
data "aws_vpc" "existing" {
  filter {
    name   = "tag:Name"
    values = ["Production-VPC"]
  }
}

# Use data source results
resource "aws_instance" "web" {
  ami       = data.aws_ami.amazon_linux.id
  subnet_id = data.aws_vpc.existing.id
}

Workspaces

Advanced

Workspaces allow you to manage multiple environments with the same configuration.

# Create and switch workspaces
terraform workspace new staging
terraform workspace new production
terraform workspace select staging

# List workspaces
terraform workspace list

# Use workspace in configuration
resource "aws_instance" "web" {
  instance_type = terraform.workspace == "production" ? "t3.large" : "t3.micro"
  count         = terraform.workspace == "production" ? 3 : 1

  tags = {
    Environment = terraform.workspace
  }
}

Remote State & Locking

Advanced
# backend.tf — S3 + DynamoDB for remote state with locking
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

# Create the backend resources first (bootstrap)
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state-bucket"

  lifecycle {
    prevent_destroy = true
  }
}

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

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Best Practices

Advanced
  1. Use remote state with locking (S3 + DynamoDB)
  2. Structure your project — Separate files: main.tf, variables.tf, outputs.tf, providers.tf
  3. Use modules for reusable components
  4. Pin provider versions — Avoid unexpected breaking changes
  5. Use terraform fmt and terraform validate in CI
  6. Tag all resources for cost tracking and organization
  7. Use prevent_destroy lifecycle on critical resources
  8. Never hardcode secrets — Use variables, AWS Secrets Manager, or Vault
  9. Keep state files small — Break large infrastructures into multiple state files
  10. Review plans in PRs — Use terraform plan output in pull request comments

CI/CD with Terraform

Advanced
# .github/workflows/terraform.yml
name: Terraform CI/CD

on:
  pull_request:
    paths: ['infrastructure/**']
  push:
    branches: [main]
    paths: ['infrastructure/**']

jobs:
  terraform:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: infrastructure

    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.7.0

      - name: Terraform Init
        run: terraform init

      - name: Terraform Format Check
        run: terraform fmt -check -recursive

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan

      - name: Comment Plan on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const plan = `${{ steps.plan.outputs.stdout }}`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Terraform Plan\n\`\`\`\n${plan}\n\`\`\``
            });

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve tfplan