Infrastructure as code eliminates manual deployment steps. No more "click this button then that button." Your infrastructure is version controlled, reviewable, and reproducible.
Why Infrastructure as Code Matters
Manual infrastructure is:
- Unreproducible: Disaster recovery requires remembering steps. People forget. Systems break.
- Undocumented: Your infrastructure lives in someone's head. That person leaves. Knowledge walks out the door.
- Unmaintainable: Changes are ad-hoc. No audit trail. No rollback.
Terraform Basics
Terraform describes your infrastructure as code. You define what you want. Terraform figures out what to create, update, or destroy:
# main.tf
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# Define your infrastructure
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
}
resource "aws_security_group" "web" {
name = "web-security-group"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}State Management
Terraform tracks state. State is critical. Lose it, lose control of your infrastructure:
# terraform.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# CRITICAL: Enable state locking
# This prevents concurrent modifications
# that could corrupt stateVariables and Outputs
Extract values into variables. Make your code reusable:
# variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "instance_count" {
description = "Number of 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 "instance_ids" {
value = aws_instance.web[*].id
description = "IDs of the instances"
}
output "api_endpoint" {
value = aws_api_gateway_deployment.api.invoke_url
description = "API Gateway endpoint"
}Organizing with Modules
Modules allow you to package and reuse infrastructure components:
# modules/database/main.tf
resource "aws_db_instance" "default" {
allocated_storage = var.storage_size
storage_type = "gp2"
engine = "postgres"
engine_version = "14"
instance_class = var.instance_class
database_name = var.db_name
username = var.db_user
password = var.db_password
parameter_group_name = "default.postgres14"
skip_final_snapshot = false
final_snapshot_identifier = "${var.environment}-snapshot"
}
# main.tf - Using the module
module "database" {
source = "./modules/database"
environment = "production"
storage_size = 100
instance_class = "db.t3.micro"
db_name = "myapp"
db_user = "admin"
db_password = var.db_password
}Common Pitfalls
- Hardcoding values: Use variables. Don't hardcode passwords or IPs.
- Ignoring state: Treat state like your database. Back it up. Lock it.
- No destruction testing: Test your terraform destroy. You'll need it in disaster recovery.
- Skipping version control: All Terraform goes in git. Every change is reviewed.
Recommended Workflow
# 1. Plan changes (always review first)
terraform plan -out=tfplan
# 2. Review the plan carefully
cat tfplan
# 3. Apply only if it looks right
terraform apply tfplan
# 4. Commit to git
git add terraform.tf variables.tf
git commit -m "Infrastructure: Add RDS database"