Terraform modules usually start with good intentions. A team wants consistency, there is some repeated Terraform, and a shared module feels like the obvious next step. Sometimes that instinct is right. Quite often, though, the real problems begin before anyone writes the module itself. The worst module pain rarely comes from bad HCL.
It usually comes from design choices made too early or too loosely: abstracting before the pattern is stable, giving the module a vague responsibility, exposing too much of the provider surface, or quietly bundling platform concerns and workload concerns together because it was easier than drawing a firmer boundary.
That part is easy to miss because the early signs do not always look like failure. The module still validates, still deploys, and still looks neat enough in a pull request, but it is already becoming awkward in the ways that matter later. It is harder to explain, harder to review, and harder to change safely. Consumers are never completely sure what it owns and what they are supposed to supply. Optional inputs start shaping behaviour more heavily than they should, and over time the abstraction grows simply because adding another variable feels easier than admitting the original boundary was never especially good.
That was the real reason I built my terraform-module-creator agent skill. I did not want a skill that just turned a prompt into a few Terraform files and called that job done. I wanted something that helps with the design judgement that should happen before generation begins, because that is usually where the difference is made between a module that becomes genuinely useful and one that ends up as another shared layer people work around.
The module problem is not code generation
If the goal is simply to generate Terraform quickly, that problem is already fairly well served.
The more difficult problem is deciding whether a module should exist at all, what type of module it should be, where the boundary should sit, what should stay outside it, and how much flexibility is actually useful. Those are the questions that matter once a module starts being used by real teams in real environments. If they are answered badly, the fact that the generated code looked neat on day one does not help very much.
That is why I do not think the hardest part of module design is Terraform authoring. It is making a series of design calls that seem small at the time but have a disproportionate effect later: what to include, what to leave out, what dependencies should be passed in, what defaults should be opinionated, and what kind of interface another engineer can understand without having to reverse-engineer the implementation.
A module that solves one coherent problem cleanly is usually far more valuable than a broad, highly configurable module that can theoretically handle everything. The latter often ends up being harder to consume than just writing the resources directly.
Why generic AI Terraform generation is not enough
This is the gap I wanted the skill to address.
Generic AI generation is often quite good at producing plausible Terraform structure. What it is much less reliable at is challenging the request. It tends to assume that if you asked for a module, then a module is definitely the right answer. It tends to preserve optionality because that feels safer than narrowing the design. It tends to expose more than it should because it does not want to miss a scenario. The result can look capable while still being weak in the ways that matter operationally.
That is not especially helpful if you are trying to build a shared module set that other engineers can trust, understand, and support. In that context, code generation is only one piece of the job. What matters more is whether the design has been challenged properly before the files are written. You need something willing to ask whether the abstraction is justified, whether the responsibility is clear, whether the interface has already started to sprawl, and whether direct Terraform would actually be the cleaner answer.
I wanted the skill to behave more like an experienced platform engineer reviewing the design before agreeing to implement it. Sometimes that means helping create a module. Sometimes it means recommending a tighter one. Sometimes it means saying that plain Terraform would be simpler and better.
Why I built the terraform-module-creator skill
The core purpose of the skill is simple: help design and create Terraform modules from real infrastructure requirements without automatically treating abstraction as a good thing.
That is why the skill is built around a deliberate workflow rather than a straight line from prompt to files. It starts by understanding the ask, then deciding the module type, then defining the boundary, then designing the interface, then recommending the file structure, then generating the module, and finally reviewing the result critically. That order matters because it forces the design conversation to happen before generation, not after.
I wanted the skill to help answer questions such as these: should this be a module at all, is this a building block module or a composition module, what does the module actually own, what should stay outside it, what inputs do consumers genuinely need, what defaults should be opinionated, and what is likely to make this easier to support six months from now rather than simply easier to demo this afternoon.
Those are the questions that usually separate a useful module from a neat-looking one.

The principles that I wanted baked into the skill
A big part of the skill is that it does not pretend to be neutral. It has a set of design principles built into it quite deliberately, because I think shared module design is one of those areas where a clear point of view is more useful than endless optionality.
- The first is KISS: Keep the module easy to understand, easy to consume, and easy to support. Shared modules should not read like mini-frameworks. They should solve one thing clearly enough that another engineer can pick them up and understand the shape of them without needing to study the implementation.
- The second is DRY with judgement: Reducing meaningful duplication is useful. Abstracting every repeated pattern is not. A shared module should reduce repeated cognitive load, not just repeated lines of HCL. There are plenty of situations where a little local duplication is still cheaper than introducing a common abstraction that every team now has to learn.
- Then there is clear responsibility: A module should solve one coherent problem well. If it is difficult to explain what the module owns in a few lines, there is a good chance it is carrying too much responsibility or sitting at the wrong layer.
- Practical reuse matters just as much: did not want the skill encouraging speculative abstraction for future scenarios that might never happen. Shared modules should be built for proven demand, not as a pre-emptive response to imagined reuse.
- I also wanted a strong bias toward readable interfaces and operational supportability: Variables, outputs, defaults, and naming should be clear enough that a team can understand what the module does and how to consume it without fighting through unnecessary complexity. If supportability depends on the original author still being around, the module is not in a very good state.
- And finally, safe and sensible defaults matter: Shared modules are one of the best places to reduce toil and inconsistency by making the common path align with platform expectations. They should be opinionated where that helps, but they should not become opaque. The goal is not to hide behaviour. It is to make sensible behaviour easier.

Why Azure MCP and HashiCorp MCP guidance matters
One of the deliberate design choices in the skill was to make it rely on Azure MCP and HashiCorp MCP guidance rather than static documentation links or hardcoded assumptions.
That matters because module design gets worse very quickly when it is based on stale memory. If you are designing Azure modules properly, you are usually making decisions around current provider arguments, naming constraints, platform guidance, private endpoint DNS patterns, observability expectations, and security defaults. Those are exactly the sorts of details that shift over time and should not be frozen into a skill as though they will never change.
So the skill uses the Azure MCP Server early in the workflow to look up Azure Terraform best practices, service-specific Azure best practices, official Azure documentation, and Well-Architected guidance.
On the Terraform side, it uses the HashiCorp Terraform Registry MCP to inspect existing public modules for reference, check provider capabilities, retrieve the full resource documentation for the service being wrapped, and fetch the latest provider version before generating versions.tf.
That combination was important to me because it means the skill is not just recycling what it knew when it was written. It is meant to pull in current platform and provider information at design time and then apply a deliberate set of design opinions on top of that. I think that is a much better model for this sort of skill than embedding a lot of brittle static knowledge and hoping it stays current.

What the skill produces beyond Terraform
One of the things I wanted to make sure this skill did was produce more than a set of Terraform files. If the output stops at main.tf, variables.tf, and outputs.tf, you still have a long way to go before you have something that is genuinely usable by another team.
The first thing it should produce is a tighter interface. The skill uses live provider documentation to understand what the resource can do, but it does not simply mirror that provider surface into the module. That would be one of the easiest ways to build a bloated abstraction. Instead, the goal is to expose only what consumers genuinely need, keep variable names clear, prefer safe defaults where behaviour is predictable, and use validation or preconditions where they improve clarity. In practice, that usually matters more than the initial code generation because interface sprawl is one of the quickest ways to turn a shared module into a burden.
It should also produce proper module scaffolding around the Terraform itself. That means a README with purpose, boundary, and usage guidance, .terraform-docs.yml so the inputs and outputs table is generated rather than hand-maintained, and runnable examples so the module can be understood from a real usage path rather than a vague sketch. I wanted the skill to treat documentation and examples as part of the module, not as optional tidying-up work for later. If the README is maintained by hand, it will eventually drift. If the examples are not independently runnable, they tend to become decorative rather than useful.
Validation was another area I wanted built in from the start. The skill is structured around running terraform fmt, terraform init -backend=false and terraform validate, tflint, and terraform-docs as part of the handoff flow.
That is important because there is a big difference between generated Terraform and generated Terraform that has been shaped into something that can actually be reviewed and maintained properly. The same goes for .tflint.hcl, which helps bring some provider-specific discipline into the module rather than relying on syntax validation alone.

I also wanted the skill to handle more than greenfield creation. That is why it supports review mode and refactor mode as first-class use cases. In review mode, the point is not to compliment the abstraction but to challenge it properly: should this be a module at all, is the boundary clear, are the inputs sensible, are the outputs useful, and what is likely to become painful later.
In refactor mode, the focus is on identifying a stable repeated pattern, extracting only the reusable responsibility, and handling migration carefully, including moved {} blocks where resource names or structures change. That matters because shared modules are rarely static, and safe evolution is just as important as initial design.
Finally, I wanted the skill to treat shared modules like things that need release discipline, not just code generation. If a module is going to be consumed by other teams, then semver, Git tags, changelog expectations, and migration guidance stop being nice extras. They become part of how the module remains usable over time. A module without versioning discipline is really just a moving target.
Example output from the skill
When I built the skill, I wanted it to produce something a team could actually inspect and refine, not just a tidy resource block. For example, I ran:
./terraform-module-creator create me a terraform module for azure postgresql flexible server
From that single prompt, the skill generated a PostgreSQL Flexible Server module example with a proper module layout: main.tf, variables.tf, outputs.tf, locals.tf, versions.tf, .terraform-docs.yml, a README, and an examples/ folder with both basic and vnet-integrated example paths.
The README does more than describe the resource. It sets out the module purpose, defines the boundary, includes a usage example, and calls out trade-offs such as VNet integration behaviour, managed identity defaults, database protection with prevent_destroy, and documentation generation with terraform-docs.

The Terraform itself is also a decent illustration of what I wanted from the skill. It creates the flexible server, supports optional databases, optional server configuration overrides, and optional diagnostics, while keeping platform dependencies such as resource groups, delegated subnets, private DNS zones, and Log Analytics workspaces outside the module boundary.
It also carries some opinion into the implementation, for example merging managed-by = terraform into tags, disabling public access when a delegated subnet is supplied, validating constrained inputs such as HA mode and diagnostics, and exposing outputs that consumers are likely to need rather than everything the provider exposes.
That is probably the strongest addition you can make, because it moves the post from “here is the design philosophy of the skill” to “here is the kind of module shape it actually produced from one prompt.”
Here is the full module example in my GitHub repo
Some example screenshots from GitHub Copilot using the skill:



Where this fits in the engineering lifecycle
Another thing I wanted from the skill was for it to be useful beyond greenfield generation.
A lot of real platform work is not creating something from scratch. It is reviewing an existing abstraction, deciding whether repeated Terraform should be extracted into a shared module, or simplifying something that has gradually become over-abstracted. So the skill also has a review mode and a refactor mode.
Review mode is there to challenge an existing module. Should this be a module at all? Is the boundary clear? Are the inputs sensible? Are the outputs useful and limited? Is the abstraction justified? What is likely to become painful later?
Refactor mode is there for repeated Terraform patterns that probably do need extracting, but carefully. The goal is to identify the stable reusable responsibility, keep the shared contract minimal, avoid dragging one-off concerns into it, and handle migration properly, including moved {} blocks where resource names or structures change.
That broader lifecycle fit matters because module design is not a one-time authoring exercise. It is part of how platform standards evolve, how teams share patterns, and how infrastructure code either becomes easier to live with or gradually becomes harder.

Practical takeaways
The main thing I would take from building this skill is that good Terraform modules usually start with more restraint than enthusiasm.
It is very easy to build a module. It is harder to build one that is clear, stable, easy to consume, and genuinely worth maintaining. That is why I think it is useful for a skill like this to be opinionated, to use live Azure MCP and HashiCorp MCP guidance rather than static assumptions, and to push harder on the design questions before generation begins.
If a module is difficult to explain, it is probably too wide. If it needs a large number of variables to stay flexible, it may be trying to cover too many scenarios. If it adds no value beyond wrapping a single resource, it may not deserve to exist. And if it is built for future hypothetical reuse rather than current repeated demand, there is a good chance it will spend more time being maintained than being usefully consumed.
Those are not especially novel lessons, but they are the ones that keep turning out to matter.
Wrapping Up
What I wanted this skill to showcase was not just a way to generate Terraform modules, but a way to think about them more carefully.
The point was never to create more abstractions. If anything, it was the opposite. The point was to help create fewer, better modules by forcing a clearer design conversation up front, grounding decisions in live Azure and Terraform guidance, and being willing to say that simplicity is often the more mature choice.
Anyone can get a module working. The craft is building one that still makes sense when the context has faded, other teams are consuming it, and nobody wants to reverse-engineer your abstractions during an incident.
That is the standard I wanted the skill to reflect, and it is the main reason I built it this way.
Check out the terraform module creator agent skill in my repo – let us know how it goes!