Terraform enables you to define and manage infrastructure declaratively. This comprehensive guide covers everything from basic concepts to production-ready patterns.
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
}
}
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.