16 KiB
Infrastructure Repository Organization Standard
1. Executive Summary
This standard defines a clean, honest, and maintainable layout for an infrastructure monorepo that combines:
- GitOps (Flux + Kustomize)
- First-party applications
- Third-party platform services
- Custom build artifacts
- Terraform/OpenTofu declarative state
- Operational scripts and documentation
Core Philosophy
Organize by ownership and lifecycle boundary first.
vendor/— Things we only maintain manifests for (third-party + simple internal tools). Further classified by failure-mode impact intoinfrastructure/,platform/, andworkloads/.services/— Things we own source code + manifests for (first-party applications)packages/— Buildable artifacts we produce but do not run as normal servicesterraform/— Declarative state (external infrastructure + internal application configuration)clusters/— Thin Flux wiring layer only
This structure eliminates vague terms like "components", "platform", and "apps", and makes every directory's purpose obvious at a glance.
2. Final Recommended Structure
.
├── clusters/ # Flux entrypoints (thin wiring only)
│ └── main/
│ ├── infrastructure.yaml # Points to ./vendor/infrastructure
│ ├── platform.yaml # Points to ./vendor/platform
│ ├── workloads.yaml # Points to ./vendor/workloads
│ └── services.yaml # Points to `k8s` subdirectories in ./services
│
├── vendor/ # Third-party manifests only
│ ├── infrastructure/ # Core cluster engines (total meltdown if down)
│ │ ├── cilium/
│ │ └── longhorn/
│ ├── platform/ # Shared services & registries (system paralysis if down)
│ │ ├── vault/
│ │ ├── zitadel/
│ │ └── forgejo/
│ └── workloads/ # Standalone user-facing apps (service disruption if down)
│ ├── forgejo-public/
│ └── canary/
│
├── services/ # Source code + manifests (first-party apps we develop)
│ └── my-gin-app/
│ ├── cmd/
│ ├── internal/
│ ├── Dockerfile
│ └── k8s/
│
├── packages/ # Buildable artifacts only (code/build context)
│ ├── versitygw-terraform-provider/
│ └── internal-cli/
│
├── terraform/ # Terraform/OpenTofu
│ ├── external/ # External dependencies (Firewall, GCP resources, etc)
│ └── internal/ # Application internal state (Zitadel orgs, policies, etc.)
│
├── scripts/ # Human-run operational scripts
└── docs/
├── spec/ # Normative design-of-record
└── journal/ # Symlink to development-journal/<repo> (gitignored)
3. Directory Responsibilities
| Directory | Purpose | Contains | Example |
|---|---|---|---|
clusters/ |
Thin Flux reconciliation wiring | Kustomizations pointing at vendor/ and services/ |
clusters/main/platform.yaml |
vendor/ |
Third-party and simple internal services (manifests only). Organized by failure-mode impact: infrastructure/ (total meltdown), platform/ (system paralysis), workloads/ (individual service disruption). |
Kubernetes manifests only | vendor/infrastructure/cilium/, vendor/platform/forgejo/, vendor/workloads/forgejo-public/ |
services/ |
First-party applications we develop | Source code + Dockerfile + k8s/ manifests |
services/my-gin-app/ |
packages/ |
Buildable supporting artifacts | Source/build context only (no manifests) | packages/versitygw-terraform-provider/ |
terraform/ |
Declarative state | Split into external/ and internal/ |
terraform/external/, terraform/internal/ |
scripts/ |
Human-run operational utilities | Debugging, migration, and one-off scripts | scripts/analyze-hubble.py |
docs/spec/ |
Normative design-of-record | Specs, standards, settled runbooks | docs/spec/homelab-spec.md |
docs/journal/ |
Time-sensitive work record (symlink; gitignored) | Investigations, plans, postmortems, trackers | docs/journal/00_SUMMARY.md |
4. Key Design Decisions
4.1 Why vendor/ (with infrastructure/, platform/, workloads/)?
- "Platform" and "components" are vague and overloaded.
vendor/clearly signals: "We only maintain the manifests here."- Content is further classified by failure mode:
infrastructure/— Total meltdown (nodes goNotReady).platform/— System paralysis (workloads crash-loop; CI/CD stops).workloads/— Individual service disruption (users cannot reach one app).
- This includes both true third-party services and simple internal tools that have no source code in this repo.
- Internal Forgejo lives in
vendor/platform/because Flux depends on it for reconciliation and recovery. Public Forgejo lives invendor/workloads/because its failure disrupts the public Git service but leaves cluster reconciliation and shared infrastructure healthy. Product identity does not override failure-mode classification.
4.2 Why services/ for first-party?
- Clear contrast with
vendor/. - Explicitly means: "We own the source code + deployment contract."
- Preserves the important principle that code and manifests should live together for things we develop.
- Note: To keep Flux reconciliation fast, ensure a
.sourceignorefile is defined at the repository root to prevent Flux from scanning non-Kubernetes files (like.go,go.sum, etc.) underservices/.
4.3 Why separate packages/?
packages/is strictly for buildable artifacts that are consumed by something else.- No Kubernetes manifests belong here.
- Examples: custom Terraform providers, internal CLIs, code generators.
4.4 Terraform Split (external/ vs internal/)
This addresses a common real-world need:
terraform/external/— External cloud dependencies such as backup destinations on GCS.terraform/internal/— Application configuration state (Zitadel organizations, policies, grants, VersityGW buckets, Vault policies, etc.)
This split makes it obvious that Terraform is used for both traditional infrastructure and rich application configuration.
4.5 Failure-Mode Taxonomy
Vendor manifests are classified by what happens when they fail.
| Sub-tier | Failure Mode | Impact | Examples |
|---|---|---|---|
infrastructure/ |
Total Meltdown | Nodes go NotReady; control plane loses quorum; cluster unusable |
Cilium, Longhorn, OpenEBS |
platform/ |
System Paralysis | Workloads crash-loop; CI/CD stops; source or secret access fails | Vault, Zitadel, internal Forgejo, VSO |
workloads/ |
Service Disruption | Individual user-facing services offline; platform remains healthy | oCIS, Frigate, public Forgejo, canary |
5. Placement Rules
| Asset Type | Location | Notes |
|---|---|---|
| Flux Kustomization for infrastructure | clusters/main/infrastructure.yaml |
Thin wiring only |
| Flux Kustomization for platform | clusters/main/platform.yaml |
Thin wiring only |
| Flux Kustomization for workloads | clusters/main/workloads.yaml |
Thin wiring only |
| Flux Kustomization for first-party | clusters/main/services.yaml |
Thin wiring only |
| Infrastructure Kubernetes manifests | vendor/infrastructure/<name>/ |
Total meltdown scope |
| Platform Kubernetes manifests | vendor/platform/<name>/ |
System paralysis scope |
| Workload Kubernetes manifests | vendor/workloads/<name>/ |
Service disruption scope |
| First-party application (source + manifests) | services/<name>/ |
Code + k8s/ together |
| Custom Terraform provider source | packages/versitygw-terraform-provider/ |
Build artifact |
| Supporting build artifacts | packages/<name>/ |
Build artifact |
| External infrastructure state | terraform/external/<boundary>/ |
GCP resources |
| Application internal configuration state | terraform/internal/<name>/ |
Zitadel orgs, policies, etc. |
| Human-run scripts | scripts/ |
Never required for normal reconciliation |
| Documentation | docs/spec/, docs/journal/ |
Specs vs historical work records |
6. Application Configuration vs Workload Definition (Important)
Terraform is frequently used to manage application internal state, not just infrastructure.
Example — Zitadel:
vendor/platform/zitadel/ # Flux-managed workload
├── helm-release.yaml # replicas, image, resources
├── network-policies.yaml
└── route.yaml
terraform/internal/zitadel/ # Terraform-managed configuration
├── organizations.tf
├── policies.tf
├── grants.tf
└── providers.tf
Rule:
- Flux manages the workload (replicas, image, probes, network policies).
- Terraform manages the application configuration (organizations, users, policies, grants).
7. Decision Tree
flowchart TD
Start[New thing to add] --> Q1{Do we only maintain Kubernetes manifests?}
Q1 -->|Yes| Q_V{What is the failure mode if it goes down?}
Q_V -->|Nodes go NotReady / Total meltdown| VI[vendor/infrastructure/]
Q_V -->|Cluster paralyzed / Cascading failure| VP[vendor/platform/]
Q_V -->|Individual service disruption only| VW[vendor/workloads/]
Q1 -->|No| Q2{Do we maintain source code + manifests?}
Q2 -->|Yes| S[services/]
Q2 -->|No| Q3{Is it a buildable artifact consumed by something else?}
Q3 -->|Yes| P[packages/]
Q3 -->|No| Q4{Is it Terraform/OpenTofu declarative state?}
Q4 -->|Yes| T[terraform/]
Q4 -->|No| Q5{Is it a human-run script?}
Q5 -->|Yes| Scripts[scripts/]
Q5 -->|No| Docs[docs/]
8. Anti-Patterns
- Putting source code in
vendor/ - Putting Kubernetes manifests in
packages/ - Mixing first-party source with third-party manifests in the same directory
- Repeating a component suffix across sibling filenames instead of placing those files in a component directory
- Using
terraform/for Kubernetes resource definitions (use Flux instead) - Putting required production logic in
scripts/ - Classifying by product name instead of failure mode (e.g., placing both Forgejo authorities in one tier despite different recovery dependencies)
- Renaming live Flux
Kustomizationobjects or transferring resource ownership in the same prune-enabled change as a path refactor
9. Migration Safety
Repository layout changes must preserve Flux inventory ownership unless there is an explicit, tested adoption plan.
- Preserve existing Flux
Kustomizationmetadata.namevalues during path moves. - Move paths first, then verify reconciliation and inventory before renaming owners.
- Do not move directly root-owned resources under a new child Kustomization in the same change.
- Critical bootstrap resources should use non-pruned Kustomizations unless a deletion procedure has been tested.
10. Quick Reference
| I want to add... | Put it in... |
|---|---|
| New infrastructure service (CNI, storage, cert-manager) | vendor/infrastructure/<name>/ |
| New platform service (IdP, secrets, source) | vendor/platform/<name>/ |
| New third-party user-facing app | vendor/workloads/<name>/ |
| New first-party application | services/<name>/ |
| Custom Terraform provider | packages/<name>/ |
| Supporting build artifact | packages/<name>/ |
| External infrastructure | terraform/external/<name>/ |
| Application config state (orgs, policies) | terraform/internal/<name>/ |
| Operational script | scripts/ |
| Documentation | docs/spec/, docs/journal/ |
Last updated: June 26, 2026