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
┌──────────┐
│ 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
# 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:
# 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:
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:
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:
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
# 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:
modules/
└── vpc/
├── main.tf
├── variables.tf
├── outputs.tf
└── README.md
modules/vpc/main.tf:
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:
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
# 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:
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
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
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
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:
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:
# 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
# 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:
- •
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
- •
Module Development:
- →Created 20+ reusable modules (VPC, EKS, RDS, etc.)
- →Implemented comprehensive variable validation
- →Added detailed documentation and examples
- →Published modules to private registry
- •
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
- •
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
- •
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:
# 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:
# 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:
# 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:
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!