Laravel Zero Downtime-Bereitstellung

Veröffentlicht: 2022-03-11

Wenn es darum geht, eine Live-Anwendung zu aktualisieren, gibt es zwei grundlegend unterschiedliche Vorgehensweisen.

Beim ersten Ansatz nehmen wir inkrementelle Änderungen am Zustand unseres Systems vor. Beispielsweise aktualisieren wir Dateien, ändern Umgebungseigenschaften, installieren zusätzliche Notwendigkeiten und so weiter. Beim zweiten Ansatz reißen wir ganze Maschinen ab und bauen das System mit neuen Images und deklarativen Konfigurationen (z. B. mit Kubernetes) neu auf.

Laravel-Bereitstellung leicht gemacht

Dieser Artikel behandelt hauptsächlich relativ kleine Anwendungen, die möglicherweise nicht in der Cloud gehostet werden, obwohl ich erwähnen werde, wie Kubernetes uns bei Bereitstellungen jenseits des „No-Cloud“-Szenarios sehr helfen kann. Wir werden auch einige allgemeine Probleme und Tipps zur Durchführung erfolgreicher Updates besprechen, die in einer Reihe von verschiedenen Situationen anwendbar sein könnten, nicht nur bei der Laravel-Bereitstellung.

Für diese Demonstration werde ich ein Laravel-Beispiel verwenden, aber bedenken Sie, dass jede PHP-Anwendung einen ähnlichen Ansatz verwenden könnte.

Versionierung

Für den Anfang ist es entscheidend, dass wir die Codeversion kennen, die derzeit in der Produktion eingesetzt wird. Es kann in einer Datei oder zumindest im Namen eines Ordners oder einer Datei enthalten sein. Was die Benennung betrifft, so können wir, wenn wir der Standardpraxis der semantischen Versionierung folgen, mehr Informationen als nur eine einzelne Zahl darin aufnehmen.

Wenn wir uns zwei verschiedene Releases ansehen, könnten uns diese zusätzlichen Informationen dabei helfen, die Art der Änderungen, die zwischen ihnen eingeführt wurden, leicht zu verstehen.

Bild zur Erläuterung der semantischen Versionierung.

Die Versionierung des Releases beginnt mit einem Versionskontrollsystem wie Git. Angenommen, wir haben ein Release für die Bereitstellung vorbereitet, beispielsweise Version 1.0.3. Wenn es darum geht, diese Releases und Code-Flows zu organisieren, gibt es verschiedene Entwicklungsstile wie Trunk-basierte Entwicklung und Git-Flow, die Sie je nach den Vorlieben Ihres Teams und den Besonderheiten Ihres Projekts auswählen oder mischen können. Am Ende werden wir höchstwahrscheinlich unsere Releases entsprechend getaggt auf unserem Hauptzweig landen.

Nach dem Commit können wir ein einfaches Tag wie dieses erstellen:

git tag v1.0.3

Und dann fügen wir Tags ein, während wir den Push-Befehl ausführen:

git push <origin> <branch> --tags

Wir können auch Tags zu alten Commits hinzufügen, indem wir ihre Hashes verwenden.

Release-Dateien an ihr Ziel bringen

Die Laravel-Bereitstellung braucht Zeit, selbst wenn es sich nur um das Kopieren von Dateien handelt. Aber auch wenn es nicht allzu lange dauert, unser Ziel ist es, null Ausfallzeiten zu erreichen .

Daher sollten wir vermeiden, das Update direkt zu installieren, und keine Dateien ändern, die live bereitgestellt werden. Stattdessen sollten wir in einem anderen Verzeichnis bereitstellen und den Wechsel erst vornehmen, wenn die Installation vollständig abgeschlossen ist.

Tatsächlich gibt es verschiedene Tools und Dienste, die uns bei Bereitstellungen unterstützen können, wie Envoyer.io (von Laravel.com-Designer Jack McDade), Capistrano, Deployer usw. Ich habe sie noch nicht alle in der Produktion verwendet, daher kann ich das nicht Empfehlungen abgeben oder einen umfassenden Vergleich schreiben, aber lassen Sie mich die Idee hinter diesen Produkten vorstellen. Wenn einige (oder alle) Ihre Anforderungen nicht erfüllen können, können Sie jederzeit Ihre benutzerdefinierten Skripts erstellen, um den Prozess so zu automatisieren, wie Sie es für richtig halten.

Nehmen wir zum Zwecke dieser Demonstration an, dass unsere Laravel-Anwendung von einem Nginx-Server aus dem folgenden Pfad bereitgestellt wird:

/var/www/demo/public

Erstens benötigen wir ein Verzeichnis, um bei jeder Bereitstellung Release-Dateien zu platzieren. Außerdem benötigen wir einen Symlink, der auf die aktuelle Arbeitsversion verweist. In diesem Fall dient /var/www/demo als unser Symlink. Durch die Neuzuweisung des Zeigers können wir die Versionen schnell ändern.

Umgang mit Laravel-Bereitstellungsdateien

Falls wir es mit einem Apache-Server zu tun haben, müssen wir möglicherweise die folgenden Symlinks in der Konfiguration zulassen:

Options +FollowSymLinks

Unsere Struktur kann in etwa so aussehen:

 /opt/demo/release/v0.1.0 /opt/demo/release/v0.1.1 /opt/demo/release/v0.1.2

Es könnte einige Dateien geben, die wir bei verschiedenen Bereitstellungen beibehalten müssen, z. B. Protokolldateien (wenn wir natürlich nicht Logstash verwenden). Im Fall der Laravel-Bereitstellung möchten wir möglicherweise das Speicherverzeichnis und die .env-Konfigurationsdatei beibehalten. Wir können sie von anderen Dateien getrennt halten und stattdessen ihre Symlinks verwenden.

Um unsere Release-Dateien aus dem Git-Repository abzurufen, können wir entweder Klon- oder Archivierungsbefehle verwenden. Einige Leute verwenden Git Clone, aber Sie können kein bestimmtes Commit oder Tag klonen. Das bedeutet, dass das gesamte Repository abgerufen und dann das spezifische Tag ausgewählt wird. Wenn ein Repository viele Branches oder einen großen Verlauf enthält, ist seine Größe erheblich größer als das Release-Archiv. Wenn Sie das Git-Repo also nicht speziell für die Produktion benötigen, können Sie git archive verwenden. Dies ermöglicht es uns, nur ein Dateiarchiv nach einem bestimmten Tag abzurufen. Ein weiterer Vorteil der letzteren ist, dass wir einige Dateien und Ordner ignorieren können, die in der Produktionsumgebung nicht vorhanden sein sollten, zB Tests. Dazu müssen wir nur die Eigenschaft export-ignore in der .gitattributes file . In der OWASP Secure Coding Practices Checklist finden Sie die folgende Empfehlung: „Entfernen Sie Testcode oder jegliche Funktionalität, die nicht für die Produktion bestimmt ist, vor der Bereitstellung.“

Wenn wir die Version aus dem Quellversionskontrollsystem abrufen, könnten uns git archive und export-ignore bei dieser Anforderung helfen.

Schauen wir uns ein vereinfachtes Skript an (es würde eine bessere Fehlerbehandlung in der Produktion erfordern):

deploy.sh

 #!/bin/bash # Terminate execution if any command fails set -e # Get tag from a script argument TAG=$1 GIT_REMOTE_URL='here should be a remote url of the repo' BASE_DIR=/opt/demo # Create folder structure for releases if necessary RELEASE_DIR=$BASE_DIR/releases/$TAG mkdir -p $RELEASE_DIR mkdir -p $BASE_DIR/storage cd $RELEASE_DIR # Fetch the release files from git as a tar archive and unzip git archive \ --remote=$GIT_REMOTE_URL \ --format=tar \ $TAG \ | tar xf - # Install laravel dependencies with composer composer install -o --no-interaction --no-dev # Create symlinks to `storage` and `.env` ln -sf $BASE_DIR/.env ./ rm -rf storage && ln -sf $BASE_DIR/storage ./ # Run database migrations php artisan migrate --no-interaction --force # Run optimization commands for laravel php artisan optimize php artisan cache:clear php artisan route:cache php artisan view:clear php artisan config:cache # Remove existing directory or symlink for the release and create a new one. NGINX_DIR=/var/www/public mkdir -p $NGINX_DIR rm -f $NGINX_DIR/demo ln -sf $RELEASE_DIR $NGINX_DIR/demo

Für die Bereitstellung unserer Version könnten wir einfach Folgendes ausführen:

deploy.sh v1.0.3

Hinweis: In diesem Beispiel ist v1.0.3 das Git-Tag unserer Version.

Komponist auf Produktion?

Sie haben vielleicht bemerkt, dass das Skript Composer aufruft, um Abhängigkeiten zu installieren. Obwohl Sie dies in vielen Artikeln sehen, kann es bei diesem Ansatz zu einigen Problemen kommen. Im Allgemeinen ist es eine bewährte Methode, einen vollständigen Build einer Anwendung zu erstellen und diesen Build durch verschiedene Testumgebungen Ihrer Infrastruktur zu führen. Am Ende hätten Sie einen gründlich getesteten Build, der sicher in der Produktion eingesetzt werden kann. Auch wenn jeder Build von Grund auf reproduzierbar sein sollte, bedeutet das nicht, dass wir die App in verschiedenen Phasen neu erstellen sollten. Wenn wir den Composer in der Produktion installieren lassen, ist dies nicht wirklich derselbe Build wie der getestete, und hier ist, was schief gehen kann:

  • Netzwerkfehler können das Herunterladen von Abhängigkeiten unterbrechen.
  • Der Bibliotheksanbieter folgt möglicherweise nicht immer der SemVer.

Ein Netzwerkfehler kann leicht bemerkt werden. Unser Skript würde sogar mit einem Fehler die Ausführung stoppen. Aber eine bahnbrechende Änderung in einer Bibliothek ist möglicherweise sehr schwer festzumachen, ohne Tests durchzuführen, was in der Produktion nicht möglich ist. Beim Installieren von Abhängigkeiten verlassen sich Composer, npm und andere ähnliche Tools auf die semantische Versionierung – major.minor.patch. Wenn in der Datei „composer.json“ ~1.0.2 angezeigt wird, bedeutet dies, dass Version 1.0.2 oder die neueste Patch-Version, z. B. 1.0.4, installiert werden muss. Wenn Sie ^1.0.2 sehen, bedeutet dies, dass Sie Version 1.0.2 oder die neueste Neben- oder Patchversion wie 1.1.0 installieren müssen. Wir vertrauen darauf, dass der Bibliotheksanbieter die Hauptnummer erhöht, wenn eine bahnbrechende Änderung eingeführt wird, aber manchmal wird diese Anforderung übersehen oder nicht befolgt. Solche Fälle gab es in der Vergangenheit. Selbst wenn Sie feste Versionen in Ihre composer.json einfügen, können Ihre Abhängigkeiten ~ und ^ in ihrer composer.json enthalten.

Wenn es zugänglich ist, wäre es meiner Meinung nach besser, ein Artefakt-Repository (Nexus, JFrog usw.) zu verwenden. Der Release-Build, der alle notwendigen Abhängigkeiten enthält, würde zunächst einmal erstellt. Dieses Artefakt würde in einem Repository gespeichert und von dort für verschiedene Testphasen abgerufen. Außerdem wäre dies der Build, der in der Produktion bereitgestellt werden soll, anstatt die App aus Git neu zu erstellen.

Code und Datenbank kompatibel halten

Der Grund, warum ich mich auf den ersten Blick in Laravel verliebt habe, war, wie sehr der Autor auf Details geachtet, an die Bequemlichkeit von Entwicklern gedacht und auch viele Best Practices in das Framework integriert hat, wie z. B. Datenbankmigrationen.

Datenbankmigrationen ermöglichen es uns, unsere Datenbank und unseren Code zu synchronisieren. Beide Änderungen können in einem einzigen Commit, also einer einzigen Veröffentlichung, enthalten sein. Dies bedeutet jedoch nicht, dass jede Änderung ohne Ausfallzeit bereitgestellt werden kann. Zu einem bestimmten Zeitpunkt während der Bereitstellung werden verschiedene Versionen der Anwendung und der Datenbank ausgeführt. Bei Problemen kann dieser Punkt sogar zu einem Punkt werden. Wir sollten immer versuchen, beide mit den vorherigen Versionen ihrer Begleiter kompatibel zu machen: alte Datenbank – neue App, neue Datenbank – alte App.

Angenommen, wir haben eine address und müssen sie in address1 und address2 . Um alles kompatibel zu halten, benötigen wir möglicherweise mehrere Versionen.

  1. Fügen Sie zwei neue Spalten in der Datenbank hinzu.
  2. Ändern Sie die Anwendung, um wann immer möglich neue Felder zu verwenden.
  3. address in neue Spalten migrieren und löschen.

Dieser Fall ist auch ein gutes Beispiel dafür, dass kleine Änderungen viel besser für die Bereitstellung sind. Ihr Rollback ist auch einfacher. Wenn wir die Codebasis und Datenbank für mehrere Wochen oder Monate ändern, ist es möglicherweise unmöglich, das Produktionssystem ohne Ausfallzeit zu aktualisieren.

Etwas Großartigkeit von Kubernetes

Auch wenn die Skalierung unserer Anwendung möglicherweise keine Clouds, Knoten und Kubernetes benötigt, möchte ich dennoch erwähnen, wie Bereitstellungen in K8s aussehen. In diesem Fall nehmen wir keine Änderungen am System vor, sondern erklären, was wir erreichen möchten und was auf wie vielen Replikaten laufen soll. Anschließend stellt Kubernetes sicher, dass der Ist-Zustand mit dem Soll übereinstimmt.

Immer wenn wir eine neue Version fertig haben, erstellen wir ein Image mit neuen Dateien darin, markieren das Image mit der neuen Version und übergeben es an K8s. Letzteres wird unser Bild schnell in einem Cluster drehen. Es wird warten, bis die Anwendung bereit ist, basierend auf der von uns bereitgestellten Bereitschaftsprüfung, dann den Datenverkehr unbemerkt an die neue Anwendung umleiten und die alte beenden. Wir können sehr einfach mehrere Versionen unserer App ausführen, mit denen wir Blue/Green- oder Canary-Bereitstellungen mit nur wenigen Befehlen durchführen können.

Bei Interesse gibt es einige beeindruckende Demonstrationen im Vortrag „9 Steps to Awesome with Kubernetes by Burr Sutter“.

Siehe auch: Vollständige Benutzerauthentifizierung und Zugriffskontrolle – Ein Laravel-Passport-Tutorial, Pt. 1