Infrastructure as code fails in two opposite ways: too little abstraction (copy-paste roots that drift), and too much abstraction (modules so clever nobody dares touch them). The useful middle is treating Terraform modules like a product boundary: stable contracts with boring implementations.

I have built and reused modules for VPC networking, ECS services, Lambda wrappers, CodePipeline/CodeBuild, ECR, ALB, Route53, EventBridge crons, IAM roles, and Lightsail alternatives. The lesson is less about HCL syntax and more about organizational physics.

Why root-only Terraform collapses

A single root module that defines “the app” works for one service in one environment. Then you need stage. Then prod. Then a second Shopify app. Then a Lambda that must share the VPC. Then a second team.

Without modules you get:

  • duplicated security group rules with tiny dangerous differences
  • IAM policies edited in only one environment
  • naming collisions that only appear on apply
  • PRs that are impossible to review because every change touches everything

Modules convert that into: change the ECS service module once, compose it many times.

Good modules encode decisions, not just resources

A weak module is a thin wrapper around one aws_ecs_service. A strong module encodes decisions your company has already paid to learn:

  • how we name resources across env/app/service
  • which logs always exist
  • which egress defaults are acceptable
  • how ALB target groups and health checks are wired
  • how CodeBuild expects imagedefinitions.json to look
  • which outputs downstream roots need (ARNs, URLs, cluster names)

That is why module design is product work. You are choosing what future you makes easy, and what future you makes annoying on purpose.

Environment roots should be boring

dev/ and prod/ should read like composition, not invention:

  • pass env-specific variables
  • instantiate shared modules
  • avoid snowflake resources living only in prod “just this once”

When prod has secret special cases, you no longer have parity. You have hope.

Terraform Cloud (or equivalent) paired with branch → workspace flows makes this operational: merge to stage applies stage; merge to main applies prod. Infrastructure then inherits the same review culture as application code. That cultural shift matters more than any one resource type.

Multi-app maps beat one-off stacks

For hosting many Shopify apps on one AWS footing, a shopify_apps map is a powerful pattern: each entry gets ECR, ECS, ALB, DNS, and pipeline, while sharing RDS and artifact buckets. Adding an app becomes data, not a redesign.

The complexity moves to naming and migration. Existing single-app stacks need moved blocks carefully. Wrong moves recreate resources. Right moves preserve state and sleep schedules.

Lightsail vs ECS is a product decision too

Not every suite needs full VPC/ECS. I have shipped Lightsail Container Services + Lightsail Database for cost-predictable microservice suites (auth, gateway, admin, portal, Shopify apps). The tradeoff is real:

  • Lightsail: simpler networking, fixed pricing, faster path for smaller fleets
  • ECS/ALB/VPC: more control, more moving parts, better for complex traffic and shared platform standards

Terraform can express both. The mistake is pretending they are interchangeable without rewriting operational assumptions (health checks, private connectivity, CI shape, secret distribution).

What I refuse to hide in modules

Some complexity should stay visible in roots:

  • environment-specific blast radius (prod protection flags)
  • data stores that must not be casually recreated
  • DNS cutovers
  • anything that can destroy customer data on a bad apply

Modules should make the safe path easy. They should not make catastrophe elegant.

The payoff

When modules work, onboarding a service looks like: pick module, fill variables, open PR, plan, apply. When they do not, every service invents infrastructure folklore.

Treat modules as a product with versioning, examples, and deliberate constraints. That is how AWS platforms survive year three.

← Back to blog