feat(k8s): Helm chart for the whole stack on a single-node cluster (refs #25)
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.
This commit is contained in:
@@ -43,7 +43,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint help
|
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint k8s-registry k8s-images k8s-seed k8s-up k8s-reseed k8s-portals k8s-down k8s-purge help
|
||||||
|
|
||||||
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
|
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
|
||||||
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
|
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
|
||||||
@@ -350,6 +350,71 @@ k8s-lint:
|
|||||||
helm lint $(K8S_CHART)
|
helm lint $(K8S_CHART)
|
||||||
helm template big $(K8S_CHART) -n $(K8S_NS) --set images.registry=registry.invalid:5000 >/dev/null
|
helm template big $(K8S_CHART) -n $(K8S_NS) --set images.registry=registry.invalid:5000 >/dev/null
|
||||||
|
|
||||||
|
## k8s-registry: deploy the in-cluster image registry (NodePort 30500)
|
||||||
|
k8s-registry:
|
||||||
|
kubectl apply -f infra/helm/registry.yaml
|
||||||
|
kubectl -n registry rollout status deploy/registry --timeout=180s
|
||||||
|
|
||||||
|
## k8s-images: build this repo's images (via compose) and push them to $(K8S_REGISTRY)
|
||||||
|
# `docker save | crane push` rather than `docker push`: the registry speaks plain
|
||||||
|
# HTTP, which the Docker daemon refuses without a root-level insecure-registries
|
||||||
|
# entry, while crane just takes --insecure. Install: see docs/runbooks/kubernetes-talos.md.
|
||||||
|
k8s-images:
|
||||||
|
@command -v crane >/dev/null || { echo "crane not found — see docs/runbooks/kubernetes-talos.md §0" >&2; exit 2; }
|
||||||
|
@test -n "$(K8S_REGISTRY)" || { echo "set K8S_REGISTRY=<registry host:port>" >&2; exit 2; }
|
||||||
|
docker compose -f $(COMPOSE) build $(K8S_IMAGES)
|
||||||
|
@tar=$$(mktemp -t rr-img-XXXX.tar); \
|
||||||
|
for i in $(K8S_IMAGES); do \
|
||||||
|
docker save register-referentie/$$i:dev -o $$tar; \
|
||||||
|
crane push --insecure $$tar $(K8S_REGISTRY)/register-referentie/$$i:dev; \
|
||||||
|
done; rm -f $$tar
|
||||||
|
|
||||||
|
## k8s-seed: create the ConfigMaps the chart mounts (upstream config + bootstrap scripts)
|
||||||
|
k8s-seed:
|
||||||
|
bash infra/helm/seed-configmaps.sh $(K8S_NS)
|
||||||
|
|
||||||
|
## k8s-up: seed the config and install/upgrade the release
|
||||||
|
k8s-up: k8s-seed
|
||||||
|
@test -n "$(TALOS_HOST)" || { echo "set TALOS_HOST=<node ip>" >&2; exit 2; }
|
||||||
|
@test -n "$(K8S_REGISTRY)" || { echo "set K8S_REGISTRY=<registry the node can pull from>" >&2; exit 2; }
|
||||||
|
helm upgrade --install big $(K8S_CHART) -n $(K8S_NS) --create-namespace \
|
||||||
|
--set host=$(TALOS_HOST) --set images.registry=$(K8S_REGISTRY) $(K8S_SET)
|
||||||
|
kubectl -n $(K8S_NS) get pods
|
||||||
|
|
||||||
|
## k8s-reseed: re-run the bootstrap jobs (after a database was wiped, or after
|
||||||
|
## changing a Job in the chart — Job pod templates are immutable, so a plain
|
||||||
|
## `helm upgrade` is rejected)
|
||||||
|
k8s-reseed:
|
||||||
|
kubectl -n $(K8S_NS) delete job -l app.kubernetes.io/component=init --ignore-not-found
|
||||||
|
$(MAKE) k8s-up
|
||||||
|
# The projection's schema is created on service start (Projection.ReadModel migrates in a
|
||||||
|
# hosted service), so a wiped database also needs these two restarted — otherwise they keep
|
||||||
|
# writing to a schema-less DB and fail with `relation "processed_notifications" does not exist`.
|
||||||
|
kubectl -n $(K8S_NS) rollout restart deploy/event-subscriber deploy/projection-api
|
||||||
|
kubectl -n $(K8S_NS) rollout status deploy/event-subscriber deploy/projection-api --timeout=180s
|
||||||
|
|
||||||
|
## k8s-portals: forward the browser-facing services to localhost (Ctrl-C stops them all)
|
||||||
|
# The portals' OIDC flow needs a *secure context* for crypto.subtle (PKCE), and browsers
|
||||||
|
# only grant that to https or localhost — a NodePort on the VM's IP is neither. Forwarding
|
||||||
|
# to localhost on the same port numbers keeps Keycloak's pinned issuer valid. Deploy with
|
||||||
|
# TALOS_HOST=localhost for this to line up.
|
||||||
|
k8s-portals:
|
||||||
|
@echo "self-service http://localhost:30140 · openbaar :30141 · behandel :30142 · beheer :30143 · keycloak :30180"
|
||||||
|
@trap 'kill 0' INT TERM; \
|
||||||
|
for f in self-service:30140:80 openbaar:30141:80 behandel:30142:80 beheer:30143:80 keycloak:30180:8080; do \
|
||||||
|
svc=$${f%%:*}; rest=$${f#*:}; lport=$${rest%%:*}; rport=$${rest#*:}; \
|
||||||
|
kubectl -n $(K8S_NS) port-forward --address 127.0.0.1 svc/$$svc $$lport:$$rport >/dev/null & \
|
||||||
|
done; wait
|
||||||
|
|
||||||
|
## k8s-down: uninstall the release (database PVCs are kept)
|
||||||
|
k8s-down:
|
||||||
|
helm uninstall big -n $(K8S_NS)
|
||||||
|
|
||||||
|
## k8s-purge: uninstall AND drop the namespace, including the database volumes
|
||||||
|
k8s-purge:
|
||||||
|
-helm uninstall big -n $(K8S_NS)
|
||||||
|
kubectl delete namespace $(K8S_NS) --ignore-not-found
|
||||||
|
|
||||||
## help: list available targets
|
## help: list available targets
|
||||||
help:
|
help:
|
||||||
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //'
|
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //'
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: big-reference
|
||||||
|
description: >-
|
||||||
|
The BIG reference stack (Common Ground) on Kubernetes — a port of
|
||||||
|
infra/docker-compose.yml, aimed at a single-node Talos cluster.
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: dev
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{{ .Chart.Name }} {{ .Chart.Version }} deployed to namespace {{ .Release.Namespace }}.
|
||||||
|
|
||||||
|
Watch it converge (the upstream Django services migrate on first boot, so the
|
||||||
|
first bring-up takes a few minutes):
|
||||||
|
|
||||||
|
kubectl -n {{ .Release.Namespace }} get pods -w
|
||||||
|
kubectl -n {{ .Release.Namespace }} get jobs
|
||||||
|
|
||||||
|
Every bootstrap Job must reach Completions 1/1:
|
||||||
|
{{- range $name, $w := .Values.workloads }}
|
||||||
|
{{- if and (ne $w.enabled false) $w.job }}
|
||||||
|
- {{ $name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
Open in a browser (add {{ .Values.host }} to /etc/hosts if you use a name):
|
||||||
|
{{- range $name, $port := .Values.nodePorts }}
|
||||||
|
{{- $w := index $.Values.workloads $name }}
|
||||||
|
{{- if ne $w.enabled false }}
|
||||||
|
{{ printf "%-16s http://%s:%v" $name $.Values.host $port }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
Test users are in docs/synthetic-data.md. If a pod is stuck in
|
||||||
|
ContainerCreating on a missing ConfigMap, run: make k8s-seed
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
{{/*
|
||||||
|
One pod spec for every workload, Deployment and Job alike. The chart is
|
||||||
|
values-driven on purpose: `.Values.workloads` is a near-literal transcription of
|
||||||
|
infra/docker-compose.yml, so the two stacks can be diffed by eye instead of by
|
||||||
|
archaeology. Adding a service is a values edit, not a template edit.
|
||||||
|
|
||||||
|
Called as: include "big.podspec" (dict "root" $ "name" $name "w" $w)
|
||||||
|
*/}}
|
||||||
|
{{- define "big.podspec" -}}
|
||||||
|
{{- $root := .root -}}
|
||||||
|
{{- $name := .name -}}
|
||||||
|
{{- $w := .w -}}
|
||||||
|
{{- with $root.Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 2 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with $w.waitFor }}
|
||||||
|
initContainers:
|
||||||
|
- name: wait-for-deps
|
||||||
|
image: {{ $root.Values.images.busybox }}
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
for t in {{ join " " . }}; do
|
||||||
|
echo "waiting for $t"
|
||||||
|
until nc -z "${t%:*}" "${t#*:}"; do sleep 2; done
|
||||||
|
done
|
||||||
|
{{- end }}
|
||||||
|
containers:
|
||||||
|
- name: {{ $name }}
|
||||||
|
image: {{ include "big.image" (dict "root" $root "name" $name "w" $w) }}
|
||||||
|
# Only this repo's images get the configured policy: their `dev` tag is mutable.
|
||||||
|
# Upstream tags are pinned, so IfNotPresent keeps them out of pod-template diffs —
|
||||||
|
# which matters because a changed template makes a Job unpatchable (immutable).
|
||||||
|
imagePullPolicy: {{ if $w.own }}{{ $root.Values.images.pullPolicy }}{{ else }}IfNotPresent{{ end }}
|
||||||
|
{{- if $w.command }}
|
||||||
|
{{- fail (printf "workload %s: use `args`, not `command` — compose's `command:` replaces CMD, but Kubernetes' `command:` replaces the image ENTRYPOINT (postgres would run as root, keycloak would exec `start-dev`)" $name) }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with $w.args }}
|
||||||
|
args:
|
||||||
|
{{- toYaml . | nindent 6 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with $w.envFrom }}
|
||||||
|
envFrom:
|
||||||
|
{{- range . }}
|
||||||
|
- configMapRef:
|
||||||
|
# optional: an env group whose feature is disabled (e.g. otel) simply
|
||||||
|
# isn't rendered, and the pod must still start.
|
||||||
|
name: {{ printf "%s-env" . }}
|
||||||
|
optional: true
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with $w.env }}
|
||||||
|
env:
|
||||||
|
{{- include "big.env" (list $root .) | nindent 6 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with $w.ports }}
|
||||||
|
ports:
|
||||||
|
{{- range . }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
containerPort: {{ .targetPort | default .port }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with $w.probe }}
|
||||||
|
readinessProbe:
|
||||||
|
{{- toYaml . | nindent 6 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with $w.resources }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml . | nindent 6 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if or $w.files $w.data }}
|
||||||
|
volumeMounts:
|
||||||
|
{{- range $w.files }}
|
||||||
|
- name: {{ .configMap }}
|
||||||
|
mountPath: {{ .mountPath }}
|
||||||
|
{{- with .subPath }}
|
||||||
|
subPath: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
readOnly: true
|
||||||
|
{{- end }}
|
||||||
|
{{- with $w.data }}
|
||||||
|
- name: data
|
||||||
|
mountPath: {{ .mountPath }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if or $w.files $w.data }}
|
||||||
|
volumes:
|
||||||
|
{{- range $w.files }}
|
||||||
|
- name: {{ .configMap }}
|
||||||
|
configMap:
|
||||||
|
name: {{ .configMap }}
|
||||||
|
{{- with .defaultMode }}
|
||||||
|
defaultMode: {{ . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with $w.data }}
|
||||||
|
- name: data
|
||||||
|
{{- if $root.Values.persistence.storageClass }}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ $name }}-data
|
||||||
|
{{- else }}
|
||||||
|
# No StorageClass configured: the databases are emptyDir, so the stack needs
|
||||||
|
# no CSI driver to come up. Data then lives as long as the pod does — see
|
||||||
|
# docs/runbooks/kubernetes-talos.md for switching on local-path.
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/* Image ref: `own: true` workloads are built from this repo, everything else is upstream. */}}
|
||||||
|
{{- define "big.image" -}}
|
||||||
|
{{- $root := .root -}}
|
||||||
|
{{- $w := .w -}}
|
||||||
|
{{- if $w.own -}}
|
||||||
|
{{- $ref := printf "%s/%s:%s" $root.Values.images.repositoryPrefix .name $root.Values.images.tag -}}
|
||||||
|
{{- with $root.Values.images.registry }}{{ printf "%s/%s" . $ref }}{{ else }}{{ $ref }}{{ end }}
|
||||||
|
{{- else -}}
|
||||||
|
{{- $w.image -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Env list from a map. Every value is run through `tpl`, so values.yaml can name
|
||||||
|
cluster-internal hosts ({{ .Release.Namespace }}) and the node address
|
||||||
|
({{ .Values.host }}) without the chart hard-coding either.
|
||||||
|
*/}}
|
||||||
|
{{- define "big.env" -}}
|
||||||
|
{{- $root := index . 0 -}}
|
||||||
|
{{- range $k, $v := index . 1 }}
|
||||||
|
- name: {{ $k }}
|
||||||
|
value: {{ tpl (toString $v) $root | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "big.labels" -}}
|
||||||
|
app.kubernetes.io/name: {{ .name }}
|
||||||
|
app.kubernetes.io/instance: {{ .root.Release.Name }}
|
||||||
|
app.kubernetes.io/managed-by: Helm
|
||||||
|
{{- end -}}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{{- /*
|
||||||
|
Shared env blocks — the Kubernetes equivalent of the YAML anchors in
|
||||||
|
infra/docker-compose.yml (&oz-env, &nrc-env, &objecttypen-env, &objecten-env).
|
||||||
|
A workload picks them up with `envFrom`, so the web/celery/init variants of an
|
||||||
|
upstream image stay guaranteed-identical, and `kubectl get cm oz-env -o yaml`
|
||||||
|
shows what a pod actually got.
|
||||||
|
|
||||||
|
The *file* inputs (setup_configuration data.yaml, Keycloak realms, BPMN/DMN, the
|
||||||
|
seed scripts) are NOT here: they live in the repo and are turned into ConfigMaps
|
||||||
|
by infra/helm/seed-configmaps.sh, exactly as infra/seed-config.sh streams them
|
||||||
|
into the compose config volumes. Copying them into the chart would fork them.
|
||||||
|
*/ -}}
|
||||||
|
{{- range $group, $env := .Values.envGroups }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: {{ $group }}-env
|
||||||
|
labels:
|
||||||
|
{{- include "big.labels" (dict "root" $ "name" (printf "%s-env" $group)) | nindent 4 }}
|
||||||
|
data:
|
||||||
|
{{- range $k, $v := $env }}
|
||||||
|
{{ $k }}: {{ tpl (toString $v) $ | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- /*
|
||||||
|
Portal OIDC config. The images bake config.json with the compose authority
|
||||||
|
(keycloak:8080), which a browser outside the cluster cannot resolve; these
|
||||||
|
ConfigMaps mount over it with the node address Keycloak's issuer is pinned to
|
||||||
|
(KC_HOSTNAME below), so the token the browser gets and the issuer the BFF
|
||||||
|
discovers are the same string. Same mechanism as infra/host-browser.yml.
|
||||||
|
*/ -}}
|
||||||
|
{{- range $realm := list "digid" "medewerker" }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: portal-config-{{ $realm }}
|
||||||
|
labels:
|
||||||
|
{{- include "big.labels" (dict "root" $ "name" (printf "portal-config-%s" $realm)) | nindent 4 }}
|
||||||
|
data:
|
||||||
|
config.json: |
|
||||||
|
{ "authority": "{{ printf "http://%s:%v" $.Values.host (index $.Values.nodePorts "keycloak") }}/realms/{{ $realm }}" }
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{{- range $name, $w := .Values.workloads }}
|
||||||
|
{{- if and (ne $w.enabled false) (not $w.job) }}
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ $name }}
|
||||||
|
labels:
|
||||||
|
{{- include "big.labels" (dict "root" $ "name" $name) | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
# Recreate, not RollingUpdate: single node, ReadWriteOnce volumes, and nothing
|
||||||
|
# here is HA — a second pod would just fight the first for the disk.
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: {{ $name }}
|
||||||
|
app.kubernetes.io/instance: {{ $.Release.Name }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- /*
|
||||||
|
A ConfigMap mounted with subPath never picks up updates, so a portal whose
|
||||||
|
config.json content changed has to be rolled. Hashing only the values that
|
||||||
|
render it keeps the churn off the databases — an emptyDir database that is
|
||||||
|
recreated for no reason loses its data (see the runbook §6).
|
||||||
|
*/}}
|
||||||
|
{{- range $w.files }}
|
||||||
|
{{- if hasPrefix "portal-config-" .configMap }}
|
||||||
|
annotations:
|
||||||
|
checksum/portal-config: {{ printf "%s|%v" $.Values.host (index $.Values.nodePorts "keycloak") | sha256sum }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "big.labels" (dict "root" $ "name" $name) | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
{{- include "big.podspec" (dict "root" $ "name" $name "w" $w) | nindent 6 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{{- /*
|
||||||
|
The one-shot bootstrap containers from compose (oz-init, nrc-init, flowable-init,
|
||||||
|
the *-init setup_configuration runs, the zaaktype seed and the NRC abonnement)
|
||||||
|
become Jobs. All of them are idempotent, so ordering is not enforced with hooks:
|
||||||
|
each waits for the ports it needs (waitFor) and Kubernetes retries the rest.
|
||||||
|
A wiped database is re-seeded by `make k8s-reseed`.
|
||||||
|
*/ -}}
|
||||||
|
{{- range $name, $w := .Values.workloads }}
|
||||||
|
{{- if and (ne $w.enabled false) $w.job }}
|
||||||
|
---
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: {{ $name }}
|
||||||
|
labels:
|
||||||
|
{{- include "big.labels" (dict "root" $ "name" $name) | nindent 4 }}
|
||||||
|
app.kubernetes.io/component: init
|
||||||
|
spec:
|
||||||
|
backoffLimit: 20
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
{{- include "big.labels" (dict "root" $ "name" $name) | nindent 8 }}
|
||||||
|
app.kubernetes.io/component: init
|
||||||
|
spec:
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
{{- include "big.podspec" (dict "root" $ "name" $name "w" $w) | nindent 6 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{{- if .Values.persistence.storageClass }}
|
||||||
|
{{- range $name, $w := .Values.workloads }}
|
||||||
|
{{- if and (ne $w.enabled false) $w.data }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: {{ $name }}-data
|
||||||
|
labels:
|
||||||
|
{{- include "big.labels" (dict "root" $ "name" $name) | nindent 4 }}
|
||||||
|
# Keep the databases when the release is uninstalled; `make k8s-purge` drops them.
|
||||||
|
annotations:
|
||||||
|
helm.sh/resource-policy: keep
|
||||||
|
spec:
|
||||||
|
accessModes: [ReadWriteOnce]
|
||||||
|
storageClassName: {{ $.Values.persistence.storageClass }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ $w.data.size | default "2Gi" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{{- /*
|
||||||
|
Service names are the compose service names, verbatim: the portals' Caddy
|
||||||
|
proxies to http://bff:8080 and the upstream setup_configuration files name
|
||||||
|
http://openzaak:8000 / http://nrc-web:8000, so in-cluster DNS has to answer to
|
||||||
|
exactly those names. Do not rename a workload without checking both.
|
||||||
|
|
||||||
|
.Values.nodePorts is the single place a port is published outside the cluster;
|
||||||
|
a workload listed there gets a NodePort on its first (only) port.
|
||||||
|
*/ -}}
|
||||||
|
{{- range $name, $w := .Values.workloads }}
|
||||||
|
{{- if and (ne $w.enabled false) $w.ports }}
|
||||||
|
{{- $nodePort := index $.Values.nodePorts $name }}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ $name }}
|
||||||
|
labels:
|
||||||
|
{{- include "big.labels" (dict "root" $ "name" $name) | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ if $nodePort }}NodePort{{ else }}ClusterIP{{ end }}
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: {{ $name }}
|
||||||
|
app.kubernetes.io/instance: {{ $.Release.Name }}
|
||||||
|
ports:
|
||||||
|
{{- range $i, $p := $w.ports }}
|
||||||
|
- name: {{ $p.name }}
|
||||||
|
port: {{ $p.port }}
|
||||||
|
targetPort: {{ $p.targetPort | default $p.port }}
|
||||||
|
{{- if and $nodePort (eq $i 0) }}
|
||||||
|
nodePort: {{ $nodePort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,608 @@
|
|||||||
|
# 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 }]
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Throwaway in-cluster OCI registry, published on NodePort 30500.
|
||||||
|
#
|
||||||
|
# Talos has no Docker daemon and no way to side-load an image, so the images built
|
||||||
|
# from this repo must come from a registry. This one lives *inside* the cluster on
|
||||||
|
# purpose: a registry on the laptop needs an inbound port opened on firewalld's
|
||||||
|
# libvirt zone (root), while pushing from the laptop to the node is outbound and
|
||||||
|
# always allowed. The node then pulls from its own NodePort.
|
||||||
|
#
|
||||||
|
# Talos must be told it speaks plain HTTP — see the machine.registries.mirrors
|
||||||
|
# patch in docs/runbooks/kubernetes-talos.md. Storage is emptyDir: if this pod is
|
||||||
|
# replaced, re-run `make k8s-images`.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: registry
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: registry
|
||||||
|
namespace: registry
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy: { type: Recreate }
|
||||||
|
selector:
|
||||||
|
matchLabels: { app: registry }
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels: { app: registry }
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: registry
|
||||||
|
image: docker.io/library/registry:2
|
||||||
|
env:
|
||||||
|
- name: REGISTRY_STORAGE_DELETE_ENABLED
|
||||||
|
value: "true"
|
||||||
|
ports:
|
||||||
|
- containerPort: 5000
|
||||||
|
readinessProbe:
|
||||||
|
httpGet: { path: /v2/, port: 5000 }
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/lib/registry
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
emptyDir: {}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: registry
|
||||||
|
namespace: registry
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector: { app: registry }
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 5000
|
||||||
|
targetPort: 5000
|
||||||
|
nodePort: 30500
|
||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Turn the repo's config inputs into the ConfigMaps the Helm chart mounts.
|
||||||
|
#
|
||||||
|
# This is the Kubernetes sibling of infra/seed-config.sh: the upstream Common
|
||||||
|
# Ground images are used verbatim and read their config from a mounted directory,
|
||||||
|
# so the config has to be handed to the platform out-of-band. Compose gets it via
|
||||||
|
# `docker cp` into external volumes; Kubernetes gets it as ConfigMaps created from
|
||||||
|
# the files that already live in this repo. Copying those files into the chart
|
||||||
|
# would fork them from the compose stack, so we don't.
|
||||||
|
#
|
||||||
|
# Idempotent: re-run after editing any data.yaml, then `make k8s-reseed`.
|
||||||
|
#
|
||||||
|
# Usage: seed-configmaps.sh [namespace] (default: big)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ns="${1:-big}"
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo="$(cd "$here/../.." && pwd)"
|
||||||
|
|
||||||
|
kubectl get namespace "$ns" >/dev/null 2>&1 || kubectl create namespace "$ns"
|
||||||
|
|
||||||
|
seed() { # name <kubectl --from-file args...>
|
||||||
|
local name="$1"; shift
|
||||||
|
kubectl create configmap "$name" -n "$ns" "$@" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||||
|
echo " seeded configmap/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
seed rr-oz-config --from-file="$repo/infra/openzaak/setup_configuration/"
|
||||||
|
seed rr-nrc-config --from-file="$repo/infra/opennotificaties/setup_configuration/"
|
||||||
|
seed rr-kc-realms --from-file="$repo/infra/keycloak/realms/"
|
||||||
|
seed rr-objecttypen-config --from-file="$repo/infra/objecttypen/setup_configuration/"
|
||||||
|
seed rr-objecten-config --from-file="$repo/infra/objecten/setup_configuration/"
|
||||||
|
# register.py + the RegisterRecord JSON schema (the __pycache__ dir is skipped:
|
||||||
|
# kubectl only takes regular files from a --from-file directory).
|
||||||
|
seed rr-registerrecord-config --from-file="$repo/infra/objecttypen-registerrecord/"
|
||||||
|
# The BPMN and the DMN are two separate Flowable deployments (S-13, ADR-0016).
|
||||||
|
seed rr-fl-bpmn \
|
||||||
|
--from-file="$repo/workflows/registratie.bpmn" \
|
||||||
|
--from-file="$repo/workflows/diploma-eligibility.dmn"
|
||||||
|
# The two bootstrap scripts the compose local stack runs as init containers
|
||||||
|
# (S-B04, ADR-0020). Stdlib-only, so a plain python image can run them.
|
||||||
|
seed rr-seed-scripts \
|
||||||
|
--from-file="$repo/infra/openzaak/seed_catalogus.py" \
|
||||||
|
--from-file="$repo/infra/local/register-abonnement.py"
|
||||||
Reference in New Issue
Block a user