This example demonstrates all production best practices combined in a single configuration: remote state, modules, tagging, encryption, and CI/CD readiness.
Files#
| File |
Description |
main.tf |
Complete setup: remote backend, provider, VPC module, EC2, RDS, S3, CloudWatch |
variables.tf |
Well-documented variables with validation, types, and descriptions |
Code#
main.tf#
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
# Production: Use remote state with S3 + DynamoDB locking
backend "s3" {
bucket = "my-company-terraform-state" # Replace with your bucket
key = "production/infrastructure.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-locks"
}
}
provider "aws" {
region = var.region
# Production: Use assume role instead of static credentials
assume_role {
role_arn = "arn:aws:iam::${var.account_id}:role/TerraformRole"
session_name = "TerraformProduction"
}
default_tags {
tags = local.default_tags
}
}
# Locals
locals {
environment = terraform.workspace
# Consistent tagging strategy
default_tags = {
Environment = local.environment
ManagedBy = "Terraform"
Project = var.project_name
Owner = var.owner
CostCenter = var.cost_center
CreatedAt = timestamp()
}
# Environment-specific sizing
instance_type = lookup({
dev = "t2.micro"
staging = "t2.medium"
prod = "t3.large"
}, local.environment, "t2.micro")
instance_count = lookup({
dev = 1
staging = 2
prod = 3
}, local.environment, 1)
enable_monitoring = local.environment == "prod"
enable_backup = local.environment == "prod"
}
# Variables
variable "region" {
description = "AWS region"
type = string
}
variable "account_id" {
description = "AWS account ID"
type = string
}
variable "project_name" {
description = "Project name"
type = string
}
variable "owner" {
description = "Resource owner"
type = string
}
variable "cost_center" {
description = "Cost center code"
type = string
}
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "Must be a valid CIDR block."
}
}
# Production: Use community modules instead of writing everything from scratch
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.0"
name = "${var.project_name}-${local.environment}"
cidr = var.vpc_cidr
azs = var.availability_zones
private_subnets = [for i, az in var.availability_zones : cidrsubnet(var.vpc_cidr, 8, i)]
public_subnets = [for i, az in var.availability_zones : cidrsubnet(var.vpc_cidr, 8, i + length(var.availability_zones))]
enable_nat_gateway = true
enable_vpn_gateway = false
one_nat_gateway_per_az = local.environment == "prod"
enable_flow_log = true
create_flow_log_cloudwatch_log_group = true
create_flow_log_cloudwatch_iam_role = true
flow_log_max_aggregation_interval = 60
tags = local.default_tags
}
# Production: Use security group module
module "web_sg" {
source = "terraform-aws-modules/security-group/aws"
version = "5.1.0"
name = "${var.project_name}-${local.environment}-web"
description = "Security group for web servers"
vpc_id = module.vpc.vpc_id
ingress_with_cidr_blocks = [
{
from_port = 443
to_port = 443
protocol = "tcp"
description = "HTTPS"
cidr_blocks = "0.0.0.0/0"
},
{
from_port = 80
to_port = 80
protocol = "tcp"
description = "HTTP redirect"
cidr_blocks = "0.0.0.0/0"
},
]
egress_rules = ["all-all"]
tags = local.default_tags
}
# Production: EC2 instance with lifecycle protection
resource "aws_instance" "web" {
count = local.instance_count
ami = data.aws_ami.amazon_linux_2.id
instance_type = local.instance_type
subnet_id = module.vpc.public_subnets[count.index % length(module.vpc.public_subnets)]
vpc_security_group_ids = [module.web_sg.security_group_id]
monitoring = local.enable_monitoring
# Production: User data with template
user_data = templatefile("${path.module}/user_data.sh", {
environment = local.environment
app_version = var.app_version
})
root_block_device {
volume_type = "gp3"
volume_size = lookup({ dev = 20, staging = 30, prod = 50 }, local.environment, 20)
encrypted = true
}
# Production: Prevent accidental destruction
lifecycle {
create_before_destroy = true
ignore_changes = [
ami, # Allow AMI updates outside Terraform
]
}
tags = merge(local.default_tags, {
Name = "${var.project_name}-${local.environment}-web-${count.index + 1}"
})
}
# Production: Use data sources
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"]
}
}
# Production: Outputs with descriptions
output "vpc_id" {
description = "The ID of the VPC"
value = module.vpc.vpc_id
}
output "web_instance_ids" {
description = "IDs of web server instances"
value = aws_instance.web[*].id
}
output "web_public_ips" {
description = "Public IPs of web servers"
value = aws_instance.web[*].public_ip
}
output "environment_summary" {
description = "Environment deployment summary"
value = {
environment = local.environment
instance_count = local.instance_count
instance_type = local.instance_type
monitoring = local.enable_monitoring
backup_enabled = local.enable_backup
vpc_id = module.vpc.vpc_id
subnet_count = length(module.vpc.public_subnets)
}
}
variables.tf#
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
variable "app_version" {
description = "Application version to deploy"
type = string
default = "latest"
}
Best Practices Demonstrated#
| Practice |
Implementation |
| Remote State |
S3 backend with DynamoDB locking |
| Encryption |
S3 SSE, RDS encryption, KMS |
| Tagging |
Consistent tags across all resources |
| Modularity |
Reusable VPC module with versioning |
| IAM |
Least privilege roles and policies |
| Monitoring |
CloudWatch alarms and dashboards |
| Security |
Sensitive variables, secure defaults |
| Backup |
Automated RDS snapshots |
Try It#
# 1. First deploy the backend infrastructure
cd backend
terraform init && terraform apply
# 2. Then deploy the main infrastructure
cd ../infra
terraform init && terraform apply