Terraform

  • Layer 3: cloud and identity resources managed as code under terraform/, applied by Atlantis. Every root module is standalone β€” its own state, its own backend.

  • The binary is OpenTofu

    • The apply-path binary is tofu (OpenTofu), not terraform. Atlantis runs with ATLANTIS_DEFAULT_TF_DISTRIBUTION=opentofu; mise.toml installs both opentofu and terraform, with terraform kept for anything not on the OpenTofu path. CI's validate job uses opentofu/setup-opentofu and runs tofu init / tofu validate / tofu test.

    • terraform/pki requires OpenTofu specifically: it uses the opentofu/tls provider fork for max_path_length, published only on the OpenTofu registry.

    • The directory is still named terraform/ β€” only the binary changed.

  • Layout

    • Network fabric β€” terraform/network/: UniFi at the two sites (unifi/folly/, unifi/offsite/), cloudflare/, tailscale/. Covered in Architecture/Networking β€” this page doesn't duplicate VLANs, BGP, or tunnel detail.

    • Cloud & identity β€” terraform/gcp/organization/ (org-level IAM, folders, projects, billing), terraform/gcp/projects/<name>/ (one directory per GCP project), terraform/argo/ (the argocd provider wiring; it declares no resources today), terraform/google-workspace/ (users, groups, domains), and terraform/pki/ (the FML per-cluster K8s CAs and ServiceAccount token-signer certs, issued off a 1Password-held intermediate; each cluster's OIDC discovery document lives under terraform/pki/oidc/<cluster>/ and is served at oidc.lolwtf.ca β€” see Architecture/Secrets and PKI).

    • Cluster bootstrap β€” clusters/<site>/bootstrap/ (e.g. clusters/folly/bootstrap/bootstrap.tf) is also a standalone root: it installs flux-operator/flux-instance via the flux-bootstrap module.

    • Reusable modules β€” terraform/modules/: building blocks roots consume by relative source path, e.g. source = "../../../modules/cluster-topology" or source = "../../../modules/gce-vpc". A module directory has no backend of its own β€” see the state-backend discovery rule below.

  • State backends

    • Every root backs onto the same GCS bucket, homelab-ng, with a per-root prefix. Most prefixes mirror the directory path; the terraform/network/ roots use shorter ones (gs://homelab-ng/terraform/unifi, gs://homelab-ng/terraform/cloudflare, gs://homelab-ng/terraform/tailscale) that do not match their directory name. A state prefix is a stable address, not a mirror of the tree β€” read the backend block rather than inferring the prefix from the path.

  • How CI finds the roots

    • .github/scripts/validation-impact.sh is the single mechanism CI uses to turn a set of changed paths into a set of Terraform roots to validate β€” no hardcoded root list.

    • terraform_roots() greps every *.tf file under terraform/ and clusters/*/bootstrap for a backend " block and takes the unique parent directories. A backend declaration is what makes something a root instead of a module β€” terraform/modules/* has none, so it's never selected.

    • targets() maps each changed path to root(s): a changed *.tf or .terraform.lock.hcl walks up from its directory to the nearest ancestor containing *.tf files (terraform_root_for_path); clusters/folly/config/lab-topology.json and terraform/network/unifi/folly/clients.yaml map straight to the folly UniFi root; a changed validation-impact.sh or terraform.yml invalidates every root.

    • .github/workflows/terraform.yml calls this script to build the changed-directories job's matrix, then runs validate (tofu init -backend=false, tofu validate, tofu test) and a separate fmt job (tofu fmt -check -recursive) per matched root. mise run tf:init / tf:validate run the same discovery locally.

  • tofu test

    • CI's validate job runs tofu test in every matched root, not just tofu validate. Most roots have no *.tftest.hcl files, so this is a no-op assertion of "still green"; clusters/folly/bootstrap and clusters/offsite/bootstrap carry real test suites (bootstrap.tftest.hcl) that exercise the Flux bootstrap module's outputs.

    • fmt is a PR check (tofu fmt -check -recursive), not an auto-commit to main β€” branch protection rejects bot pushes. Fix drift locally with mise run tf:fmt.

  • Network facts: the topology.tf pattern

    • Roots that need network facts don't jsondecode a topology resource directly. They instantiate terraform/modules/cluster-topology in a topology.tf, selecting site and, when needed, config_map (default cluster-topology; folly also selects lab-topology). The module reads the chosen JSON ConfigMap's flat data and exposes it unchanged.

    • Callers project that output into their local interface, such as local.topology.<KEY> and the folly root's local.lab. Edit the JSON ConfigMap, never the values that reference it.

  • terraform-docs

    • Root READMEs with a BEGIN_TF_DOCS / END_TF_DOCS marker pair get their Requirements/Providers/Modules/Resources/Inputs/Outputs tables generated from the .tf source. mise run tf:docs finds every README with the markers and re-injects them with terraform-docs markdown table --output-mode inject.

    • This is a local task only β€” no CI workflow regenerates or checks these tables, so a stale README next to current .tf files is a real possibility until someone runs the task.

  • Applying

    • Open a PR touching a root β†’ Atlantis autoplans it β†’ comment atlantis apply after reviewing the plan β†’ a successful apply automerges. Full flow, including the Atlantis ↔ ArgoCD auth wiring, is on Architecture/GitOps.

    • Never run tofu apply (or terraform apply) against remote state locally. It races Atlantis for the state lock and produces drift. Local tofu init / tofu plan (or mise run tf:plan with TF_DIR set) are for inspection only.

Linked references 5

Runbooks/Terraform Change

Use this when changing Terraform under terraform/ or cluster bootstrap Terraform under clusters/<site>/bootstrap/. Background lives in Architecture/Terraform; the apply model (Atlantis autoplan β†’ review β†’ atlantis apply β†’ automerge) is on Architecture/GitOps.

Runbooks/Terraform Change

Network facts come from the cluster topology single source of truth (clusters/<site>/config/cluster-topology.json) where a root already has a topology.tf; see Architecture/Terraform.

Architecture

Layer 3 β€” Cloud and network (Architecture/Terraform)

Home

Cloud and network β€” UniFi, Cloudflare, Tailscale, GCP and Google Workspace under Architecture/Terraform, with applies gated through Atlantis. The network fabric itself is Architecture/Networking.