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: /srv/htdocs/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.
Kommentare von Martin Drößler