From 489812dd1c3ea7b1a4b4f7d1f930c2aa14811848 Mon Sep 17 00:00:00 2001 From: Tristan Jahnke Date: Sun, 10 May 2026 20:31:18 -0500 Subject: [PATCH 1/2] style: Format and lint --- .markdownlint.yaml | 2 +- Makefile | 2 +- README.md | 8 +++--- docs/api/pagination.md | 4 +-- docs/assets/external-links.js | 2 +- docs/assets/extra.css | 30 ++++++++++++++--------- docs/containers/distroless.md | 4 +-- docs/containers/dockerfile-go.md | 4 +-- docs/containers/dockerfile-node.md | 4 +-- docs/containers/dockerfile-python.md | 4 +-- docs/data/dbt-skeleton.md | 2 +- docs/data/postgres-conventions.md | 2 +- docs/data/postgres-indexes.md | 4 +-- docs/github-actions/oidc-aws.md | 4 +-- docs/github-actions/reusable-workflows.md | 2 +- docs/hygiene/index.md | 2 +- docs/hygiene/makefile.md | 2 +- docs/hygiene/pre-commit.md | 2 +- docs/index.md | 6 ++--- docs/kubernetes/deployment-baseline.md | 2 +- docs/kubernetes/probes.md | 2 +- docs/kubernetes/scaling.md | 2 +- docs/observability/index.md | 2 +- docs/observability/prometheus.md | 2 +- docs/terraform/aws/backends.md | 4 +-- docs/terraform/aws/iam-policies.md | 8 +++--- docs/terraform/aws/index.md | 2 +- docs/terraform/aws/module-skeleton.md | 2 +- docs/terraform/aws/providers.md | 4 +-- docs/terraform/aws/variables.md | 2 +- docs/terraform/azure/backends.md | 4 +-- docs/terraform/azure/iam-policies.md | 4 +-- docs/terraform/azure/index.md | 2 +- docs/terraform/azure/module-skeleton.md | 4 +-- docs/terraform/azure/providers.md | 14 +++++------ docs/terraform/azure/variables.md | 4 +-- docs/terraform/gcp/backends.md | 8 +++--- docs/terraform/gcp/iam-policies.md | 10 ++++---- docs/terraform/gcp/index.md | 2 +- docs/terraform/gcp/providers.md | 8 +++--- docs/terraform/gcp/variables.md | 4 +-- docs/terraform/terragrunt/index.md | 16 ++++++------ docs/terraform/terragrunt/root-config.md | 12 ++++----- docs/terraform/terragrunt/stacks.md | 6 ++--- pyproject.toml | 2 +- 45 files changed, 115 insertions(+), 107 deletions(-) diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 6844c93..556f077 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -2,7 +2,7 @@ # https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md default: true -# Line length — match other linters at 120, ignore code blocks/tables. +# Line length: match other linters at 120, ignore code blocks/tables. MD013: line_length: 120 code_blocks: false diff --git a/Makefile b/Makefile index 9713cab..1509f3b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# boilplate — local dev convenience targets +# boilplate: local dev convenience targets # Run `make help` to see what's available. # # All Python work is done through `uv`, which manages the .venv automatically. diff --git a/README.md b/README.md index b096065..4b0f8dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # boilplate -A personal stash of copy-paste-ready snippets for things I keep rewriting — +A personal stash of copy-paste-ready snippets for things I keep rewriting: Terraform/Terragrunt, GitHub Actions, containers, Kubernetes, Postgres, and so on. Built primarily for me, but published in case anyone else finds bits of it useful: @@ -22,7 +22,7 @@ useful: | `.pre-commit-config.yaml` | [pre-commit](https://pre-commit.com/) hooks (hygiene + `zensical build` + lychee). | | `lychee.toml` | [lychee](https://lychee.cli.rs/) link-checker config (used by pre-commit and CI). | | `.editorconfig` | Whitespace defaults across editors. | -| `Makefile` | Convenience targets — run `make help`. | +| `Makefile` | Convenience targets: run `make help`. | | `terragrunt/` | Legacy Terragrunt example. Modern patterns live in [`docs/terraform/terragrunt/`](docs/terraform/terragrunt/). | | `.github/workflows/` | `ci.yml` (PR checks), `deploy.yml` (Pages deploy on push to `main`), `links.yml` (lychee on PR + weekly cron). | @@ -33,7 +33,7 @@ manages Python versions and the virtual env). Everything else goes through the Makefile: ```sh -make install # uv sync — create .venv and install deps +make install # uv sync: create .venv and install deps make hooks # install pre-commit git hooks (one-time) make serve # live-reload dev server at http://localhost:8000 make build # build site into ./site @@ -58,7 +58,7 @@ One-time setup in the repo settings: 1. Add a Markdown file under `docs//`. 2. Reference it in the `nav` block in [`zensical.toml`](zensical.toml). -3. Add front-matter, including `tags:` (block-style YAML — flow-style +3. Add front-matter, including `tags:` (block-style YAML: flow-style `[a, b]` collides with Zensical's reference-link parser): ```yaml diff --git a/docs/api/pagination.md b/docs/api/pagination.md index 95cd111..35a83de 100644 --- a/docs/api/pagination.md +++ b/docs/api/pagination.md @@ -1,12 +1,12 @@ --- -title: Pagination — cursor vs offset +title: Pagination: cursor vs offset description: Cursor and offset pagination patterns for REST APIs. status: stub tags: - api --- -# Pagination — cursor vs offset +# Pagination: cursor vs offset !!! note "Stub page" When to use which, with copy-paste implementations. diff --git a/docs/assets/external-links.js b/docs/assets/external-links.js index 1a609fb..829ad3c 100644 --- a/docs/assets/external-links.js +++ b/docs/assets/external-links.js @@ -14,7 +14,7 @@ a.target = '_blank'; a.rel = 'noopener noreferrer'; } - } catch (_) { /* malformed URL — ignore */ } + } catch (_) { /* malformed URL: ignore */ } }); } diff --git a/docs/assets/extra.css b/docs/assets/extra.css index 4ac9ba2..c5e8d66 100644 --- a/docs/assets/extra.css +++ b/docs/assets/extra.css @@ -1,5 +1,5 @@ /* ========================================================================== - boilplate — muted dark-grey + sage-green palette + boilplate: muted dark-grey + sage-green palette ========================================================================== */ /* Brand greens */ @@ -17,7 +17,7 @@ } /* -------------------------------------------------------------------------- - Light mode — warm off-white with sage accents + Light mode: warm off-white with sage accents -------------------------------------------------------------------------- */ [data-md-color-scheme="default"] { --md-primary-fg-color: var(--bp-green-500); @@ -44,7 +44,7 @@ } /* -------------------------------------------------------------------------- - Dark mode — warm dark grey with sage accents + Dark mode: warm dark grey with sage accents -------------------------------------------------------------------------- */ [data-md-color-scheme="slate"] { --md-hue: 200; @@ -77,7 +77,7 @@ } /* -------------------------------------------------------------------------- - Header polish — solid muted bar in dark mode (no harsh primary stripe) + Header polish: solid muted bar in dark mode (no harsh primary stripe) -------------------------------------------------------------------------- */ [data-md-color-scheme="slate"] .md-header { background-color: #1a1d20; @@ -114,7 +114,7 @@ [data-md-color-scheme="default"] .md-tabs__link--active { color: var(--bp-green-600); } /* -------------------------------------------------------------------------- - Code blocks — slightly inset, subtle border, monospaced clarity + Code blocks: slightly inset, subtle border, monospaced clarity -------------------------------------------------------------------------- */ .md-typeset pre > code, .md-typeset .highlight pre { @@ -138,7 +138,7 @@ padding: 0.1em 0.35em; } -/* Copy button — subtler */ +/* Copy button: subtler */ .md-clipboard { color: var(--md-default-fg-color--lighter); } @@ -164,7 +164,7 @@ padding-bottom: 0.3em; } -/* Links — accent on hover */ +/* Links: accent on hover */ .md-typeset a { text-decoration: none; border-bottom: 1px solid transparent; @@ -176,7 +176,7 @@ } /* -------------------------------------------------------------------------- - Admonitions / callouts — quieter colors that fit the palette + Admonitions / callouts: quieter colors that fit the palette -------------------------------------------------------------------------- */ [data-md-color-scheme="slate"] .md-typeset .admonition, [data-md-color-scheme="slate"] .md-typeset details { @@ -191,7 +191,7 @@ } /* -------------------------------------------------------------------------- - Tables — calmer, no zebra fight with the bg + Tables: calmer, no zebra fight with the bg -------------------------------------------------------------------------- */ .md-typeset table:not([class]) { border: 1px solid var(--md-default-fg-color--lightest); @@ -204,7 +204,7 @@ } /* -------------------------------------------------------------------------- - Search box — no harsh primary background + Search box: no harsh primary background -------------------------------------------------------------------------- */ [data-md-color-scheme="slate"] .md-search__form { background-color: #22262a; @@ -220,7 +220,15 @@ } /* -------------------------------------------------------------------------- - Footer — muted, no jarring color block + Page tags: breathing room above the tag chips so they don't butt up + against the article content. + -------------------------------------------------------------------------- */ +nav.md-tags { + margin-top: 2.5rem; +} + +/* -------------------------------------------------------------------------- + Footer: muted, no jarring color block -------------------------------------------------------------------------- */ .md-footer-meta { background-color: #0f1214; diff --git a/docs/containers/distroless.md b/docs/containers/distroless.md index 9022ce7..2bf8d20 100644 --- a/docs/containers/distroless.md +++ b/docs/containers/distroless.md @@ -13,6 +13,6 @@ tags: ## Planned content -- gcr.io/distroless/* — language-specific bases -- cgr.dev/chainguard — wolfi-based, frequently rebuilt +- gcr.io/distroless/*: language-specific bases +- cgr.dev/chainguard: wolfi-based, frequently rebuilt - Debugging distroless (`:debug` tag, ephemeral containers) diff --git a/docs/containers/dockerfile-go.md b/docs/containers/dockerfile-go.md index a7e82fe..c620b06 100644 --- a/docs/containers/dockerfile-go.md +++ b/docs/containers/dockerfile-go.md @@ -1,5 +1,5 @@ --- -title: Go — static binary, distroless +title: Go: static binary, distroless description: Multi-stage Dockerfile producing a static Go binary on a distroless runtime. status: stub tags: @@ -7,7 +7,7 @@ tags: - go --- -# Go — static binary, distroless +# Go: static binary, distroless !!! note "Stub page" Single-binary Go services in a tiny image. diff --git a/docs/containers/dockerfile-node.md b/docs/containers/dockerfile-node.md index c8922cf..3833153 100644 --- a/docs/containers/dockerfile-node.md +++ b/docs/containers/dockerfile-node.md @@ -1,5 +1,5 @@ --- -title: Node — multi-stage, pnpm +title: Node: multi-stage, pnpm description: Multi-stage Dockerfile for Node services with pnpm and a slim runtime stage. status: stub tags: @@ -7,7 +7,7 @@ tags: - node --- -# Node — multi-stage, pnpm +# Node: multi-stage, pnpm !!! note "Stub page" Production Node images with pnpm or npm, no dev deps. diff --git a/docs/containers/dockerfile-python.md b/docs/containers/dockerfile-python.md index 779b357..aa6953f 100644 --- a/docs/containers/dockerfile-python.md +++ b/docs/containers/dockerfile-python.md @@ -1,5 +1,5 @@ --- -title: Python — multi-stage with uv +title: Python: multi-stage with uv description: Multi-stage Dockerfile for Python services using uv and a slim runtime stage. status: stub tags: @@ -7,7 +7,7 @@ tags: - python --- -# Python — multi-stage with uv +# Python: multi-stage with uv !!! note "Stub page" Small, reproducible Python images using uv for dependency install. diff --git a/docs/data/dbt-skeleton.md b/docs/data/dbt-skeleton.md index df13186..2a94f79 100644 --- a/docs/data/dbt-skeleton.md +++ b/docs/data/dbt-skeleton.md @@ -1,6 +1,6 @@ --- title: dbt project skeleton -description: Opinionated dbt project skeleton — sources, staging, marts, and tests. +description: Opinionated dbt project skeleton: sources, staging, marts, and tests. status: stub tags: - data diff --git a/docs/data/postgres-conventions.md b/docs/data/postgres-conventions.md index 5241f12..44bf7ba 100644 --- a/docs/data/postgres-conventions.md +++ b/docs/data/postgres-conventions.md @@ -14,7 +14,7 @@ tags: ## Planned content -- Primary keys: UUID v7 vs identity vs serial — when each wins +- Primary keys: UUID v7 vs identity vs serial: when each wins - Audit columns: created_at / updated_at / created_by + trigger - Soft delete: deleted_at vs separate archive table - Naming: snake_case, plural tables, FK suffix diff --git a/docs/data/postgres-indexes.md b/docs/data/postgres-indexes.md index 4593d5f..ab8a43a 100644 --- a/docs/data/postgres-indexes.md +++ b/docs/data/postgres-indexes.md @@ -1,6 +1,6 @@ --- title: Postgres indexing & partitioning -description: When and how to add indexes — B-tree, partial, GIN, and covering indexes. +description: When and how to add indexes: B-tree, partial, GIN, and covering indexes. status: stub tags: - data @@ -14,7 +14,7 @@ tags: ## Planned content -- Composite indexes — column order rules +- Composite indexes: column order rules - Partial indexes - GIN indexes for jsonb / array / full-text - Range partitioning by time + pg_partman diff --git a/docs/github-actions/oidc-aws.md b/docs/github-actions/oidc-aws.md index 9b66178..b63e786 100644 --- a/docs/github-actions/oidc-aws.md +++ b/docs/github-actions/oidc-aws.md @@ -1,6 +1,6 @@ --- title: OIDC → AWS (no static keys) -description: Authenticate GitHub Actions to AWS via OIDC — no long-lived access keys. +description: Authenticate GitHub Actions to AWS via OIDC: no long-lived access keys. status: stub tags: - github-actions @@ -17,4 +17,4 @@ tags: - IAM identity provider creation (one-time per account) - IAM role with sub-claim conditions: branch, environment, tag, PR - `aws-actions/configure-aws-credentials` invocation -- Common 'Not authorized' debugging — what to check +- Common 'Not authorized' debugging: what to check diff --git a/docs/github-actions/reusable-workflows.md b/docs/github-actions/reusable-workflows.md index 5b7b38b..18f4086 100644 --- a/docs/github-actions/reusable-workflows.md +++ b/docs/github-actions/reusable-workflows.md @@ -1,6 +1,6 @@ --- title: Reusable workflows -description: Reusable workflow patterns — inputs, secrets, permissions, and composite actions. +description: Reusable workflow patterns: inputs, secrets, permissions, and composite actions. status: stub tags: - github-actions diff --git a/docs/hygiene/index.md b/docs/hygiene/index.md index 24ecd71..ab9b3c4 100644 --- a/docs/hygiene/index.md +++ b/docs/hygiene/index.md @@ -1,6 +1,6 @@ --- title: Repo hygiene -description: Repo hygiene snippets — .gitignore, .editorconfig, pre-commit, and Makefiles. +description: Repo hygiene snippets: .gitignore, .editorconfig, pre-commit, and Makefiles. status: stub tags: - hygiene diff --git a/docs/hygiene/makefile.md b/docs/hygiene/makefile.md index 1a16901..277e6fb 100644 --- a/docs/hygiene/makefile.md +++ b/docs/hygiene/makefile.md @@ -1,6 +1,6 @@ --- title: Makefile patterns -description: Makefile patterns — self-documenting help target, .PHONY hygiene, and uv integration. +description: Makefile patterns: self-documenting help target, .PHONY hygiene, and uv integration. status: stub tags: - hygiene diff --git a/docs/hygiene/pre-commit.md b/docs/hygiene/pre-commit.md index dfa5208..704e950 100644 --- a/docs/hygiene/pre-commit.md +++ b/docs/hygiene/pre-commit.md @@ -172,7 +172,7 @@ For project-specific checks that don't warrant their own repo: ``` !!! warning "`language: system` requires the binary on PATH" - `system` hooks won't be installed for you — make sure `uv`, `lychee`, etc. + `system` hooks won't be installed for you: make sure `uv`, `lychee`, etc. are available in CI and locally, or use `language: python` / `language: docker` / `language: golang` to let pre-commit manage them. diff --git a/docs/index.md b/docs/index.md index e4a5312..77af0ed 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,9 +12,9 @@ hide: This site collects small, opinionated, well-validated snippets you can drop straight into a project. Everything here is designed to be: -- **Self-contained** — copy a single block and it works. -- **Validated** — variables come with `validation` blocks, types, and sane defaults. -- **Cited** — links back to the upstream docs so you can verify behavior. +- **Self-contained**: copy a single block and it works. +- **Validated**: variables come with `validation` blocks, types, and sane defaults. +- **Cited**: links back to the upstream docs so you can verify behavior. --- diff --git a/docs/kubernetes/deployment-baseline.md b/docs/kubernetes/deployment-baseline.md index 2050427..24aef28 100644 --- a/docs/kubernetes/deployment-baseline.md +++ b/docs/kubernetes/deployment-baseline.md @@ -1,6 +1,6 @@ --- title: Deployment + Service + Ingress baseline -description: Production-ready Deployment baseline — resources, probes, security context, and topology. +description: Production-ready Deployment baseline: resources, probes, security context, and topology. status: stub tags: - kubernetes diff --git a/docs/kubernetes/probes.md b/docs/kubernetes/probes.md index 340acb2..87af3e8 100644 --- a/docs/kubernetes/probes.md +++ b/docs/kubernetes/probes.md @@ -1,6 +1,6 @@ --- title: Probes done right -description: Liveness, readiness, and startup probes done right — common mistakes and fixes. +description: Liveness, readiness, and startup probes done right: common mistakes and fixes. status: stub tags: - kubernetes diff --git a/docs/kubernetes/scaling.md b/docs/kubernetes/scaling.md index 6bc8a1e..f1cebe6 100644 --- a/docs/kubernetes/scaling.md +++ b/docs/kubernetes/scaling.md @@ -14,5 +14,5 @@ tags: ## Planned content - HPA on CPU + custom metrics -- PodDisruptionBudget — minAvailable vs maxUnavailable +- PodDisruptionBudget: minAvailable vs maxUnavailable - Default-deny NetworkPolicy + targeted allow rules diff --git a/docs/observability/index.md b/docs/observability/index.md index 1e743f7..892cb41 100644 --- a/docs/observability/index.md +++ b/docs/observability/index.md @@ -9,7 +9,7 @@ tags: # Observability !!! note "Stub page" - Logging, tracing, and metrics — same patterns across stacks. + Logging, tracing, and metrics: same patterns across stacks. ## Planned content diff --git a/docs/observability/prometheus.md b/docs/observability/prometheus.md index 704a582..3f5da51 100644 --- a/docs/observability/prometheus.md +++ b/docs/observability/prometheus.md @@ -14,7 +14,7 @@ tags: ## Planned content -- Naming: `http_requests_total`, `_seconds`, `_bytes` — units in the name +- Naming: `http_requests_total`, `_seconds`, `_bytes`: units in the name - RED method (Rate, Errors, Duration) for request-driven services - USE method (Utilization, Saturation, Errors) for resources - Histogram bucket selection diff --git a/docs/terraform/aws/backends.md b/docs/terraform/aws/backends.md index 536cc28..d113f8c 100644 --- a/docs/terraform/aws/backends.md +++ b/docs/terraform/aws/backends.md @@ -141,7 +141,7 @@ resource "aws_kms_alias" "tfstate" { resource "aws_s3_bucket" "tfstate" { bucket = var.bucket_name - # Belt and braces — never let someone delete this by accident. + # Belt and braces: never let someone delete this by accident. lifecycle { prevent_destroy = true } @@ -242,7 +242,7 @@ resource "aws_dynamodb_table" "tflocks" { ## References - [Terraform: S3 backend](https://developer.hashicorp.com/terraform/language/backend/s3) -- [Terraform 1.10 release notes — S3 native locking](https://github.com/hashicorp/terraform/releases/tag/v1.10.0) +- [Terraform 1.10 release notes: S3 native locking](https://github.com/hashicorp/terraform/releases/tag/v1.10.0) - [OpenTofu: S3 backend](https://opentofu.org/docs/language/settings/backends/s3/) - [AWS: Protecting data with server-side encryption (SSE-KMS)](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html) - [AWS: Using versioning in S3 buckets](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html) diff --git a/docs/terraform/aws/iam-policies.md b/docs/terraform/aws/iam-policies.md index d9a9803..04aa325 100644 --- a/docs/terraform/aws/iam-policies.md +++ b/docs/terraform/aws/iam-policies.md @@ -1,6 +1,6 @@ --- title: IAM policy patterns -description: Least-privilege IAM trust and resource policy snippets — GitHub Actions OIDC, cross-account assume-role with ExternalId, S3 TLS-only and encryption-required bucket policies, and a separated KMS key policy. +description: Least-privilege IAM trust and resource policy snippets: GitHub Actions OIDC, cross-account assume-role with ExternalId, S3 TLS-only and encryption-required bucket policies, and a separated KMS key policy. tags: - terraform - aws @@ -78,7 +78,7 @@ You also need the OIDC provider itself once per account: resource "aws_iam_openid_connect_provider" "github" { url = "https://token.actions.githubusercontent.com" client_id_list = ["sts.amazonaws.com"] - # GitHub publishes thumbprints — let AWS pick them up automatically since + # GitHub publishes thumbprints: let AWS pick them up automatically since # the 2023-07 change. An empty list works on current provider versions. thumbprint_list = [] } @@ -349,7 +349,7 @@ resource "aws_kms_key" "this" { ``` !!! warning "Don't drop the root statement" - AWS will let you save a key policy without the root principal — and then + AWS will let you save a key policy without the root principal: and then nobody can edit it again. The "EnableIAMUserPermissions" statement is your one and only break-glass. Keep it. @@ -359,7 +359,7 @@ resource "aws_kms_key" "this" { - [AWS: Configuring OIDC for GitHub Actions](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) - [AWS: The confused deputy problem and ExternalId](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html) -- [AWS: Bucket policy examples — require HTTPS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html#example-bucket-policies-secure-transport) +- [AWS: Bucket policy examples: require HTTPS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html#example-bucket-policies-secure-transport) - [AWS: Protecting data with SSE-KMS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html) - [AWS: Key policies in AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html) - [Terraform: `aws_iam_policy_document` data source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) diff --git a/docs/terraform/aws/index.md b/docs/terraform/aws/index.md index 43ca60d..140c612 100644 --- a/docs/terraform/aws/index.md +++ b/docs/terraform/aws/index.md @@ -1,6 +1,6 @@ --- title: AWS -description: AWS-specific Terraform snippets — variables, modules, backends, providers, IAM. +description: AWS-specific Terraform snippets: variables, modules, backends, providers, IAM. tags: - terraform - aws diff --git a/docs/terraform/aws/module-skeleton.md b/docs/terraform/aws/module-skeleton.md index c3b90d7..c862363 100644 --- a/docs/terraform/aws/module-skeleton.md +++ b/docs/terraform/aws/module-skeleton.md @@ -1,6 +1,6 @@ --- title: Module skeleton -description: Opinionated layout for a reusable AWS Terraform / OpenTofu module — file structure, version pinning, default_tags, terraform-docs, native tftest, and pre-commit-terraform. +description: Opinionated layout for a reusable AWS Terraform / OpenTofu module: file structure, version pinning, default_tags, terraform-docs, native tftest, and pre-commit-terraform. tags: - terraform - aws diff --git a/docs/terraform/aws/providers.md b/docs/terraform/aws/providers.md index 12664a8..d6c8e6e 100644 --- a/docs/terraform/aws/providers.md +++ b/docs/terraform/aws/providers.md @@ -1,6 +1,6 @@ --- title: Provider configuration -description: Production defaults for the AWS Terraform / OpenTofu provider — version pinning, default_tags, assume_role, retries, multi-region aliases, and OIDC for GitHub Actions. +description: Production defaults for the AWS Terraform / OpenTofu provider: version pinning, default_tags, assume_role, retries, multi-region aliases, and OIDC for GitHub Actions. tags: - terraform - aws @@ -168,7 +168,7 @@ terraform { ## OIDC + GitHub Actions (`assume_role_with_web_identity`) Inside a GitHub Actions runner, exchange the workflow's OIDC token for AWS -credentials — no static keys, no `aws-actions/configure-aws-credentials` +credentials: no static keys, no `aws-actions/configure-aws-credentials` env-var dance required by Terraform itself. ```hcl diff --git a/docs/terraform/aws/variables.md b/docs/terraform/aws/variables.md index 6a62785..071c57e 100644 --- a/docs/terraform/aws/variables.md +++ b/docs/terraform/aws/variables.md @@ -297,7 +297,7 @@ variable "subnets" { ```hcl variable "db_password" { - description = "Database admin password. Provide via TF_VAR_db_password or a secrets manager — do not commit." + description = "Database admin password. Provide via TF_VAR_db_password or a secrets manager: do not commit." type = string sensitive = true diff --git a/docs/terraform/azure/backends.md b/docs/terraform/azure/backends.md index 59745ef..3d6e658 100644 --- a/docs/terraform/azure/backends.md +++ b/docs/terraform/azure/backends.md @@ -1,6 +1,6 @@ --- title: Remote state backends -description: Azure Storage backend configuration for Terraform / OpenTofu — blob lease locking, OIDC auth from CI, and the bootstrap pattern. +description: Azure Storage backend configuration for Terraform / OpenTofu: blob lease locking, OIDC auth from CI, and the bootstrap pattern. tags: - terraform - azure @@ -121,7 +121,7 @@ itself. Solve it with a one-shot bootstrap module that runs against **local** state, then migrate it. ```hcl -# bootstrap/main.tf — apply with local state, then `terraform state push`. +# bootstrap/main.tf : apply with local state, then `terraform state push`. terraform { required_version = ">= 1.3" required_providers { diff --git a/docs/terraform/azure/iam-policies.md b/docs/terraform/azure/iam-policies.md index a76a8af..c9e58fc 100644 --- a/docs/terraform/azure/iam-policies.md +++ b/docs/terraform/azure/iam-policies.md @@ -1,6 +1,6 @@ --- title: RBAC role assignments -description: Azure RBAC patterns for Terraform — built-in role assignments, custom role definitions, and federated CI identities (OIDC) for GitHub Actions. +description: Azure RBAC patterns for Terraform: built-in role assignments, custom role definitions, and federated CI identities (OIDC) for GitHub Actions. tags: - terraform - azure @@ -15,7 +15,7 @@ or resource). The binding itself is an `azurerm_role_assignment`. !!! tip "Prefer built-in roles" Microsoft maintains 200+ [built-in roles](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles). - Reach for a custom role only when no built-in role fits — custom roles + Reach for a custom role only when no built-in role fits: custom roles are scoped to one tenant and harder to audit. ## Built-in role assignment diff --git a/docs/terraform/azure/index.md b/docs/terraform/azure/index.md index be8cb2d..e7e8b52 100644 --- a/docs/terraform/azure/index.md +++ b/docs/terraform/azure/index.md @@ -1,6 +1,6 @@ --- title: Azure -description: Azure-specific Terraform snippets — variables, modules, backends, providers, RBAC. +description: Azure-specific Terraform snippets: variables, modules, backends, providers, RBAC. tags: - terraform - azure diff --git a/docs/terraform/azure/module-skeleton.md b/docs/terraform/azure/module-skeleton.md index 197033f..e493d59 100644 --- a/docs/terraform/azure/module-skeleton.md +++ b/docs/terraform/azure/module-skeleton.md @@ -1,6 +1,6 @@ --- title: Module skeleton -description: Opinionated layout for a reusable Azure Terraform / OpenTofu module — file structure, version pinning, naming conventions, terraform-docs, native tftest, and pre-commit-terraform. +description: Opinionated layout for a reusable Azure Terraform / OpenTofu module: file structure, version pinning, naming conventions, terraform-docs, native tftest, and pre-commit-terraform. tags: - terraform - azure @@ -341,7 +341,7 @@ pre-commit run --all-files - [Terraform: Standard module structure](https://developer.hashicorp.com/terraform/language/modules/develop/structure) - [Terraform: Tests (`terraform test`)](https://developer.hashicorp.com/terraform/language/tests) -- [Microsoft Learn: Cloud Adoption Framework — naming conventions](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming) +- [Microsoft Learn: Cloud Adoption Framework: naming conventions](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming) - [Microsoft Learn: Recommended abbreviations for Azure resource types](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations) - [azurerm provider reference](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) - [terraform-docs](https://terraform-docs.io/) diff --git a/docs/terraform/azure/providers.md b/docs/terraform/azure/providers.md index 99d77c6..d31ee30 100644 --- a/docs/terraform/azure/providers.md +++ b/docs/terraform/azure/providers.md @@ -1,6 +1,6 @@ --- title: Provider configuration -description: Sensible azurerm + azuread provider defaults for Terraform / OpenTofu — features block, OIDC auth from CI, multi-subscription aliases, and ARM environment variables. +description: Sensible azurerm + azuread provider defaults for Terraform / OpenTofu: features block, OIDC auth from CI, multi-subscription aliases, and ARM environment variables. tags: - terraform - azure @@ -14,7 +14,7 @@ Targets **azurerm ≥ 4.0** and **azuread ≥ 3.0**. !!! note "`features {}` is mandatory" The empty `features {}` block is required even when you don't override - anything — `terraform validate` will fail without it. Use it to control + anything: `terraform validate` will fail without it. Use it to control destroy-time behaviours like Key Vault soft-delete recovery and resource group force-delete. @@ -93,7 +93,7 @@ The provider tries auth methods in this order: **environment variables → managed identity → OIDC → CLI**. Pick exactly one method per environment so behaviour stays predictable. -### Local development — Azure CLI +### Local development: Azure CLI ```bash az login @@ -104,11 +104,11 @@ az account set --subscription "" provider "azurerm" { features {} # subscription_id is read from the CLI context if omitted. - use_cli = true # default — shown for clarity + use_cli = true # default: shown for clarity } ``` -### CI — GitHub Actions OIDC +### CI: GitHub Actions OIDC ```hcl provider "azurerm" { @@ -142,7 +142,7 @@ jobs: - run: terraform apply -auto-approve ``` -### Self-hosted runners — Managed Identity +### Self-hosted runners: Managed Identity ```hcl provider "azurerm" { @@ -187,7 +187,7 @@ provider "azurerm" { subscription_id = var.shared_subscription_id } -# Workload-subscription resource (default-ish — pick the alias explicitly) +# Workload-subscription resource (default-ish: pick the alias explicitly) resource "azurerm_resource_group" "app" { provider = azurerm.workload name = "rg-app-prod" diff --git a/docs/terraform/azure/variables.md b/docs/terraform/azure/variables.md index bba49e2..f0a215c 100644 --- a/docs/terraform/azure/variables.md +++ b/docs/terraform/azure/variables.md @@ -1,6 +1,6 @@ --- title: Common variables -description: Reusable, well-validated Terraform / OpenTofu variable blocks for Azure — location, subscription, tenant, resource groups, tags, CIDRs, VM sizes, and more. +description: Reusable, well-validated Terraform / OpenTofu variable blocks for Azure: location, subscription, tenant, resource groups, tags, CIDRs, VM sizes, and more. tags: - terraform - azure @@ -291,7 +291,7 @@ variable "subnets" { ```hcl variable "sql_admin_password" { - description = "Azure SQL administrator password. Provide via TF_VAR_sql_admin_password or Key Vault — do not commit." + description = "Azure SQL administrator password. Provide via TF_VAR_sql_admin_password or Key Vault: do not commit." type = string sensitive = true diff --git a/docs/terraform/gcp/backends.md b/docs/terraform/gcp/backends.md index af51a12..9c4623d 100644 --- a/docs/terraform/gcp/backends.md +++ b/docs/terraform/gcp/backends.md @@ -1,6 +1,6 @@ --- title: Remote state backends -description: GCS backend configuration for Terraform/OpenTofu — versioning, encryption, automatic locking, bootstrap, and Workload Identity Federation auth from CI. +description: GCS backend configuration for Terraform/OpenTofu: versioning, encryption, automatic locking, bootstrap, and Workload Identity Federation auth from CI. tags: - terraform - gcp @@ -9,13 +9,13 @@ tags: # Remote state backends The `gcs` backend stores Terraform state in a Google Cloud Storage bucket. -State **locking is automatic** — GCS uses object generations to coordinate +State **locking is automatic**: GCS uses object generations to coordinate concurrent writers, so there is no DynamoDB-equivalent table to provision. !!! tip "Pick a single regional bucket per state" Use a regional (not multi-region) bucket close to where you run plans, enable **Object Versioning**, **Uniform bucket-level access**, and either - a Google-managed key or a CMEK. One bucket can hold many state files — + a Google-managed key or a CMEK. One bucket can hold many state files; use `prefix` to namespace them. --- @@ -101,7 +101,7 @@ Standard pattern: 4. Commit and delete the local `terraform.tfstate*` files. ```hcl -# bootstrap/main.tf — run with local state, ONCE per org +# bootstrap/main.tf: run with local state, ONCE per org terraform { required_version = ">= 1.3" required_providers { diff --git a/docs/terraform/gcp/iam-policies.md b/docs/terraform/gcp/iam-policies.md index addbf47..60d1bdf 100644 --- a/docs/terraform/gcp/iam-policies.md +++ b/docs/terraform/gcp/iam-policies.md @@ -1,6 +1,6 @@ --- title: IAM bindings & custom roles -description: GCP IAM in Terraform — additive vs authoritative bindings, custom roles, Workload Identity Federation for GitHub Actions, and service-account impersonation. +description: GCP IAM in Terraform: additive vs authoritative bindings, custom roles, Workload Identity Federation for GitHub Actions, and service-account impersonation. tags: - terraform - gcp @@ -18,9 +18,9 @@ will silently wipe other teams' access. Read this page before you reach for | Resource family | Scope | Authoritative? | Safe default? | | ---------------------- | ------------------------------------------- | ------------------ | ------------------------------------- | -| `google_*_iam_member` | Single (role, member) pair | No — additive | ✅ Yes | -| `google_*_iam_binding` | Whole role (all members for that one role) | Yes — for the role | ⚠️ Only if Terraform owns *that role* | -| `google_*_iam_policy` | The entire resource's IAM policy | Yes — total | ❌ Almost never | +| `google_*_iam_member` | Single (role, member) pair | No (additive) | ✅ Yes | +| `google_*_iam_binding` | Whole role (all members for that one role) | Yes (for the role) | ⚠️ Only if Terraform owns *that role* | +| `google_*_iam_policy` | The entire resource's IAM policy | Yes (total) | ❌ Almost never | !!! warning "`google_project_iam_policy` is destructive" `google_project_iam_policy` overwrites **every** binding on the project, @@ -218,7 +218,7 @@ deploy SA says *what they can do*, and rotating one doesn't disturb the other. ## References -- [google_project_iam_*][gpi-resources] — member vs binding vs policy +- [google_project_iam_*][gpi-resources]: member vs binding vs policy - [google_project_iam_custom_role][gpi-custom-role] - [google_iam_workload_identity_pool][wif-pool] - [google_iam_workload_identity_pool_provider][wif-provider] diff --git a/docs/terraform/gcp/index.md b/docs/terraform/gcp/index.md index 21aa40c..170d874 100644 --- a/docs/terraform/gcp/index.md +++ b/docs/terraform/gcp/index.md @@ -1,6 +1,6 @@ --- title: GCP -description: GCP-specific Terraform snippets — variables, modules, backends, providers, IAM. +description: GCP-specific Terraform snippets: variables, modules, backends, providers, IAM. tags: - terraform - gcp diff --git a/docs/terraform/gcp/providers.md b/docs/terraform/gcp/providers.md index 613affd..5b0705e 100644 --- a/docs/terraform/gcp/providers.md +++ b/docs/terraform/gcp/providers.md @@ -1,6 +1,6 @@ --- title: Provider configuration -description: Sensible defaults for the google and google-beta providers — pinning, project/region/zone, user_project_override, ADC vs Workload Identity Federation, aliases, and impersonation. +description: Sensible defaults for the google and google-beta providers: pinning, project/region/zone, user_project_override, ADC vs Workload Identity Federation, aliases, and impersonation. tags: - terraform - gcp @@ -79,7 +79,7 @@ provider "google" { ## Authentication -### Local development — Application Default Credentials +### Local development: Application Default Credentials ```bash gcloud auth application-default login @@ -89,7 +89,7 @@ gcloud config set project acme-platform-dev The provider picks up ADC automatically. No `credentials = ...` argument, no JSON key on disk. -### CI — Workload Identity Federation (no keys) +### CI: Workload Identity Federation (no keys) In CI, exchange the runner's OIDC token for a short-lived Google token. With GitHub Actions: @@ -207,7 +207,7 @@ provider "google" { ``` The caller (you, or the CI SA) only needs `roles/iam.serviceAccountTokenCreator` -on each deploy SA — nothing else. +on each deploy SA: nothing else. --- diff --git a/docs/terraform/gcp/variables.md b/docs/terraform/gcp/variables.md index 947f322..67c8594 100644 --- a/docs/terraform/gcp/variables.md +++ b/docs/terraform/gcp/variables.md @@ -1,6 +1,6 @@ --- title: Common variables -description: Reusable, well-validated Terraform / OpenTofu variable blocks for GCP — project, region, zone, labels, CIDRs, machine type, and more. +description: Reusable, well-validated Terraform / OpenTofu variable blocks for GCP: project, region, zone, labels, CIDRs, machine type, and more. tags: - terraform - gcp @@ -294,7 +294,7 @@ variable "subnets" { ```hcl variable "db_password" { - description = "Cloud SQL admin password. Provide via TF_VAR_db_password or Secret Manager — do not commit." + description = "Cloud SQL admin password. Provide via TF_VAR_db_password or Secret Manager: do not commit." type = string sensitive = true diff --git a/docs/terraform/terragrunt/index.md b/docs/terraform/terragrunt/index.md index b5408f0..c39bf7d 100644 --- a/docs/terraform/terragrunt/index.md +++ b/docs/terraform/terragrunt/index.md @@ -9,24 +9,24 @@ tags: # Terragrunt [Terragrunt](https://terragrunt.gruntwork.io/) is a thin orchestration layer that -sits on top of Terraform / OpenTofu. It does not replace HCL modules — you still -write modules the same way — it just removes the copy-paste involved in wiring +sits on top of Terraform / OpenTofu. It does not replace HCL modules (you still +write modules the same way); it just removes the copy-paste involved in wiring the same module into many environments. ## What it adds on top of Terraform / OpenTofu -- **DRY remote state** — one `remote_state` block at the root generates the +- **DRY remote state**: one `remote_state` block at the root generates the `backend "s3" {}` for every unit, with the state key derived from the unit's path on disk. -- **DRY provider blocks** — one `generate "provider"` block writes a +- **DRY provider blocks**: one `generate "provider"` block writes a `provider.tf` into every working directory, so units don't repeat `provider "aws" { region = ... }` boilerplate. -- **Dependency graph** — `dependency "vpc" { config_path = "../vpc" }` lets one +- **Dependency graph**: `dependency "vpc" { config_path = "../vpc" }` lets one unit consume another unit's outputs without manual `terraform_remote_state` data sources, and Terragrunt sequences applies in topological order. -- **`run --all`** (formerly `run-all`) — plan/apply/destroy across every unit +- **`run --all`** (formerly `run-all`): plan/apply/destroy across every unit under a directory, respecting the dependency graph. -- **OpenTofu support** — set `terraform_binary = "tofu"` in `terraform_binary` +- **OpenTofu support**: set `terraform_binary = "tofu"` in `terraform_binary` or via the `TG_TF_PATH` env var to drive `tofu` instead of `terraform`. ## When to use it @@ -39,7 +39,7 @@ Reach for Terragrunt when you have: - A platform team that wants `cd live/prod && terragrunt run --all plan` to be the daily driver. -Skip it for a single-environment, single-state-file project — a plain +Skip it for a single-environment, single-state-file project: a plain `terraform` root with a backend block is simpler. !!! tip "OpenTofu users" diff --git a/docs/terraform/terragrunt/root-config.md b/docs/terraform/terragrunt/root-config.md index 6f185a4..c8f481e 100644 --- a/docs/terraform/terragrunt/root-config.md +++ b/docs/terraform/terragrunt/root-config.md @@ -1,6 +1,6 @@ --- title: root.hcl + env.hcl + unit pattern -description: The modern 3-file Terragrunt layout — shared root, per-environment locals, thin per-unit configs. +description: The modern 3-file Terragrunt layout: shared root, per-environment locals, thin per-unit configs. tags: - terragrunt - terraform @@ -11,11 +11,11 @@ tags: The current Gruntwork-recommended layout splits configuration into three files that compose by location on disk: -1. **`root.hcl`** — one per repo, at the top of `live/`. Holds the remote-state +1. **`root.hcl`**: one per repo, at the top of `live/`. Holds the remote-state backend, generated provider, and locals shared by every unit. -2. **`env.hcl`** — one per environment directory. Holds variables that differ +2. **`env.hcl`**: one per environment directory. Holds variables that differ per env (region, account ID, environment name). -3. **Unit `terragrunt.hcl`** — one per deployable unit. Includes `root` (and +3. **Unit `terragrunt.hcl`**: one per deployable unit. Includes `root` (and optionally `env`), points at a module `source`, and supplies `inputs`. ## Directory layout @@ -194,12 +194,12 @@ inputs = { `dependency` blocks make one unit's outputs available to another. Terragrunt sequences `run --all apply` so that `vpc` applies before `eks`. `mock_outputs` lets `terragrunt plan` succeed for downstream units before upstream ones have -ever been applied — useful in CI on a fresh branch. +ever been applied: useful in CI on a fresh branch. !!! tip "Scope the mocks" Always pair `mock_outputs` with `mock_outputs_allowed_terraform_commands`. Without it, `terragrunt apply` will happily apply against the mock values if - the dependency hasn't been applied — which is almost never what you want. + the dependency hasn't been applied: which is almost never what you want. !!! warning "Don't put `inputs` in `root.hcl` that depend on the unit" Locals like `path_relative_to_include()` are evaluated in the **including** diff --git a/docs/terraform/terragrunt/stacks.md b/docs/terraform/terragrunt/stacks.md index f401cba..81971c0 100644 --- a/docs/terraform/terragrunt/stacks.md +++ b/docs/terraform/terragrunt/stacks.md @@ -9,7 +9,7 @@ tags: # Explicit stacks (terragrunt.stack.hcl) The **stacks** feature (Terragrunt 0.66+, stable in the 1.0 line) lets you -declare a bundle of related units — e.g. *vpc + eks + rds + sqs* — in a single +declare a bundle of related units: e.g. *vpc + eks + rds + sqs*: in a single `terragrunt.stack.hcl` file and instantiate it anywhere you want. It replaces the traditional pattern of copy-pasting 5–10 unit folders per new environment or tenant. @@ -50,7 +50,7 @@ unit "eks" { } } -# A nested stack — a stack that pulls in its own units/stacks. +# A nested stack: a stack that pulls in its own units/stacks. stack "service" { source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/service?ref=v0.7.0" path = "svc-checkout" @@ -95,7 +95,7 @@ inputs = { } ``` -`values` is a top-level identifier inside a unit reached via a stack — that's +`values` is a top-level identifier inside a unit reached via a stack: that's how `terragrunt.stack.hcl` plumbs configuration in without each unit needing its own per-environment HCL file. diff --git a/pyproject.toml b/pyproject.toml index 3dd8544..1180ce3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dev = [ package = false # --------------------------------------------------------------------------- -# Codespell — typo hunter (runs in pre-commit). +# Codespell: typo hunter (runs in pre-commit). # --------------------------------------------------------------------------- [tool.codespell] skip = "uv.lock,site,.venv,*.svg,terragrunt" From 19aa0db325947f0eed64685b7c484c05f46abcc7 Mon Sep 17 00:00:00 2001 From: Tristan Jahnke Date: Sun, 10 May 2026 20:31:57 -0500 Subject: [PATCH 2/2] feat(tf/ot): Add a Style Guide and useful commands pages --- docs/terraform/commands.md | 321 ++++++++++++++++++++++++++++++++++ docs/terraform/index.md | 12 ++ docs/terraform/style-guide.md | 265 ++++++++++++++++++++++++++++ zensical.toml | 4 +- 4 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 docs/terraform/commands.md create mode 100644 docs/terraform/style-guide.md diff --git a/docs/terraform/commands.md b/docs/terraform/commands.md new file mode 100644 index 0000000..5f917cb --- /dev/null +++ b/docs/terraform/commands.md @@ -0,0 +1,321 @@ +--- +title: Useful Commands +description: A collection of nice, helpful Terraform / OpenTofu commands for everyday work. +tags: + - terraform +--- + +# Useful Commands + +A grab-bag of Terraform / OpenTofu commands worth keeping handy. Most work identically with `tofu` swapped in for +`terraform`. Use the one your project standardises on. + +## Init + +Standard init. + +```bash +terraform init +``` + +Re-pull modules and providers without touching backend config. + +```bash +terraform init -upgrade +``` + +Re-configure the backend (e.g. switching workspaces / accounts). + +```bash +terraform init -reconfigure +``` + +Migrate state to a newly-changed backend without prompting. + +```bash +terraform init -migrate-state +``` + +## Plan + +Plan to a file you can review and apply atomically. + +```bash +terraform plan -out=tfplan +``` + +Target a single resource (use sparingly: it bypasses dependency tracking). + +```bash +terraform plan -target=aws_s3_bucket.assets +``` + +Plan with a specific tfvars file (per-environment workflow). + +```bash +terraform plan -var-file=envs/prod.tfvars +``` + +### Generate import commands from a plan + +Useful when you need to adopt a pile of pre-existing real-world resources into a fresh state file in bulk. The +command below scans plan output for `will be created` lines and emits a `terraform import` template for each. +You still have to fill in `RESOURCE_ID` (the cloud-provider ID) for every line before running them: + +```bash +terraform plan -no-color -var-file=.tfvars \ + | grep "will be created" \ + | sed "s/.*# \(.*\) will be created/terraform import -lock=false -var-file=.tfvars '\1' RESOURCE_ID/" +``` + +## Apply + +Apply a saved plan: no surprises between plan and apply. + +```bash +terraform apply tfplan +``` + +Auto-approve (CI only, never local). + +```bash +terraform apply -auto-approve +``` + +Replace a resource that has drifted or is corrupt. + +```bash +terraform apply -replace=aws_instance.web +``` + +## State inspection + +List every resource Terraform manages. + +```bash +terraform state list +``` + +Show the full attributes of one resource. + +```bash +terraform state show aws_s3_bucket.assets +``` + +Pull the raw state JSON (great for grep / jq). + +```bash +terraform state pull | jq '.resources[] | select(.type=="aws_iam_role")' +``` + +## State surgery + +!!! warning "State mutation is destructive" + Always back up state before running anything below. Never run these from CI. + +Back up state first. + +```bash +terraform state pull > backup.tfstate +``` + +Rename a resource address after a refactor (no destroy/create). + +```bash +terraform state mv aws_s3_bucket.old aws_s3_bucket.new +``` + +Move a resource into a module. + +```bash +terraform state mv aws_s3_bucket.assets module.assets.aws_s3_bucket.this +``` + +Forget a resource without destroying the real-world object. + +```bash +terraform state rm aws_s3_bucket.assets +``` + +Adopt an existing real-world resource into state. + +```bash +terraform import aws_s3_bucket.assets my-existing-bucket +``` + +## Refactoring with `moved` blocks + +Prefer `moved {}` blocks in code over `terraform state mv` whenever possible: they're versioned, reviewable, +and run automatically for every collaborator. + +```hcl +moved { + from = aws_s3_bucket.old + to = aws_s3_bucket.new +} +``` + +## Drift detection + +Detect drift without applying. Exit code 2 means changes are pending. + +```bash +terraform plan -detailed-exitcode +``` + +Refresh state against real-world resources without planning changes. + +```bash +terraform apply -refresh-only +``` + +## Outputs + +Print all outputs. + +```bash +terraform output +``` + +Get a single output as raw text (script-friendly). + +```bash +terraform output -raw bucket_name +``` + +Get all outputs as JSON. + +```bash +terraform output -json | jq +``` + +## Formatting & validation + +Format every `.tf` file under cwd. + +```bash +terraform fmt -recursive +``` + +Check formatting without changing files (use in CI). + +```bash +terraform fmt -check -recursive +``` + +Validate syntax and types. + +```bash +terraform validate +``` + +## Workspaces + +List workspaces. + +```bash +terraform workspace list +``` + +Create a new workspace. + +```bash +terraform workspace new dev +``` + +Switch to a workspace. + +```bash +terraform workspace select prod +``` + +Show the current workspace. + +```bash +terraform workspace show +``` + +## Graph & dependency inspection + +Render the dependency graph as Graphviz DOT. + +```bash +terraform graph | dot -Tsvg > graph.svg +``` + +Show the providers a config / state requires. + +```bash +terraform providers +``` + +Dump every provider's schema as JSON. + +```bash +terraform providers schema -json | jq +``` + +## Modules + +Refresh installed modules without re-initialising the backend. + +```bash +terraform get -update +``` + +## Console + +Interactive REPL for evaluating expressions against state and config. + +```bash +terraform console +``` + +Inside the console: + +```text +> jsonencode(var.tags) +> [for s in aws_subnet.private : s.cidr_block] +``` + +## Testing + +Run native module tests under `tests/`. + +```bash +terraform test +``` + +Filter to a single test file. + +```bash +terraform test -filter=tests/basic.tftest.hcl +``` + +## OpenTofu-only + +State encryption (built-in, no third-party tooling required). + +```bash +tofu init -encryption=... +``` + +Early-evaluation: variables in backend / module source blocks. + +```bash +tofu plan +``` + +## Cleanup + +Remove the local `.terraform` directory and lockfile. + +```bash +rm -rf .terraform .terraform.lock.hcl +``` + +Destroy every managed resource (read the plan twice). + +```bash +terraform destroy +``` diff --git a/docs/terraform/index.md b/docs/terraform/index.md index 96e6944..40f60cb 100644 --- a/docs/terraform/index.md +++ b/docs/terraform/index.md @@ -14,6 +14,18 @@ Snippets here work with both [Terraform](https://developer.hashicorp.com/terrafo
+- :material-book-open-variant:{ .lg .middle } **[Style Guide](style-guide.md)** + + --- + + Conventions for variables, validation messages, modules, file layout, tagging, and more. + +- :material-console:{ .lg .middle } **[Useful Commands](commands.md)** + + --- + + Day-to-day Terraform / OpenTofu CLI commands worth keeping handy. + - :material-aws:{ .lg .middle } **[AWS](aws/index.md)** --- diff --git a/docs/terraform/style-guide.md b/docs/terraform/style-guide.md new file mode 100644 index 0000000..0579ce1 --- /dev/null +++ b/docs/terraform/style-guide.md @@ -0,0 +1,265 @@ +--- +title: Style Guide +description: Conventions for Terraform / OpenTofu modules ~ variables, validation messages, modules, file layout, tagging, and more. +tags: + - terraform +--- + +# Style Guide + +Conventions we follow across every Terraform / OpenTofu module in this repo. Apply these to all new code and bring +existing code in line as you touch it. + +## Variables + +```hcl +variable "project_id" { # (1)! + description = "Project ID (e.g. acme-platform-prod). 6–30 chars, lowercase letter start." # (2)! + type = string # (3)! + + validation { # (4)! + condition = can(regex("^[a-z][a-z0-9-]{4,28}[a-z0-9]$", var.project_id)) # (5)! + error_message = <<-EOT + Variable `project_id` must start with a lowercase letter, be 6–30 chars, and contain only lowercase + letters, digits, or hyphens (no trailing hyphen). + EOT # (6)! + } +} +``` + +1. Variable names should be short but as descriptive of their purpose as possible. They should also always be +lowercase with only letters and underscores. +2. All variables should have a description with detail of what it is and its purpose. If required use `<<-EOT` to allow +for multiline detailed descriptions. +3. Be explicit when it comes to variable types. If it is a map or object be diligent with marking that in the type along +with its internal types. +4. All variables should have at least one level of validation. Even if it is a simple variable that is always set in a +vars file. Typos and mistakes happen. Validation catches that ahead of time. +5. Create a condition that is as strict to the type and desired state of the variable without it becoming its own +project to manage. Strict enough to catch real mistakes, loose enough not to require updating every time the +upstream naming rules shift. +6. See [Validation messages](#validation-messages) below. + +## Validation messages + +Treat validation `error_message` strings like user-facing copy and like variable `description` fields: + +- **Start with a capital letter.** Begin with a real word (e.g. `Variable`, `Value`, `Argument`) rather than a + lowercase identifier or backticked token. +- **End with a period.** Write complete sentences, not fragments. +- **Name the offending variable.** Reference it explicitly (`` Variable `project_id` `` …) so the failure is obvious + in plan/apply output. +- **State the rule, not the regex.** Describe the constraint in plain English (length, allowed characters, allowed + values) instead of pasting the pattern. +- **Suggest a fix when possible.** If the valid set is small, list it (e.g. `Must be one of: dev, stg, prd.`). + +This matches the [HashiCorp style guide](https://developer.hashicorp.com/terraform/language/style#error-messages) +and is enforced by [`tflint`](https://github.com/terraform-linters/tflint)'s `terraform_documented_variables` and +related rules. + +### Multi-line messages + +Keep every line at or under **120 characters**. When a message would otherwise overflow, switch to an indented +heredoc (`<<-EOT`); the same form you use for long `description` fields: + +```hcl +variable "project_id" { + description = <<-EOT + Project ID (e.g. acme-platform-prod). 6–30 chars, lowercase letter start. + EOT + type = string + + validation { + condition = can(regex("^[a-z][a-z0-9-]{4,28}[a-z0-9]$", var.project_id)) + error_message = <<-EOT + Variable `project_id` is invalid. It must: + - start with a lowercase letter, + - be 6–30 characters long, + - contain only lowercase letters, digits, or hyphens, and + - not end with a hyphen. + EOT + } +} +``` + +Two gotchas to be aware of: + +- Terraform still enforces the "full sentence" rule on the **rendered** string and so it must start with an uppercase + letter and end with `.` or `?` (see [hashicorp/terraform#24214](https://github.com/hashicorp/terraform/issues/24214)). +- `terraform fmt` has a long-standing bug ([hashicorp/terraform#34877](https://github.com/hashicorp/terraform/issues/34877)) + where indented heredocs that contain multi-line `${ ... }` interpolations get reformatted incorrectly. Keep any + interpolation on a single line inside the heredoc to avoid it. + +## Outputs + +Every output should have a `description`, mark `sensitive = true` when the value contains credentials or other +secrets, and expose attributes rather than entire resource objects so consumers don't depend on provider-internal +fields. + + + +## Locals + +Use `locals` for values that are derived, repeated, or computed from variables and data sources. Reach for a +variable when callers need to override the value; reach for a local when the module owns it. Keep names short and +descriptive (`common_tags`, `name_prefix`). + +## Resources + +- Use `this` as the resource name when a module manages a single instance of that resource type. Otherwise pick a + short descriptive name (`primary`, `replica`, `web`). +- Prefer `for_each` over `count` so resource addresses stay stable when the input set changes. +- Add `lifecycle` blocks (`prevent_destroy`, `ignore_changes`, `create_before_destroy`) deliberately, with a comment + explaining why. + +## Modules + +- **Pin every `source`.** Use a registry version constraint (`version = "~> 5.2"`) or a git ref pinned to a tag or + commit SHA. Never reference `main` / `master` / `latest`. +- **DRY the pin with a variable or local.** Newer Terraform / OpenTofu allow static references in `module.source` + and `module.version`, so when the same module is called many times it's good practice to centralize the ref: + + ```hcl + locals { + # Bump this once to roll every caller forward. + vpc_module_ref = "git::https://github.com/acme/terraform-aws-vpc.git?ref=v1.4.2" + modules = { + "s3" = "~> 3.2" + } + } + + module "vpc_primary" { + source = local.vpc_module_ref + # ... + } + + module "s3_assets" { + source = "example.registry.com/aws/s3" + version = local.modules.s3 + # ... + } + ``` + +- One logical concern per module. If a module's variables describe two unrelated systems, split it. +- Every module ships with a `README.md` (generated by [`terraform-docs`](https://terraform-docs.io/)), an `examples/` + directory, and a `versions.tf`. + +## File layout + +A module's root directory should follow a predictable layout so contributors know where to look: + +- `main.tf`: primary resources +- `variables.tf`: inputs +- `outputs.tf`: outputs +- `versions.tf`: `terraform { required_version, required_providers }` +- `locals.tf`: derived values (when non-trivial) +- `data.tf`: data sources (when non-trivial) +- `providers.tf`: provider configuration (root modules only) + +When `main.tf` grows beyond a few hundred lines, split by resource group (e.g. `network.tf`, `iam.tf`) rather than +by resource type. + +## Provider & version pinning + +Every module declares a `versions.tf` that pins the Terraform / OpenTofu version and every provider with `~>` so +patch and minor updates flow in but breaking changes don't: + +```hcl +terraform { + required_version = "~> 1.9" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.60" + } + } +} +``` + +## Tags and labels + +A common tag set is required on every taggable resource. Define it once in a local and merge it in: + +```hcl +locals { + common_tags = { + environment = var.environment + owner = var.owner + cost_center = var.cost_center + managed_by = "terraform" + } +} +``` + +Per-resource tags merge on top: `tags = merge(local.common_tags, { Name = "..." })`. + +If the provider allows for it utilize their default tags in the provider block to apply good defaults across the board +automatically. + +## Naming convention + +Resource names rendered into the cloud provider follow `${project}-${environment}-${component}` (e.g. +`acme-prd-vpc`). Respect provider-specific length and character limits; encode them as `validation` blocks on the +input variables that feed into the name. + +## Comments + +- Use `#` for all comments (HCL convention). + - Terraform does allow a couple older comment block strategies for backwards compatibility but steer away from + those as the preferred standard is `#` per-line. +- Document the *why*, not the *what*. The code already says what it does. +- Doc comments belong above the block they describe. +- Never commit commented-out code; delete it and rely on git history. + - Exceptions can be made for known *temporary* blocks or examples but should be used sparingly. + +## Sensitive data + +- Never commit secrets, even encrypted, to the repo. +- Mark sensitive variables and outputs with `sensitive = true`. +- Pull secrets from the cloud's secret manager via a data source at apply time; don't pass them in as plain `tfvars`. + +## `count` vs `for_each` + +Default to `for_each` over a map or set. Resource addresses stay stable when the input changes. Reserve `count` +for the on/off toggle pattern (`count = var.enabled ? 1 : 0`). + +## Dynamic blocks + +Use `dynamic` blocks sparingly. They obscure the resource shape; prefer explicit, repeated blocks unless the +contents are truly variable in number. When you do use one, keep the iterator name short and the body small. One should +first see if a block using `for_each` can do the task first. + +## State and backends + +- Remote backend is required for every root module: no local state checked in. +- State locking must be enabled (S3 native locking, blob lease for AzureRM, GCS native locking). +- Never run `terraform state` mutation commands from CI; do them locally with explicit review. + +## Testing + +- Use `terraform test` for module contract tests (input → expected plan / output). +- Run `terraform plan` on every PR and post the output as a check. +- For modules with side effects worth verifying end-to-end, add a [Terratest](https://terratest.gruntwork.io/) suite + under `test/`. + +## Formatting and linting + +Run these against every change, both locally and in CI: + +- `terraform fmt -recursive`: formatting (non-negotiable). +- `terraform validate`: syntax and type checks. +- [`tflint`](https://github.com/terraform-linters/tflint) with the relevant cloud ruleset. +- [`trivy config`](https://aquasecurity.github.io/trivy/) or [`checkov`](https://www.checkov.io/): security scanning. +- [`terraform-docs`](https://terraform-docs.io/): keep module READMEs in sync. + - There is a known bug with `terraform-docs` if you are using local or variable for module source and or versions. + +## Pre-commit hooks + +Wire the above into [`pre-commit`](https://pre-commit.com/) so they run on every commit: + +- `terraform_fmt` +- `terraform_validate` +- `terraform_tflint` +- `terraform_trivy` (or `terraform_checkov`) +- `terraform_docs` diff --git a/zensical.toml b/zensical.toml index 8d717fd..6af5c19 100644 --- a/zensical.toml +++ b/zensical.toml @@ -19,6 +19,8 @@ nav = [ { Home = "index.md" }, { "Terraform / OpenTofu" = [ "terraform/index.md", + { "Style Guide" = "terraform/style-guide.md" }, + { "Useful Commands" = "terraform/commands.md" }, { "AWS" = [ "terraform/aws/index.md", {"Common Variables" = "terraform/aws/variables.md"}, @@ -301,7 +303,7 @@ generator = false # Plugins # --------------------------------------------------------------------------- -# Offline usage — makes site search work when serving via file:// (downloads, +# Offline usage: makes site search work when serving via file:// (downloads, # air-gapped envs, distributing alongside a product). # https://zensical.org/docs/setup/offline/ #