Skip to content

Principles

These are the rules the whole estate follows. They exist because the alternative — per-repo drift, duplicated subnet maps, hand-edited DNS — was tried (gen-1) and audited into retirement.

Intent, not reality

The registry (jacaranda-inventory) holds what should be true: VLANs, IP allocations, hostnames, DNS records. Real systems (Proxmox, Pi-hole, step-ca, Cloudflare) are brought into line by appliers.

  • The registry never reads from real systems.
  • "Update the registry to match what Pi-hole serves" is a category error — that's drift detection. Decide which side is wrong, then either fix the registry or re-run the applier.
  • Nothing is ever deleted from the registry: allocations are retired with a date and held through a cooldown before their IP can be reused.

One-way dependency direction

jacaranda-inventoryintentjacaranda-modulesshared modulesjacaranda-foundationjacaranda-homelab

Foundation (DNS, secrets, SSH CA) must be able to rebuild the world from nothing, so it depends only on the registry and shared modules — never on anything above it. The monorepo depends on foundation services, the registry, and modules. App source repos publish container images and contain no IPs at all.

The boundary rule

Where does a thing live?

If it is…It goes in
Must exist before anything else (DNS, CA, secrets)jacaranda-foundation
Stateful and environment-specificjacaranda-homelab (tiered)
Reusable across services (modules, just helpers)jacaranda-modules, version-tagged
Intent — an IP, a name, a VLANjacaranda-inventory
Application source codeIts own repo; publishes images

Ground rules (enforced by convention and CI)

  • Never hardcode an IP, VLAN, or VMID in Terraform — allocations come from the registry. VMID = 1000 + host octet, always.
  • Reserve before you build: registry reserve first, then Terraform.
  • Secrets are read with op read (1Password) in justfiles only — never in Ansible vars, never committed.
  • State is encrypted (PBKDF2/AES-GCM) and stored via terraform-backend-git on a tfstate branch, one state path per stack env, never shared.
  • Modules are consumed as git:: sources pinned to exact version tags — no submodules, no relative ../../lib paths.
  • test/prod parity: same code, different tfvars. No bootstrap specials outside recovery recipes.
  • Naming follows a DNS-correct grammar, general→specific: env.instance.service (e.g. prod.primary.standard.dns), with FQDNs computed from the VLAN's DNS zone. Uniqueness is enforced at FQDN level.
  • Commits state intent, not mechanics: reserve: <hostname> (<ip>) on <vlan>, retire: <hostname> (<ip>) — one reservation per commit.

Decisions of record

The load-bearing decisions are written down as ADRs in the monorepo (docs/adr/). The foundational one:

ADR-0001 — live monorepo + modules repo + inventory repo. Gen-2's per-service repos drifted (submodule pinned at four different versions, the subnet map duplicated in six places). The fix is a Terragrunt-style live/modules split — with the amendment that foundation stays a standalone repo, because the layer everything depends on must be able to rebuild the world without the monorepo existing.