Kubernetes
Für Kubernetes gibt es eine Datei mit allem, was die Installation braucht – ddm.yaml –, und zwei Dateien mit Ihren Einstellungen. Alle drei stehen auf dieser Seite.
Was ddm.yaml anlegt
| Objekt | Bemerkung |
|---|---|
postgres (StatefulSet) | Mit eigenem Volume. Wer schon eine Postgres-Datenbank betreibt, kann stattdessen sie nutzen – siehe unten. |
cms (Deployment) | Genau eine Instanz, Aktualisierung mit Recreate: Das CMS spielt die Migrationen ein und führt die geplanten Aufgaben aus. |
render-worker (Deployment) | Kann auf mehrere Instanzen wachsen. |
client (Deployment) | Die Leser-App. |
ddm-data (PersistentVolumeClaim) | Dokumente und Sicherungen, gemeinsam für CMS und Render-Worker – ReadWriteMany, außer die Dokumente liegen in einem S3-Bucket. |
ddm (Ingress) | Beide Hostnamen, für ingress-nginx mit Uploads bis 1 GB. |
1. Den Namespace und das Registry-Secret anlegen
kubectl create namespace ddm
kubectl -n ddm create secret docker-registry ddm-registry \
--docker-server=registry.gitlab.com \
--docker-username=<Name des Tokens> --docker-password=<Token>
2. Die Einstellungen anlegen
config.env – Ihre Einstellungen, dieselben wie bei Docker Compose:
# The installation's settings, made into the ConfigMap `ddm`:
# kubectl -n ddm create configmap ddm --from-env-file=config.env
# Every setting of the user guide's „Konfiguration" / „Configuration" may be
# added. Secrets go in `secrets.env`.
DDM_CMS_HOST=cms.example.com
DDM_APP_HOST=app.example.com
AUTH_COOKIE_DOMAIN=example.com
PASSKEY_RP_ID=example.com
MAIL_DRIVER=smtp
MAIL_FROM=ddm@example.com
SMTP_HOST=mail.example.com
SMTP_PORT=587
SMTP_USER=ddm
# The documents in an S3-compatible bucket instead of the `ddm-data` volume
# (its keys in `secrets.env`).
# S3_ENDPOINT=https://s3.example.com
# S3_BUCKET=ddm
# S3_REGION=eu-central-1
secrets.env – Passwörter, BETTER_AUTH_SECRET und LICENCE_KEY. Diese Datei gehört in keine Versionsverwaltung – oder nur verschlüsselt, etwa mit SOPS.
# The installation's secrets, made into the Secret `ddm`:
# kubectl -n ddm create secret generic ddm --from-env-file=secrets.env
# Copy this file to `secrets.env`, fill it in, and keep it out of version
# control. `openssl rand -base64 32` for each. The database's password is kept by
# Postgres on its first start; changing it here later locks the cms out.
POSTGRES_PASSWORD=
BETTER_AUTH_SECRET=
SMTP_PASSWORD=
# The licence we sent you (`ddm1.…`) — needed from the first start; one
# entered later under Settings › Licence replaces it.
LICENCE_KEY=
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=
Daraus werden die ConfigMap und das Secret ddm:
kubectl -n ddm create configmap ddm --from-env-file=config.env
kubectl -n ddm create secret generic ddm --from-env-file=secrets.env
3. ddm.yaml anpassen
Speichern Sie die Datei als ddm.yaml und ändern Sie darin zwei Dinge:
- Die Version:
:latestbeicms,render-workerundclientdurch die Version, die Sie betreiben – zum Beispiel:0.26. Postgres bleibt bei:18. - Ihre Hostnamen:
cms.example.comundapp.example.comim Ingress am Ende der Datei.
# DDM on Kubernetes — everything an installation runs, in one file:
#
# kubectl create namespace ddm
# kubectl -n ddm create secret docker-registry ddm-registry \
# --docker-server=registry.gitlab.com --docker-username=<token name> --docker-password=<token>
# kubectl -n ddm create configmap ddm --from-env-file=config.env
# kubectl -n ddm create secret generic ddm --from-env-file=secrets.env
# kubectl -n ddm apply -f ddm.yaml
#
# Before applying, set in this file:
# - the release, the same for cms, render-worker and client: `:latest`
# below, e.g. `:0.26` (Postgres stays at its major, 18). The comment
# behind each of those lines is where Flux writes a new version, when it
# updates the installation (`ddm-updates.yaml`); without Flux it does nothing;
# - your two hostnames, for `cms.example.com` and `app.example.com` in the
# Ingress at the end.
#
# The settings are in the ConfigMap `ddm` and the Secret `ddm`, made from
# `config.env` and `secrets.env`. After changing them, restart the apps:
# kubectl -n ddm rollout restart deployment
#
# The cms runs once, and is replaced, not rolled: its scheduled tasks and the
# migrations it runs on start are one process's. The documents are on a
# volume the cms and the render worker share (ReadWriteMany), unless the
# configuration names an S3 bucket.
---
# Postgres with the German dictionary DDM searches with, by its major — never
# updated with the rest: a new major needs its data upgraded by hand.
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app.kubernetes.io/name: postgres
ports:
- name: postgres
port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: postgres
template:
metadata:
labels:
app.kubernetes.io/name: postgres
spec:
imagePullSecrets:
- name: ddm-registry
# As the image's `postgres` user (70) from the start — no root to drop
# from — the volume made its group's.
securityContext:
runAsNonRoot: true
runAsUser: 70
runAsGroup: 70
fsGroup: 70
seccompProfile:
type: RuntimeDefault
containers:
- name: postgres
image: registry.gitlab.com/iq-me/new-ddm-images/postgres:18
# Postgres' own fixes come under the same tag: a restart brings them (user guide, „Postgres").
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
env:
- name: POSTGRES_USER
value: ddm
- name: POSTGRES_DB
value: ddm
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: ddm
key: POSTGRES_PASSWORD
ports:
- name: postgres
containerPort: 5432
readinessProbe:
exec:
command: [pg_isready, -U, ddm, -d, ddm]
periodSeconds: 5
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 2Gi
volumeMounts:
- name: postgres
mountPath: /var/lib/postgresql
# Writable besides its data: the socket, and scratch.
- name: run
mountPath: /var/run/postgresql
- name: tmp
mountPath: /tmp
volumes:
- name: run
emptyDir: {}
- name: tmp
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: postgres
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 20Gi
---
# The documents, and the database's dumps taken before each migration (the
# last three): read and written by the cms and the render worker, which may
# run on different nodes. With an S3 bucket configured (`S3_ENDPOINT`) nothing
# is kept here, and its size may be small.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ddm-data
spec:
accessModes: [ReadWriteMany]
resources:
requests:
storage: 100Gi
---
# The management app, and the API the reader app, the render worker's
# documents and every integration go through. One replica, replaced rather
# than rolled: it runs the scheduled tasks, and migrates the database to its
# release before it answers.
apiVersion: v1
kind: Service
metadata:
name: cms
spec:
selector:
app.kubernetes.io/name: cms
ports:
- name: http
port: 3000
targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cms
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: cms
template:
metadata:
labels:
app.kubernetes.io/name: cms
spec:
imagePullSecrets:
- name: ddm-registry
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: cms
image: registry.gitlab.com/iq-me/new-ddm-images/cms:latest # {"$imagepolicy": "flux-system:ddm-cms"}
# Asked anew at every start: a tag that moves (`stable`, `0.26`) brings its newest image.
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
envFrom:
- configMapRef:
name: ddm
- secretRef:
name: ddm
env:
# Which version of this file this is: a release that needs a newer
# one says so to the administrators, rather than failing (user guide, „Updates").
- name: DDM_DEPLOY_VERSION
value: "1"
- name: DATABASE_URL
value: postgres://ddm:$(POSTGRES_PASSWORD)@postgres:5432/ddm
- name: BETTER_AUTH_URL
value: https://$(DDM_CMS_HOST)
- name: CLIENT_ORIGIN
value: https://$(DDM_APP_HOST)
- name: STORAGE_DIR
value: /data/storage
- name: BACKUP_DIR
value: /data/backups
# Uploads are stored here and drawn by the render worker.
- name: RENDER_MODE
value: worker
ports:
- name: http
containerPort: 3000
# Migrations first, on a new release: up to five minutes.
startupProbe:
httpGet:
path: /api/auth/ok
port: http
periodSeconds: 2
failureThreshold: 150
readinessProbe:
httpGet:
path: /api/auth/ok
port: http
periodSeconds: 10
livenessProbe:
httpGet:
path: /api/auth/ok
port: http
periodSeconds: 30
timeoutSeconds: 5
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 2Gi
volumeMounts:
- name: data
mountPath: /data
# The one place written besides `/data`: uploads on their way, LibreOffice's profiles.
- name: tmp
mountPath: /tmp
volumes:
- name: data
persistentVolumeClaim:
claimName: ddm-data
- name: tmp
emptyDir: {}
---
# Reads the text, draws the thumbnails and converts the Office files of what
# is uploaded, sharing the work through the database: more replicas for more
# imports. Each finishes the documents in hand before it stops — LibreOffice
# may take minutes over a large deck.
apiVersion: apps/v1
kind: Deployment
metadata:
name: render-worker
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: render-worker
template:
metadata:
labels:
app.kubernetes.io/name: render-worker
spec:
imagePullSecrets:
- name: ddm-registry
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
terminationGracePeriodSeconds: 660
containers:
- name: render-worker
image: registry.gitlab.com/iq-me/new-ddm-images/render-worker:latest # {"$imagepolicy": "flux-system:ddm-render-worker"}
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
envFrom:
- configMapRef:
name: ddm
- secretRef:
name: ddm
env:
- name: DATABASE_URL
value: postgres://ddm:$(POSTGRES_PASSWORD)@postgres:5432/ddm
- name: STORAGE_DIR
value: /data/storage
- name: RENDER_LARGE_OFFICE
value: "on"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
memory: 3Gi
volumeMounts:
- name: data
mountPath: /data
# The one place written besides `/data`: uploads on their way, LibreOffice's profiles.
- name: tmp
mountPath: /tmp
volumes:
- name: data
persistentVolumeClaim:
claimName: ddm-data
- name: tmp
emptyDir: {}
---
# The reader app: its pages, and everything under /api forwarded to the cms.
apiVersion: v1
kind: Service
metadata:
name: client
spec:
selector:
app.kubernetes.io/name: client
ports:
- name: http
port: 3000
targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: client
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: client
template:
metadata:
labels:
app.kubernetes.io/name: client
spec:
imagePullSecrets:
- name: ddm-registry
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
containers:
- name: client
image: registry.gitlab.com/iq-me/new-ddm-images/client:latest # {"$imagepolicy": "flux-system:ddm-client"}
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
envFrom:
- configMapRef:
name: ddm
env:
- name: NUXT_API_ORIGIN
value: http://cms:3000
- name: NUXT_PUBLIC_MGMT_URL
value: https://$(DDM_CMS_HOST)
# The reader app on this installation, for the mobile app's „Server".
- name: NUXT_PUBLIC_SERVER_URL
value: https://$(DDM_APP_HOST)
- name: RENDER_MODE
value: worker
ports:
- name: http
containerPort: 3000
# Through to the cms: what the reader app forwards answers, too.
readinessProbe:
httpGet:
path: /api/auth/ok
port: http
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 512Mi
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
# Both hostnames — replace `cms.example.com` and `app.example.com` with yours —
# with TLS from the Secret `ddm-tls`, or from cert-manager with its
# annotation added here. Uploads are up to 1 GiB and may take a while:
# ingress-nginx's limits raised; another controller needs its own.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ddm
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 1g
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/proxy-request-buffering: "off"
spec:
tls:
- hosts: [cms.example.com, app.example.com]
secretName: ddm-tls
rules:
- host: cms.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cms
port:
name: http
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: client
port:
name: http
4. Anwenden
kubectl -n ddm apply -f ddm.yaml
kubectl -n ddm rollout status deployment/cms
Dann legen Sie wie bei Docker Compose das erste Konto an.
Einstellungen ändern
Nach einer Änderung an config.env oder secrets.env schreiben Sie ConfigMap und Secret neu und starten die Apps neu:
kubectl -n ddm create configmap ddm --from-env-file=config.env --dry-run=client -o yaml | kubectl apply -f -
kubectl -n ddm create secret generic ddm --from-env-file=secrets.env --dry-run=client -o yaml | kubectl apply -f -
kubectl -n ddm rollout restart deployment
Eine eigene Datenbank nutzen
Betreiben Sie Postgres schon selbst, braucht DDM Postgres 18 mit dem deutschen Wörterbuch für die Volltextsuche, das unser Image postgres:18 mitbringt. Dann:
- Tragen Sie die Adresse in
secrets.envein:EXTERNAL_DATABASE_URL=postgres://ddm:<Passwort>@db.firma.de:5432/ddm. - Ersetzen Sie in
ddm.yamlbeicmsundrender-workerden Wert vonDATABASE_URLdurch$(EXTERNAL_DATABASE_URL). - Löschen Sie in
ddm.yamldie beiden ersten Abschnitte – den Service und das StatefulSetpostgres.
Ohne ReadWriteMany
CMS und Render-Worker teilen sich das Volume ddm-data. Kann Ihr Cluster kein Volume mit ReadWriteMany bereitstellen, legen Sie die Dokumente in einen S3-kompatiblen Bucket – S3_ENDPOINT und S3_BUCKET in config.env, die Schlüssel in secrets.env. Die Sicherungen landen dann im selben Bucket. Das Volume braucht danach nur noch das CMS. Ändern Sie in ddm.yaml:
- beim PersistentVolumeClaim
ddm-datadieaccessModesauf[ReadWriteOnce], - beim Deployment
render-workerdie EinträgedatauntervolumeMountsund untervolumes– löschen Sie beide;tmpbleibt.