PostgreSQL in Kubernetes und OpenShift
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.
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.
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.
---
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.
---
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.
---
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.
$ 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.
$ 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.
Weiterlesen: PostgreSQL mit Helm in Kubernetes und OpenShift
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.