agentsclimarketplace

Terraform

Skill schubergphilis/agents.md/skills/terraform

Generic Terraform / OpenTofu guidance — module structure, variable + output design, block ordering, version pinning, native `terraform test`, CI/CD, security scanning, and state hygiene. Use when authoring or reviewing any Terraform/OpenTofu module, making IaC architecture decisions, picking a testing approach, setting up pipelines, or debugging state. For Schuberg Philis MCAF modules, the MCAF-specific overlays live in the `mcaf-module` and `review-mcaf` skills — use this one for baseline rules that apply regardless of organisation.From its SKILL.md

Install
npx -y skills add schubergphilis/agents.md --skill terraform

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.4 KB, as published. Nobody here has run it

Terraform / OpenTofu — baseline way of working

Inspired by Anton Babenko's terraform-skill and the patterns at terraform-best-practices.com. This is the generic base; organisation-specific overlays live in separate skills.

When to use this skill

Use when:

  • Creating or reviewing a Terraform/OpenTofu module.
  • Choosing between testing approaches (validate, plan tests, native terraform test, Terratest).
  • Structuring multi-environment deployments.
  • Setting up CI/CD for IaC.
  • Reviewing or refactoring existing configs.
  • Deciding between module patterns or state-management approaches.

Don't use for:

  • Basic HCL syntax questions.
  • Provider-specific API reference (link to upstream docs).

Reference files

Detailed guidance lives in references/:

  • module-patterns.md — module hierarchy, layout, variable/output design, tag patterns, anti-patterns.
  • code-patterns.md — block ordering, count vs for_each, optional(…), moved {}, version management.
  • testing.md — the decision matrix across terraform validate, plan tests, native terraform test, and Terratest.
  • ci-cd.md — GitHub Actions shape, conventional commits, release-drafter, pre-commit.
  • security-compliance.md — checkov/tfsec/trivy, secrets hygiene, state security.
  • quick-reference.md — cheat sheets, decision flowchart, troubleshooting.

Core principles

1. Module hierarchy

LevelScopeExample
Resource moduleOne logical group of connected resourcesVPC + subnets; Security group + rules
Infrastructure moduleA set of resource modules for one purpose in one region/accountLanding zone in one account
CompositionA complete deployment, potentially multi-region/accountWhole org

Build bottom-up: resource → resource module → infrastructure module → composition. Never skip a level by inlining across levels.

2. Repository layout for a resource module

<root>/
├── main.tf
├── variables.tf           # every variable with type + description
├── outputs.tf             # every output with description
├── terraform.tf           # required_version + required_providers
├── locals.tf              # only if non-empty
├── data.tf                # only if non-empty
├── README.md              # prose + auto-injected terraform-docs
├── CHANGELOG.md
├── LICENSE
├── .github/workflows/     # CI
├── .pre-commit-config.yaml
├── examples/              # one subdir per scenario, each runnable
│   ├── default/
│   └── <feature>/
└── tests/                 # native terraform test
    ├── main.tftest.hcl
    └── setup/             # optional fixture module

3. Version pinning

terraform {
  required_version = ">= 1.9"   # a reasonable modern floor, no upper bound

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 6.0"            # floor only for AWS; some providers need an upper bound
    }
  }
}
  • required_version floor-only with >=. No upper bound on Terraform. The exact floor is not important — focus on the constraint shape.
  • Provider version — floor-only for most providers (no upper bound). Some providers need an upper bound at the next major. Never = X.Y.Z (exact) in a reusable module.
  • Patch-tight (~> X.Y.Z) is almost always wrong — it blocks security patches.

4. Variables

variable "name" {
  type        = string
  description = "The name of the resource. Conflicts with `name_prefix`."
  default     = null
}

variable "mode" {
  type        = string
  default     = null
  description = "The operating mode."

  validation {
    condition     = var.mode == null || contains(["A", "B"], var.mode)
    error_message = "mode must be one of A or B."
  }
}

Rules:

  • Every variable has type + description.
  • Internal arg order: type → default → description → nullable → sensitive → validation.
  • Prefer complex typed objects with optional(field, default) over flat variable sprawl.
  • Use validation for enumerated values, regex constraints, cross-field invariants.
  • sensitive = true on secrets.
  • nullable = false when null is not a valid caller value.
  • Use default = null for "unset", not "".
  • Snake_case names. Booleans read as statements of state (versioning, not enable_versioning).
  • No redundant prefixes (arn, not bucket_arn).

5. Outputs

  • Every output has a description.
  • First outputs: id, arn, name — whatever maps to downstream caller references.
  • Mark sensitive outputs sensitive = true.
  • Never return whole resources. output "resource" { value = aws_x.this } leaks every attribute, including sensitive ones. Curate.
  • Minimum useful surface — not every attribute re-exported.

6. Tags (cloud providers that support them)

resource "aws_s3_bucket" "default" {
  # ...
  tags = var.tags
}
  • Expose variable "tags" as map(string) with default = {}.
  • Reference var.tags on every taggable resource. No local.tags indirection unless the module genuinely needs to inject extra tags.
  • Drop try(var.tags, {}) noise — var.tags has a default, try() around it hides type errors.

7. Testing

Use the layered strategy in references/testing.md:

  1. Pre-commit: terraform fmt, tflint, terraform validate, terraform_docs, checkov/tfsec/trivy.
  2. CI static: same tools, plus per-example terraform init && terraform validate.
  3. Native terraform test: mock_provider, run blocks, assert/expect_failures. Free of cloud credentials, fast.
  4. Terratest (Go): only when you need real apply against a real cloud.

Prefer native tests unless you actually need to hit a cloud. Keep tests under tests/.

8. CI/CD

At minimum:

  • PR validation (title format, labels).
  • Terraform validation (fmt + lint + validate on every example + terraform test).
  • Docs injection (terraform-docs into README).
  • Security scan (checkov).
  • Release automation (release-drafter + CHANGELOG).

See references/ci-cd.md for GitHub Actions shape.

9. Security and state

  • Run checkov in pre-commit + CI. Skip rules only with explicit rationale in comments.
  • Never commit provider credentials, .terraform/, lock files for root modules where they could leak.
  • State: remote backend with locking. Encrypt at rest. Never share state across environments.
  • Secrets: caller passes ARNs/IDs of secret managers; modules don't take plaintext secrets. If they must, mark sensitive = true.

10. Release flow

  • Conventional-commit PR titles: feat, fix, breaking, docs, chore.
  • Labels drive version bump: breaking → major; feat/enhancement → minor; fix/chore/docs → patch.
  • release-drafter maintains a draft release note on every merge; publish when ready.
  • Add UPGRADING.md entries for breaking changes.

Module anti-patterns

Things that bite teams repeatedly:

  • Whole-resource outputs that leak sensitive state.
  • Hard-pinned child-module refs (source = "…?ref=vX.Y.Z") — propagate version debt.
  • Two sources of truth for the same input (a flat var.name AND a nested var.obj.name).
  • try(var.tags) cargo-cult around variables that already have a default.
  • default = null on required inputs — contradicts the description.
  • Empty outputs.tf — module is a black box.
  • Missing sensitive = true on passwords/keys/tokens.
  • Floating :latest / :main image/version defaults — non-idempotent.
  • Dead variables / locals / files.
  • File-naming drift (camelCase, backend.tf/module.tf/versions.tf in place of canonical names).
  • Nested ternaries where optional(field, default) would do it.
  • null_resource / deprecated resources (aws_s3_bucket_object, azurerm_function_app).

Decision quick-reference

  • Alphabetical or grouped variable order? Either, but pick one per repo. CI's terraform-docs --sort-by required normalises the rendered README regardless.
  • count vs for_each? for_each on maps/sets for stable keys. count only when the multiplicity is truly positional.
  • locals.tf or inline locals {}? Separate file when you have >~3 locals or when locals cross domains.
  • Providers in the module or in the caller? In the caller. Modules use configuration_aliases only when they genuinely need multi-provider orchestration (e.g. cross-account S3 replication).

Further reading

  • references/module-patterns.md
  • references/code-patterns.md
  • references/testing.md
  • references/ci-cd.md
  • references/security-compliance.md
  • references/quick-reference.md

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.