Layout and naming
Rules for the GitOps tree: flux/, infra/ and nodes/. Follow them when adding a component or
a node app, so kustomize build passes and Flux reconciles in the right order.
File names
Only two file names exist for Kubernetes YAML in this repo:
ks.yaml, a FluxKustomizationCR, one per directory, named for what it reconciles.kustomization.yaml, a plain kustomize resource list, which is the namekustomize builditself requires.
If you are naming a Kubernetes YAML file something else, you are naming it wrong.
Directory layout
infra/<component>/{ks.yaml, app/}: one FluxKustomizationper component.infra/<component>/{config-ks.yaml, config/}: only when that component’s CRs need CRDs its ownks.yamlcannot guarantee exist yet. This is a chicken-and-egg on first apply, and it is whycert-manager’sClusterIssuerandinfisical-operator’sInfisicalConnectioneach sit behind a second Kustomization.infra/<component>/app/<workload>/: subdivideapp/when a component reconciles several distinct workloads. One directory per workload, each a plainkustomization.yamlresource list. The component still has exactly one FluxKustomization, onedependsOnset and onepostBuild.monitoringis the case here, with five workloads that start together and share a namespace but are read and edited one at a time. Anything genuinely shared by all of them, such as itsHelmRepositorylist, stays flat inapp/. Each subdirectory has tokustomize buildon its own, because pre-commit builds every directory holding akustomization.yaml. That is also why a reusable Component cannot live underinfra/.config/is where those go.nodes/<hostname>.k8s/<app>/{ks.yaml, app/}: one directory per node app. See Node apps.nodes/<hostname>.podman/{units/, config/}: a node that runs containers without Kubernetes. Noks.yamland nokustomization.yaml, because Flux reconciles none of it: a timer on the node rsyncsunits/into/etc/containers/systemd/andconfig/into/etc/futhark/. The file-name rule above does not apply, since nothing in it is Kubernetes YAML. Add the directory to.sourceignore, not tonodes/kustomization.yaml. See The standalone Podman plane.
Cluster-wide infra that happens to be pinned to a specific node is not a nodes/ entry.
Pocket ID runs only on ogma, and expresses that as a nodeSelector under infra/. nodes/ is for tenant workloads, not infra controllers.
Flux Kustomization boilerplate
interval, prune, sourceRef, and the flux-system namespace are shared by every Flux
Kustomization CR, so they are patched in once, from infra/kustomization.yaml and
nodes/kustomization.yaml. A ks.yaml itself declares only metadata.name, spec.path,
and spec.dependsOn.
flux/infra/ks.yaml and flux/nodes/ks.yaml are the exception and keep their full spec.
flux/ has no kustomization.yaml of its own to patch from, because Flux auto-generates one from
flux/cluster.yaml’s sync.path: flux, and adding a real one would pull cluster.yaml itself
into reconciliation.
Version pins
One rule, everywhere: nothing floats. Every chart version, image, provider constraint, collection and release binary names an exact version, so the commit is the record of what runs. A range is a version the repo cannot state.
- Container images pin
tag@sha256:…. The tag stays for readability; the digest is what actually resolves. A tag alone can be repointed at a different binary, and Flux would never reconcile, because nothing it watches changed. This holds for a QuadletImage=line too, where the reasoning is the same and the reconciler is a timer rather than Flux. - Helm charts pin
MAJOR.MINOR.PATCH. A chart patch is still a template change reaching the cluster, and under aMAJOR.MINOR.*range it arrived with no commit behind it.
Each chart pin carries a comment recording the chart-to-app mapping, such as
# chart 41.0.2 -> Traefik v3.7.6, so what a bump changes is readable without opening the chart.
Where a chart’s image tag defaults to .Chart.AppVersion, as csi-driver-rclone does, that
mapping is the only place the app version appears at all.
Keeping this many exact pins current by hand is not the intent. Renovate opens the bumps. See Dependency updates.
Adding a node app
- Create
nodes/<hostname>.k8s/<app>/{ks.yaml, app/}. Theks.yamlneedsdependsOn: [infra-policies], plus whatever the app itself needs, usuallytraefik-internalandinfisical-operator-config.nodes/kenaz.k8s/actual/ks.yamlis the worked example. - Add the directory to the sibling
kustomization.yaml’sresources:. - Add the namespace to
infra/namespaces/app/namespaces.yaml, labeledfutk.eu/tier: nodeandfutk.eu/node: <hostname>. Then createinfra/policies/namespaces/<app>/with the default-deny, same-namespace and from-monitoring network policy templates. - Add the ingress-bridge template only if
app/ships anIngress. - If
app/ships anInfisicalStaticSecret, add its namespace to the right tier’sscopedNamespacesininfra/infisical-operator/app/. The operator has no RBAC there otherwise. - Add the new overlay directory to
infra/policies/kustomization.yaml. - If the app needs a database, it gets one on the shared PostgreSQL rather than one of its own.
That is four more files and a
dependsOn: postgres-config, all listed in Giving a service a database.
An app’s own configuration, whether an env list or a settings file, ships through
configMapGenerator rather than a ConfigMap manifest. The generated name carries a content
hash, so editing the file rolls the Deployment. A fixed name does not: the new values reach the
cluster and the running pod keeps the ones it started with, silently, until something else
restarts it. The generator also needs the overlay to set namespace:, because kustomize only
rewrites a reference when both sides agree on one.
Verify: pre-commit run kustomize-build --all-files passes, then after pushing,
just fx failing is empty and the app’s pods reach Running.