Terraform Modules Best Practices for Production Systems
The Pattern We Found
Every cloudless foundation needs the same building blocks:
- Foundation (
cloudless-infra) - labeling, environment, consistency - Compute (
terraform-aws-lambda-function+terraform-aws-lambda-runtime) - functions, layers, custom runtimes - Interface (
terraform-aws-rest-apiorterraform-aws-website) - how users reach your system - 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 FQDNterraform-aws-lambda-function= Lambda + layers + IAM + CloudWatch logsterraform-aws-lambda-runtime= ECR + SSM for runtime sharingterraform-aws-rest-api= API Gateway + keys + usage plansterraform-aws-rest-api-sns-proxy= API Gateway → SNS (no SDK needed)terraform-aws-analytics-pipeline= Firehose → S3 with dynamic partitioningterraform-aws-analytics-topic= SNS topic for analytics events
Infrastructure as Code Best Practices:
- CloudPosse Compatible: All modules use
context.tffor 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-websitewraps CloudPosse’scloudfront-s3-cdn+ ACM + Route53terraform-aws-lambda-functionwraps Lambda + IAM role + CloudWatch log groupterraform-aws-lambda-runtimewraps CloudPosse’secr+ SSM parametersterraform-aws-rest-apiwraps raw API Gateway resourcesterraform-aws-rest-api-sns-proxywraps API Gateway → SNS integration (plain HTTP publishing)terraform-aws-analytics-pipelinewraps 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 infrastructureterraform-aws-lambda-function- Lambda function with full IAM and loggingterraform-aws-lambda-runtime- custom runtime supportterraform-aws-lambda-layer- layer packagingterraform-aws-rest-api- API Gateway setupterraform-aws-rest-api-sns-proxy- API Gateway → SNS without SDKterraform-aws-analytics-pipeline- Firehose → S3 data laketerraform-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-apppattern for grouping function + trigger + storage as a single deployable unit
The goal: Every common AWS pattern becomes a 5-line module call.