ADR-0033 records why one values-driven chart rather than 30 subcharts, the four
platform-forced deviations from compose, and the alternatives (kompose, bitnami
subcharts, ingress-nginx, Helm hooks for ordering, a laptop-side registry).
The runbook is the walkthrough as actually performed on a single-node Talos v1.14
VM under virt-manager, including the parts that bite: virt-manager ejecting the
install ISO on first shutdown, Talos 1.14 moving the install disk into its own
config document, the control-plane taint, and why the portals must be reached
over localhost (crypto.subtle needs a secure context for PKCE).
One chart whose values.yaml is a near-literal transcription of
infra/docker-compose.yml, rendered by three generic templates (Deployment, Job,
Service) over a `workloads` map — so the two stacks can be diffed by eye instead
of by archaeology, and adding a service is a values edit.
Platform-forced deviations, each commented where it appears:
- `args`, never `command`: compose replaces the image CMD, Kubernetes replaces the
ENTRYPOINT. The chart fails to render on `command`, because the symptom (postgres
refusing to run as root, Keycloak exec-ing `start-dev`) is nothing like the cause.
- The four Django services apply their own setup_configuration in the web pod
rather than in a separate init Job: both scripts migrate, and without compose's
depends_on they race the same database.
- OpenZaak and Objecten are addressed by service FQDN, because Django rejects a
single-label host in a URL — the reason compose passes container IPs around.
- NodePorts, no ingress; databases are emptyDir until persistence.storageClass is
set, so the stack comes up on a cluster with no CSI driver.
The upstream config inputs stay in the repo and become ConfigMaps via
infra/helm/seed-configmaps.sh — the Kubernetes sibling of infra/seed-config.sh —
so the compose stack and the chart cannot fork. infra/helm/registry.yaml runs an
in-cluster registry because Talos cannot side-load an image and a laptop-side one
needs a root-level firewall change.
`make k8s-lint` runs `helm lint` plus a full `helm template`, so a values typo or a
malformed resource is caught without a cluster — the only automated check the chart
can have while CI has no Kubernetes to deploy into.
Red: there is no chart to lint yet.
Records the decision, the directive-order footgun that shapes the Caddyfiles, and
the measured cost (the images grew 75.7 MB → 90.6 MB). Also updates the three
frontend-decisions entries and the two other docs that named nginx.
nginx resolves a variable `proxy_pass` upstream itself, using only the `resolver`
directive and never the search domains in /etc/resolv.conf. That cost two
workarounds in one script: rewriting the resolver address for rootless podman
(Docker's 127.0.0.11 is wrong there), and injecting a full FQDN so the bare `bff`
name could resolve on Kubernetes at all.
Caddy dials its upstream per request through the system resolver, which reads
nameserver *and* search domains, so `reverse_proxy bff:8080` resolves on every
engine with no per-engine configuration — and it still starts before the BFF
exists and picks up its restarts. Both workarounds are deleted with the script.
Routing uses mutually-exclusive `handle` blocks, not a bare `try_files`: Caddy
sorts rewrites *before* reverse_proxy, so a top-level SPA fallback would rewrite
every API path to /index.html before the proxy saw it.
The four portal proxy configs are near-identical, so a copy-paste slip is cheap to
introduce and expensive to find: proxying another portal's endpoint group hands a
browser an endpoint its token is not for, and the failure surfaces as a 401 three
services away. Asserts each portal proxies exactly its own groups to the BFF and
keeps the SPA fallback for Angular's client-side routes.
Red: the Caddyfiles it reads do not exist yet.