GitOps
How a change actually ships. Desired state lives in git; an operator applies it after a PR check or a merge to
main. Nobody applies by hand.Apply paths at a glance
Layer Trigger Applies via Never Terraform ( terraform/,clusters/*/bootstrap/)PR opened/updated β autoplan; atlantis applycommentAtlantis β successful apply automerges the PR tofu/terraform applyagainst remote state, locallyKubernetes ( clusters/**)Merge to mainFlux reconciliation kubectl applyto author stateNixOS ( nix/**)Merge to main(daily auto-upgrade) or manual dispatchnixos-rebuild switch/bootout-of-band edits outside git
Terraform β Atlantis
Every root under
terraform/and eachclusters/<site>/bootstrap/is a standalone Terraform root, applied through Atlantis (clusters/offsite/apps/atlantis, itself a FluxHelmRelease) on the PR β never locally against remote state.The binary is OpenTofu (
tofu), not Terraform: Atlantis's chart valuedefaultTFDistribution: opentofusetsATLANTIS_DEFAULT_TF_DISTRIBUTION=opentofu, and CI (terraform.yml) installs viaopentofu/setup-opentofuand runstofu init -backend=false,tofu validate, andtofu testper affected root, plus a separatetofu fmt -check -recursivejob.mise.tomlinstalls bothtofuandterraform, butmise run tf:*(tf:init,tf:validate,tf:fmt,tf:docs,tf:plan) is the local command source of truth.Flow: open a PR touching a root β Atlantis autoplans it (
ATLANTIS_AUTOPLAN_MODULES: "true", file list**/*.tf*,terraform/**/*.conf) and runs apolicy_check(conftest-style policy set, ownersjonpulsifer/rowbutt) β review the plan comment β commentatlantis applyβATLANTIS_AUTOMERGE: "true"merges the PR once the apply succeeds.trivy.ymladditionally scans every changed.tfdirectory (andclusters/**) for CRITICAL/HIGH IaC findings on the PR.Step-by-step: Runbooks/Terraform Change. Root/module layout: Architecture/Terraform.
Kubernetes β Flux, and where ArgoCD fits
Merge to
mainβ Flux reconcilesclusters/**. Each cluster's root sync comes from itsFluxInstanceβclusters/<site>/bootstrap/bootstrap.tfinstallsflux-operator/flux-instance, andflux-values.yaml'sinstance.syncpoints it at theinfraGitRepository(pullSecret: flux-github-app-credentials,ref: refs/heads/main,path: clusters/<site>/flux-system). There's no hand-applied rootKustomization.Flux owns platform namespaces and installs Spindrift's control plane, target RBAC, policy engine, shared authentication, and edge workload. Spindrift then reconciles only its delegated App namespace and resources inside pre-provisioned vessels. This controller boundary is desired state too; Architecture/Spindrift names both sides.
CoreDNS is part of the Terraform bootstrap boundary alongside Flux itself. The shared bootstrap module creates cluster DNS before installing Flux; Flux owns the resources that reconcile after bootstrap.
kustomize.ymlruns the configured render seam over both clusters' app overlays, theirarcoverlays, and folly monitoring on touching pushes/PRs β a render check, not an apply.topology-contract.ymlrunsconftestagainst.github/policy/cluster-topology.regoon both clusters'cluster-topology.jsonbefore Flux ever substitutes those values into a manifest.ArgoCD ships the same way as everything else: it's a Flux
HelmRelease(clusters/folly/apps/argo). Its RBAC grants anatlantisAPI-key account full access toapplications/applicationsets, and the AtlantisHelmReleasecarriesARGOCD_SERVER/ARGOCD_AUTH_TOKENβ wiring for Atlantis to manage ArgoCDApplicationresources throughterraform/argo, anargocd-provider Terraform root (state prefixterraform/argo). As of today that root declares only the provider β its generatedREADME.mdreads "No resources" β so ArgoCD is installed but owns no applications.argocd-diff-preview.yamlalready posts a diff-preview PR comment for changes underclusters/**/argo/**orterraform/argo/**, ready for when that path is used.Never
kubectl applyto author state;kubectl,flux get,flux reconcileare for inspection or forcing a sync.Step-by-step: Runbooks/Kubernetes GitOps Change. Cluster internals: Architecture/Kubernetes.
NixOS β nixos-rebuild + daily auto-upgrade
Apply command:
nixos-rebuild switch|boot --flake .#<host> --target-host <host> --sudo.bootinstalls the generation and activates on the next reboot;switchactivates immediately.Every NixOS host β including the k8s nodes (
optiplex,riptide,shale,oldschool,retrofit, declared innix/hosts/, like every other host) β carriessystem.autoUpgradeenabled by default (nix/system/nixos.nix: flakegithub:jonpulsifer/infra, daily at03:37plus up to a 1h random delay). Hosts self-pull and rebuild frommaindaily with no workflow run involved.The microSD-rooted Pi 4 and Pi Zero hosts disable
system.autoUpgrade; their generations are built elsewhere and pushed withnixos-rebuild --target-host. They update throughnixos-deploy.yaml, a manualworkflow_dispatchthat builds on an aarch64 GitHub runner, joins the tailnet with a short-lived Tailscale OIDC key (tag:ci), and runsnixos-rebuildover SSH to<host>.pirate-musical.ts.net.Implication: a host deployed straight from a branch stays live only until the next auto-upgrade cycle pulls
mainand reverts it. Merge promptly, or treat a branch deploy as a test.nix-ci.yamlgates all of this:nix flake check, a build ofoptiplex's closure, a native-ARM build ofspore, and thecontainer/wsl/forgeimage outputs β scoped to changed paths through the same routing mechanism as Terraform (below).Step-by-step: Runbooks/Deploy a NixOS Host. Host inventory: Fleet. Layer background: Architecture/NixOS.
How CI decides what to validate
.github/scripts/validation-impact.shis the shared routing seam behind bothterraform.ymlandnix-ci.yaml: given changed paths on stdin, itstargetscommand prints one stable target per line βterraform:<root>(walking up from changed Terraform files, with explicit routes forclients.yamlandlab-topology.json) ornix:flake-check(Nix/flake inputs,apps/ddnsd, dotfiles, both cluster topologies, lab topology, and the Nix workflow).A Terraform root is any directory (under
terraform/orclusters/*/bootstrap) whose.tffile has abackend "block β that's what separates an independently-applied root from a reusable module underterraform/modules/. A change tovalidation-impact.shorterraform.ymlitself fans out to every root.terraform.yml'schanged-directoriesjob feeds the routed roots into a matrix sovalidate/fmtonly run against what changed;nix-ci.yaml'schanged-pathsjob checks for the literalnix:flake-checkline to gate itscheck/nixos/spore/imagesjobs. Arouting-testsjob runsvalidation-impact_test.shto unit-test the routing script itself.containers.ymlandkustomize.ymlroute independently, by path prefix rather than throughvalidation-impact.sh:containers.yml'sdetect-containers.shmatchesapps/**/packages/**/images/**against.github/containers.json'sbuild/ignoreclassification β an image with aDockerfilethat isn't classified fails CI.kustomize.yml,trivy.yml, andtopology-contract.ymlkey off their own workflow-levelpaths:filters.
Renovate
.github/renovate.json5opens PRs for Terraform providers, Helm charts, container images, GitHub Actions, Nix packages (including the in-repodotfilesinput), and npm packages, grouped and labeled by kind.Automerge (GitHub-native,
platformAutomerge: true) is broad but tiered: Terraform provider bumps useupdate-lockfileand automerge once CI passes β lockfile-only, so noatlantis applyis needed; container/Helm patch+digest and Flux minor/patch bumps automerge; GitHub Actions automerge (majors sit out a 3-day age gate first); non-major npm updates automerge after a 7-day cooldown.miseis not among them: it comes from nixpkgs like any other package, so it moves when thenixpkgsinput does and Renovate has nothing to bump.Anything touching
clusters/**still only takes effect once its PR merges and Flux reconciles; anything touching a Terraform root with real resource changes still needs a reviewedatlantis apply.An image with
deploytargets in.github/containers.jsondoesn't wait for a Renovate pass:containers.yml's digest step resolves the digest that run just pushed, rewrites the pinned digest in each mapped manifest, and opens an automerging PR β so a merge tomainships the build, and Flux rolls it out on theinfraGitRepository's next sync. Renovate still owns the same lines as the backstop.The digest PR needs an App identity to merge. A pull request opened with
GITHUB_TOKENdoes not get its checks run β GitHub reports every workflow on the branch asaction_requiredand waits for someone to press "Approve and run" β so a required status context is never created andgh pr merge --autohas nothing to wait on. The digest then sits unmerged while every step of the job reports green, and because a CD PR is only opened when none is already open, the next build force-pushes onto the same stuck branch and reuses it, so an image is built, signed, scanned, pushed, and never deployed.containers.ymlmints an App installation token whenvars.CD_APP_IDis set, which is a different actor and runs like anyone else's PR; with the variable unset it falls back toGITHUB_TOKENand the old behaviour. The App needscontents: writeandpull requests: writeon this repo, its id in theCD_APP_IDvariable, and its private key in theCD_APP_PRIVATE_KEYsecret.
Docs publish
wiki.ymlbuildsdocs/β this Logseq graph β with the Bun SSG inapps/wiki(bun run buildβdist/) on every push and PR touchingdocs/**,apps/wiki/**, or the workflow itself. Onmainit additionally runsbun x wrangler pages deploy dist --project-name=infra-wikito Cloudflare Pages (project and DNS Terraform-managed interraform/network/cloudflare/). A docs page goes live at wiki.lolwtf.ca the moment its PR merges tomainβ there's no separate publish step.Validate before opening a docs PR: Runbooks/Validate Infra Changes.
Linked references 12
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.
clusters/base/operator-rbac.yaml is the identity, applied to both clusters through Architecture/GitOps. See Architecture/Secrets and PKI for the certificate chain the break-glass user rides on.
Layer 2 of Architecture: two Kubernetes clusters under clusters/, reconciled by FluxCD on every merge to main. The Terraform bootstrap roots own CoreDNS and Flux itself; Flux owns post-bootstrap cluster state. ArgoCD is installed as a Flux HelmRelease and owns no applications; terraform/argo/ wires the provider and declares no resources. Reconciliation mechanics and the apply model live on Architecture/GitOps.
This root applies through Atlantis like any other Terraform module β see Architecture/GitOps.
Everything here ships through git. Terraform applies through Atlantis on the PR (Runbooks/Terraform Change); Kubernetes applies on merge to main through Flux (Architecture/GitOps). Nothing below is a kubectl apply or a local tofu apply.
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.
Architecture/GitOps β how a change actually ships, layer by layer
Use this when changing manifests under clusters/ or inspecting Flux deployment state. Architecture lives in Architecture/Kubernetes; the apply model is on Architecture/GitOps.
| Directory | What it is |
|---|---|
atlantis | The Terraform apply path for every PR β see Architecture/GitOps. |
dave.yaml | An ai-agent chart release in the agents-sandbox namespace. |
descheduler | Cluster-local descheduler release. |
hub | apps/hub, deployed through the first-party packages/charts/app chart. |
prowler | Cloud security posture scanning, via packages/charts/prowler. |
spindrift | The Spindrift control plane itself β Architecture/Spindrift. |
spindrift-target | Pulled straight from clusters/base/platform/spindrift-target: namespace, RBAC, and network policy for Spindrift-owned workloads. |
Everything ships the same way: open a PR and let the operators apply it. See Architecture/GitOps.
terraform/argo/ is the root module for the argocd Terraform provider (use_local_config = true: it authenticates through a local ArgoCD session rather than an explicit token in the module). Atlantis applies it like any other root. The token/session wiring is scoped and rotated, not a shared static credential β see Architecture/GitOps for the apply flow and the runbook it points to for rotation.
A runbook that changes desired state still ships through git β see Architecture/GitOps.