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), notterraform. Atlantis runs withATLANTIS_DEFAULT_TF_DISTRIBUTION=opentofu;mise.tomlinstalls bothopentofuandterraform, withterraformkept for anything not on the OpenTofu path. CI'svalidatejob usesopentofu/setup-opentofuand runstofu init/tofu validate/tofu test.terraform/pkirequires OpenTofu specifically: it uses theopentofu/tlsprovider fork formax_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/(theargocdprovider wiring; it declares no resources today),terraform/google-workspace/(users, groups, domains), andterraform/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 underterraform/pki/oidc/<cluster>/and is served atoidc.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 installsflux-operator/flux-instancevia theflux-bootstrapmodule.Reusable modules β
terraform/modules/: building blocks roots consume by relativesourcepath, e.g.source = "../../../modules/cluster-topology"orsource = "../../../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-rootprefix. Most prefixes mirror the directory path; theterraform/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 thebackendblock rather than inferring the prefix from the path.
How CI finds the roots
.github/scripts/validation-impact.shis 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*.tffile underterraform/andclusters/*/bootstrapfor abackend "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*.tfor.terraform.lock.hclwalks up from its directory to the nearest ancestor containing*.tffiles (terraform_root_for_path);clusters/folly/config/lab-topology.jsonandterraform/network/unifi/folly/clients.yamlmap straight to the folly UniFi root; a changedvalidation-impact.shorterraform.ymlinvalidates every root..github/workflows/terraform.ymlcalls this script to build thechanged-directoriesjob's matrix, then runsvalidate(tofu init -backend=false,tofu validate,tofu test) and a separatefmtjob (tofu fmt -check -recursive) per matched root.mise run tf:init/tf:validaterun the same discovery locally.
tofu testCI's
validatejob runstofu testin every matched root, not justtofu validate. Most roots have no*.tftest.hclfiles, so this is a no-op assertion of "still green";clusters/folly/bootstrapandclusters/offsite/bootstrapcarry real test suites (bootstrap.tftest.hcl) that exercise the Flux bootstrap module's outputs.fmtis a PR check (tofu fmt -check -recursive), not an auto-commit tomainβ branch protection rejects bot pushes. Fix drift locally withmise run tf:fmt.
Network facts: the
topology.tfpatternRoots that need network facts don't
jsondecodea topology resource directly. They instantiateterraform/modules/cluster-topologyin atopology.tf, selectingsiteand, when needed,config_map(defaultcluster-topology; folly also selectslab-topology). The module reads the chosen JSON ConfigMap's flatdataand exposes it unchanged.Callers project that output into their local interface, such as
local.topology.<KEY>and the folly root'slocal.lab. Edit the JSON ConfigMap, never the values that reference it.
terraform-docs
Root READMEs with a
BEGIN_TF_DOCS/END_TF_DOCSmarker pair get their Requirements/Providers/Modules/Resources/Inputs/Outputs tables generated from the.tfsource.mise run tf:docsfinds every README with the markers and re-injects them withterraform-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
.tffiles is a real possibility until someone runs the task.
Applying
Open a PR touching a root β Atlantis autoplans it β comment
atlantis applyafter reviewing the plan β a successful apply automerges. Full flow, including the Atlantis β ArgoCD auth wiring, is on Architecture/GitOps.Never run
tofu apply(orterraform apply) against remote state locally. It races Atlantis for the state lock and produces drift. Localtofu init/tofu plan(ormise run tf:planwithTF_DIRset) are for inspection only.
Linked references 5
Step-by-step: Runbooks/Terraform Change. Root/module layout: Architecture/Terraform.
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.
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.
Layer 3 β Cloud and network (Architecture/Terraform)
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.