Infrastructure as Code with Terraform: A Production-Ready Guide
Infrastructure as Code with Terraform: A Production-Ready Guide
Infrastructure as Code (IaC) transforms how we provision and manage cloud resources. This guide covers everything you need to know to implement Terraform in production.
Why Infrastructure as Code?
Traditional infrastructure management is:
- Manual: Point-and-click in AWS Console
- Error-prone: No version control or review process
- Undocumented: No single source of truth
- Difficult to replicate: Hard to create staging environments
IaC solves all of these problems.
Terraform Basics
Core Concepts
Resources: The infrastructure you want to create
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "WebServer"
Environment = "production"
}
}
Providers: Cloud platform integration
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
Variables: Reusable configuration values
variable "environment" {
description = "Environment name"
type = string
default = "dev"
}
variable "instance_count" {
description = "Number of instances"
type = number
default = 2
}
Production Best Practices
1. State Management
Never commit terraform.tfstate to git!
Use remote state with S3 + DynamoDB locking:
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "prod/infrastructure.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
2. Module Structure
Organize your code with modules:
terraform/
├── modules/
│ ├── vpc/
│ ├── ec2/
│ └── rds/
├── environments/
│ ├── dev/
│ ├── staging/
│ └── prod/
└── global/
└── s3/
3. Variable Management
Use terraform.tfvars for each environment:
dev.tfvars:
environment = "dev"
instance_type = "t3.micro"
instance_count = 1
enable_backups = false
prod.tfvars:
environment = "prod"
instance_type = "t3.large"
instance_count = 3
enable_backups = true
4. Security Best Practices
- Store secrets in AWS Secrets Manager or SSM Parameter Store
- Use data sources to reference existing resources
- Enable resource tagging for cost tracking
- Implement least-privilege IAM policies
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/database/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
# ... other configuration
}
5. CI/CD Integration
Example GitHub Actions workflow:
name: Terraform Apply
on:
push:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
- name: Terraform Init
run: terraform init
- name: Terraform Plan
run: terraform plan -var-file="prod.tfvars"
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve -var-file="prod.tfvars"
Real-World Example: Complete VPC Setup
Here's a production-ready VPC module:
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
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.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-${count.index + 1}"
Type = "public"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.environment}-igw"
}
}
Common Pitfalls to Avoid
1. Hardcoded Values
Bad:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0" # Don't hardcode AMI IDs
}
Good:
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
}
2. Not Using Count or For_Each
Create multiple similar resources efficiently:
resource "aws_instance" "web" {
for_each = toset(var.availability_zones)
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
availability_zone = each.value
tags = {
Name = "web-${each.value}"
}
}
3. Ignoring Terraform Plan
Always review terraform plan output before applying. Unexpected changes can cause downtime.
Cost Optimization with Terraform
Use lifecycle rules to prevent accidental resource deletion:
resource "aws_db_instance" "main" {
# ... configuration
lifecycle {
prevent_destroy = true
ignore_changes = [password]
}
}
Migration from Manual Infrastructure
Step-by-Step Process:
- Audit existing infrastructure: Document everything you have
- Import existing resources: Use
terraform import - Write Terraform code: Match current configuration
- Validate with plan: Ensure no changes detected
- Gradually add new features: Start managing with Terraform
Example import command:
terraform import aws_instance.web i-1234567890abcdef0
Testing Terraform Code
Use terraform validate and terraform fmt:
# Format code
terraform fmt -recursive
# Validate configuration
terraform validate
# Check with tflint
tflint --recursive
Conclusion
Infrastructure as Code with Terraform provides:
- ✅ Version control for infrastructure
- ✅ Reproducible environments
- ✅ Team collaboration
- ✅ Documentation through code
- ✅ Audit trail of changes
Start small, iterate, and gradually expand your Terraform usage.
Need help implementing IaC? Download our Terraform starter templates or schedule a consultation.
Related Articles
DevOps Best Practices for Startups: Deploy 10x Faster Without Breaking Things
A practical guide to implementing DevOps from day one. Learn how to set up CI/CD pipelines, automate deployments, and scale your infrastructure without hiring a DevOps team.
14 min readBuilding Production CI/CD Pipelines with GitHub Actions and AWS
Step-by-step guide to implementing automated deployment pipelines that improve deployment frequency and reduce errors.
15 min read