All posts

Terraform Infrastructure as Code: Complete Guide

· Pavan Y N · terraform, iac, infrastructure, automation, hashicorp

Published: October 30, 2025
Reading Time: 26 minutes
Tags: Terraform, IaC, Infrastructure, Automation, HashiCorp

# Introduction

Terraform by HashiCorp is the industry-leading Infrastructure as Code (IaC) tool that enables you to define, provision, and manage infrastructure using declarative configuration files. With support for 3000+ providers, Terraform has become the de facto standard for multi-cloud infrastructure management.

This comprehensive guide covers Terraform fundamentals, advanced patterns, state management, and production best practices.

# What is Terraform?

Terraform is an open-source IaC tool that allows you to build, change, and version infrastructure safely and efficiently across multiple cloud providers.

## Key Concepts

1. Providers: Plugins that interact with APIs (AWS, Azure, GCP, Kubernetes, etc.)

2. Resources: Infrastructure components (VMs, networks, databases, etc.)

3. State: Current infrastructure state tracked by Terraform

4. Modules: Reusable Terraform configurations

5. Variables: Parameterize configurations

6. Outputs: Export values from configurations

## Terraform Workflow

code
┌──────────┐
│  Write   │  Define infrastructure in .tf files
└────┬─────┘
     │
┌────▼─────┐
│   Init   │  Initialize providers and modules
└────┬─────┘
     │
┌────▼─────┐
│   Plan   │  Preview changes
└────┬─────┘
     │
┌────▼─────┐
│  Apply   │  Create/update infrastructure
└────┬─────┘
     │
┌────▼─────┐
│ Destroy  │  Remove infrastructure (when needed)
└──────────┘

# Getting Started

## Installation

code
# macOS
brew install terraform

# Linux
wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
unzip terraform_1.6.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/

# Verify installation
terraform version

## First Terraform Configuration

main.tf:

code
# Configure Terraform
terraform {
  required_version = ">= 1.6.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"
  }
}

# Configure AWS Provider
provider "aws" {
  region = var.aws_region
  
  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "Terraform"
      Project     = var.project_name
    }
  }
}

# Create VPC
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${var.project_name}-vpc"
  }
}

# Create Internet Gateway
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project_name}-igw"
  }
}

# Create Public Subnet
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 = {
    Name = "${var.project_name}-public-subnet-${count.index + 1}"
    Type = "public"
  }
}

# Create Route Table
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 = "${var.project_name}-public-rt"
  }
}

# Associate Route Table with Subnets
resource "aws_route_table_association" "public" {
  count          = length(aws_subnet.public)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

# Create Security Group
resource "aws_security_group" "web" {
  name        = "${var.project_name}-web-sg"
  description = "Security group for web servers"
  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"]
  }

  egress {
    description = "Allow all outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-web-sg"
  }
}

# Create EC2 Instance
resource "aws_instance" "web" {
  count                  = var.instance_count
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public[count.index % length(aws_subnet.public)].id
  vpc_security_group_ids = [aws_security_group.web.id]
  
  user_data = templatefile("${path.module}/user-data.sh", {
    app_name = var.project_name
  })

  root_block_device {
    volume_size           = 20
    volume_type           = "gp3"
    encrypted             = true
    delete_on_termination = true
  }

  tags = {
    Name = "${var.project_name}-web-${count.index + 1}"
  }
}

# Data source for 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"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

variables.tf:

code
variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"
}

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

variable "project_name" {
  description = "Project name for resource naming"
  type        = string
}

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
  default     = "10.0.0.0/16"
  
  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "Must be a valid IPv4 CIDR block."
  }
}

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

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

variable "instance_count" {
  description = "Number of EC2 instances"
  type        = number
  default     = 2
  
  validation {
    condition     = var.instance_count > 0 && var.instance_count <= 10
    error_message = "Instance count must be between 1 and 10."
  }
}

outputs.tf:

code
output "vpc_id" {
  description = "ID of the VPC"
  value       = aws_vpc.main.id
}

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

output "web_instance_ids" {
  description = "IDs of web server instances"
  value       = aws_instance.web[*].id
}

output "web_instance_public_ips" {
  description = "Public IPs of web servers"
  value       = aws_instance.web[*].public_ip
}

output "security_group_id" {
  description = "ID of web security group"
  value       = aws_security_group.web.id
}

terraform.tfvars:

code
aws_region         = "us-east-1"
environment        = "production"
project_name       = "my-web-app"
vpc_cidr           = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
instance_type      = "t3.small"
instance_count     = 3

## Terraform Commands

code
# Initialize Terraform
terraform init

# Validate configuration
terraform validate

# Format code
terraform fmt -recursive

# Preview changes
terraform plan

# Apply changes
terraform apply

# Apply without confirmation
terraform apply -auto-approve

# Destroy infrastructure
terraform destroy

# Show current state
terraform show

# List resources in state
terraform state list

# Show specific resource
terraform state show aws_instance.web[0]

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

# Refresh state
terraform refresh

# Output values
terraform output

# Specific output
terraform output vpc_id

# Advanced Terraform Patterns

## 1. Modules

Module Structure:

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

modules/vpc/main.tf:

code
resource "aws_vpc" "this" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = var.enable_dns_hostnames
  enable_dns_support   = var.enable_dns_support

  tags = merge(
    var.tags,
    {
      Name = var.name
    }
  )
}

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

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

resource "aws_subnet" "private" {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = merge(
    var.tags,
    {
      Name = "${var.name}-private-${count.index + 1}"
      Type = "private"
    }
  )
}

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

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

resource "aws_eip" "nat" {
  count  = var.enable_nat_gateway ? length(var.public_subnet_cidrs) : 0
  domain = "vpc"

  tags = merge(
    var.tags,
    {
      Name = "${var.name}-nat-eip-${count.index + 1}"
    }
  )
}

resource "aws_nat_gateway" "this" {
  count         = var.enable_nat_gateway ? length(var.public_subnet_cidrs) : 0
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = merge(
    var.tags,
    {
      Name = "${var.name}-nat-${count.index + 1}"
    }
  )

  depends_on = [aws_internet_gateway.this]
}

Using the Module:

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

  name                 = "production-vpc"
  cidr_block           = "10.0.0.0/16"
  availability_zones   = ["us-east-1a", "us-east-1b", "us-east-1c"]
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
  enable_nat_gateway   = true
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Environment = "production"
    ManagedBy   = "Terraform"
  }
}

# Use module outputs
resource "aws_instance" "app" {
  subnet_id = module.vpc.public_subnet_ids[0]
  # ... other configuration
}

## 2. Workspaces

code
# List workspaces
terraform workspace list

# Create new workspace
terraform workspace new staging

# Switch workspace
terraform workspace select production

# Show current workspace
terraform workspace show

# Delete workspace
terraform workspace delete staging

Using Workspaces in Configuration:

code
locals {
  environment = terraform.workspace
  
  instance_counts = {
    default    = 1
    staging    = 2
    production = 5
  }
  
  instance_types = {
    default    = "t3.micro"
    staging    = "t3.small"
    production = "t3.medium"
  }
}

resource "aws_instance" "app" {
  count         = local.instance_counts[local.environment]
  instance_type = local.instance_types[local.environment]
  
  tags = {
    Name        = "app-${local.environment}-${count.index + 1}"
    Environment = local.environment
  }
}

## 3. Dynamic Blocks

code
variable "ingress_rules" {
  type = list(object({
    description = string
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  
  default = [
    {
      description = "HTTP"
      from_port   = 80
      to_port     = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "HTTPS"
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Security group for web servers"
  vpc_id      = aws_vpc.main.id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      description = ingress.value.description
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

## 4. Conditional Resources

code
variable "create_database" {
  description = "Whether to create database"
  type        = bool
  default     = true
}

variable "environment" {
  description = "Environment name"
  type        = string
}

resource "aws_db_instance" "main" {
  count = var.create_database ? 1 : 0
  
  identifier        = "myapp-db"
  engine            = "postgres"
  engine_version    = "15.3"
  instance_class    = var.environment == "production" ? "db.t3.medium" : "db.t3.micro"
  allocated_storage = var.environment == "production" ? 100 : 20
  
  # ... other configuration
}

# Reference with conditional
output "database_endpoint" {
  value = var.create_database ? aws_db_instance.main[0].endpoint : null
}

## 5. For Expressions

code
variable "users" {
  type = list(string)
  default = ["alice", "bob", "charlie"]
}

# Create IAM users
resource "aws_iam_user" "users" {
  for_each = toset(var.users)
  name     = each.value
}

# Create map from list
locals {
  user_arns = {
    for user in aws_iam_user.users :
    user.name => user.arn
  }
}

# Filter and transform
locals {
  production_instances = {
    for k, v in aws_instance.app :
    k => v.id
    if v.tags["Environment"] == "production"
  }
}

# State Management

## Remote State with S3

backend.tf:

code
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
    kms_key_id     = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
  }
}

Create S3 Backend Resources:

code
# S3 bucket for state
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.terraform_state.arn
    }
  }
}

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

# KMS key for encryption
resource "aws_kms_key" "terraform_state" {
  description             = "KMS key for Terraform state encryption"
  deletion_window_in_days = 10
  enable_key_rotation     = true
}

## State Commands

code
# List resources in state
terraform state list

# Show resource details
terraform state show aws_instance.web[0]

# Move resource in state
terraform state mv aws_instance.web aws_instance.app

# Remove resource from state
terraform state rm aws_instance.web[0]

# Pull remote state
terraform state pull > terraform.tfstate

# Push local state to remote
terraform state push terraform.tfstate

# Replace provider in state
terraform state replace-provider hashicorp/aws registry.terraform.io/hashicorp/aws

# STAR Approach: Terraform Implementation

Situation: Company managing infrastructure manually across AWS, Azure, and GCP, leading to configuration drift, slow provisioning, and frequent errors.

Task: Implement Terraform to standardize infrastructure provisioning, eliminate drift, and enable self-service for development teams.

Action:

  1. •

    Assessment & Planning:

    • →Audited existing infrastructure across all cloud providers
    • →Designed module structure for reusability
    • →Created naming conventions and tagging standards
    • →Established Git workflow for infrastructure changes
  2. •

    Module Development:

    • →Created 20+ reusable modules (VPC, EKS, RDS, etc.)
    • →Implemented comprehensive variable validation
    • →Added detailed documentation and examples
    • →Published modules to private registry
  3. •

    State Management:

    • →Set up remote state in S3 with encryption
    • →Implemented state locking with DynamoDB
    • →Created separate state files per environment
    • →Configured state backup and recovery procedures
  4. •

    CI/CD Integration:

    • →Automated terraform plan on pull requests
    • →Implemented automated testing with Terratest
    • →Added security scanning with tfsec and Checkov
    • →Created approval workflow for production changes
  5. •

    Migration Execution:

    • →Imported existing resources into Terraform state
    • →Migrated environments incrementally (dev → staging → prod)
    • →Validated infrastructure parity
    • →Documented migration procedures

Result:

  • →Infrastructure provisioning time reduced from days to 30 minutes
  • →Configuration drift eliminated (100% compliance)
  • →Infrastructure changes now peer-reviewed like code
  • →Self-service enabled for 50+ developers
  • →Cost reduced by 30% through standardization
  • →Time to recover from failures decreased by 80%
  • →Multi-cloud management unified under single tool

# Common Issues & Troubleshooting

## Issue 1: State Lock Errors

Symptoms:

  • →"Error locking state" messages
  • →Cannot run terraform commands

Diagnosis:

code
# Check DynamoDB for locks
aws dynamodb scan --table-name terraform-locks

# View lock details
terraform force-unlock <LOCK_ID>

Solutions:

  • →Wait for other operations to complete
  • →Force unlock if operation failed (use with caution)
  • →Check network connectivity to backend
  • →Verify IAM permissions for DynamoDB

## Issue 2: Resource Already Exists

Symptoms:

  • →"AlreadyExists" errors during apply
  • →Resources created outside Terraform

Solutions:

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

# Or remove from configuration and manage separately
terraform state rm aws_instance.web

## Issue 3: Dependency Errors

Symptoms:

  • →Resources created in wrong order
  • →Dependency cycle errors

Solutions:

code
# Explicit dependency
resource "aws_instance" "web" {
  # ... configuration
  
  depends_on = [aws_security_group.web]
}

# Implicit dependency (preferred)
resource "aws_instance" "web" {
  vpc_security_group_ids = [aws_security_group.web.id]
}

## Issue 4: Provider Version Conflicts

Symptoms:

  • →Provider initialization failures
  • →Incompatible provider versions

Solutions:

code
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"  # Allow 5.x versions
    }
  }
}

# Lock provider versions
terraform providers lock \
  -platform=linux_amd64 \
  -platform=darwin_amd64 \
  -platform=windows_amd64

# Best Practices

## 1. Code Organization

  • →Use modules for reusability
  • →Separate environments with workspaces or directories
  • →Keep configurations DRY
  • →Use consistent naming conventions

## 2. State Management

  • →Always use remote state
  • →Enable state locking
  • →Encrypt state files
  • →Regular state backups

## 3. Security

  • →Never commit secrets to version control
  • →Use variables for sensitive data
  • →Implement least privilege IAM policies
  • →Scan configurations for security issues

## 4. Version Control

  • →Store all Terraform code in Git
  • →Use meaningful commit messages
  • →Require pull requests for changes
  • →Tag releases

## 5. Testing

  • →Validate configurations before apply
  • →Use terraform plan extensively
  • →Implement automated testing
  • →Test in non-production first

# Conclusion

Terraform has revolutionized infrastructure management by bringing software development practices to operations. By treating infrastructure as code, teams can version, test, and deploy infrastructure with the same rigor as application code.

Whether managing a single cloud provider or orchestrating multi-cloud infrastructure, Terraform provides the tools and flexibility to build reliable, scalable, and maintainable infrastructure.

# Additional Resources


Ready to start with Terraform? Begin with a simple resource and gradually build more complex infrastructure as you learn!