Design Terraform Modules for the Right Consumer
Avoid fake abstractions by tailoring modules to the engineers who actually use them - and handling edge cases the right way.
Gabriel Levasseur
Founder
Design Terraform Modules for the Right Consumer
It starts with a DM at 3:45 PM on a Friday: "Hey, the deployment failed. It says subnet-12345 isn't found."
You check their pull request. They didn't ask you which subnets to use. They just copied the subnet_ids from your README example (literally ["subnet-12345"]) and hoped for the best.
You built the aws-ecs-service module to save them time. You added 40 variables to cover every edge case. But instead of shipping code, the product team is stuck debugging networking concepts they shouldn't need to understand.
Meanwhile, your infrastructure peers are quietly forking the same module because it’s too opinionated for their specific edge case.
As a platform engineer, every Terraform module you publish is a product. But if you try to build one product for everyone, you end up with a tool that serves no one. Two very different audiences depend on your work:
- Other infrastructure engineers who want building blocks they can wire together however they need.
- Product engineers who just want to ship features and expect a paved path.
If you blur those audiences, you end up with abstractions that satisfy no one. Let’s clarify who you’re supporting and shape your modules accordingly.
The Cost of Getting It Wrong
When you mismatch the module to the consumer, you create a subtle but persistent drag on your team's efficiency. This "friction tax" isn't always obvious immediately, but it compounds daily across three dimensions:
1. The Velocity Tax
Product teams stop seeing the platform as an accelerator and start seeing it as a blocker. If deploying a simple service requires understanding CIDR blocks and security group rules, developers will either wait for you to do it for them (creating a bottleneck) or copy-paste configurations they don't understand (creating risk).
2. The Support Tax
Your platform team becomes a help desk. Instead of building high-leverage tooling, you spend your days answering "What is a subnet ID?" or debugging why a team's vibe-coded config isn't working in your VPC. This reactive work leads to burnout and prevents you from shipping the improvements that would actually solve the problem.
3. The Fragmentation Tax
When infrastructure engineers encounter a "product module" that is too rigid (e.g., hardcoded log retention), they don't file a feature request—they fork it. Six months later, you have 12 slightly different versions of aws-ecs-service floating around the codebase. When a critical security patch lands, you now have to hunt down and patch 12 different modules instead of one.
To avoid this, you must be intentional.
Know which audience you’re serving
Before you write a single line of HCL, you need to define the contract. Is this module a "lego block" for a peer who understands the nuances of AWS networking, or is it a "vending machine" for a developer who just wants a database?
Ambiguity here is the root cause of most bad abstractions. If you don't explicitly state the intended consumer, you'll inevitably drift toward a "middle ground" implementation that frustrates everyone—too complex for product teams, yet too opinionated for infrastructure teams.
The Module Header Template
Don't just guess - write it down. Add this snippet to the top of your README.md to align everyone before they write a line of code:
# Module: [Name]
**Primary Consumer:** [Product Engineers | Platform Engineers]
**Goal:** [e.g. "Provide a secure, compliant S3 bucket for application data"]
**Anti-Goal:** [e.g. "Support static website hosting or public access"]
Defining the consumer unlocks two distinct mindsets:
| Consumer | What they need | Interface Style | Default Behavior |
|---|---|---|---|
| Product engineers | Outcomes, not infrastructure trivia | Intent-based. Hide the knobs and wrap common tasks in opinionated interfaces. | Strong defaults. If 90% of teams need it, enable it by default. |
| Infrastructure engineers | Fine-grained control with guardrails | Transparent. Expose detailed inputs to allow flexible wiring. | Safe defaults. Enforce policy (encryption, tagging) but allow overrides. |
The clearer you are up front, the easier it becomes to decide what goes inside - and what deserves a separate module.
Designing modules for product engineers
A product engineer copies 10 lines, sets three variables, and gets a working result on day one.
Product engineers shouldn’t learn VPC trivia just to get an HTTPS endpoint. Give them an interface that reads like their intention.
When a product engineer reaches for a Terraform module, they are usually in "delivery mode." They aren't looking to explore the nuances of your cloud architecture; they are looking to satisfy a dependency so they can ship their feature. Your job is to minimize the cognitive load required to do the right thing.
Principles for Product Modules
- Ship a real abstraction. Expose the handful of inputs that represent the outcome—
service_name,container_port,domain_name. Everything else should be an opinionated default. - Be boldly opinionated. If 90% of teams should enable access logs, flip them on by default. Document how to opt out instead of forcing every team to make the same decision. This reduces drift across your organization by keeping configuration consistent.
- Duplicate when necessary. It’s okay to publish
ecs-web-serviceandecs-worker-serviceif the ergonomics differ. Avoid the temptation to combine them behindservice_mode = "worker"flags that confuse consumers. - Compose behind the scenes. Build your higher-level module by stitching together smaller primitives (networking, logging, alarms). You can swap pieces later without breaking the contract.
Designing modules for other infrastructure engineers
When you’re building for peers on the infra team, you can expose more surface area - but discipline still matters.
Unlike product engineers, your infrastructure peers do care about the implementation details. They need to know how the security groups are attached, which subnets are being used, and how the IAM policies are scoped. For this audience, "magic" is a bug, not a feature. They need composability and transparency so they can debug issues when the pager goes off at 3 AM.
Principles for Infrastructure Modules
- Prioritize Composability. Build focused primitives (e.g.,
alb,alb-listener) rather than monolithic "kitchen sinks." This allows engineers to assemble custom stacks without fighting against rigid logic. - Maximize Transparency. Your peers need to debug this when it breaks. Avoid hidden side effects—if a module creates a resource, it should be obvious from the name or inputs. Magic is a liability at 3 AM.
- Enforce Guardrails, Not Implementation. Use
validationblocks to block unsafe configurations (like unencrypted buckets) instead of hardcoding values. This enforces the what (security) without dictating the how (implementation details). - Design for Chaining. Treat your module's outputs as a public API. Return rich data (ARNs, IDs, endpoints) so consumers can easily pipe your module into others without friction or extra lookups.
Think of these modules as a curated toolbox - flexible, but still safe.
Wrap up
Great Terraform libraries feel intentional. Decide who you’re serving, shape the abstraction to that audience, and lean on composable building blocks so you can evolve quickly.
Next step: Audit your top 3 most-used modules this week. Ask yourself: "Who is this actually for?" If the answer is "everyone," it might be time to split it up.
Share your module library with Cora
See your infrastructure as an interactive diagram. Easy setup, no complexity.
Get Started Today!Keep reading
View allTerraform Complex Data Structures Are a Smell—Use Modules Instead
Why giant JSON inputs plus `for_each` loops hurt teams, and how to replace them with composable Terraform modules.
Gabriel Levasseur
Founder
Stop Building Terraform God Modules
Why modules with 40 inputs and passthrough variables slow teams down—and how to design reusable building blocks instead.
Gabriel Levasseur
Founder
Terraform: Unlock Flexibility with the Open-Closed Principle
Discover how to build modules that grow with your team. Learn to design for extension and create a 'batteries-included' experience without the bloat.
Gabriel Levasseur
Founder