Magento 2 auf Kubernetes – wie funktioniert das?

In diesem Artikel werden wir uns näher ansehen, wie Sie Magento 2 auf Kubernetes ausführen können. Fangen wir an!

Voraussetzungen

Dieser Artikel setzt voraus, dass Sie über grundlegende Kenntnisse verfügen im Umgang mit Magento 2, Containern (Docker) und grundlegenden Konzepten von Kubernetes.

Sie benötigen ein laufendes Kubernetes-Cluster mit mindestens 4 CPU und 8 GB RAM. Minikube ist eine gute Option, um das Cluster lokal zu testen.

Wir benötigen außerdem einen Ingress-Controller (NGINX), damit wir auf Magento zugreifen können, sobald es installiert ist – Anweisungen zur Bereitstellung finden Sie in der Dokumentation.

Wenn Sie Minikube installieren möchten, können Sie ein Cluster mit den erforderlichen Kapazitäten starten, indem Sie make minikube im zugehörigen Repository ausführen.

Zusätzlich werden wir die folgenden Tools verwenden:

  • Kubectl mit richtig konfiguriertem Kontext,
  • Eine eigenständige Version von Kustomize,
  • (Optional, aber empfohlen) Make.

Sobald das Cluster und die Tools eingerichtet sind, können wir mit der Bereitstellung von Magento beginnen. Sie finden alle notwendigen Dateien in unserem Magento 2 auf Kubernetes GitHub-Repository.

Lassen Sie uns Schritt für Schritt durch den Bereitstellungsprozess gehen.

Schritt 1: Erstellen Sie eine minimale Magento-2-Bereitstellung

Magento

Magento 2 auf Kubernetes GitHub-Repository
Natürlich brauchen wir einen Container, auf dem Magento selbst ausgeführt wird. Beginnen wir also damit.

Aber zuerst müssen wir einige der Aspekte durchgehen, die Sie bei der Ausführung einer PHP-Webanwendung auf Kubernetes berücksichtigen müssen. Diese brauchen Sie, um diverse Architekturentscheidungen in diesem Artikel zu verstehen.

PHP-Webanwendungs-Pod-Muster

Es gibt verschiedene Muster für die Bereitstellung einer PHP-Webanwendung auf Kubernetes – von Einzelprozessen, Einzelcontainern und Multiprozesscontainern bis hin zu mehreren Einzelprozesscontainern.

All-in-One
Apache 2 with mod_php
Apache mit mod_php in einem Einzelcontainer

Am einfachsten ist es, einen einzelnen Container zu haben, auf dem Apache 2 mit mod_php in einem einzigen Prozess ausgeführt wird – eine Anordnung, die in Tutorials recht häufig verwendet wird. Obwohl ein All-in-One-Container am einfachsten zu konfigurieren und zu verwalten ist, sollten Sie dennoch in Betracht ziehen, NGINX für die Bereitstellung statischer Inhalte zu verwenden – entweder als dedizierten Pod oder als Reverse-Proxy für das Caching.

NGINX + PHP-FPM in einem Einzelcontainer
NGINX and PHP-FPM
NGINX und PHP-FPM in einem Einzelcontainer

Wenn Sie sich für NGINX entscheiden, benötigen Sie PHP-FPM dazu. Sie benötigen außerdem entweder ein benutzerdefiniertes Skript oder einen Prozessmanager (z.B. Supervisord), um beide in einem einzigen Container auszuführen. In einigen Fällen ist dies zwar in Ordnung, aber als Faustregel gilt, dass Sie mehrere Prozesse in einem Container vermeiden sollten. Dennoch bietet dies Ihnen die Vorteile, bessere Leistung zu erbringen und alle Codes in einem einzigen Container zu haben. Sie sollten es daher möglicherweise in Betracht ziehen.

Es ist in Ordnung, mehrere Prozesse zu haben. Um aber den größten Nutzen aus Docker zu ziehen, vermeiden Sie, dass ein Container für mehrere Aspekte Ihrer Gesamtanwendung verantwortlich ist. Sie können mehrere Container über benutzerdefinierte Netzwerke und gemeinsame Datenträger verbinden.

Einzelner Pod, der zwei Container ausführt
NGINX and PHP-FPM in separate containers
NGINX und PHP-FPM in getrennten Containern, aber in einem Pod

In dieser Konfiguration werden NGINX und PHP-FPM in getrennten Containern ausgeführt und kommunizieren über das Netzwerk, anstatt über einen Socket. So brauchen wir Supervisord nicht mehr. Stattdessen können wir jedem Container spezifische Bereitschafts- und Lebendigkeitssonden zuweisen und haben mehr Kontrolle über die Ressourcenzuteilung.

Es gibt jedoch einen Vorbehalt: Wir müssen sicherstellen, dass NGINX auf statische und Mediendateien zugreifen kann. Dies kann auf zwei Arten erreicht werden: Entweder durch die Erstellung eines benutzerdefinierten NGINX-Images mit darin enthaltenen Projektdateien oder durch die gemeinsame Nutzung von Projektdateien zwischen NGINX- und PHP-Containern über einen Datenträger. Bei Letzterem müssen Sie einen Datenträger erstellen, der von den Containern im Pod gemeinsam genutzt wird (emptyDir würde hier passen), und die Dateien vom PHP-Container auf den Datenträger bei der Pod-Initialisierung (d.h. in einen Init-Container) kopiert.

In diesem Artikel verwenden wir die zweite Methode, da wir so vermeiden, dass wir zwei Images versionieren müssen, und um sicherzustellen, dass die bereitgestellten Versionen immer synchron sind. Die Einzelheiten der Implementierung werden in einem der folgenden Abschnitte beschrieben.

Webserver und PHP in separaten Pods
NGINX and PHP-FPM in separate containers
NGINX und PHP-FPM in separaten Pods

Dieses Muster ist dem vorherigen recht ähnlich, außer dass es uns erlaubt, die PHP- und NGINX-Pods unabhängig voneinander zu skalieren. Wir können hier keinen emptyDir-Datenträger verwenden, um Dateien zwischen den Pods auszutauschen, und müssen die richtigen persistenten Datenträger für statische Assets und Mediendateien konfigurieren.

Was ist am besten?

Einerseits sind Einzelprozess-Apache+PHP-Container einfacher zu verwalten, andererseits ist NGINX für seine überlegene Leistung bei der Bereitstellung statischer Inhalte bekannt. Wenn man es und PHP-FPM auf separate Pods verteilt, können sie unabhängig voneinander skaliert werden. Dennoch beruht diese Skalierbarkeit auf einer höheren Komplexität, sodass es immer am besten ist, selbst Benchmarks durchzuführen und dabei Faktoren wie erwartete Verkehrsmuster, CDN-Nutzung und Caching zu berücksichtigen.

Leider gibt es keinen Königsweg. Die Entscheidung, welches Muster bei einem bestimmten Projekt verwendet werden soll, liegt letztendlich bei Ihnen.

Magento-Docker-Image

Wie oben besprochen, verwenden wir ein Docker-Image auf der Basis von php:7.2-fpm und ein einfaches NGINX:mainline-Image.

Konfiguration über die Umgebung

Bei der Bereitstellung von Kubernetes-Anwendungen ist es normalerweise am besten, jedes Programm durch das Setzen von Umgebungsvariablen auf seinem Container zu konfigurieren.

Es ist zwar möglich, ConfigMaps als normale Konfigurationsdateien in Container einzubinden. Dies ist jedoch nicht ideal. Verschiedene Anwendungen verwenden unterschiedliche Konfigurationsformate und häufig müssen die Werte in mehreren Anwendungen übereinstimmen. Die Konfigurationsverwaltung wird so schnell unnötig kompliziert.

Umgekehrt müssen Sie bei Umgebungsvariablen alles nur einmal als Schlüssel-Werte-Paare definieren. Sie können diese Werte dann einfach durch Bezugnahme auf ihren Schlüssel (Variablenname) übergeben. Auf diese Weise haben Sie eine einzige wahre Quelle für jeden benötigten Konfigurationswert.

Umgebungsvariablen in Magento 2

Eines der Merkmale von Magento 2 ist die Fähigkeit, Einstellungen aus der Umgebung zu übernehmen – das Setzen einer Umgebungsvariable wie <SCOPE>__<SYSTEM__VARIABLE__NAME> hat die gleiche Wirkung, wie wenn Sie die Einstellung in app/etc/config.php schreiben.

Wenn Sie zum Beispiel Elasticsearch als Suchmaschine konfigurieren möchten, weist das Setzen der Umgebungsvariable CONFIG__DEFAULT__CATALOG__SEARCH__ENGINE=elasticsearch6 Magento an, die Katalogsuchmaschinenoption auf „Elasticsearch 6“ für den Standardbereich zu setzen. Außerdem wird dabei die Einstellung im Verwaltungsbereich gesperrt, um versehentliche Änderungen zu verhindern.

Leider kann diese Funktion nicht zur Steuerung von umgebungsspezifischen Einstellungen wie z.B. Datenbank-Anmeldeinformationen verwendet werden. Es gibt jedoch einige Möglichkeiten, dies zu umgehen:

  • app/etc/env.php aus einer ConfigMap mounten. Das ist keine ideale Lösung, da Magento an verschiedenen Stellen prüft, ob es Schreibzugriff auf die Datei hat. Sie könnte jedoch während der Pod-Initialisierung von einer ConfigMap kopiert werden, um den Schreibzugriff zu ermöglichen. (https://github.com/magento/magento2/issues/4955)
  • Verwenden Sie bin/magento, um die Konfiguration aus den Umgebungsvariablen zu übernehmen und sie während der Pod-Initialisierung an Magento zu übergeben. Es ist im Wesentlichen dasselbe wie die Konfiguration einer Magento-Instanz über CLI, nur dass es automatisiert abläuft. Das Speichern der Konfiguration dauert jedoch recht lange, wodurch sich die Startzeit jedes Magento-Pods erheblich verlängert.
  • Modifizieren Sie app/etc/env.php und fügen Sie sie in das Container-Image ein. Da env.php eine normale PHP-Datei ist, die ein Array mit der Konfiguration zurückgeben muss, ist die in PHP eingebaute Funktion getenv() perfekt geeignet, um während der Ausführung Werte aus der Umgebung zu übernehmen, z.B. 'dbname' => getenv('DB_NAME'). Diese Methode stellt sicher, dass Magento in die env.php-Datei schreiben kann, ohne dass während der Pod-Initialisierung zusätzliche Zeit benötigt wird.

Verwaltung von Protokollen

Ein weiterer Punkt, der bei der Bereitstellung von Magento 2 auf Kubernetes zu berücksichtigen ist, ist sicherzustellen, dass alle relevanten Protokolle Container-Neustarts überstehen und leicht zugänglich sind.

Die einfachste Lösung wäre, PersistentVolume für die Verzeichnisse var/log und var/reports zu verwenden. Ein Datenträger löst das Problem der Log-Persistenz, kann aber bei vielen Magento-Instanzen, die in dieselben Dateien schreiben, Performance-Probleme verursachen. Außerdem werden die Protokolle selbst schnell zu lang, um effizient in ihnen navigieren zu können.

Um beiden Anforderungen gerecht zu werden, verwenden wir das Sidecar-Muster – ein separater Container, der für das Lesen von wachsenden Protokolldateien und die Ausgabe ihres Inhalts an stdout für ein separates Tool (Elastic Stack, Fluentd) zum Speichern und Verarbeiten verantwortlich ist.

Tipp: In diesem Beispiel verwenden wir einen Container pro Protokolldatei, die mit tail -f ausgeführt wird, um Protokolle in stdout zu erstellen. Während dies für eine einfache Magento-Bereitstellung recht gut funktioniert, lässt es sich bei mehreren zu verarbeitenden Dateien nicht gut skalieren.

Eine bessere Lösung wäre es, die PSR-3-Kompatibilität von Magento zu nutzen und alle relevanten Log-Handler so zu konfigurieren, dass sie direkt auf stdout/stderr protokollieren. Damit würde Faktor XI der Zwölf-Faktor-App-Methodik erfüllt und die Sidecar-Container überflüssig werden.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: magento-php
  labels:
    app: magento-php
    k8s-app: magento
spec:
  selector:
    matchLabels:
      app: magento-php
  template:
    metadata:
      labels:
        app: magento-php
        k8s-app: magento
    spec:
      containers:
      - image: kiweeteam/magento2:vanilla-2.3.2-php7.2-fpm
        name: magento-php
        volumeMounts:
        - name: logs
          mountPath: /var/www/html/var/log
      - image: busybox
        name: system-log
        command: ["/bin/sh"]
        args:
        - -c
        - |
          touch /var/www/html/var/log/system.log
          chown 33:33 /var/www/html/var/log/system.log
          tail -n+1 -f /var/www/html/var/log/system.log
        resources:
          limits:
            cpu: 5m
            memory: 64Mi
          requests:
            cpu: 5m
            memory: 64Mi
        volumeMounts:
        - name: logs
          mountPath: /var/www/html/var/log
      volumes:
      - name: logs
        emptyDir: {}
Teil des Magento-Bereitstellungsmanifests, in dem die Sidecars definiert sind.

Cron

Magento verlässt sich bei einigen seiner Funktionen auf Cronjobs.

In einem typischen Einsatzszenario würden Sie einen der Hosts mit der Ausführung von Cronjobs beauftragen und diese direkt über Crontab konfigurieren. Eine solche Einrichtung würde jedoch nicht mit Kubernetes funktionieren, da es keine einzige Magento-Instanz (Container) gibt, die garantiert immer ausgeführt wird. Deshalb werden wir einen Kubernetes-CronJob verwenden, der bin/magento cron:run jederzeit ausführt.

Auf diese Weise delegieren wir die Verantwortung für die Ausführung von Cron an Kubernetes, das einen neuen temporären Container startet und die gegebenen Befehle bis zur Fertigstellung ausführt – ganz, ohne dass Sie sich Sorgen machen müssen, dass eine der Magento-Instanzen immer läuft.

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: magento-cron
  namespace: default
spec:
  schedule: '* * * * *'
  jobTemplate:
    metadata:
      creationTimestamp: null
    spec:
      template:
        metadata:
          labels:
            app: magento-cron
            k8s-app: magento
        spec:
          containers:
          - name: magento-cron
            image: kiweeteam/magento2:vanilla-2.3.2-php7.2-fpm
            command: ["/bin/sh"]
            args:
            - -c
            - |
              php bin/magento cron:run
            envFrom:
            - configMapRef:
                name: config
            - configMapRef:
                name: aux
            resources:
              limits:
                cpu: 500m
                memory: 4Gi
              requests:
                cpu: 50m
                memory: 1Gi
          restartPolicy: Never
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 600
  failedJobsHistoryLimit: 20
  successfulJobsHistoryLimit: 5
CronJob-Manifest zur Ausführung von Magento-2-Cronjobs auf Kubernetes.

Tipp: Wenn Sie Magento-Cron als Kubernetes-CronJobs ausführen, stellen Sie sicher, dass jeder von ihnen als Einzelprozess konfiguriert ist. Dies können Sie leicht durch das Setzen der folgenden Umgebungsvariablen erreichen:

CONFIG__DEFAULT__SYSTEM__CRON__INDEX__USE_SEPARATE_PROCESS=0
CONFIG__DEFAULT__SYSTEM__CRON__DEFAULT__USE_SEPARATE_PROCESS=0
CONFIG__DEFAULT__SYSTEM__CRON__CONSUMERS__USE_SEPARATE_PROCESS=0
CONFIG__DEFAULT__SYSTEM__CRON__DDG_AUTOMATION__USE_SEPARATE_PROCESS=0

Andernfalls kann es vorkommen, dass der Croncontainer vor der Ausführung aller geplanten Jobs beendet wird.

Zusätzliche Jobs

Ein weiterer Kubernetes-Objekttyp, den wir in diesem Projekt verwenden werden, ist ein Job.

Ein Job führt einen oder mehrere Pods aus, die für den Abschluss einer Aufgabe verantwortlich sind. Wir werden Jobs verwenden, um Aufgaben auszuführen, die bei der Bereitstellung von Magento erforderlich sind, aber nicht für die Init-Container in Magento-Pods geeignet wären:

  • magento-unpack ist verantwortlich für das Entpacken aller statischer Assets (die während der Erstellung in das Container-Image eingebrannt wurden) in einen von PHP und NGINX gemeinsam genutzten Datenträger. Da die Assets für jede Instanz einer bestimmten Version genau gleich sind, brauchen wir dies nur einmal pro Versionsaufstellung zu tun.
  • magento-install automatisiert die Anwendungsinstallation: Es installiert das Datenbankschema, generiert Performance-Einrichtungen, die wir als Beispieldaten für die Demonstration verwenden, und stellt sicher, dass sich alle Indizes im „On Schedule“-Aktualisierungsmodus befinden. In einem realen Szenario würden Sie wahrscheinlich an dieser Stelle bin/magento setup:upgrade ausführen, um das Schema bei jeder neuen Bereitstellung zu aktualisieren.
apiVersion: batch/v1
kind: Job
metadata:
  name: magento-unpack
spec:
  template:
    metadata:
      name: unpack
      labels:
        app: magento-unpack
        k8s-app: magento
    spec:
      containers:
      - name: magento-unpack
        image: kiweeteam/magento2:vanilla-2.3.2-php7.2-fpm
        command: ["/bin/sh"]
        args:
        - -c
        - |
          /bin/bash <<'EOF'
          rsync -avc /var/www/html/pub/static/frontend/ /tmp/static/frontend/ --delete
          rsync -avc /var/www/html/pub/static/adminhtml/ /tmp/static/adminhtml/ --delete
          rsync -avc /var/www/html/pub/static/deployed_version.txt /tmp/static/deployed_version.txt --delete
          EOF
        volumeMounts:
        - name: static
          mountPath: /tmp/static
      restartPolicy: OnFailure
      volumes:
      - name: static
        persistentVolumeClaim:
          claimName: static
Magento-unpack-Job
apiVersion: batch/v1
kind: Job
metadata:
  name: magento-install
spec:
  template:
    metadata:
      name: install
      labels:
        app: magento-install
        k8s-app: magento
    spec:
      containers:
      - name: magento-setup
        image: kiweeteam/magento2:vanilla-2.3.2-php7.2-fpm
        command: ["/bin/sh"]
        args:
        - -c
        - |
          /bin/bash <<'EOF'
          bin/install.sh
          php bin/magento setup:perf:generate-fixtures setup/performance-toolkit/profiles/ce/small.xml
          magerun index:list | awk '{print $2}' | tail -n+4 | xargs -I{} magerun index:set-mode schedule {}
          magerun cache:flush
          EOF
        envFrom:
        - configMapRef:
            name: config
        volumeMounts:
        - mountPath: /var/www/html/pub/media
          name: media
      volumes:
      - name: media
        persistentVolumeClaim:
          claimName: media
      restartPolicy: OnFailure
Magento-install-Job

Tipp: Beachten Sie, dass es nicht möglich ist, die Jobs mit jeder neuen Version zu aktualisieren, da das Pod-Vorlagenfeld des Jobs unveränderlich ist. Stattdessen müssen Sie sicherstellen, dass Sie die alten löschen und für jede bereitgestellte Revision neue erstellen.

Datenbank

Für die Datenbank verwenden wir einfach ein StatefulSet, auf dem Percona 5.7 mit den in einem PersistentVolume gespeicherten Daten ausgeführt wird.

Ein einfaches StatefulSet funktioniert gut für ein kleines, lokales Cluster/Entwicklungscluster. Für größere Bereitstellungen sollten Sie aber die Einrichtung eines Xtradb-Clusters (z.B. mit Percona Kubernetes Operator) in Erwägung ziehen. Eine solche Lösung erfordert mehr Ressourcen und erhöht die Komplexität. Führen Sie daher auf alle Fälle geeignete Benchmarks durch, um sicherzustellen, dass sich die Investition lohnt.

apiVersion: v1
kind: Service
metadata:
  name: db
  labels:
    app: db
    k8s-app: magento
spec:
  selector:
    app: db
  ports:
  - name: db
    port: 3306

---

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  selector:
    matchLabels:
      app: db
  serviceName: db
  template:
    metadata:
      labels:
        app: db
        k8s-app: magento
    spec:
      containers:
      - args:
        - --max_allowed_packet=134217728
        volumeMounts:
        - mountPath: /var/lib/mysql
          name: data
        env:
        - name: MYSQL_DATABASE
          valueFrom:
            configMapKeyRef:
              name: config
              key: DB_NAME
        - name: MYSQL_PASSWORD
          valueFrom:
            configMapKeyRef:
              name: config
              key: DB_PASS
        - name: MYSQL_USER
          valueFrom:
            configMapKeyRef:
              name: config
              key: DB_USER
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            configMapKeyRef:
              name: config
              key: DB_ROOT_PASS
        image: percona:5.7
        name: db
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
      restartPolicy: Always
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
      - ReadWriteMany
      resources:
        requests:
          storage: 10Gi
Datenbank-Bereitstellungsmanifest

Ingress

An diesem Punkt fehlt uns für eine funktionierende Magento-2-Instanz auf Kubernetes nur noch eine Möglichkeit, auf das Frontend zuzugreifen.

Wir könnten den Magento-Webservice einfach exponieren, indem wir seinen Typ entweder als NodePort einstellen, um ihn auf einem bestimmten Port zu exponieren, oder als LoadBalancer, um ihn über einen externen Load Balancer zu exponieren. In diesem Fall werden wir jedoch einen Ingress-Controller verwenden – auf diese Weise erhalten wir eine Out-of-the-Box-TLS-Terminierung zusammen mit der Möglichkeit, die TLS-Zertifikate auf deklarative Weise zu verwalten (z.B. mithilfe eines Zertifikatsmanagers). Wir könnten sogar zusätzliche Dienste mit Routing auf der Grundlage von Pfaden oder (Unter-)Domänen exponieren, falls gewünscht.

Unter der Annahme, dass der NGINX-Ingress-Controller bereits installiert ist, müssen wir hier nur eine Ingress-Definition erstellen, die den gesamten Datenverkehr zum Magento-Webservice auf dem HTTP-Port proxyliert.

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: "nginx"
    kubernetes.io/tls-acme: "false"
  name: main
spec:
  backend:
    serviceName: magento-web
    servicePort: http
Ingress-Manifest

Fassen wir alles zusammen

Um den bisher besprochenen Stack zu implementieren, führen Sie make step-1 im zugehörigen Repository aus. Make lädt automatisch alle Abhängigkeiten herunter, die für die Ausführung dieses Schrittes benötigt werden, und stellt alles auf Ihrem Kubernetes-Cluster bereit.

apiVersion: v1
kind: Service
metadata:
  name: magento-web
  labels:
    app: magento-web
    k8s-app: magento
spec:
  ports:
  - name: "http"
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: magento-web

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: magento-web
  labels:
    app: magento-web
    k8s-app: magento
spec:
  selector:
    matchLabels:
      app: magento-web
  strategy:
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 30%
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: magento-web
        k8s-app: magento
    spec:
      containers:
      - image: nginx:mainline
        imagePullPolicy: Always
        name: magento-web
        ports:
        - containerPort: 80
          protocol: TCP
        resources:
          limits:
            cpu: 10m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 128Mi
        volumeMounts:
        - mountPath: /etc/nginx/conf.d/default.conf
          name: nginx-config
          subPath: default.conf
        - mountPath: /var/www/html/magento2.conf
          name: nginx-config
          subPath: magento2.conf
        - name: media
          mountPath: /var/www/html/pub/media
        - mountPath: /var/www/html/pub/static
          name: static
      volumes:
      - configMap:
          defaultMode: 420
          name: nginx
        name: nginx-config
      - name: media
        persistentVolumeClaim:
          claimName: media
      - name: static
        persistentVolumeClaim:
          claimName: static
Magento-Web-Bereitstellungsmanifest
apiVersion: v1
kind: Service
metadata:
  name: magento-php
  labels:
    app: magento-php
    k8s-app: magento
spec:
  ports:
  - name: "fpm"
    port: 9000
    protocol: TCP
    targetPort: 9000
  selector:
    app: magento-php

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: magento-php
  labels:
    app: magento-php
    k8s-app: magento
spec:
  replicas: 1
  selector:
    matchLabels:
      app: magento-php
  strategy:
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 30%
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: magento-php
        k8s-app: magento
    spec:
      containers:
      - image: kiweeteam/magento2:vanilla-2.3.2-php7.2-fpm
        imagePullPolicy: IfNotPresent
        name: magento-php
        ports:
        - containerPort: 9000
          protocol: TCP
        readinessProbe:
          failureThreshold: 5
          initialDelaySeconds: 10
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: 9000
          timeoutSeconds: 1
        livenessProbe:
          failureThreshold: 5
          initialDelaySeconds: 10
          periodSeconds: 10
          successThreshold: 1
          tcpSocket:
            port: 9000
          timeoutSeconds: 1
        resources:
          limits:
            cpu: 250m
            memory: 1Gi
          requests:
            cpu: 250m
            memory: 1Gi
        envFrom:
        - configMapRef:
            name: config
        - configMapRef:
            name: aux
        volumeMounts:
        - name: logs
          mountPath: /var/www/html/var/log
        - name: media
          mountPath: /var/www/html/pub/media
        - name: static
          mountPath: /var/www/html/pub/static
      - image: busybox
        imagePullPolicy: IfNotPresent
        name: system-log
        command: ["/bin/sh"]
        args:
        - -c
        - |
          touch /var/www/html/var/log/system.log
          chown 33:33 /var/www/html/var/log/system.log
          tail -n+1 -f /var/www/html/var/log/system.log
        resources:
          limits:
            cpu: 5m
            memory: 64Mi
          requests:
            cpu: 5m
            memory: 64Mi
        volumeMounts:
        - name: logs
          mountPath: /var/www/html/var/log
      - image: busybox
        imagePullPolicy: IfNotPresent
        name: exception-log
        command: ["/bin/sh"]
        args:
        - -c
        - |
          touch /var/www/html/var/log/exception.log
          chown 33:33 /var/www/html/var/log/exception.log
          tail -n+1 -f /var/www/html/var/log/exception.log
        resources:
          limits:
            cpu: 5m
            memory: 64Mi
          requests:
            cpu: 5m
            memory: 64Mi
        volumeMounts:
        - name: logs
          mountPath: /var/www/html/var/log
      - image: busybox
        imagePullPolicy: IfNotPresent
        name: debug-log
        command: ["/bin/sh"]
        args:
        - -c
        - |
          touch /var/www/html/var/log/debug.log
          chown 33:33 /var/www/html/var/log/debug.log
          tail -n+1 -f /var/www/html/var/log/debug.log
        resources:
          limits:
            cpu: 5m
            memory: 64Mi
          requests:
            cpu: 5m
            memory: 64Mi
        volumeMounts:
        - name: logs
          mountPath: /var/www/html/var/log
      volumes:
      - name: logs
        emptyDir: {}
      - name: media
        persistentVolumeClaim:
          claimName: media
      - name: static
        persistentVolumeClaim:
          claimName: static
Magento-PHP-Bereitstellungsmanifest

Inzwischen sollten wir eine funktionierende, wenn auch nackte Magento-Bereitstellung auf Kubernetes haben. Uns fehlen noch einige wesentliche Teile: nämlich eine anständige Suchmaschine und ein Cache.

Machen wir weiter und fügen sie hinzu!

Schritt 2: Elasticsearch

Was ist ein Online-Shop ohne die Möglichkeit, ihn zu durchsuchen? Mit Magento 2, das Elasticsearch out-of-the-box unterstützt, müssen wir nur noch Elasticsearch selbst bereitstellen, um sicherzustellen, dass die Kunden leicht das finden, was sie suchen. Lassen Sie uns also damit anfangen.

Während es vollkommen in Ordnung wäre, eine benutzerdefinierte Bereitstellung oder ein StatefulSet zu erstellen, werden wir die Möglichkeiten der Elastic Cloud auf Kubernetes Operator nutzen, um den Prozess zu vereinfachen.

Elasticsearch-Cluster

Nachdem wir den Operator installiert haben, müssen wir ihm die gewünschte Elasticsearch-Clusterarchitektur mitteilen und Magento die Informationen zur Verbindung mit diesem Cluster zur Verfügung stellen.
Aufgrund begrenzter Ressourcen (Minikube auf einem Laptop) entscheiden wir uns für ein einfaches Elasticsearch-6.x-Cluster mit einem Knoten und einigermaßen begrenzten Ressourcen. Da es keinem öffentlichen Netzwerk ausgesetzt ist, können wir der Einfachheit halber die Authentifizierung und TLS deaktivieren.

Für unsere Zwecke reicht diese Einrichtung aus. Für ein größeres Projekt mit höherem Verkehrsaufkommen sollten Sie aber die Einrichtung eines Elastic-Clusters mit mehr Knoten und jeweils mehr Ressourcen in Erwägung ziehen. Und auch hier ist es immer eine gute Idee, Benchmarks speziell für Ihr Projekt durchzuführen, um sicherzustellen, dass Sie genau die Konfiguration haben, die für Sie geeignet ist.

apiVersion: elasticsearch.k8s.elastic.co/v1beta1
kind: Elasticsearch
metadata:
  name: elasticsearch
  namespace: default
spec:
  version: 6.8.5
  nodeSets:
  - name: elasticsearch
    count: 1
    config:
      node.master: true
      node.data: true
      node.ingest: true
      node.store.allow_mmap: false
      xpack.security.authc:
        anonymous:
          username: anonymous
          roles: superuser
          authz_exception: false
    podTemplate:
      spec:
        containers:
        - name: elasticsearch
          env:
          - name: ES_JAVA_OPTS
            value: -Xms512m -Xmx512m
          resources:
            requests:
              memory: 1Gi
              cpu: 0.5
            limits:
              memory: 1Gi
              cpu: 1
  http:
    tls:
      selfSignedCertificate:
        disabled: true
Elasticsearch-Manifest unter Verwendung von Elastic Cloud auf Kubernetes

Um Elastic Stack zu implementieren und zu konfigurieren, führen Sie make step-2 aus.

Tipp: Elastic Cloud kann in ähnlicher Weise auf Kubernetes Operator verwendet werden, um zusammen mit Kibana ein weiteres Elasticsearch-Cluster zur Protokollverwaltung einzusetzen.

Magento-Konfiguration

Jetzt muss Magento nur noch auf die neu geschaffene Elasticsearch-Instanz verwiesen werden. Hierfür können wir leicht auf die aux.env-Konfiguration verweisen mit

CONFIG__DEFAULT__CATALOG__SEARCH__ELASTICSEARCH6_SERVER_HOSTNAME=elasticsearch-es-http
CONFIG__DEFAULT__CATALOG__SEARCH__ELASTICSEARCH6_SERVER_PORT=9200
CONFIG__DEFAULT__CATALOG__SEARCH__ENGINE=elasticsearch6

und Kustomize sich um das Zusammenführen von Konfigurationsdateien und deren Übergabe an Magento als Umgebungsvariablen kümmern lassen.

Schritt 3: Redis und Autoscaling

Nachdem Elasticsearch im vorherigen Abschnitt konfiguriert wurde, haben wir nun alle funktionsbezogenen Teile parat. Vielleicht ist Ihnen aber auch aufgefallen, dass die Leistung von Magento in dieser Konfiguration nicht gerade überragend ist. Machen Sie sich keine Sorgen – wir haben bisher fast kein Caching genutzt!
Mit anderen Worten: Wir haben es geschafft! Jetzt beschleunigen wir es aber noch mit Redis und Autoscaling.

Redis

Redis spielt zwei wesentliche Rollen in jeder leistungsfähigen Magento-2-Bereitstellung:

  • Schnelle Sitzungsspeicherung, um mehreren Anwendungsinstanzen die Verfolgung von Sitzungsinformationen zwischen den Anfragen zu ermöglichen, und
  • Cache-Speicherung für den internen Magento-Cache (z.B. Konfiguration, Layout, HTML-Fragmente).

Auch hier werden wir ein einfaches StatefulSet verwenden, um eine einzelne Redis-Instanz mit separaten Datenbanken für Sitzungen und Cache zu betreiben. Wir müssen keine PersistentVolumes anzuhängen, also lassen wir es.

Wie bei Elasticsearch müssen wir zum Schluss Magento anweisen, die neu eingesetzte Redis-Instanz zu verwenden. Wie zuvor werden wir aux.env um ein paar Schlüssel erweitern und Kustomize die Zusammenführung der Teile überlassen:

REDIS_CACHE_HOST=redis
REDIS_CACHE_PORT=6379
REDIS_CACHE_DB=0
REDIS_SESSION_HOST=redis
REDIS_SESSION_PORT=6379
REDIS_SESSION_DB=2
apiVersion: v1
kind: Service
metadata:
  name: redis
  labels:
    app: redis
    k8s-app: magento
spec:
  selector:
    app: redis
  ports:
  - name: redis
    port: 6379
    protocol: TCP
    targetPort: 6379

---

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  selector:
    matchLabels:
      app: redis
  serviceName: redis
  template:
    metadata:
      labels:
        app: redis
        k8s-app: magento
    spec:
      containers:
      - name: redis
        image: redis
        imagePullPolicy: Always
        resources:
          limits:
            cpu: 500m
            memory: 4Gi
          requests:
            cpu: 50m
            memory: 1Gi
Redis-StatefulSet

Horizontale Pod-Autoscaler

Mit Redis können wir jetzt mehrere Instanzen von Magento ausführen und Sitzungsinformationen und Cache gemeinsam nutzen. Wir könnten die Anzahl der Replikate bei Bedarf im Manifest für die Bereitstellung von Magento einfach erhöhen. Aber warum sollen wir nicht das volle Potenzial nutzen, das uns Magento 2 auf Kubernetes bietet? Lassen Sie uns stattdessen horizontale Pod-Autoscaler erstellen. So kann Kubernetes die optimale Anzahl zu jedem beliebigen Zeitpunkt selbst ermitteln.

Zu diesem Zweck werden wir einen neuen HorizontalPodAutoscaler erstellen. Er überwacht die Ressourcennutzung der in scaleTargetRef definierten Pods und startet neue, wenn targetCPUUtilizationPercentage über den definierten Schwellenwert hinausgeht, bis es maxReplicas erreicht.

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: magento-php
spec:
  maxReplicas: 5
  minReplicas: 2
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: magento-php
  targetCPUUtilizationPercentage: 75

---

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: magento-web
spec:
  maxReplicas: 3
  minReplicas: 1
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: magento-web
  targetCPUUtilizationPercentage: 75
HorizontalPodAutoscaler-Manifesten

Tip zu HorizontalPodAutoscaler-Manifesten: In diesem Artikel haben wir Magento-Pods absichtlich begrenzte Ressourcen zugewiesen, um das Autoscaling besser in Aktion zu erleben. Bei der Bereitstellung von Magento 2 auf Kubernetes in einem realen Szenario sollten Sie sicherstellen, dass Sie sowohl die PHP-Einstellungen und Pod-Ressourcenbeschränkungen als auch die Skalierungsregeln in der Konfiguration von HorizontalPodAutoscaler anpassen.

Um Redis und Autoscaler zu implementieren, führen Sie wie bisher einfach make step-3 aus.

Schritt 4: Varnish

Als letztes Puzzleteil muss ein Caching-Reverse-Proxy hinzugefügt werden, um Magento zu entlasten. Natürlich verwenden wir Varnish, da es out-of-the-box unterstützt wird.

Nicht anders als in den vorherigen Schritten werden wir mit der Erstellung eines Varnishs beginnen. Zwei bemerkenswerte Dinge sind an dieser Stelle, dass wir nicht einen, sondern zwei Ports freilegen und einen benutzerdefinierten Befehl zum Starten des Containers ausführen. Dabei starten wir zuerst varnishd im Daemon-Modus und führen dann varnishncsa aus.

Das Freilegen von zwei Ports ermöglicht es uns, einfache Zugriffsregeln in der Varnish VCL zu konfigurieren, sodass Magento den Cache mit einem Port löschen kann, während der andere sicher der Außenwelt ausgesetzt werden kann.

Als Nächstes müssen wir Magento mitteilen, wie es eine Verbindung zu Varnish herstellen kann, indem wir die Konfigurationsdatei aux.env wie bisher erweitern:

VARNISH_HOST=varnish
VARNISH_PORT=80
VARNISH_HOST=varnish
VARNISH_PORT=80
apiVersion: v1
kind: Service
metadata:
  name: varnish
  labels:
    app: varnish
    k8s-app: magento
spec:
  selector:
    app: varnish
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  - name: proxy
    port: 6091
    protocol: TCP
    targetPort: 6091

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: varnish
spec:
  selector:
    matchLabels:
      app: varnish
  replicas: 1
  template:
    metadata:
      labels:
        app: varnish
        k8s-app: magento
    spec:
      containers:
      - image: varnish:6.2
        name: varnish
        command: ["/bin/sh"]
        args:
          - -c
          - |
            varnishd -a :80 -a :6091 -f /etc/varnish/default.vcl -s default,512M;
            varnishncsa -F '%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-agent}i" %{Varnish:handling}x'
        ports:
        - containerPort: 80
        - containerPort: 6091
        resources:
          requests:
            cpu: 50m
            memory: 512Mi
        env:
        - name: VARNISH_BACKEND_HOST
          value: web
        - name: VARNISH_BACKEND_PORT
          value: "80"
        volumeMounts:
        - name: config
          mountPath: /etc/varnish/default.vcl
          subPath: default.vcl
      restartPolicy: Always
      volumes:
      - name: config
        configMap:
          name: varnish
Varnish-Bereitstellungsmanifest

Schließlich möchten wir, dass der Ingress alle eingehenden Anfragen an Varnish weiterleitet. Dazu ist es erforderlich, die in der Ingress-Definition zuvor angegebene Ziel-Sevice zu ändern. Ein einfacher Weg, um die Ingress-Definition zu aktualisieren, ist die Verwendung von patchesJson6902 von Kustomize.

- op: replace
  path: /spec/backend/serviceName
  value: varnish
- op: replace
  path: /spec/backend/servicePort
  value: 6091
step-4/patch/ingress.yaml

Tipp: Während sich Varnish dadurch auszeichnet, dass es die anderen Komponenten entlastet und die Performance von sich selten ändernden Seiten verbessert, steigert es nicht die Performance der interaktiven Elemente wie Einkaufswagen, Kasse oder Kundenbereich.

Um Varnish zu implementieren und zu konfigurieren, führen Sie make step-4 aus.

Zusammenfassung

Jetzt haben Sie einen Überblick über alle wesentlichen Voraussetzungen für die Ausführung von Magento 2 auf Kubernetes, wobei die Magento-Bereitstellung über Umgebungsvariablen, Cronjobs, Elasticsearch, Redis, Autoscaling und Varnish konfiguriert wird.

Alle Manifeste und Konfigurationsdateien werden von Kustomize verwaltet, sodass sie bequem an die Bedürfnisse eines bestimmten Projekts angepasst werden können.
Wir würden zwar nicht empfehlen, es im aktuellen Zustand zu betreiben, aber es sollte Ihnen einen guten Ausgangspunkt für die Erstellung einer produktionsbereiten, projektspezifischen Konfiguration bieten.

FacebookTwitterPinterest

Maciej Lewkowicz

Full Stack Developer