Neuigkeiten von trion.
Immer gut informiert.

PostgreSQL in Kubernetes und OpenShift

Kubernetes

Galt vor einigen Jahren noch das Motto, dass Datenbanken besser außerhalb von (Docker-)Containern oder gar eines Kubernetes Clusters betrieben werden sollten, so hat sich das Bild inzwischen stark gewandelt. Nicht nur, dass sich Kubernetes als das neue Betriebssystem von Rechenzentren und der Cloud etabliert hat, es gibt sogar unmittelbare Vorteile bei der Integration von Plattformdiensten mit Kubernetes.
Welche grundsätzlichen Optionen es gibt, und wie das Vorgehen aussehen kann, beleuchtet dieser Beitrag.

Persistenz und Container

Datenbanken dienen zum dauerhaften Speichern von Daten, wozu typischerweise das Dateisystem benutzt wird. Container besitzen ein Filesystem, dass aus den zu einem Image gehörenden verschiedenen Layern besteht. Diese Layer sind nicht veränderbar, jedoch kann typischerweise ein Container in sein Dateisystem schreiben. Das wird durch einen weiteren, beschreibbaren, Layer gelöst, der zu dem Container gehört.

Das Zusammenspiel der verschiedenen Layer führt jedoch zu reduzierter Effizienz, was sich auch in niedrigerer Geschwindigkeit zeigt. Aus dem Grund wird typischerweise ein separates Verzeichnis außerhalb des Layer-Filesystems verwendet: Entweder ein VOLUME, oder ein Bind-Mount auf ein Verzeichnis auf dem Server.

PostgreSQL im Docker Container

Für PostgreSQL gibt es ein offizielles Docker-Container-Image: https://hub.docker.com/\_/postgres bzw. postgres als Repository auf DockerHub. Wie bei vielen anderen Images auch gibt es verschiedene Geschmacksrichtungen, die über Tags ausgedrückt werden.
Der wesentliche Unterschied ist dabei, welches Betriebssystem als Fundament genutzt wird: Es gibt das minimale alpine Linux und verschiedene Debian Versionen zur Auswahl.
Dazu kommen dann die verschiedenen PostgreSQL Versionen, jeweils in Kombination mit dem Basisimage. So setzt sich dann das Image postgres:14.1-alpine3.15 aus PostgreSQL Version 14.1 und Alpine Linuze 3.15 zusammen.

Der Plattformsupport ist dabei außergewöhnlich breit: Neben Intel und ARM werden auch MIPS, PowerPC und S390 unterstützt. Docker wählt das richtige Image dabei automatisch aus.

Zur Konfiguration werden im wesentlichen Umgebungsvariablen eingesetzt. Zumindest ein initiales Passwort muss gesetzt werden, sonst verweigert der Container den Start. Damit werden unsichere Installationen vermieden.
Die Umgebungsvariable dazu lautet POSTGRES_PASSWORD.

Weitere wichtige Konfigurationen sind POSTGRES_USER (default: postgres) und POSTGRES_DB (default: Wert von POSTGRES_USER), die den Namen des Default-Users und der Default-Datenbank bestimmen.
Soll die automatisch erzeugte Datebank weitergehend konfiguriert werden, können Parameter in POSTGRES_INITDB_ARGS für postgres initdb hinterlegt werden, z.B. der Schalter --data-checksums.

Damit ergibt sich als simpelstes Kommando zum Start einer PostgreSQL Datenbank als Container mit Namen pg und Alpine Linux folgendes Docker Kommando:

$ docker run --name pg -e POSTGRES_PASSWORD=samplepassword postgres:alpine

Für den Container erzeugt der Docker Daemon automatisch ein Volume, falls der Pfad /var/lib/postgresql/data nicht per Bind-Mount bereits gemappt ist.
Das Verzeichnis kann durch die Umgebungsvariable PGDATA geändert werden.

Für das Write-ahead-log (WAL, früher Xlog) kann ein separates Verzeichnis angegeben werden, wenn dies nicht unterhalb von PGDATA liegen soll und das nicht durch einen separaten Bindmount abgebildet wird. Dazu wird die Environment-Variable POSTGRES_INITDB_WALDIR verwendet.

Zur Initialisierung werden, falls noch keine Daten im PGDATA Verzeichnis existieren, einmalig Scripte unter /docker-entrypoint-initdb.d/ ausgeführt.
Das können sowohl (optional gzip komprimierte) SQL Scripte sein, als auch Shellscripte.
Beim Start wird zudem der Besitzer des Ordners PGDATA auf den User POSTGRES_USER angepasst, falls nötig. Dies kann Berechtigungsprobleme nach sich ziehen, in dem Fall schafft die Verwendung eines Unterordners von dem Container-Mount oft Abhilfe.

Wichtig für den Containerbetrieb ist noch, dass die Docker-Containerruntime standardmäßig ledglich 64 MB shared Memory konfiguriert. Für einen produktiven Einsatz kann durch den Docker Schalter --shm-size entsprechend großzügiger der Speicher konfiguriert werden.

Neben dem offiziellen Image gibt es natürlich noch alternative Images. Diese stammen dann z.B. von anderen Distributionen, wie EnterpriseDB oder Percona. Hier gelten dann nicht zwangsläufig die selben Konfigurationsoptionen.

Kubernetes

Um PostgreSQL in Kubernetes zu betreiben, gilt im Prinzip erst einmal alles weiter, wie bereits am Beispiel eines simplen Docker Containers beschrieben.
Kubernetes hat erweitere Konzepte, um bestimmte Typen von Anwendungen optimal abbilden zu können. Für zustandsbehaftete Anwendungen, wie Datenbanken, wird typischerweise ein StatefulSet verwendet.
Dadurch erhalten die einzelnen Instanzen, in Kubernetes durch einen Pod repräsentiert, eine stabile Identität. Damit ist der Name des Pods deterministisch. Je nach verwendeter Persistenz wird sichergestellt, dass jeder Pod auch seinen Speicher zugeordnet bekommt. Fällt zum Beispiel eine Node aus, wird bei einem StatefulSet ein neuer Pod mit dem selben Namen auf einer anderen Node neu erzeugt und der selbe Speicher zugeordnet. Dafür ist natürlich ein entsprechender Speicher erforderlich, der durch mehrere Nodes eingebunden werden kann.

Die Konfiguration von PostgreSQL wird in Kubernetes typischerweise in ConfigMap und Secret Objekten definiert. Damit wird das StatefulSet (oder Alternativen, wie ein Deployment) von der Konfiguration getrennt.

Beispiel für eine PostgreSQL ConfigMap
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
  labels:
    app: postgres
data:
  PGDATA: /var/lib/postgresql/data/pgdata

Alle vertraulichen Daten, wie Passwörter und andere Credentials, werden in Kubernetes durch ein Secret umgesetzt. Es handelt sich dabei primär um einen semantischen Unterschied, die Verwendung ist analog zu ConfigMap Objekten. In neueren Kubernetes Versionen können die enthaltenen Daten im etcd auch verschlüsselt abgelegt werden.

Beispiel für ein PostgreSQL Secret
---
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
  labels:
    app: postgres
data:
  POSTGRES_PASSWORD: c2FtcGxlcGFzc3dvcmQ= # base64

Um innerhalb des Kubernetes Clusters auf Container zuzugreifen werden Service Objekte verwendet. Diese bilden die Aspekte Service Discovery durch DNS und Loadbalancing durch virtuelle IPs (ClusterIP) ab.

Beispiel für einen Service zum Zugriff auf PostgreSQL
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-svc
  labels:
    app: postgres
spec:
  ports:
  - port: 5432
    name: postgresql
  selector:
    app: postgres

Nun fehlt noch das StatefulSet. Im Gegensatz zu einzelnen Pods oder Deployments kann ein StatefulSet eine feste Beziehung zwischen einer Instanz und einem Volume herstellen. Dazu werden die VolumeClaim Objekte nicht direkt referenziert, sondern als Template angegeben.

Beispiel für ein StatefulSet zum Betrieb einer PostgreSQL Instanz in Kubernetes
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  labels:
    app: postgres
spec:
  serviceName: postgres-svc
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:13-alpine
          env:
            - name: PGDATA
              valueFrom:
                configMapKeyRef:
                  name: postgres-config
                  key: PGDATA
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: POSTGRES_PASSWORD
          ports:
            - name: postgresql
              containerPort: 5432
              protocol: TCP
          livenessProbe:
            exec:
              command:
                - bash
                - -ec
                - 'PGPASSWORD=$POSTGRES_PASSWORD psql -w -U "postgres" -d "postgres"  -h 127.0.0.1 -c "SELECT 1"'
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 3
            successThreshold: 1
            failureThreshold: 3
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        storageClassName: "sample"
        accessModes:
          - "ReadWriteOnce"
        resources:
          requests:
            storage: "1Gi"

Falls im Cluster keine automatische Provisionierung von Volumes möglich ist, können diese manuell angelegt werden.
Wichtig ist dabei darauf zu achten, dass der Speicher nicht nur lokal auf einer Node verfügbar ist, wenn die Verfügbarkeit von PostgreSQL nicht durch Datenreplikation auf PostgreSQL-Ebene umgesetzt wird, sondern durch das Storage.

Das folgende Beispiel stellt ein lokales Verzeichnis von einer Kubernetes Node in der StorageClass sample bereit. Das ist für einfache Beispiele, z.B. mit Minikube geeignet, jedoch nicht für produktiven Betrieb.

Beispiel um ein existierendes Verzeichnis als Volume in Kubernetes bereitzustellen
---
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: sample
provisioner: kubernetes.io/no-provisioner
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer

---
kind: PersistentVolume
apiVersion: v1
metadata:
  name: pgdata
spec:
  capacity:
    storage: 1Gi
  local:
    path: /var/lib/storage/pgdata
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: sample
  volumeMode: Filesystem
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - localhost
status:
  phase: Available

Wenn die Manifeste sich in einem Verzeichnis befinden, kann man nun mit kubectl die Objekte in Kubernetes erzeugen. Am besten verwendet man einen eigenen Namespace für erste Schritte. Der Namespace kann z.B. durch kubectl create namespace pgdemo erzeugt werden.

Anwendung der Manifeste in einem Namespace
$ kubectl -n pgdemo create -f .
configmap/postgres-config created
secret/postgres-secret created
service/postgres-svc created
statefulset.apps/postgres created
storageclass.storage.k8s.io/sample created
persistentvolume/pgdata configured

Anschliessend kann von einem Container auf das laufende PostgreSQL im Cluster zugegriffen werden.

Beispiel Zugriff auf die Datenbank im Cluster
$ export POSTGRES_PASSWORD=$(kubectl -n pgdemo get secret postgres-secret -o jsonpath="{.data.POSTGRES_PASSWORD}" | base64 --decode)
$ kubectl run pg -n pgdemo --tty -i --restart='Never' --rm  --image postgres:alpine --env="PGPASSWORD=${POSTGRES_PASSWORD}" -- psql postgres --host postgres-svc -U postgres -d postgres -p 5432
If you don't see a command prompt, try pressing enter.

postgres=# SELECT 1;
 ?column?
'---------
        1
(1 row)
postgres=# \c postgres;
psql (14.1, server 13.5)
You are now connected to database "postgres" as user "postgres".

Basierend auf diesen Beispielen können erweiterte Szenarien abgebildet werden.

PostgreSQL kommt nicht allein

Der Betrieb von PostgreSQL in Kubernetes wirkt - je nach Perspektive - recht aufwändig oder auch viel zu simpel.
Nur um eine einzelne Instanz bereitzustellen, ist sicherlich viel geschehen. Auf der anderen Seite wird PostgreSQL in einem Cluster betrieben und auch bereits grundlegend durch Healthchecks und automatische Neustarts im Fehlerfall gemanaged.

Für einen robusten Produktivbetrieb reicht PostgreSQL allein natürlich nicht: Loadbalancing wird typischerweise durch pgbouncer vorgenommen. Replikation von Daten als Master-Slave oder Multi-Master-Setup fehlt, genauso der verwandte Aspekt der Skalierung.
In dem Kontext kommt dann auch das Thema Topologie auf, schließlich bringt die Replikation zur Erhöhung der Verfügbarkeit nur etwas, wenn nicht alle Repliken auf der selben Node platziert werden.

Auch Monitoring - vielleicht mit Prometheus - und grafische Dashboards zur Betriebsunterstützung sind sicherlich valide Anforderungen.
Ganz zu schweigen von Prozessen für Backup und Restore und Abbildung eines Rollouts neuer Versionen.

Fazit

Mit den grundlegenden Kubernetes Konzepten lässt sich eine PostgreSQL Instanz sehr schnell in Kubernetes bereitstellen. Für komplexere Setups werden zusätzliche Konzepte, wie z.B. Jobs oder CronJobs zur Datensicherung benötigt. Weitere Dienste zur Abbildung der erwähnten Anforderungen kommen zum reinen PostgreSQL dazu.

Zum Glück finden sich zu dem Thema einige fertige Lösungen, zum Beispiel unter Verwendung des Helm-Packagemanagers.
Eine weitere Option ist die Verwendung von Kubernetes Operators.
In einem folgenden Beitrag werden diese Optionen eingehend vorgestellt.




Zu den Themen Kubernetes, Docker und Cloudarchitektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.