Teil 1: Anforderungen und Problem.. äh, Herausforderungen
Teil 2: Lösungsansätze – Dockerimages und Initialisierung
Teil 3: Lösungsansätze – Dockerimage mit WordPress
Teil 4: Lösungsansätze – Kubernetes
Teil 5: Installation automatisieren
Teil 6: Updates automatisieren
Teil 7: Feinschliff


Im letzten Teil der Reihe wollen wir noch zwei Themen beleuchten, die bislang noch außen vor gelassen wurden.

Mails versenden

Spätestens wenn die eMail-Adresse vom Admin-Account geändert werden soll, ist es unerlässlich, dass WordPress in der Lage ist, Mails zu versenden. Die will er nämlich bestätigt haben. Nun ist Mail versenden generell kein triviales Thema. Und aus einem Docker-Container heraus schon gar nicht!
Vorweg sei auch schon gesagt, dass wir gar nicht erst den Versuch unternehmen werden, einen Mail-Server in dem Docker-Container zu betreiben. Wir konzentrieren uns stattdessen darauf, einen vorhandenen Mail-Server anzubinden. Generell gilt: eMail ist kein Spielzeug. Wenn die Erfahrung im Aufsetzen eines sicheren Mailservers fehlt, ist es mitunter besser diese Dienstleistung einzukaufen.

WordPress nutzt zum Versenden von Mails standardmäßig die Funktion wp_mail die wiederum die mail()-Funktion von PHP benutzt. Und die erwartet standardmäßig ein lokales sendmail um die Mails zu verschicken. Jetzt ist sendmail nicht gerade dafür bekannt, einfach konfigurierbar zu sein – um es mal vorsichtig zu formulieren. Und deswegen benutzen wir es auch nicht!

msmtp

Als kompatible Alternative bietet sich stattdessen msmtp an. Es ist nicht nur sehr einfach zu konfigurieren, sondern bietet außerdem:

„Sendmail compatible interface (command line options and exit codes)“

Also genau das, was wir haben wollen.

Dockerimage erweitern

Um das Tool nutzen zu können müssen wir unser Dockerimage nur um zwei Pakete erweitern.
apt-get install -fy msmtp msmtp-mta
Da wir bereits ein RUN-Statement haben um das Image auf den aktuellen Stand zu bringen, können wir das einfach dort integrieren:

RUN export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get -y upgrade && apt-get install -fy msmtp msmtp-mta \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

Das war es auch schon. Den Rest ergänzen wir in

Kubernetes

Hier müssen wir nur das Secret erweitern, und um die msmtp-config erweitern:

apiVersion: v1
kind: Secret
type: Opaque
metadata:
  labels:
    app: wordpress-cloud
  name: wordpress-secret
stringData:
  msmtprc: |
    # Set default values for all following accounts.
    defaults
    auth on
    tls on
    tls_trust_file /etc/ssl/certs/ca-certificates.crt
    logfile -

    account        wp
    host           your.mail-relay.tld
    port           587
    from           [email protected]
    user           [email protected]
    password       #{wordpress-mail-password}#

    # Set a default account
    account default: wp

    # Map local users to mail addresses (for crontab)
    aliases /etc/aliases
  wp-config.php: |
  ...

Die Config müssen wir jetzt nur noch im Deployment und Cronjob einbinden:

        ...
          volumeMounts:
            - mountPath: /var/www/html/wp-config.php
              name: config-volume
              subPath: wp-config.php
            - mountPath: /etc/msmtprc
              name: config-volume
              subPath: msmtprc
        ...

DNS

Die konfigurierte Absenderadresse dient nur als Fallback. WordPress wird für gewöhnlich eine eigene setzen. Die muss ggfs. noch in WordPress konfiguriert werden.
Und im Anschluss muss ebenfalls noch ein passender SPF-Record für die Absender-Domain eingerichtet oder erweitert werden!
Andernfalls wird die Mail höchst wahrscheinlich als Spam markiert und entsprechend einsortiert.

Existierendes WordPress migrieren

Bisher haben wir uns nur damit beschäftigt, wie wir ein neues WordPress in Kubernetes aufsetzen können. Aber der wahrscheinlichere Fall ist vermutlich, dass es bereits ein WordPress gibt, das migriert werden soll.
Prinzipiell ist der Ablauf auch weitgehend identisch. Ausnahmen sind dabei:

  • Datenbank
  • Usercontent

Datenbank

Da wir schon eine Datenbank haben, können wir uns die Pipeline für das Setup sparen und uns stattdessen ein Paar Shellscripte her nehmen, mit denen wir einfacher an einen Dump kommen.

create_db_dump.sh:

#!/bin/bash

path=$1

dbName=$(grep "DB_NAME" "${path}/wp-config.php" | sed -e "s/.*, '//gi" -e "s/');//gi")
dbUser=$(grep "DB_USER" "${path}/wp-config.php" | sed -e "s/.*, '//gi" -e "s/');//gi")
dbPassword=$(grep "DB_PASSWORD" "${path}/wp-config.php" | sed -e "s/.*, '//gi" -e "s/');//gi")

mysqldump -h localhost --password="${dbPassword}" -u ${dbUser} ${dbName} > /tmp/${dbName}_dump.sql

gzip -f "/tmp/${dbName}_dump.sql"

echo "DUMP_FILE: /tmp/${dbName}_dump.sql.gz"

#!/bin/bash

remoteWordpressPath=$1

scp create_db_dump.sh [email protected]:/home/user/create_db_dump.sh
output=$(ssh -t [email protected] /home/user/create_db_dump.sh "${remoteWordpressPath}")

dumpFile=$(echo "${output}" | grep "DUMP_FILE" | sed -e 's/DUMP_FILE: //gi')
dumpFile=${dumpFile//[$'\t\r\n']} # remove linefeeds and carriage returns!

echo "downloading dump-file: '${dumpFile}'";
scp [email protected]:"${dumpFile}" .

##
# Azure DB for MySQL doesn't like "MyISAM"!
#
dumpName=$(ls -1 *.sql.gz)
needsFix=$(zgrep "ENGINE=MyISAM" "${dumpName}" | wc -l)
if [ ${needsFix} -gt 0 ]; then
  echo "Fixing Table-Annotations in Dump...";
  zcat "${dumpName}" | sed -e 's/ENGINE=MyISAM/ENGINE=InnoDB/gi' > "$(basename "${dumpName}" ".gz")"
  gzip -f "$(basename "${dumpName}" ".gz")"
fi

Die Scripte setzen voraus, dass auf dem „alten“ Server mit dem WordPress auch ein mysqldump installiert ist. Und falls das DBMS nicht auf dem selben Host läuft, muss das in create_db_dump.sh auch noch geändert werden.
Aber ansonsten können wir das zweite Script einfach mit dem absoluten Pfad zum WordPress aufrufen, und erhalten einen komprimierten Dump der richtigen Datenbank.

Diesen können wir dann direkt am Ende vom Setup-Script mit einspielen:

gunzip < /path/to/db-dump.sql.gz | mysql -h "${dbHost}" --ssl=TRUE --ssl-ca=BaltimoreCyberTrustRoot.crt.pem -p -u "wp_${siteid}@team-database" wp_${siteid}

Oder, falls das schon durchgelaufen ist, manuell (was wohl auch der empfohlene Weg für größere Dumps ist):

gunzip /path/to/db-dump.sql.gz
mysql -h "${dbHost}" --ssl=TRUE --ssl-ca=BaltimoreCyberTrustRoot.crt.pem -p -u "wp_${siteid}@team-database" wp_${siteid}
> source /path/to/db-dump.sql
> exit

Beachtet, dass hier der neu erstellte User für diese Datenbank zum Einsatz kommt, und entsprechend dessen Passwort erforderlich ist.

Usercontent

Dieser Teil ist etwas umständlicher. Der Inhalt von wp-content/uploads muss auf das Volume für „user-upload-storage“ kopiert werden. Dafür muss es natürlich irgendwo gemounted sein. Und genauer gesagt auch nicht irgendwo, sondern da, wo man auch per ssh/scp drauf kommt. Womit unser WordPress-Image/POD schon mal ausscheidet. Tools und Services für Maintenance haben in Produktions-Images/PODs nix zu suchen!

Wir müssen uns also einen extra Deployment dafür zusammen schustern.
Und weil so ein Fall ggfs. öfter vorkommt, bietet es sich an, ein eigenes, kleines Dockerimage auf Basis von bspw. Alpine zu bauen, wo die passenden Public-Keys bereits hinterlegt sind:

FROM alpine:edge

RUN apk update && apk add openssh bash
RUN sed -i s/#PermitRootLogin.*/PermitRootLogin\ yes/ /etc/ssh/sshd_config
RUN sed -i s/#PasswordAuthentication.*/PasswordAuthentication\ no/ /etc/ssh/sshd_config
RUN sed -i -e 's/\/bin\/ash/\/bin\/bash/gi' /etc/passwd
RUN echo "root:wjb2VeVDoXGHb5vQC8USwoIB3xjqWZqV" | chpasswd

COPY keys /root/.ssh
COPY .bashrc /root/.bashrc
COPY .profile /root/.profile

RUN cat /root/.ssh/* > /root/.ssh/authorized_keys
RUN ssh-keygen -A

CMD [ "/usr/sbin/sshd", "-D", "-e"]

Und weil wir clever sind, deaktivieren wir bei der Gelegenheit direkt noch PasswordAuthentication und ändern das root-Passwort auf etwas sehr langes und zufälliges! Den root-Login müssen wir erlauben, denn wir brauchen die Rechte! Da an diesem Punkt das Dockerimage mit dem WordPress noch nie gestartet wurde, konnte auch der initContainer nicht laufen, der für die Volumes das chown auf den passenden User macht.

Damit bewaffnet können wir uns jetzt ein Deployment (oder ein StatefulSet) aufsetzen, dass unser benötigtes Volume in einen Container einbindet, den wir dann per SSH erreichen können (und den wir wieder abbauen können, sobald wir ihn nicht mehr brauchen!)

apiVersion: apps/v1
kind: StatefulSet
metadata:
  labels:
    app: wordpress-cloud
  name: wordpress-ssh
spec:
  replicas: 1
  serviceName: wordpress-sftp-service-headless
  selector:
    matchLabels:
      app: wordpress-cloud
      instance: ssh
  template:
    metadata:
      labels:
        app: wordpress-cloud
        instance: ssh
    spec:
      containers:
        - image: yourcompany.azurecr.io/wp-cloud-sshd:latest
          imagePullPolicy: Always
          name: wp-cloud-sshd
          ports:
            - containerPort: 22
          resources:
            limits:
              cpu: 500m
              memory: 512Mi
            requests:
              cpu: 250m
              memory: 128Mi
          volumeMounts:
            - mountPath: /mnt
              name: user-upload-storage
      volumes:
        - name: user-upload-storage
          persistentVolumeClaim:
            claimName: wordpress-pvc

Und um es uns einfach zu machen, verzichten wir auf einen Ingress und nehmen stattdessen einen Service den wir direkt erreichen können:

apiVersion: v1
kind: Service
metadata:
  labels:
    app: wordpress-cloud
  name: wordpress-ssh-svc-lb
spec:
  externalTrafficPolicy: Cluster
  ports:
    - port: 22
      protocol: TCP
      targetPort: 22
  selector:
    app: wordpress-cloud
    instance: ssh
  sessionAffinity: None
  type: LoadBalancer

Dann müssen wir nur noch die IP herausfinden, die für den LB-Service vergeben wurde (beispielhaft nehmen wir einfach mal 192.168.178.42 …weil, Beispiel und so!) und können die Daten kopieren:

ssh [email protected]
> cd /path/to/wordpress/wp-content/uploads
> scp -i ~/.ssh/your.key -r * [email protected]:/mnt/

Fazit

Wenn ihr bis hierhin durchgehalten habt: Respekt!
Die Reihe hat sich recht umfangreich gestaltet, zeigt aber damit auch, dass das Thema keineswegs trivial ist – zumindest, wenn man es richtig machen möchte!

Und wer sich jetzt fragt, „Ist das nicht nur Spielkram und Proof-of-Concept? Wird das irgendwo produktiv eingesetzt?“, dem sei gesagt: nicht nur wir, auch ihr nutzt es bereits! Dieses Blog hier läuft bereits in Kubernetes.

Vielleicht motiviert das den ein oder anderen, ebenfalls diesen Weg zu beschreiten.