Skip to content
by skunxicat

Terraform Modules Best Practices for Production Systems

The Pattern We Found

Every cloudless foundation needs the same building blocks:

  1. Foundation (cloudless-infra) - labeling, environment, consistency
  2. Compute (terraform-aws-lambda-function + terraform-aws-lambda-runtime) - functions, layers, custom runtimes
  3. Interface (terraform-aws-rest-api or terraform-aws-website) - how users reach your system
  4. Data (terraform-aws-analytics-pipeline + terraform-aws-analytics-topic) - event ingestion and storage

Terraform Module Design Principles

Single Responsibility Principle: Each terraform module does one thing perfectly

  • terraform-aws-website = CloudFront + S3 + SSL + Route53 from FQDN
  • terraform-aws-lambda-function = Lambda + layers + IAM + CloudWatch logs
  • terraform-aws-lambda-runtime = ECR + SSM for runtime sharing
  • terraform-aws-rest-api = API Gateway + keys + usage plans
  • terraform-aws-rest-api-sns-proxy = API Gateway → SNS (no SDK needed)
  • terraform-aws-analytics-pipeline = Firehose → S3 with dynamic partitioning
  • terraform-aws-analytics-topic = SNS topic for analytics events

Infrastructure as Code Best Practices:

  • CloudPosse Compatible: All modules use context.tf for consistent labeling
  • Git Sourceable: Direct GitHub references for version control
  • Minimal Interface: Expose only what users need, abstract the complexity
module "website" {
  source  = "git::https://github.com/ql4b/terraform-aws-website.git"
  fqdn    = "example.com"
  context = module.label.context
}

The Abstraction Strategy

We’re not reinventing AWS services. We’re wrapping complexity with sensible defaults:

  • terraform-aws-website wraps CloudPosse’s cloudfront-s3-cdn + ACM + Route53
  • terraform-aws-lambda-function wraps Lambda + IAM role + CloudWatch log group
  • terraform-aws-lambda-runtime wraps CloudPosse’s ecr + SSM parameters
  • terraform-aws-rest-api wraps raw API Gateway resources
  • terraform-aws-rest-api-sns-proxy wraps API Gateway → SNS integration (plain HTTP publishing)
  • terraform-aws-analytics-pipeline wraps Firehose + S3 + dynamic partitioning + optional transform Lambda

Why This Works

Composability: Mix and match modules as needed Upgradability: Replace modules when you outgrow them
Transparency: Every resource is explicit in the module code Reusability: Same patterns across all cloudless foundations

Module Naming Convention

terraform-aws-{purpose}

  • terraform-aws-website - complete website infrastructure
  • terraform-aws-lambda-function - Lambda function with full IAM and logging
  • terraform-aws-lambda-runtime - custom runtime support
  • terraform-aws-lambda-layer - layer packaging
  • terraform-aws-rest-api - API Gateway setup
  • terraform-aws-rest-api-sns-proxy - API Gateway → SNS without SDK
  • terraform-aws-analytics-pipeline - Firehose → S3 data lake
  • terraform-aws-analytics-topic - SNS topic for event fan-out

The Network Effect

Each module makes the others more valuable. Real production compositions:

  • github-analytics uses lambda-function + lambda-layer + rest-api-sns-proxy + analytics-pipeline
  • airlytics uses analytics-pipeline + analytics-topic + transform Lambda
  • cloudless-signals uses lambda-function + website (CloudFront + Lambda URLs)
  • cloudless-web uses website
  • farecrumbs uses rest-api + lambda-function
  • airsim uses rest-api + lambda-function

The pattern: a new system is 3-5 module calls wired together in a root module.

Composition Example

A typical analytics-enabled service:

module "function" {
  source     = "git::https://github.com/ql4b/terraform-aws-lambda-function.git"
  context    = module.label.context
  source_dir = "./app"
  handler    = "handler.run"
  runtime    = "provided.al2023"
  layers     = [module.layer.arn]
}

module "sns_proxy" {
  source  = "git::https://github.com/ql4b/terraform-aws-restapi-sns-proxy.git"
  context = module.label.context
  topic   = module.topic.arn
}

module "pipeline" {
  source  = "git::https://github.com/ql4b/terraform-aws-analytics-pipeline.git"
  context = module.label.context
  topic   = module.topic.arn
  partitioning = {
    source = "source"
    org    = "org"
    metric = "metric"
  }
}

Repository Strategy

Each module is its own repo for:

  • Independent versioning
  • Clear ownership
  • Focused documentation
  • Git source references

What’s Next

The current module set covers compute, interface, and data ingestion. Areas where we still reach for raw resources:

  • Queues — SQS + DLQ patterns (currently inline in each project)
  • Scheduled tasks — EventBridge rules + Lambda targets
  • Higher-order composition — the serverless-app pattern for grouping function + trigger + storage as a single deployable unit

The goal: Every common AWS pattern becomes a 5-line module call.