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
|
||||
|
||||
.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)
|
||||
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
|
||||
@@ -347,6 +347,71 @@ k8s-lint:
|
||||
helm lint $(K8S_CHART)
|
||||
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:
|
||||
@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