Shopware6 on Kubernetes Development

Shopware 6 auf Kubernetes Entwicklung

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.

Diagram: Shopware 6 Dev on Kubernetes
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: Xdebug listen for connection icon 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.

Webinar-Alarm!

Wenn das Thema für Sie interessant ist, besteht die Möglichkeit, Shopware 6 in der Cloud im Kubernetes-Cluster während eines Webinars zu sehen. Ich werde es am 29. Juni 2021 um 11:00 Uhr leiten.

Jetzt anmelden!

Live webinar banner. 29 June 2021, 11AM (CEST)

FacebookTwitterPinterest