terraform-module-developer
Skill1 starsChanged 4 months agoMITView on GitHub
Build production-ready, multi-provider Terraform modules with the strict 8-file structure (4 root + 4 wrappers/) — typed variables prefixed with the resource name, count-conditional resources, dynamic blocks with iterator pattern, outputs using try/element/concat, full heredoc provider docs, and a wrapper module supporting batch ops via for_each. Generates a polished README.md with BEGIN_TF_DOCS / END_TF_DOCS markers so terraform-docs auto-injects inputs/outputs/providers/resources tables. Bundles a generic .terraform-docs.yml, .tflint.hcl, pre-commit fragment, security scanning (tflint + tfsec + checkov) and module testing patterns. Works with any Terraform provider — AWS, Azure, GCP, HuaweiCloud, OCI, DigitalOcean, Cloudflare, and community providers. Use whenever the user asks to create a Terraform module, IaC module, .tf module, scaffold a tf resource, wire terraform-docs, or hits you with 'make me a terraform module', 'scaffold a tf resource', 'wire up terraform-docs', or 'add a wrapper for batch ops'.
In this repository24 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- architecture-auditSkill
- autodreamSkill
- deep-discoverySkill
- generic-by-designSkill
- gitops-cd-developerSkill
- gitops-pipeline-developerSkill
- golang-script-developerSkill
- humanizerSkill
- information-architectureSkill
- interactive-deck-builderSkill
- javascript-script-developerSkill
- linux-script-developerSkill
- makefile-script-developerSkill
- plan-interviewSkill
- pre-commit-setupSkill
- python-script-developerSkill
- qaSkill
- skill-builderSkill
- teacher-modeSkill
- typescript-script-developerSkill
- ui-deslopSkill
- xquik-x-dataSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
26 rules. About 0 are common, 3 are distinctive, and 23 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
23
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 50
Every module is exactly this — no fewer files, no extra files, this layout:
- Very specific: line 76
Confirm the provider: Default: ask the user which provider (aws, azurerm, google, huaweicloud, oci, digitalocean, cloudflare, etc.) and pin …
- Very specific: line 78
Scaffold the structure: Run bash scripts/scaffold-module.sh <service> <resource> <provider> <provider-version>.
- Very specific: line 86
Validate: Run bash scripts/validate-module.sh <module-path> — checks file count (2 dirs / 9 files), no list(any), prefix consistency, …
- Very specific: line 116
list(any)is banned: Always use list(object({...})) with optional() for optional fields. - Very specific: line 120
.terraform-docs.ymllives at repo root: Not per-module. It applies to every module via terraform-docs -c .terraform-docs.yml <path>. - Very specific: line 127
Always start from scripts/scaffold-module.sh.
- Very specific: line 128
Use the variable prefix <service>_<resource>_* consistently.
- Very specific: line 129
Use count = var.<prefix>_create ?
- Distinctive: line 130
Write heredoc descriptions on every variable that mirror the provider registry.
- Very specific: line 131
Use list(object({...})) with optional() for any list-of-records variable.
- Very specific: line 132
Use the <prefix>_timeouts object variable (not three separate vars) when the resource supports timeouts.
- Very specific: line 133
Use the iterator pattern (iterator = <name>_cfg) on every dynamic block.
- Very specific: line 134
Use the try(element(concat(...), 0), <fallback>) pattern for every output.
- Very specific: line 135
Write a wrappers/ directory with typed defaults (object({...})) and items (map(object({...}))), mirroring every root variable as …
- Very specific: line 136
Place BEGIN_TF_DOCS / END_TF_DOCS markers in every README between Usage and Notes.
- Very specific: line 139
Add an examples/ subdirectory with a working basic example, then tests/*.tftest.hcl for the native test framework.
- Very specific: line 140
Run scripts/validate-module.sh after every edit.
- Very specific: line 141
Run terraform fmt -recursive && terraform validate before declaring done.
- Very specific: line 146
Use list(any) or map(any) anywhere — the wrapper's defaults and items are also strictly typed (object({...}) / map(object({...}))).
- Very specific: line 149
Use ~> or unpinned floating refs for provider versions.
- Very specific: line 150
Place data.tf / locals.tf as separate files (fold into main.tf).
- Very specific: line 152
Use for_each = var.x != null ?
- Distinctive: line 153
Write the inputs / outputs / providers tables manually in README — let terraform-docs inject them.
- Very specific: line 154
Commit a module without running terraform fmt.
- Very specific: line 155
Create a module without an examples/ working example.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Terraform Module Developer
The strict 8-file pattern: 4 root files (main.tf + variables.tf + outputs.tf + versions.tf) + 4 wrapper files (wrappers/*.tf) + a polished README.md with terraform-docs auto-injection. Multi-provider; works for any Terraform/OpenTofu provider.
When to use
- The user asks for a new Terraform module, IaC module,
.tfresource, or a wrapper module. - The user wants to harden, refactor, or review an existing module.
- A polyglot infra repo needs consistent module structure across services / regions / providers.
- The user wants
terraform-docswired up so READMEs stay in sync with code. - The user wants
tflint/tfsec/checkovrunning pre-commit and in CI.
Skip this skill for: cloud-vendor-specific business logic (use a vendor SDK skill), application code (different language skills), or non-IaC config like Helm charts (use gitops-cd-developer).
Required structure
Every module is exactly this — no fewer files, no extra files, this layout:
modules/<service>/<resource>/
├── main.tf # resource definition, count = var.<prefix>_create ? 1 : 0
├── variables.tf # all vars prefixed `<prefix>_*`, heredoc descriptions
├── outputs.tf # try/element/concat pattern for every output
├── versions.tf # required_version + required_providers (pinned)
├── README.md # static intro + BEGIN_TF_DOCS / END_TF_DOCS markers
└── wrappers/
├── main.tf # module "wrapper" { for_each = var.items, ... }
├── variables.tf # `defaults` (any) + `items` (any)
├── outputs.tf # output "wrapper" { value = module.wrapper }
└── versions.tf # same constraints as rootTotal: 2 directories, 9 files (8 .tf + 1 README.md). Validators check this exactly.
Plus, repo-root tooling (one-time setup):
.terraform-docs.yml— config that injects between the README markers..tflint.hcl— linting rules + provider plugins..pre-commit-config.yaml(or fragment) — wires terraform-fmt, terraform-docs, tflint, tfsec into commit hooks.
Workflow
- Confirm the provider. Default: ask the user which provider (
aws,azurerm,google,huaweicloud,oci,digitalocean,cloudflare, etc.) and pin a version. Save provider preference to memory after first run. - Confirm the service + resource.
<service>is the namespace (e.g.,vpc,compute,storage).<resource>is the specific resource type (e.g.,subnet,instance,bucket). Variable prefix derived as<service>_<resource>with hyphens turned into underscores. - Scaffold the structure. Run
bash scripts/scaffold-module.sh <service> <resource> <provider> <provider-version>. Creates the 9 files fromassets/templates/terraform/. Seereferences/module-anatomy.mdfor the file-by-file breakdown. - Fetch provider docs. Open the resource page on the Terraform Registry. Capture the full set of arguments, optional fields, dynamic blocks, and exported attributes. The heredoc descriptions in
variables.tfshould mirror the registry exactly. - Fill in
main.tf. Map provider arguments to module variables. Use the count conditional. Applyreferences/dynamic-blocks.mdfor any nested blocks. - Fill in
variables.tf. Applyreferences/variable-patterns.md— naming, heredoc, complex types withoptional(), the<prefix>_timeoutsobject pattern when the resource supports timeouts. - Fill in
outputs.tf. Applyreferences/output-patterns.md—try(element(concat(<resource>.this.*.<attr>, [<default>]), 0), <fallback>)for every export. - Fill in
wrappers/. Applyreferences/wrapper-pattern.md— every root variable gets a correspondingtry(each.value.<x>, var.defaults.<x>, <default>)line inwrappers/main.tf. - Wire terraform-docs. Apply
references/readme-and-terraform-docs.md— confirm.terraform-docs.ymlexists at repo root, README has theBEGIN_TF_DOCS/END_TF_DOCSmarkers, and the pre-commit hook is registered. - Wire security scanning + tests. Apply
references/security-scanning.md(tflint + tfsec/trivy + checkov) andreferences/module-testing.md(terraform test framework + examples/ directory). - Validate. Run
bash scripts/validate-module.sh <module-path>— checks file count (2 dirs / 9 files), nolist(any), prefix consistency, iterator patterns, README markers. Aim for 100%. - Format + native validate.
terraform fmt -recursive && terraform validatefrom the module directory. - Generate docs.
terraform-docs -c .terraform-docs.yml <module-path>— confirm the README inputs/outputs tables fill in correctly.
Available resources
references/module-anatomy.md— 8-file structure, four-file pattern, naming conventions, count conditional, file-by-file breakdown.references/variable-patterns.md— naming convention, heredoc descriptions, complex object types withoptional(),<prefix>_timeoutsobject pattern.references/output-patterns.md—try(element(concat(...), 0), <fallback>)pattern with examples for string / list / map / object outputs.references/dynamic-blocks.md— iterator pattern (iterator = <name>_cfg), nested dynamic blocks,can(length())guard.references/wrapper-pattern.md—for_each = var.items,defaults+itemsvariables,try(each.value, var.defaults, <fallback>)chain, type-safe wrapper variables for timeouts.references/readme-and-terraform-docs.md— README template structure,BEGIN_TF_DOCS/END_TF_DOCSmarkers,.terraform-docs.ymlconfig (mode: inject,formatter: markdown table), pre-commit hook integration, CI integration.references/provider-conventions.md— multi-provider notes (AWS / Azure / GCP / HuaweiCloud / OCI / DigitalOcean / Cloudflare): naming differences, region handling, tagging conventions, provider-specific gotchas.references/security-scanning.md— tflint config + plugins, tfsec / trivy security scanning, checkov compliance scanning, integrating all three into pre-commit and CI.references/module-testing.md— Terraform 1.6+ native test framework (*.tftest.hcl),examples/directory pattern, integration tests, contract tests.references/anti-patterns.md— 12 named anti-patterns with the better alternative.assets/templates/terraform/{main,variables,outputs,versions}.tf.tmpl+README.md.tmpl— root module starting points (renamed.tmplso IDE Terraform plugins don't try to parse the placeholder tokens).assets/templates/terraform/wrappers-{main,variables,outputs,versions}.tf.tmpl— wrapper module starting points.assets/templates/configs/{terraform-docs.yml,tflint.hcl,pre-commit-fragment.yaml,editorconfig}— repo-root tooling configs.assets/examples/{simple-module,dynamic-blocks,multi-block,module-test}.md— full worked examples (network / storage / firewall / test).scripts/scaffold-module.sh— generic, multi-provider scaffolder. Run it instead of writing 9 files by hand.scripts/validate-module.sh— score a module against the checklist. Run after editing.scripts/setup-tooling.sh— install terraform-docs, tflint, tfsec, checkov via standard package managers.
Top gotchas (always inline — do not skip)
- 8 files, 2 directories. No fewer, no extras. A module without
wrappers/is incomplete. A module with extra.tffiles (e.g.,data.tf,locals.tf) is the wrong shape — fold them intomain.tf. - Variable prefix is
<service>_<resource>_*. Always.vpc_subnet_name, notname. Prefix-less variables fight every consumer that imports more than one of your modules. - Dynamic blocks REQUIRE
iterator = <name>_cfg. Without the iterator, the block name shadows the variable. With it, you get a clean<name>_cfg.value.fieldreference. - Output pattern is non-negotiable:
try(element(concat(<resource>.this.*.<attr>, [<default>]), 0), <fallback>). Handles count = 0, null attrs, and missing fields in one expression. list(any)is banned. Always uselist(object({...}))withoptional()for optional fields.list(any)loses type safety and breaks consumer-side validation.- Timeouts use ONE object variable, not three.
<prefix>_timeoutsof typeobject({create=optional, update=optional, delete=optional}). Never<prefix>_timeout_create/_update/_deleteas separate vars. - Wrapper variables:
defaults+itemsonly, both strictly typed.defaultsisobject({...}),itemsismap(object({...})). Every field isoptional(<type>)and mirrors a root variable — same name, same type, same nested shape.type = anyis the old shortcut; typed wrappers are the contract because misspelled fields and wrong types fail at plan time instead of silently being ignored. - README has BEGIN_TF_DOCS / END_TF_DOCS markers. terraform-docs runs in
mode: injectand replaces only the content between markers. The static portion (Usage, Examples, Notes, Provider Documentation link) lives outside the markers. .terraform-docs.ymllives at repo root. Not per-module. It applies to every module viaterraform-docs -c .terraform-docs.yml <path>.- Pin every
required_providersversion. Lower bound minimum (>= 1.91.0). Floating refs (~> 1,latest) break reproducibility across team members and CI. enterprise_project_idand similar provider-specific fields are alwaysoptional. Don't make them required even when the provider treats them as semi-required — different organizations have different setups.- Module-level region defaults to
null, not a hardcoded value. Let the provider-level region win when the module caller doesn't override. Hardcoded defaults ("us-east-1","westus2", or any other concrete region) leak organizational choices into reusable modules.
What you DO
- Always start from
scripts/scaffold-module.sh. Never hand-write the 9 files. - Use the variable prefix
<service>_<resource>_*consistently. - Use
count = var.<prefix>_create ? 1 : 0on the resource block. - Write heredoc descriptions on every variable that mirror the provider registry.
- Use
list(object({...}))withoptional()for any list-of-records variable. - Use the
<prefix>_timeoutsobject variable (not three separate vars) when the resource supports timeouts. - Use the iterator pattern (
iterator = <name>_cfg) on every dynamic block. - Use the
try(element(concat(...), 0), <fallback>)pattern for every output. - Write a
wrappers/directory with typeddefaults(object({...})) anditems(map(object({...}))), mirroring every root variable asoptional(<type>). Thread per-resource values via thetry(each.value, var.defaults, <fallback>)chain inwrappers/main.tf. - Place
BEGIN_TF_DOCS/END_TF_DOCSmarkers in every README between Usage and Notes. - Wire terraform-docs as a pre-commit hook (and a CI check).
- Wire tflint + tfsec/trivy + checkov as pre-commit + CI.
- Add an
examples/subdirectory with a working basic example, thentests/*.tftest.hclfor the native test framework. - Run
scripts/validate-module.shafter every edit; aim for 100%. - Run
terraform fmt -recursive && terraform validatebefore declaring done.
What you do NOT do
- Skip the
wrappers/directory. - Use
list(any)ormap(any)anywhere — the wrapper'sdefaultsanditemsare also strictly typed (object({...})/map(object({...}))). - Hand-write per-resource wrapper variables (only
defaults+items). - Hardcode regions, project IDs, or organizational defaults.
- Use
~>or unpinned floating refs for provider versions. - Place
data.tf/locals.tfas separate files (fold intomain.tf). - Forget the
iterator = <name>_cfgon a dynamic block. - Use
for_each = var.x != null ? var.x : [](usecan(length(var.x)) ? var.x : []). - Write the inputs / outputs / providers tables manually in README — let terraform-docs inject them.
- Commit a module without running
terraform fmt. - Create a module without an
examples/working example. - Mix
countandfor_eachon the same resource — pick one. - Reference provider-specific resources without including the provider in
required_providers. - Make tags optional with no
merge()for theNametag (every module merges aNametag from<prefix>_name).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

