Shopware 6 auf Kubernetes Entwicklung
Tomasz GajewskiLesezeit: 8 Minuten
Warum Kubernetes und Shopware 6?
Der Einsatz einer eCommerce-Plattform wie Shopware in einem Kubernetes-Cluster hat zweifelsohne Vorteile. Die wichtigsten sind hohe Skalierbarkeit, Zuverlässigkeit und automatisierte Bereitstellungen. Shopware ist keine eigenständige PHP-Anwendung, sondern nutzt die MySQL-Datenbank, Elasticsearch oder auch optional Redis und Varnish. Daher ist es entscheidend, dass die Entwicklungsumgebung der Produktionsumgebung so ähnlich wie möglich ist. Dann sinkt das Fehlerrisiko wegen unterschiedlichen Konfigurationen der Umgebungen deutlich. Nimmt die Komplexität des Software-Stacks zu, kann dessen Verwaltung dank Kubernetes effizienter gestaltet werden.
Dieser Artikel erklärt, wie Sie eine Shopware-6-Plattform in einem Cluster mit Kubernetes bereitstellen, direkt in diesem Cluster entwickeln sowie Bugs beheben.
Das brauchst Du, um zu beginnen
Das obligatorische Element ist natürlich ein Kubernetes-Cluster. Die beste Wahl für Entwicklungs- oder Testzwecke ist Minikube oder MicroK8s. Beide kannst Du gleichermaßen einfach für ein Single-Node-Cluster einrichten. Während Minikube Kubernetes auf einer lokalen virtuellen Maschine installiert, kann MicroK8s entweder lokal oder auf einer Remote-Maschine installiert werden.
as hier vorgestellte vollständige Set-up findest Du im Repository Shopware 6 auf Kubernetes Entwicklung. Die Mindestanforderungen für das lokale Cluster sind 8 GB RAM und 40 GB freier Festplattenspeicher, Dual-Core-CPU, Linux oder Mac OS. 16 GB RAM sind jedoch empfehlenswert.
Erstelle den lokalen Minikube mit:
./kubernetes/bin/create_minikube.sh
DevSpace ist eine gute Lösung für die Build-Automatisierung, die umgekehrte Port-Weiterleitung für Xdebug und die Synchronisierung Deines Codes mit den laufenden Shopware-Instanzen innerhalb des Clusters. Kustomize ist sehr nützlich für das Konfigurationsmanagement.
Diagramm: Shopware 6 Dev auf Kubernetes
So bereitest Du das Shopware 6 Container-Image vor
Erstelle zunächst ein Docker-Image basierend auf NGINX und PHP-FPM und baue Shopware 6 mit Composer auf. Dankenswerterweise ist das Shopware/Produktions-Template auf Packagist verfügbar. Dafür führen wir den folgenden Befehl aus:
composer create-project --no-interaction -- shopware/production .
So wird das Projekt zusammen mit den Abhängigkeiten heruntergeladen.
Hier ist ein Beispiel für das Dockerfile. Dies ist eine einfachere Version, die keine Administration-Watch- und Storefront-Watch-Server enthält. Das vollständige Beispiel findest Du im Repository.
FROM webdevops/php-nginx-dev:7.4
ENV COMPOSER_HOME=/.composer
ENV NPM_CONFIG_CACHE=/.npm
ENV WEB_DOCUMENT_ROOT=/app/public
ENV PROJECT_ROOT=/app
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN mkdir -p /usr/share/man/man1 \
&& curl -sL https://deb.nodesource.com/setup_12.x | bash \
&& mkdir -p ${NPM_CONFIG_CACHE} \
&& mkdir ${COMPOSER_HOME} \
&& chown ${USER_ID}:${GROUP_ID} ${COMPOSER_HOME} \
&& apt-install software-properties-common dirmngr nodejs libicu-dev graphviz vim gnupg2 \
&& npm i npm -g \
&& npm i forever -g \
&& chown -R ${USER_ID}:${GROUP_ID} ${NPM_CONFIG_CACHE} \
&& pecl install pcov-1.0.6 \
&& docker-php-ext-enable pcov \
&& apt-key adv --fetch-keys 'https://mariadb.org/mariadb_release_signing_key.asc' \
&& add-apt-repository 'deb [arch=amd64] https://ftp.icm.edu.pl/pub/unix/database/mariadb/repo/10.5/debian buster main' \
&& apt-install mariadb-client
# Hier findest Du benutzerdefinierte php.ini-Einstellungen.
COPY config/php/php-config.ini /usr/local/etc/php/conf.d/zzz-shopware.ini
# Shopware-spezifische NGINX-Einstellungen
COPY config/nginx/shopware.conf /opt/docker/etc/nginx/vhost.conf
# NGINX PHP-bezogene Einstellungen
COPY config/nginx/php.conf /opt/docker/etc/nginx/conf.d/10-php.conf
# Shopware-spezifische PHP-FPM-Einstellungen
COPY config/php/php-fpm.conf /usr/local/etc/php-fpm.d/zzz-shopware.conf
WORKDIR ${PROJECT_ROOT}
USER application
RUN composer create-project --no-interaction -- shopware/production . "${SHOPWARE_VERSION}"
RUN composer install --no-interaction --optimize-autoloader --no-suggest
# Alle benutzerdefinierten Plug-ins können zu Beginn der Erstellung kopiert werden
COPY --chown=${USER_ID}:${GROUP_ID} shopware/custom/plugins custom/plugins
# Dieses Skript ist nützlich, damit das Init-Skript wartet, bis die Datenbankserver bereit ist.
COPY --chown=${USER_ID}:${GROUP_ID} config/shopware/wait-for-it.sh bin/wait-for-it.sh
# Das Init-Skript bereitet Shopware auf eingehende Verbindungen vor.
COPY --chown=${USER_ID}:${GROUP_ID} config/shopware/shopware-init.sh bin/shopware-init.sh
ENTRYPOINT ["/entrypoint"]
CMD ["supervisord"]
Das Basis-Image ist webdevops/php-nginx-dev
. Es ist bereits mit allen Grundkomponenten ausgestattet, die Shopware 6 benötigt – NGINX, PHP-FPM mit Xdebug.
Der Build sollte sowohl das Storefront- als auch das Administrations-Build-Skript ausführen. Die Originaldateien bin/build-administration.sh
oder bin/build-storefront.sh
geben jedoch einen Fehler zurück, da sie bin/console
verwenden, was eine bestehende Verbindung zur Datenbank voraussetzt.
Lass uns die npm-build
-Befehle als Workaround extrahieren.
Für die Erstellung der Administration ist die Datei plugins.json
erforderlich, die Du initial erzeugst mit:
bin/console bundle:dump
Wie bereits erwähnt, funktioniert die Konsole nicht ohne eine gültige Datenbankverbindung. Für das ersten Mal kannst Du die Datei also aus dem Repository herunterladen.
Kopiere plugins.json
in das Verzeichnis app/var
. Stelle sicher, dass jeder Build diese Datei dorthin kopiert, um sie nicht jedes Mal neu erzeugen zu müssen.
COPY --chown=${USER_ID}:${GROUP_ID} config/shopware/plugins.json var/plugins.json
...
# initialisiere Storefront und Administration
RUN npm clean-install --prefix vendor/shopware/administration/Resources/app/administration
RUN npm clean-install --prefix vendor/shopware/storefront/Resources/app/storefront/
RUN node vendor/shopware/storefront/Resources/app/storefront/copy-to-vendor.js
# erstelle das Storefront
RUN npm --prefix vendor/shopware/storefront/Resources/app/storefront/ run production
# erstelle die Administration
RUN npm run --prefix vendor/shopware/administration/Resources/app/administration/ build
ENTRYPOINT ["/entrypoint"]
CMD ["supervisord"]
# Dockerfile-Ende
Zu guter Letzt dürfen wir die Plug-ins nicht vergessen. Kopiere sie nach app/custom/plugins
.
COPY --chown=${USER_ID}:${GROUP_ID} shopware/custom/plugins custom/plugins
Erstelle Kubernetes-Objekte
Kubernetes folgt einem deklarativen Modell. Das bedeutet, dass die bereitgestellten Objekte den gewünschten Zustand Deines Clusters beschreiben. Am häufigsten verwenden wir YAML-Dateien, um Manifeste zu definieren, die dann in Objekte umgewandelt werden. Diese werden mit kubectl angewendet.
Neben dem Anwendungsserver (PHP-FPM 7.3 oder neuer + NGINX) muss der minimale Stack aus MySQL 5.7 oder neuer und Elasticsearch bestehen. Zur Entwicklungsunterstützung sind zudem Adminer und MailHog sehr nützlich.
Um zustandsbehaftete Anwendungen wie Elasticsearch oder MySQL einzusetzen, sollten wir den Objekttyp StatefulSet
verwenden. Der Typ Deployment
beschreibt den Roll-out von nicht zustandsbehafteten Anwendungen: Shopware-App-Server, Adminer und MailHog. Sowohl Deployment als auch StatefulSet erstellen Pods gemäß den Angaben in den Manifesten.
Wenn Du die Deployments hast, brauchst Du etwas, um die Ports der Anwendungen (Pods) zugänglich und sie innerhalb des Clusters sichtbar zu machen. Die Lösung dafür sind Services.
apiVersion: v1
kind: Service
metadata:
labels:
app: app-server
name: app-server
namespace: development
spec:
type: ClusterIP
ports:
- name: http-app
port: 8000
targetPort: 8000
selector:
app: app-server
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: app-server
name: app-server
namespace: development
spec:
replicas: 1
selector:
matchLabels:
app: app-server
template:
metadata:
labels:
app.network/shopware: "true"
app: app-server
spec:
containers:
- name: app-server
image: kiweeteam/shopware6-dev
imagePullPolicy: IfNotPresent
envFrom:
- configMapRef:
name: env
ports:
- containerPort: 8000
resources: {}
volumeMounts:
- mountPath: /tmp
name: app-server-tmpfs0
- mountPath: /app/public/media
name: media
- mountPath: /app/public/thumbnail
name: thumbnail
- mountPath: /app/.env
subPath: .env
name: env-file
restartPolicy: Always
volumes:
- emptyDir:
medium: Memory
name: app-server-tmpfs0
- name: media
persistentVolumeClaim:
claimName: media
- name: thumbnail
persistentVolumeClaim:
claimName: thumbnail
- name: env-file
configMap:
name: env-file
MySQL-Dienst und -Bereitstellung
apiVersion: v1
kind: Service
metadata:
labels:
app: db
name: db
namespace: development
spec:
ports:
- name: mysql
port: 3306
targetPort: 3306
selector:
app: db
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app: app-mysql
name: db
namespace: development
spec:
replicas: 1
serviceName: db
selector:
matchLabels:
app: db
template:
metadata:
labels:
app.network/db: "true"
app: db
spec:
containers:
- env:
- name: MYSQL_DATABASE
valueFrom:
configMapKeyRef:
name: env
key: DB_NAME
- name: MYSQL_PASSWORD
valueFrom:
configMapKeyRef:
name: env
key: DB_PASSWORD
- name: MYSQL_ROOT_PASSWORD
valueFrom:
configMapKeyRef:
name: env
key: DB_ROOT_PASSWORD
- name: MYSQL_USER
valueFrom:
configMapKeyRef:
name: env
key: DB_USER
image: mysql:8
name: db
ports:
- containerPort: 3306
volumeMounts:
- mountPath: /var/lib/mysql
name: dbdata
restartPolicy: Always
volumes:
- name: dbdata
persistentVolumeClaim:
claimName: dbdata
volumeClaimTemplates:
- metadata:
name: dbdata
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Elasticsearch
Vor Elasticsearch musst Du ECK (Elastic Cloud auf Kubernetes). anwenden. Es enthält benutzerdefinierte Ressourcendefinitionen, die für die Bereitstellung von Elasticsearch-Clustern oder anderen Elastic-Apps wie Kibana oder Filebeat erforderlich sind. Andernfalls wird ein Objekt vom Typ Elasticsearch
nicht erkannt.
kubectl apply -f https://download.elastic.co/downloads/eck/1.3.0/all-in-one.yaml
Dann kommt die Elasticsearch-Manifestdatei:
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: elasticsearch
namespace: development
spec:
version: 7.10.1
nodeSets:
- name: default
count: 1
podTemplate:
metadata:
labels:
app.network/db: "true"
spec:
containers:
- name: elasticsearch
env:
- name: ES_JAVA_OPTS
value: -Xms512m -Xmx512m
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
config:
node.store.allow_mmap: false
xpack.security.enabled: false
http:
tls:
selfSignedCertificate:
disabled: true
service:
spec:
type: ClusterIP
TLS ist deaktiviert, da wir nicht beabsichtigen, die Suche der Öffentlichkeit zugänglich zu machen. Der einzige Verbraucher ist Shopware.
MailHog
apiVersion: v1
kind: Service
metadata:
labels:
app: mailhog
name: mailhog
namespace: development
spec:
ports:
- name: mailhog
port: 8025
targetPort: 8025
- name: smtp
port: 1025
targetPort: 1025
selector:
app: mailhog
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app: mailhog
name: mailhog
namespace: development
spec:
replicas: 1
serviceName: mailhog
selector:
matchLabels:
app: mailhog
template:
metadata:
labels:
app.network/mail: "true"
app: mailhog
spec:
containers:
- image: mailhog/mailhog
name: mailhog
ports:
- containerPort: 8025
- containerPort: 1025
resources: {}
restartPolicy: Always
Persistent volumes
Mediendateien und die Datenbank benötigen Persistenz. Andernfalls gehen die Daten beim Neustart eines Pods mit MySQL, NGINX oder Elasticsearch verloren. Dieses Beispiel befasst sich mit der dynamischen Bereitstellung von Volumes. Bei der dynamischen Bereitstellung werden PersistentVolumes automatisch auf Basis von PersistentVolumeClaims erstellt. StatefulSet
-Objekte können VolumeClaims haben, die als Templates im Abschnitt volumeClaimTemplates
definiert sind.
Hier ist ein Beispiel für ein VolumeClaimTemplate für Elasticsearch:
...
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
...
Dann brauchen wir zwei weitere Volumes, um Mediendateien (und Thumbnails) bereitzuhalten. Da auf diesem Dev-Cluster nur eine Instanz des Shopware-App-Servers läuft, darf der Zugriffsmodus ReadWriteOnce
sein. Für die Produktion ist es jedoch idealerweise ReadWriteMany
, um ein gemeinsames Volume für alle Replikate zu haben.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
app: media
name: media
namespace: development
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
app: thumbnail
name: thumbnail
namespace: development
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Mi
Beim Anlegen eines Minikube-Clusters musst Du daran denken, zwei Add-ons zu aktivieren: default-storageclass
und storage-provisioner
; create_minikube.sh
erledigt dies.
minikube addons enable default-storageclass
minikube addons enable storage-provisioner
Für Microk8s ist es:
microk8s enable storage
Definiere Netzwerkrichtlinien
Netzwerkrichtlinien sind nützlich, um den Netzwerkfluss in einem Cluster zu steuern. Wir wollen nicht alle Dienste für das gesamte Cluster zugänglich machen, sondern nur für die Pods, die Du nutzen willst. In diesem Beispiel erlauben wir, dass MySQL, Elasticsearch und MailHog für alle Pods verfügbar sind, die das Label app.network/shopware: "true"
enthalten.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db
namespace: development
spec:
podSelector:
matchLabels:
app.network/db: "true"
app.network/mail: "true"
policyTypes:
- Egress
# Lasse eingehende Verbindungen nur von Pods mit den Labeln
# app.network/db: "true", app.network/mail: "true" zu den Pods zu,
# welche das Label app.network/shopware: "true" enthalten.
ingress:
- from:
- podSelector:
matchLabels:
app.network/shopware: "true"
ports:
- protocol: TCP
port: 3306
- protocol: TCP
port: 9200
- protocol: TCP
port: 1025
Verwende Kustomize, um die Konfigurationen zu erstellen.
Shopware ist eine Symfony-Anwendung, die in Umgebungsvariablen definierte Parameter verwendet. Der Administration-Dev-Server benötigt zusätzlich die Parameter in der .env-Datei. Wir können Kustomize verwenden, um die ConfigMap zu erstellen, die Variablen als Umgebungsvariablen im laufenden Pod zu verwenden und die .env-Datei in ein Volume einzusetzen.
Hier ist unsere .env-Beispieldatei:
APP_ENV=dev
APP_SECRET=8583a6ff63c5894a3195331701749943
APP_URL=http://localhost:8000
APP_WATCH=true
COMPOSER_HOME=/.composer
DATABASE_URL=mysql://app:app@db:3306/shopware
DB_NAME=shopware
DB_USER=app
DB_PASSWORD=app
DB_ROOT_PASSWORD=root
DEVPORT=8080
GROUP_ID=1000
MAILER_URL=smtp://mailhog:1025
SHOPWARE_ES_ENABLED=1
SHOPWARE_ES_HOSTS=elasticsearch-es-http:9200
SHOPWARE_ES_INDEX_PREFIX=sw
SHOPWARE_ES_INDEXING_ENABLED=1
SHOPWARE_HTTP_CACHE_ENABLED=0
SHOPWARE_HTTP_DEFAULT_TTL=7200
STOREFRONT_PROXY_PORT=9998
USER_ID=1000
PORT=8080
ESLINT_DISABLE=true
Füge als Nächstes den configMapGenerator
-Abschnitt zu kustomization.yaml
hinzu - einen für die Variablen und einen für die .env-Datei.
configMapGenerator:
- name: env
envs:
- .env
- name: env-file
files:
- .env
Der folgende Schritt dient der Aktualisierung der Shopware-Bereitstellung durch:
- Hinzufügen des Feldes
spec.template.spec.containers.envFrom[]
.... envFrom: - configMapRef: name: env ...
- Erstellen eines Volumes in
spec.template.spec.volumes[]
mit dem Inhalt der .env-Datei.... volumes: - name: env-file configMap: name: env-file ...
- Einsetzen des Volumes als .env-Datei in
spec.template.spec.containers[].volumeMounts
;subPath: .env
ist wichtig, damit .env als Datei eingesetzt werden kann.... volumeMounts: - mountPath: /app/.env subPath: .env name: env-file ...
Starte die Entwicklung mit dem Befehl devspace dev
Du musst zuerst die folgenden Elemente konfigurieren:
Quelldateien müssen zwischen Deinem Computer und Kubernetes synchronisiert werden. Portweiterleitung müssen auf das Storefront auf http://localhost:8000
zugreifen können. Protokolle müssen zu Stdout fließen. DevSpace muss die Konfiguration aus der lokalen devspace.yaml-Datei lesen. Du kannst diese Datei erzeugen, indem Du devspace init
ausführst und den Pfad zur Dockerfile
angibst.
Meine Empfehlung ist jedoch, die vorkonfigurierte, projektspezifische Datei zu verwenden.
Der Entwicklungsbereich in devspace.yaml
ist dev
. Du findest es im Stammverzeichnis der Datei.
Einstellungen für die Dateisynchronisation
Wichtig ist hier die Einstellung initialSync: preferLocal
. Die Synchronisation ist bidirektional. Diese Einstellung stellt sicher, dass DevSpace bei der ersten Synchronisierung keine lokalen Dateien löscht.
...
dev:
sync:
- labelSelector:
app: app-server
excludePaths:
- .gitignore
- .gitkeep
- .git
initialSync: preferLocal
localSubPath: ./docker/shopware/custom/plugins
containerPath: /app/custom/plugins
namespace: development
onUpload:
restartContainer: false
...
IIm obigen Beispiel wird nur der Ordner /app/custom/plugins
synchronisiert - dort sind alle benutzerdefinierten Plug-ins gespeichert. Es ist nicht notwendig, etwas anderes zu synchronisieren, da sich der gesamte benutzerdefinierte Code im Plug-in-Verzeichnis befindet.
Webserver-Portweiterleitung
...
dev:
ports:
- labelSelector:
app: app-server
remotePort: 9003
forward:
- port: 8000
remotePort: 8000
...
Logs output
...
dev:
logs:
showLast: 100
sync: true
selectors:
- labelSelector:
app: app-server
Dies findet den laufenden Pod anhand seines Labels, druckt zunächst die letzten 100 Zeilen der Protokolle aus und streamt weiter.
Debugge Shopware mit Xdebug
Im Basis-Container-Image ist das PHP-Modul Xdebug bereits installiert, sodass hier keine weiteren Änderungen erforderlich sind. Aktiviere einfach die umgekehrte Weiterleitung auf Port 9003 (SSH-Tunnel).
...
- labelSelector:
app: app-server
reverseForward:
- port: 9003
remotePort: 9003
Installiere als Nächstes die Xdebug-Chrome-Extension oder füge Xdebug-Bookmarklets zur Symbolleiste Deines Browsers hinzu. Erstelle sie einfach und ziehe sie in die Symbolleiste.
Starte dann Dev Mode mit dem Befehl: devspace dev
. In der Konsole sollte die folgende Meldung erscheinen:
[done] √ Reverse port forwarding started at 9003:9003
Vergiss nicht, den Debug Connection Listener in Deiner IDE zu aktivieren. Für PhpStorm ist es: Jetzt bist Du bereit für die Fehlerbehebung. Setze den Haltepunkt im Code und lade die Seite neu. Es kann auch notwendig sein, dass Du die Pfade zwischen Deinem lokalen Projektpfad und dem Server zuordnen musst.
Summary
Dieses Projekt befindet sich noch in der Proof-of-Concept-Phase. Es zeigt aber bereits, wie effizient Shopware 6 innerhalb eines Kubernetes-Clusters entwickelt werden kann. Ich möchte Dich ermutigen, es selbst auszuprobieren. Natürlich bist Du herzlich eingeladen, ebenfalls einen Beitrag zu leisten.
Shopware auf Kubernetes Webinar
Wenn das Thema für dich interessant ist, findest du hier eine Aufzeichnung des Webinars Shopware 6 in der Cloud im Kubernetes-Cluster.