Terraform Reference: AWS Basic Infrastructure
Terraform reference: AWS basic infrastructure
TL;DR
- Bottom line: Terraform with the AWS provider (
hashicorp/aws ~> 5.0) lets you define VPC, EC2, S3, RDS, and IAM as code — version-controlled, reproducible, and reviewable infrastructure. - Key tool/command:
terraform init && terraform plan && terraform apply - Watch out for: Local state files — always configure an S3 + DynamoDB backend before your first apply or you risk state loss and team conflicts.
- Works with: Terraform 1.6+, AWS provider ~> 5.0, OpenTofu 1.6+ (drop-in compatible).
Constraints
- Always use remote state (S3 + DynamoDB) — never commit terraform.tfstate to version control.
- Pin provider versions with
~>constraint — unversioned providers break on major updates. - Never store secrets in .tf files or state — use aws_secretsmanager_secret or SSM Parameter Store.
- Separate state per environment — dev/staging/prod must never share a state file.
- Run
terraform planand review output before every apply. - Enable state locking (DynamoDB) to prevent concurrent modifications.
Quick Reference
| Resource | Terraform Type | Key Arguments | Notes |
|---|---|---|---|
| VPC | aws_vpc | cidr_block, enable_dns_hostnames | Foundation of networking |
| Subnet | aws_subnet | vpc_id, cidr_block, availability_zone | Public vs private via route table |
| Internet Gateway | aws_internet_gateway | vpc_id | Required for public subnets |
| NAT Gateway | aws_nat_gateway | subnet_id, allocation_id | For private subnet egress |
| Security Group | aws_security_group | vpc_id, ingress, egress | Stateful firewall rules |
| EC2 Instance | aws_instance | ami, instance_type, subnet_id | Use data.aws_ami for latest |
| S3 Bucket | aws_s3_bucket | bucket | Separate resources for ACL, versioning |
| RDS Instance | aws_db_instance | engine, instance_class, db_name | Use aws_db_subnet_group |
| IAM Role | aws_iam_role | assume_role_policy | Attach policies separately |
| Key Pair | aws_key_pair | public_key | For SSH access to EC2 |
| S3 Backend | backend "s3" | bucket, key, dynamodb_table | State storage + locking |
Decision Tree
START
├── First time setting up Terraform?
│ ├── YES → Start with S3 backend + VPC module (Step 1-3)
│ └── NO ↓
├── Need full VPC with public/private subnets?
│ ├── YES → Use terraform-aws-modules/vpc (saves 100+ lines)
│ └── NO ↓
├── Single server or container workload?
│ ├── Single server → EC2 with Security Group
│ ├── Containers → ECS/Fargate with ALB
│ └── NO ↓
├── Need a database?
│ ├── YES → RDS in private subnet
│ └── NO ↓
└── DEFAULT → VPC + EC2 + S3 (full stack)
Step-by-Step Guide
1. Configure S3 backend for remote state
Set up remote state before anything else. [src3]
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
backend "s3" {
bucket = "my-terraform-state-123456"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = { Environment = var.environment, ManagedBy = "terraform" }
}
}
Verify: terraform init → Successfully configured the backend "s3"!
2. Define VPC with subnets
Use the community VPC module. [src5]
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.0"
name = "${var.project_name}-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
enable_dns_hostnames = true
}
Verify: terraform plan → shows VPC resources
3. Launch EC2 instance
Deploy a server with security group. [src2]
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter { name = "name", values = ["al2023-ami-*-x86_64"] }
}
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
subnet_id = module.vpc.public_subnets[0]
}
Verify: terraform apply → instance running
4. Apply and verify
Review plan and apply. [src2]
terraform plan -out=tfplan
terraform apply tfplan
terraform output
Verify: terraform state list → shows all created resources
Code Examples
HCL: VPC + EC2 + RDS stack
# Input: Terraform configuration
# Output: VPC, EC2, RDS PostgreSQL in private subnet
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-db"
subnet_ids = module.vpc.private_subnets
}
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db"
engine = "postgres"
engine_version = "16.3"
instance_class = "db.t4g.micro"
allocated_storage = 20
storage_encrypted = true
db_name = "appdb"
username = "dbadmin"
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
backup_retention_period = 7
deletion_protection = var.environment == "prod"
}
HCL: S3 + CloudFront static website
# Input: Domain name, ACM certificate ARN
# Output: S3 bucket + CloudFront distribution
resource "aws_s3_bucket" "website" {
bucket = "www.${var.domain_name}"
}
resource "aws_cloudfront_distribution" "website" {
enabled = true
default_root_object = "index.html"
aliases = [var.domain_name]
origin {
domain_name = aws_s3_bucket.website.bucket_regional_domain_name
origin_id = "S3-${aws_s3_bucket.website.id}"
origin_access_control_id = aws_cloudfront_origin_access_control.website.id
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${aws_s3_bucket.website.id}"
viewer_protocol_policy = "redirect-to-https"
forwarded_values { query_string = false; cookies { forward = "none" } }
}
viewer_certificate {
acm_certificate_arn = var.acm_certificate_arn
ssl_support_method = "sni-only"
}
restrictions { geo_restriction { restriction_type = "none" } }
}
Anti-Patterns
Wrong: Local state with no backend
# ❌ BAD — no backend block = local state, no locking
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
Correct: S3 backend with DynamoDB locking
# ✅ GOOD — remote state with encryption and locking
terraform {
backend "s3" {
bucket = "my-tf-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Wrong: Hardcoded values
# ❌ BAD — hardcoded AMI and subnet
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
subnet_id = "subnet-abc123"
}
Correct: Variables and data sources
# ✅ GOOD — dynamic lookup
data "aws_ami" "al" {
most_recent = true
owners = ["amazon"]
filter { name = "name"; values = ["al2023-ami-*-x86_64"] }
}
resource "aws_instance" "web" {
ami = data.aws_ami.al.id
subnet_id = module.vpc.public_subnets[0]
}
Wrong: Monolithic single file
# ❌ BAD — 2000 lines in one main.tf
Correct: Modular file structure
# ✅ GOOD — backend.tf, network.tf, compute.tf, storage.tf, variables.tf, outputs.tf
Common Pitfalls
- Forgetting prevent_destroy on state bucket: Accidentally deleting state bucket loses all state. Fix:
lifecycle { prevent_destroy = true }. [src3] - No bucket versioning on state: State corruption with no recovery. Fix: enable versioning on state bucket. [src4]
- Using default security group: Allows all traffic within VPC. Fix: create explicit security groups. [src1]
- Unencrypted EBS and S3: Compliance violation. Fix: set
encrypted = trueeverywhere. [src6] - Stale AMI IDs: Hardcoded AMIs become outdated. Fix: use
data "aws_ami"with filters. [src2] - Missing create_before_destroy: SG updates fail. Fix: add lifecycle rule. [src7]
Diagnostic Commands
# Initialize and download providers
terraform init
# Preview changes
terraform plan -out=tfplan
# Apply saved plan
terraform apply tfplan
# List resources in state
terraform state list
# Show resource details
terraform state show aws_instance.web
# Validate syntax
terraform validate
# Format files
terraform fmt -recursive
Version History & Compatibility
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Terraform 1.9+ | Current | None | HCL improvements |
| Terraform 1.6–1.8 | Supported | terraform test GA | — |
| AWS Provider 5.x | Current | S3 bucket args split | Use separate aws_s3_bucket_* resources |
| AWS Provider 4.x | Deprecated | — | Change acl to aws_s3_bucket_acl |
| OpenTofu 1.6+ | Active | Fork of TF 1.6 | Drop-in compatible |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Reproducible multi-resource infrastructure | One-off CLI commands | AWS CLI, bash scripts |
| Team collaboration with review workflows | Rapid prototyping | AWS Console, Pulumi |
| Multi-cloud deployments | AWS-only with CFN expertise | CloudFormation, SAM |
| Long-lived infra (VPCs, RDS, S3) | Ephemeral environments | Docker Compose |
Important Caveats
- State contains sensitive data in plaintext — encrypt the S3 backend and restrict IAM access.
terraform destroyis irreversible for stateful resources — back up data first and useprevent_destroy.- Provider upgrades can change behavior — test in dev first, read the changelog.
- The AWS provider does NOT wait for resources to be fully operational — use depends_on and health checks.