Terraform Infrastructure Guide 2026 — Infrastructure as Code for AWS, GCP, and Azure

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

"ClickOps" — manually creating resources in the AWS console — does not scale and cannot be audited. Terraform lets you define infrastructure as version-controlled code, reproduce environments exactly, and review infrastructure changes in pull requests. In 2026, Terraform remains the most widely adopted infrastructure-as-code tool across cloud providers.

Core Concepts

Provider   → Plugin that manages a cloud (aws, google, azurerm)
Resource   → A cloud object to create (aws_s3_bucket, aws_instance)
Variable   → Input to parameterize configurations
Output     → Values exported for use by other modules
State      → Terraform's record of what it created
Module     → Reusable, parameterized group of resources
Backend    → Where remote state is stored (S3, Terraform Cloud)

Basic AWS Setup with Remote State

# main.tf
terraform {
  required_version = ">= 1.7"
 
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
 
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}
 
provider "aws" {
  region = var.aws_region
 
  default_tags {
    tags = {
      Project     = "myapp"
      Environment = var.environment
      ManagedBy   = "terraform"
    }
  }
}

Variables and Validation

# variables.tf
variable "environment" {
  description = "Deployment environment"
  type        = string
  validation {
    condition     = contains(["development", "staging", "production"], var.environment)
    error_message = "Environment must be development, staging, or production."
  }
}
 
variable "db_password" {
  description = "Database master password"
  type        = string
  sensitive   = true
}
 
variable "app_name" {
  type    = string
  default = "myapp"
}
 
variable "aws_region" {
  type    = string
  default = "us-east-1"
}

Complete AWS Infrastructure

# network.tf
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
}
 
resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true
}
 
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]
}
 
# S3 bucket with public access blocked
resource "aws_s3_bucket" "assets" {
  bucket = "${var.app_name}-assets-${var.environment}"
}
 
resource "aws_s3_bucket_public_access_block" "assets" {
  bucket                  = aws_s3_bucket.assets.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
 
# RDS PostgreSQL — multi-AZ in production only
resource "aws_db_instance" "postgres" {
  identifier        = "${var.app_name}-${var.environment}"
  engine            = "postgres"
  engine_version    = "16.2"
  instance_class    = var.environment == "production" ? "db.t3.medium" : "db.t3.micro"
  allocated_storage = 20
  max_allocated_storage = 100
  storage_encrypted = true
 
  db_name  = replace(var.app_name, "-", "_")
  username = "dbadmin"
  password = var.db_password
 
  multi_az            = var.environment == "production"
  skip_final_snapshot = var.environment != "production"
  deletion_protection = var.environment == "production"
 
  vpc_security_group_ids = [aws_security_group.rds.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name
 
  backup_retention_period = var.environment == "production" ? 7 : 1
  backup_window           = "03:00-04:00"
}

Reusable Modules

# modules/ecs-service/main.tf
variable "service_name"  { type = string }
variable "image"         { type = string }
variable "port"          { type = number }
variable "cpu"           { type = number; default = 256 }
variable "memory"        { type = number; default = 512 }
variable "desired_count" { type = number; default = 2 }
 
resource "aws_ecs_task_definition" "service" {
  family                   = var.service_name
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.cpu
  memory                   = var.memory
 
  container_definitions = jsonencode([{
    name         = var.service_name
    image        = var.image
    portMappings = [{ containerPort = var.port }]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        awslogs-group  = "/ecs/${var.service_name}"
        awslogs-region = "us-east-1"
      }
    }
  }])
}
 
output "task_definition_arn" {
  value = aws_ecs_task_definition.service.arn
}
 
# Use the module
module "api" {
  source       = "./modules/ecs-service"
  service_name = "api"
  image        = "ghcr.io/myapp/api:${var.api_version}"
  port         = 3000
  desired_count = 3
}

Workspaces for Multiple Environments

# Create and switch workspaces
terraform workspace new staging
terraform workspace new production
terraform workspace select production
terraform workspace list
# Use workspace name to vary resources
resource "aws_instance" "app" {
  count = terraform.workspace == "production" ? 3 : 1
 
  instance_type = terraform.workspace == "production" ? "t3.medium" : "t3.micro"
  # ...
}

CI/CD with GitHub Actions

# .github/workflows/terraform.yml
name: Terraform
 
on:
  pull_request:
    paths: ['terraform/**']
  push:
    branches: [main]
    paths: ['terraform/**']
 
jobs:
  plan:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      - run: terraform plan -no-color
        env:
          TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
 
  apply:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform apply -auto-approve
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}

Common Mistakes

  • No remote state backend — local state files get lost and cannot be shared with teammates; always use S3 + DynamoDB locking
  • Committing terraform.tfvars with secrets — use sensitive = true on variables and pass secrets via CI/CD environment variables
  • No state locking — without DynamoDB locking, concurrent terraform apply runs corrupt state
  • Hardcoding AMI IDs — use data sources (data "aws_ami") to always fetch the latest approved image
  • No drift detection — run terraform plan in CI regularly to catch resources changed outside Terraform

Best Practices

  • Pin provider versions (~> 5.0) to prevent unexpected breaking changes on terraform init
  • Use terraform fmt and terraform validate in pre-commit hooks to catch syntax errors before code review
  • Separate state per environment (production/terraform.tfstate, staging/terraform.tfstate)
  • Use terraform import to bring manually created resources under Terraform management
  • Run checkov or tfsec in CI to catch security misconfigurations before applying

Key Takeaways

  • Terraform state tracks every resource it creates; never delete or manually edit the state file
  • Remote state in S3 with DynamoDB locking is required for team workflows — prevents concurrent apply collisions
  • Modules encapsulate reusable infrastructure patterns; use them to avoid copy-pasting VPC, security group, or ECS definitions
  • Workspaces let a single Terraform codebase manage multiple environments with different variable values
  • terraform plan shows exactly what will change before apply — always review the plan output in CI as part of code review
  • Mark secrets as sensitive = true so Terraform redacts them from logs and plan output
  • Use data sources to reference existing resources (like VPC IDs or AMI IDs) without hardcoding values
  • The HashiCorp Terraform Associate certification validates these skills and is recognized by most cloud employers

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading