## What & why Two changes, made and verified together on a real cluster. **S-24 / #25 — a Helm chart for the platform.** One chart, `infra/helm/big-reference`, 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. Adding a service is a values edit. `make k8s-lint` renders and schema-checks the whole stack without a cluster. The issue asked for a *sketch*; this is deployed and verified end to end (see below), which is more than it asked for — the part it asked for that is **not** here is the production-posture write-up (HA, secrets, backup), see Known gaps. **#166 — Caddy replaces nginx in the portals.** nginx resolves a variable `proxy_pass` upstream itself, using only the `resolver` directive and never `/etc/resolv.conf`'s search domains. That had cost two workarounds in one script: rewriting the resolver address for rootless podman, and injecting a full FQDN so the bare `bff` name could resolve on Kubernetes. Caddy dials per request through the system resolver, so `reverse_proxy bff:8080` works on every engine unchanged; `apps/portal-nginx-resolver.sh` and the chart's `BFF_HOST` env are deleted. Closes #25 Closes #166 ## Definition of Done - [x] Linked Gitea issue (above). - [x] Failing test committed before the implementation — twice: the Caddyfile contract test before the Caddyfiles, `make k8s-lint` before the chart. - [x] Implementation makes the test pass. - [x] Conventional Commits referencing the issues (`refs #25` / `refs #166`). - [ ] CI green — awaiting the run on this PR (`make k8s-lint`, `dotnet format` and the new unit self-check pass locally; the compose e2e and mutation lanes are CI's). - [ ] `docker compose up` from a fresh clone reaches green health checks within 3 minutes — the portal images were rebuilt and verified standalone, but a full `make up` run has not been done on this branch. Please confirm in review or let CI's smoke test speak. - [x] Docs updated — `docs/runbooks/kubernetes-talos.md` (new), `frontend-decisions.md`, `demo-script.md`, and the docs that named nginx. - [x] ADR added — ADR-0033 (chart) and ADR-0034 (Caddy). - [ ] Demo note in `docs/demo-script.md` — not added: the deployment target is not a user-visible slice, and the Caddy swap is invisible to the demo script beyond the wording fix included here. ## How it was verified Brought up from scratch on a single-node Talos v1.14.0 VM (6 vCPU / 10 GB, virtio disk) under virt-manager: **29 pods ready and four bootstrap Jobs complete in under three minutes, zero restarts**, using ~4.4 GB of the VM's 10 GB. - Full Common Ground path: portal Caddy → BFF → domain → Flowable → ACL → OpenZaak + Objecten → NRC → event-subscriber → projection → public register (`INGEDIEND`, reference matching the submitted registration). - Werkbak read with an MFA'd medewerker token → 200. - The browser flow driven with Playwright against `http://localhost:30140`: secure context, `crypto.subtle` present, Keycloak form reached, login completed, **no console errors**. - Routing checked against a stub BFF: SPA fallback serves deep links, each portal proxies its own groups, and a portal does *not* proxy a neighbour's group. ## Notes for reviewers Three bugs this shook out, each fixed at the cause rather than the symptom: 1. **`command` vs `args`.** Compose's `command:` replaces the image CMD; Kubernetes' replaces the ENTRYPOINT. Transcribing one to the other broke every upstream image that relies on its entrypoint — postgres refused to run as root, Keycloak tried to exec `start-dev`. The chart now `fail`s at render time on `command`. 2. **Concurrent migrations.** Both `/setup_configuration.sh` and `/start.sh` run `manage.py migrate`; compose serialises them with `depends_on`, Kubernetes has no such edge, so the init Job and its web pod raced (`relation "zgw_consumers_service" already exists`). The four Django services now do both steps in order in the web pod — which also deletes four workloads. 3. **`emptyDir` databases are wiped by any pod-template change.** `make k8s-reseed` now also restarts `event-subscriber` and `projection-api`, which create the projection schema on start and otherwise keep writing to a schema-less database. Known gaps / follow-ups: - **Secrets.** `values.yaml` carries the dev credentials in plain text (`admin/admin`, the ZGW client secret, the two Objecten tokens) and the chart has no `Secret` objects. Fine for a laptop demo, and exactly what #25's "production posture" ADR should address — I suggest a follow-up issue rather than stretching this PR. - **No CI gate for the chart yet.** `make k8s-lint` exists but is not wired into `.gitea/workflows/ci.yaml`, and nothing enforces that the chart and the compose file stay in step. Worth a small follow-up. - **This is two slices in one PR.** They were built and verified together and the diff is entangled (the chart was written against Caddy from the start), so splitting now would mean re-creating an nginx-shaped chart to throw away. Happy to split if you'd rather. - **Rebased onto #161** (merged as #165) rather than merged, to keep the history linear. One conflict, in the `unit:` target where both branches add a self-check line — resolved by keeping both. #161's `infra/host-browser.yml` arrived with `/usr/share/nginx/html/config.json` and is fixed to `/usr/share/caddy/` inside the `feat(portals)` commit, so no commit on this branch leaves that overlay pointing at a path the images no longer have.Reviewed-on: #167
609 lines
26 KiB
YAML
609 lines
26 KiB
YAML
# Values for the BIG reference stack on Kubernetes.
|
|
#
|
|
# `workloads` is a near-literal transcription of infra/docker-compose.yml — same
|
|
# service names, same images, same env, same one-shots — so the two stacks can be
|
|
# diffed by eye. Read that file's comments for the *why* behind each setting; only
|
|
# the deviations forced by Kubernetes are re-explained here.
|
|
#
|
|
# Every env value is rendered with Helm's `tpl`, so it may use:
|
|
# {{ .Release.Namespace }} — for a cluster-internal FQDN
|
|
# {{ .Values.host }} — the node address a browser reaches the cluster on
|
|
#
|
|
# Deviations from compose, all of them consequences of the platform:
|
|
# * The compose stack hands the ACL and the seeds OpenZaak's *container IP*,
|
|
# because OpenZaak and NRC validate URLs with Django's URLValidator and a
|
|
# single-label host ("openzaak") is rejected. In Kubernetes the service FQDN
|
|
# (openzaak.<ns>.svc.cluster.local) is already multi-label, so the IP dance and
|
|
# the `objecten.local` network alias both disappear.
|
|
# * `depends_on: service_healthy` becomes a `waitFor` init container (TCP wait)
|
|
# plus readiness probes. Ordering is otherwise not enforced: every bootstrap
|
|
# job is idempotent and Kubernetes retries.
|
|
# * The published ports are NodePorts (see `nodePorts`), not host ports.
|
|
|
|
# The address a browser outside the cluster uses to reach the node: your Talos
|
|
# VM's IP. It pins Keycloak's issuer and the portals' OIDC authority to one
|
|
# string, so browser tokens and the BFF's discovered issuer agree.
|
|
host: 192.168.122.100
|
|
|
|
# Set when pulling from a private registry (e.g. the Gitea Container Registry).
|
|
imagePullSecrets: []
|
|
|
|
images:
|
|
# Where the images built from THIS repo live. Empty = the bare
|
|
# `register-referentie/<svc>:dev` names, which only works if the node already
|
|
# has them. On Talos it never does — point this at a registry the node can
|
|
# reach (see docs/runbooks/kubernetes-talos.md).
|
|
registry: ""
|
|
repositoryPrefix: register-referentie
|
|
tag: dev
|
|
# Applies to this repo's images only (see _helpers.tpl). Always, because `dev`
|
|
# is a mutable tag: with IfNotPresent the node keeps the first image it pulled
|
|
# and `make k8s-images` would appear to do nothing. The registry is in-cluster,
|
|
# so a re-pull is local and cheap — but the pods do depend on it being up.
|
|
pullPolicy: Always
|
|
busybox: docker.io/library/busybox:stable
|
|
|
|
persistence:
|
|
# Empty = every database is an emptyDir, so the stack comes up on a bare
|
|
# cluster with no CSI driver. Set to a StorageClass (e.g. `local-path`) to keep
|
|
# the data across pod restarts.
|
|
storageClass: ""
|
|
|
|
# The only place a port is published outside the cluster. A workload listed here
|
|
# gets a NodePort on its single port; everything else stays ClusterIP.
|
|
nodePorts:
|
|
openzaak: 30000
|
|
nrc-web: 30001
|
|
objecttypen: 30020
|
|
objecten: 30021
|
|
bff: 30080
|
|
flowable-rest: 30090
|
|
self-service: 30140
|
|
openbaar: 30141
|
|
behandel: 30142
|
|
beheer: 30143
|
|
keycloak: 30180
|
|
grafana: 30300
|
|
|
|
# ── Shared env blocks (the compose YAML anchors) ────────────────────────────────
|
|
envGroups:
|
|
|
|
oz:
|
|
UWSGI_PROCESSES: "1"
|
|
UWSGI_THREADS: "2"
|
|
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
|
SECRET_KEY: dev-only-not-for-production
|
|
DB_HOST: oz-db
|
|
DB_NAME: openzaak
|
|
DB_USER: openzaak
|
|
DB_PASSWORD: openzaak
|
|
IS_HTTPS: "no"
|
|
ALLOWED_HOSTS: "*"
|
|
CACHE_DEFAULT: oz-redis:6379/0
|
|
CACHE_AXES: oz-redis:6379/0
|
|
CELERY_BROKER_URL: redis://oz-redis:6379/1
|
|
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
|
|
DISABLE_2FA: "true"
|
|
NOTIFICATIONS_DISABLED: "false"
|
|
OPENZAAK_SUPERUSER_USERNAME: admin
|
|
DJANGO_SUPERUSER_PASSWORD: admin
|
|
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
|
|
RUN_SETUP_CONFIG: "true"
|
|
|
|
nrc:
|
|
UWSGI_PROCESSES: "1"
|
|
UWSGI_THREADS: "2"
|
|
DJANGO_SETTINGS_MODULE: nrc.conf.docker
|
|
SECRET_KEY: dev-only-not-for-production
|
|
DB_HOST: nrc-db
|
|
DB_NAME: opennotificaties
|
|
DB_USER: opennotificaties
|
|
DB_PASSWORD: opennotificaties
|
|
IS_HTTPS: "no"
|
|
ALLOWED_HOSTS: "*"
|
|
CACHE_DEFAULT: nrc-redis:6379/0
|
|
CACHE_AXES: nrc-redis:6379/0
|
|
CELERY_BROKER_URL: redis://nrc-redis:6379/1
|
|
CELERY_RESULT_BACKEND: redis://nrc-redis:6379/1
|
|
DISABLE_2FA: "true"
|
|
OPENNOTIFICATIES_SUPERUSER_USERNAME: admin
|
|
DJANGO_SUPERUSER_PASSWORD: admin
|
|
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
|
|
RUN_SETUP_CONFIG: "true"
|
|
NOTIFICATION_SEC_INTERVAL: "5"
|
|
|
|
objecttypen:
|
|
UWSGI_PROCESSES: "1"
|
|
UWSGI_THREADS: "2"
|
|
DJANGO_SETTINGS_MODULE: objecttypes.conf.docker
|
|
SECRET_KEY: dev-only-not-for-production
|
|
DB_HOST: objecttypen-db
|
|
DB_NAME: objecttypes
|
|
DB_USER: objecttypes
|
|
DB_PASSWORD: objecttypes
|
|
ALLOWED_HOSTS: "*"
|
|
CACHE_DEFAULT: objecttypen-redis:6379/0
|
|
CACHE_AXES: objecttypen-redis:6379/0
|
|
DISABLE_2FA: "true"
|
|
OTEL_SDK_DISABLED: "true"
|
|
RUN_SETUP_CONFIG: "true"
|
|
|
|
objecten:
|
|
UWSGI_PROCESSES: "1"
|
|
UWSGI_THREADS: "2"
|
|
DJANGO_SETTINGS_MODULE: objects.conf.docker
|
|
SECRET_KEY: dev-only-not-for-production
|
|
DB_HOST: objecten-db
|
|
DB_NAME: objects
|
|
DB_USER: objects
|
|
DB_PASSWORD: objects
|
|
ALLOWED_HOSTS: "*"
|
|
CACHE_DEFAULT: objecten-redis:6379/0
|
|
CACHE_AXES: objecten-redis:6379/0
|
|
DISABLE_2FA: "true"
|
|
OTEL_SDK_DISABLED: "true"
|
|
CELERY_BROKER_URL: redis://objecten-redis:6379/1
|
|
CELERY_RESULT_BACKEND: redis://objecten-redis:6379/1
|
|
NOTIFICATIONS_DISABLED: "false"
|
|
RUN_SETUP_CONFIG: "true"
|
|
|
|
# Traces for the .NET services. Always set, like compose: the exporter fails
|
|
# harmlessly when Tempo is absent (services/*/Program.cs).
|
|
otel:
|
|
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317
|
|
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
|
|
|
|
# ── Workloads ──────────────────────────────────────────────────────────────────
|
|
# Per entry: image | own (built here) · args · envFrom (env groups) · env
|
|
# ports · probe (a literal readinessProbe) · files (ConfigMap mounts) · data
|
|
# (a database volume) · waitFor (host:port to wait for) · job · enabled
|
|
#
|
|
# `args` (never `command`) is the compose `command:` equivalent: compose replaces
|
|
# the image's CMD, and so does Kubernetes' `args` — Kubernetes' `command` would
|
|
# replace the ENTRYPOINT instead. The chart fails to render if you use `command`.
|
|
workloads:
|
|
|
|
# ── OpenZaak (S-01) ─────────────────────────────────────────────────────────
|
|
oz-db:
|
|
image: docker.io/postgis/postgis:17-3.5
|
|
args: [postgres, -c, max_connections=300]
|
|
env:
|
|
POSTGRES_USER: openzaak
|
|
POSTGRES_PASSWORD: openzaak
|
|
POSTGRES_DB: openzaak
|
|
ports: [{ name: postgres, port: 5432 }]
|
|
data: { mountPath: /var/lib/postgresql/data, size: 4Gi }
|
|
probe:
|
|
exec:
|
|
command: [sh, -c, "pg_isready -U openzaak -d openzaak && psql -U openzaak -d openzaak -c 'SELECT PostGIS_Version();' -q"]
|
|
periodSeconds: 5
|
|
|
|
oz-redis:
|
|
image: docker.io/library/redis:7
|
|
ports: [{ name: redis, port: 6379 }]
|
|
probe: { tcpSocket: { port: 6379 } }
|
|
openzaak:
|
|
image: docker.io/openzaak/open-zaak:1.28.2
|
|
# setup_configuration first, then the server — in ONE container, on purpose.
|
|
# Both /setup_configuration.sh and /start.sh run `manage.py migrate`, so a
|
|
# separate init Job (as compose has, ordered by depends_on) races this pod for
|
|
# the same database and Django fails with "relation already exists".
|
|
args: [sh, -c, "/setup_configuration.sh && exec /start.sh"]
|
|
envFrom: [oz]
|
|
ports: [{ name: http, port: 8000 }]
|
|
# /admin/ answers 302 when Django is up — a redirect counts as ready.
|
|
probe:
|
|
httpGet: { path: /admin/, port: 8000 }
|
|
initialDelaySeconds: 30
|
|
periodSeconds: 10
|
|
failureThreshold: 30
|
|
files: [{ configMap: rr-oz-config, mountPath: /app/setup_configuration }]
|
|
waitFor: [oz-db:5432, oz-redis:6379]
|
|
|
|
oz-celery:
|
|
image: docker.io/openzaak/open-zaak:1.28.2
|
|
args: [/celery_worker.sh]
|
|
envFrom: [oz]
|
|
waitFor: [oz-db:5432, oz-redis:6379]
|
|
|
|
# ── Open Notificaties / NRC (S-01-c) ────────────────────────────────────────
|
|
nrc-db:
|
|
image: docker.io/postgis/postgis:17-3.5
|
|
args: [postgres, -c, max_connections=300]
|
|
env:
|
|
POSTGRES_USER: opennotificaties
|
|
POSTGRES_PASSWORD: opennotificaties
|
|
POSTGRES_DB: opennotificaties
|
|
ports: [{ name: postgres, port: 5432 }]
|
|
data: { mountPath: /var/lib/postgresql/data, size: 2Gi }
|
|
probe:
|
|
exec: { command: [pg_isready, -U, opennotificaties, -d, opennotificaties] }
|
|
periodSeconds: 5
|
|
|
|
nrc-redis:
|
|
image: docker.io/library/redis:7
|
|
ports: [{ name: redis, port: 6379 }]
|
|
probe: { tcpSocket: { port: 6379 } }
|
|
nrc-web:
|
|
image: docker.io/openzaak/open-notificaties:1.16.1
|
|
# setup_configuration first, then the server — in ONE container, on purpose.
|
|
# Both /setup_configuration.sh and /start.sh run `manage.py migrate`, so a
|
|
# separate init Job (as compose has, ordered by depends_on) races this pod for
|
|
# the same database and Django fails with "relation already exists".
|
|
args: [sh, -c, "/setup_configuration.sh && exec /start.sh"]
|
|
envFrom: [nrc]
|
|
ports: [{ name: http, port: 8000 }]
|
|
probe:
|
|
httpGet: { path: /admin/, port: 8000 }
|
|
initialDelaySeconds: 30
|
|
periodSeconds: 10
|
|
failureThreshold: 30
|
|
files: [{ configMap: rr-nrc-config, mountPath: /app/setup_configuration }]
|
|
waitFor: [nrc-db:5432, nrc-redis:6379, openzaak:8000]
|
|
|
|
nrc-celery:
|
|
image: docker.io/openzaak/open-notificaties:1.16.1
|
|
args: [/celery_worker.sh]
|
|
envFrom: [nrc]
|
|
waitFor: [nrc-db:5432, nrc-redis:6379]
|
|
|
|
# Without beat, notifications are accepted but never delivered (ADR-0007).
|
|
nrc-beat:
|
|
image: docker.io/openzaak/open-notificaties:1.16.1
|
|
args: [/celery_beat.sh]
|
|
envFrom: [nrc]
|
|
waitFor: [nrc-db:5432, nrc-redis:6379]
|
|
|
|
# ── Keycloak (S-02) ─────────────────────────────────────────────────────────
|
|
keycloak:
|
|
image: quay.io/keycloak/keycloak:26.1
|
|
args: [start-dev, --import-realm]
|
|
env:
|
|
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
|
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
|
KEYCLOAK_ADMIN: admin
|
|
KEYCLOAK_ADMIN_PASSWORD: admin
|
|
KC_HEALTH_ENABLED: "true"
|
|
KC_HTTP_ENABLED: "true"
|
|
# Pin the issuer to the address the browser uses, and let backchannel calls
|
|
# keep using keycloak:8080 — the BFF discovers metadata in-cluster and gets
|
|
# this issuer back, which is what browser tokens carry (infra/host-browser.yml).
|
|
KC_HOSTNAME: "http://{{ .Values.host }}:{{ index .Values.nodePorts \"keycloak\" }}"
|
|
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
|
|
ports: [{ name: http, port: 8080 }]
|
|
# TCP, not /health/ready on the management port: nothing here gates on realm
|
|
# import, and a wrong health path would leave the Service with no endpoints.
|
|
probe: { tcpSocket: { port: 8080 }, initialDelaySeconds: 15 }
|
|
files: [{ configMap: rr-kc-realms, mountPath: /opt/keycloak/data/import }]
|
|
|
|
# ── Flowable (S-03) ─────────────────────────────────────────────────────────
|
|
flowable-db:
|
|
image: docker.io/library/postgres:16
|
|
env:
|
|
POSTGRES_USER: flowable
|
|
POSTGRES_PASSWORD: flowable
|
|
POSTGRES_DB: flowable
|
|
ports: [{ name: postgres, port: 5432 }]
|
|
data: { mountPath: /var/lib/postgresql/data, size: 2Gi }
|
|
probe:
|
|
exec: { command: [pg_isready, -U, flowable, -d, flowable] }
|
|
periodSeconds: 5
|
|
|
|
flowable-rest:
|
|
image: docker.io/flowable/flowable-rest:latest
|
|
env:
|
|
SPRING_DATASOURCE_DRIVER-CLASS-NAME: org.postgresql.Driver
|
|
SPRING_DATASOURCE_URL: jdbc:postgresql://flowable-db:5432/flowable
|
|
SPRING_DATASOURCE_USERNAME: flowable
|
|
SPRING_DATASOURCE_PASSWORD: flowable
|
|
ports: [{ name: http, port: 8080 }]
|
|
# Every REST path needs basic auth, so an httpGet probe would read 401 as
|
|
# not-ready. TCP is the honest signal here.
|
|
probe: { tcpSocket: { port: 8080 }, initialDelaySeconds: 20 }
|
|
waitFor: [flowable-db:5432]
|
|
|
|
# Deploys the BPMN to the process engine and the DMN to the DMN engine as two
|
|
# separate deployments — flowable-rest does not cascade one into the other
|
|
# (S-13, ADR-0016). Idempotent.
|
|
flowable-init:
|
|
job: true
|
|
image: docker.io/curlimages/curl:latest
|
|
args:
|
|
- sh
|
|
- -c
|
|
- |
|
|
svc=http://flowable-rest:8080/flowable-rest/service/repository/deployments
|
|
dmn=http://flowable-rest:8080/flowable-rest/dmn-api/dmn-repository/deployments
|
|
until curl -sf -u rest-admin:test "$svc" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done
|
|
if curl -s -u rest-admin:test "$dmn" | grep -q '"name":"diploma-eligibility.dmn"'; then
|
|
echo "diploma-eligibility DMN already deployed; skip"
|
|
else
|
|
curl -sf -u rest-admin:test -F 'file=@/work/diploma-eligibility.dmn;filename=diploma-eligibility.dmn' "$dmn" >/dev/null && echo "deployed diploma-eligibility DMN"
|
|
fi
|
|
if curl -s -u rest-admin:test "$svc?name=registratie" | grep -q '"name":"registratie"'; then
|
|
echo "registratie BPMN already deployed; skip"
|
|
else
|
|
curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$svc" >/dev/null && echo "deployed registratie BPMN"
|
|
fi
|
|
files: [{ configMap: rr-fl-bpmn, mountPath: /work }]
|
|
waitFor: [flowable-rest:8080]
|
|
|
|
# ── ACL ─────────────────────────────────────────────────────────────────────
|
|
acl:
|
|
own: true
|
|
envFrom: [otel]
|
|
env:
|
|
OTEL_SERVICE_NAME: acl
|
|
# The FQDN, not `openzaak`: OpenZaak rejects a single-label host on
|
|
# zaak-create. It must be the same host the zaaktype was seeded through
|
|
# (see the seed-zaaktype job) so the URLs stay host-consistent (ADR-0009).
|
|
Acl__OpenZaak__BaseUrl: "http://openzaak.{{ .Release.Namespace }}.svc.cluster.local:8000/"
|
|
Acl__OpenZaak__ClientId: big-reference-seed
|
|
Acl__OpenZaak__Secret: insecure-dev-secret-change-me
|
|
Acl__Defaults__Bronorganisatie: "517439943"
|
|
Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943"
|
|
Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar
|
|
Acl__Defaults__ZaaktypeIdentificatie: BIG-REGISTRATIE
|
|
Acl__Defaults__InformatieobjecttypeOmschrijving: Diploma
|
|
# Objecten reflects the request Host into the object url it returns, and
|
|
# publishes that url to NRC — which rejects a single-label host. The FQDN
|
|
# replaces compose's `objecten.local` alias (ADR-0029).
|
|
Acl__Objecten__BaseUrl: "http://objecten.{{ .Release.Namespace }}.svc.cluster.local:8000/"
|
|
Acl__Objecten__Token: 1234567890abcdef1234567890abcdef12345678
|
|
# Short name on purpose: Objecten only accepts an objecttype URL that
|
|
# matches the one it was configured with (infra/objecten/setup_configuration
|
|
# /data.yaml → http://objecttypen:8000/api/v2/).
|
|
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
|
Acl__Objecten__ObjecttypenToken: 0123456789abcdef0123456789abcdef01234567
|
|
Acl__Objecten__ObjecttypeName: RegisterRecord
|
|
ports: [{ name: http, port: 8080 }]
|
|
probe: { httpGet: { path: /health, port: 8080 }, periodSeconds: 5 }
|
|
|
|
# ── BIG Domain Service (S-05) ───────────────────────────────────────────────
|
|
domain:
|
|
own: true
|
|
envFrom: [otel]
|
|
env:
|
|
OTEL_SERVICE_NAME: domain
|
|
Flowable__BaseUrl: http://flowable-rest:8080/flowable-rest/
|
|
Flowable__Username: rest-admin
|
|
Flowable__Password: test
|
|
Acl__BaseUrl: http://acl:8080/
|
|
ports: [{ name: http, port: 8080 }]
|
|
probe: { httpGet: { path: /health, port: 8080 }, periodSeconds: 5 }
|
|
|
|
# ── BFF ─────────────────────────────────────────────────────────────────────
|
|
bff:
|
|
own: true
|
|
envFrom: [otel]
|
|
env:
|
|
OTEL_SERVICE_NAME: bff
|
|
# In-cluster authority: Keycloak's discovery document returns the pinned
|
|
# KC_HOSTNAME issuer, which is what browser tokens carry (ADR-0010).
|
|
Keycloak__Authority: http://keycloak:8080/realms/digid
|
|
Keycloak__MedewerkerAuthority: http://keycloak:8080/realms/medewerker
|
|
Downstream__Domain__BaseUrl: http://domain:8080/
|
|
Downstream__Projection__BaseUrl: http://projection-api:8080/
|
|
Downstream__Acl__BaseUrl: http://acl:8080/
|
|
ports: [{ name: http, port: 8080 }]
|
|
probe: { httpGet: { path: /health, port: 8080 }, periodSeconds: 5 }
|
|
|
|
# ── Read projection (S-06) ──────────────────────────────────────────────────
|
|
projection-db:
|
|
image: docker.io/library/postgres:16
|
|
env:
|
|
POSTGRES_USER: projection
|
|
POSTGRES_PASSWORD: projection
|
|
POSTGRES_DB: projection
|
|
ports: [{ name: postgres, port: 5432 }]
|
|
data: { mountPath: /var/lib/postgresql/data, size: 2Gi }
|
|
probe:
|
|
exec: { command: [pg_isready, -U, projection, -d, projection] }
|
|
periodSeconds: 5
|
|
|
|
event-subscriber:
|
|
own: true
|
|
envFrom: [otel]
|
|
env:
|
|
OTEL_SERVICE_NAME: event-subscriber
|
|
ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection
|
|
Acl__BaseUrl: http://acl:8080/
|
|
EventSubscriber__Webhook__AuthToken: Bearer big-reference-notifications
|
|
ports: [{ name: http, port: 8080 }]
|
|
probe: { httpGet: { path: /health, port: 8080 }, periodSeconds: 5 }
|
|
# It migrates the projection schema on start and throws if the DB is absent.
|
|
waitFor: [projection-db:5432]
|
|
|
|
projection-api:
|
|
own: true
|
|
envFrom: [otel]
|
|
env:
|
|
OTEL_SERVICE_NAME: projection-api
|
|
ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection
|
|
ports: [{ name: http, port: 8080 }]
|
|
probe: { httpGet: { path: /health, port: 8080 }, periodSeconds: 5 }
|
|
waitFor: [projection-db:5432]
|
|
|
|
# ── Portals (S-08/S-09/S-12/S-15) ───────────────────────────────────────────
|
|
# Caddy serves the Angular app and reverse-proxies its endpoint group to
|
|
# http://bff:8080 — hence the Service must stay named `bff`. Caddy resolves that
|
|
# name through the system resolver, so the DNS search domains apply and no
|
|
# upstream rewriting is needed here (ADR-0034).
|
|
self-service:
|
|
own: true
|
|
ports: [{ name: http, port: 80 }]
|
|
probe: { httpGet: { path: /, port: 80 }, periodSeconds: 5 }
|
|
files:
|
|
- configMap: portal-config-digid
|
|
mountPath: /usr/share/caddy/config.json
|
|
subPath: config.json
|
|
|
|
openbaar:
|
|
own: true
|
|
ports: [{ name: http, port: 80 }]
|
|
probe: { httpGet: { path: /, port: 80 }, periodSeconds: 5 }
|
|
|
|
behandel:
|
|
own: true
|
|
ports: [{ name: http, port: 80 }]
|
|
probe: { httpGet: { path: /, port: 80 }, periodSeconds: 5 }
|
|
files:
|
|
- configMap: portal-config-medewerker
|
|
mountPath: /usr/share/caddy/config.json
|
|
subPath: config.json
|
|
|
|
beheer:
|
|
own: true
|
|
ports: [{ name: http, port: 80 }]
|
|
probe: { httpGet: { path: /, port: 80 }, periodSeconds: 5 }
|
|
files:
|
|
- configMap: portal-config-medewerker
|
|
mountPath: /usr/share/caddy/config.json
|
|
subPath: config.json
|
|
|
|
# ── Objecttypen API (S-18a) ─────────────────────────────────────────────────
|
|
objecttypen-db:
|
|
image: docker.io/library/postgres:17-alpine
|
|
env:
|
|
POSTGRES_USER: objecttypes
|
|
POSTGRES_PASSWORD: objecttypes
|
|
POSTGRES_DB: objecttypes
|
|
ports: [{ name: postgres, port: 5432 }]
|
|
data: { mountPath: /var/lib/postgresql/data, size: 2Gi }
|
|
probe:
|
|
exec: { command: [pg_isready, -U, objecttypes] }
|
|
periodSeconds: 5
|
|
|
|
objecttypen-redis:
|
|
image: docker.io/library/redis:7
|
|
ports: [{ name: redis, port: 6379 }]
|
|
probe: { tcpSocket: { port: 6379 } }
|
|
objecttypen:
|
|
image: docker.io/maykinmedia/objecttypes-api:3.4.2
|
|
# setup_configuration first, then the server — in ONE container, on purpose.
|
|
# Both /setup_configuration.sh and /start.sh run `manage.py migrate`, so a
|
|
# separate init Job (as compose has, ordered by depends_on) races this pod for
|
|
# the same database and Django fails with "relation already exists".
|
|
args: [sh, -c, "/setup_configuration.sh && exec /start.sh"]
|
|
envFrom: [objecttypen]
|
|
ports: [{ name: http, port: 8000 }]
|
|
probe:
|
|
httpGet: { path: /admin/, port: 8000 }
|
|
initialDelaySeconds: 30
|
|
periodSeconds: 10
|
|
failureThreshold: 30
|
|
files: [{ configMap: rr-objecttypen-config, mountPath: /app/setup_configuration }]
|
|
waitFor: [objecttypen-db:5432, objecttypen-redis:6379]
|
|
|
|
# The RegisterRecord objecttype + published version, over the API (S-18c,
|
|
# ADR-0020/ADR-0027). The uuid is pinned — Objecten identifies it by uuid.
|
|
registerrecord-init:
|
|
job: true
|
|
image: docker.io/library/python:3-slim
|
|
args: [python, /config/register.py]
|
|
env:
|
|
OBJECTTYPEN: http://objecttypen:8000
|
|
OBJECTTYPEN_TOKEN: 0123456789abcdef0123456789abcdef01234567
|
|
SCHEMA: /config/registerrecord.schema.json
|
|
files: [{ configMap: rr-registerrecord-config, mountPath: /config }]
|
|
waitFor: [objecttypen:8000]
|
|
|
|
# ── Objecten API (S-18b) ────────────────────────────────────────────────────
|
|
objecten-db:
|
|
image: docker.io/postgis/postgis:17-3.5
|
|
env:
|
|
POSTGRES_USER: objects
|
|
POSTGRES_PASSWORD: objects
|
|
POSTGRES_DB: objects
|
|
ports: [{ name: postgres, port: 5432 }]
|
|
data: { mountPath: /var/lib/postgresql/data, size: 2Gi }
|
|
probe:
|
|
exec: { command: [pg_isready, -U, objects] }
|
|
periodSeconds: 5
|
|
|
|
objecten-redis:
|
|
image: docker.io/library/redis:7
|
|
ports: [{ name: redis, port: 6379 }]
|
|
probe: { tcpSocket: { port: 6379 } }
|
|
objecten:
|
|
image: docker.io/maykinmedia/objects-api:3.4.0
|
|
# setup_configuration first, then the server — in ONE container, on purpose.
|
|
# Both /setup_configuration.sh and /start.sh run `manage.py migrate`, so a
|
|
# separate init Job (as compose has, ordered by depends_on) races this pod for
|
|
# the same database and Django fails with "relation already exists".
|
|
args: [sh, -c, "/setup_configuration.sh && exec /start.sh"]
|
|
envFrom: [objecten]
|
|
ports: [{ name: http, port: 8000 }]
|
|
probe:
|
|
httpGet: { path: /admin/, port: 8000 }
|
|
initialDelaySeconds: 30
|
|
periodSeconds: 10
|
|
failureThreshold: 30
|
|
files: [{ configMap: rr-objecten-config, mountPath: /app/setup_configuration }]
|
|
waitFor: [objecten-db:5432, objecten-redis:6379, objecttypen:8000]
|
|
|
|
# Delivers Objecten's notifications to NRC; without it every register write is
|
|
# silently undelivered (ADR-0029).
|
|
objecten-celery:
|
|
image: docker.io/maykinmedia/objects-api:3.4.0
|
|
args: [/celery_worker.sh]
|
|
envFrom: [objecten]
|
|
waitFor: [objecten-db:5432, objecten-redis:6379]
|
|
|
|
# ── Bootstrap the flow, like the local compose stack does (S-B04, ADR-0020) ──
|
|
# Seeds + publishes the BIG zaaktype through the same FQDN the ACL uses, so the
|
|
# server-assigned URLs are host-consistent. The ACL then resolves them by
|
|
# identificatie (S-27, ADR-0021) — nothing is injected back.
|
|
# Publishing validates the resultaattype against the external Selectielijst
|
|
# API, so the node needs outbound internet for this one job (ADR-0006).
|
|
seed-zaaktype:
|
|
job: true
|
|
image: docker.io/library/python:3-slim
|
|
args: [python, /seed/seed_catalogus.py]
|
|
env:
|
|
OZ_BASE: "http://openzaak.{{ .Release.Namespace }}.svc.cluster.local:8000"
|
|
OZ_PUBLISH: "1"
|
|
files: [{ configMap: rr-seed-scripts, mountPath: /seed }]
|
|
waitFor: [openzaak:8000]
|
|
|
|
# Registers the NRC abonnement on the `objecten` kanaal pointing at the
|
|
# event-subscriber, so register writes reach the projection (ADR-0030).
|
|
# Without it the openbaar register stays empty. Restart-safe and idempotent.
|
|
nrc-subscribe:
|
|
job: true
|
|
image: docker.io/library/python:3-slim
|
|
args: [python, /seed/register-abonnement.py]
|
|
env:
|
|
NRC_BASE: http://nrc-web:8000
|
|
# The script resolves this to an address for the callback URL; the FQDN
|
|
# resolves to the Service's (stable) ClusterIP, which NRC's URLValidator
|
|
# accepts — the compose stack uses the container IP for the same reason.
|
|
SINK_HOST: "event-subscriber.{{ .Release.Namespace }}.svc.cluster.local"
|
|
SINK_PORT: "8080"
|
|
SINK_AUTH: Bearer big-reference-notifications
|
|
files: [{ configMap: rr-seed-scripts, mountPath: /seed }]
|
|
waitFor: [nrc-web:8000, event-subscriber:8080]
|
|
|
|
# ── Observability backplane (S-16a, ADR-0023) ───────────────────────────────
|
|
# Off by default: these are built images too (config baked in), so switching
|
|
# them on also means pushing three more images. Enable all three together.
|
|
tempo:
|
|
enabled: false
|
|
own: true
|
|
args: ["-config.file=/etc/tempo.yaml"]
|
|
ports: [{ name: otlp, port: 4317 }, { name: http, port: 3200 }]
|
|
|
|
prometheus:
|
|
enabled: false
|
|
own: true
|
|
ports: [{ name: http, port: 9090 }]
|
|
|
|
grafana:
|
|
enabled: false
|
|
own: true
|
|
env:
|
|
GF_SECURITY_ADMIN_USER: admin
|
|
GF_SECURITY_ADMIN_PASSWORD: admin
|
|
GF_AUTH_ANONYMOUS_ENABLED: "true"
|
|
ports: [{ name: http, port: 3000 }]
|