Wdrożenie Laravel Zero Downtime

Opublikowany: 2022-03-11

Jeśli chodzi o aktualizację działającej aplikacji, istnieją dwa zasadniczo różne sposoby na zrobienie tego.

W pierwszym podejściu dokonujemy stopniowych zmian stanu naszego systemu. Na przykład aktualizujemy pliki, modyfikujemy właściwości środowiska, instalujemy dodatkowe elementy i tak dalej. W drugim podejściu rozbijamy całe maszyny i odbudowujemy system za pomocą nowych obrazów i konfiguracji deklaratywnych (na przykład za pomocą Kubernetes).

Łatwe wdrożenie Laravel

Ten artykuł dotyczy głównie stosunkowo małych aplikacji, które mogą nie być hostowane w chmurze, chociaż wspomnę, jak Kubernetes może nam bardzo pomóc we wdrożeniach poza scenariuszem „bez chmury”. Omówimy również kilka ogólnych problemów i wskazówek dotyczących przeprowadzania udanych aktualizacji, które mogą mieć zastosowanie w wielu różnych sytuacjach, nie tylko przy wdrażaniu Laravela.

Na potrzeby tej demonstracji użyję przykładu Laravela, ale pamiętaj, że każda aplikacja PHP może używać podobnego podejścia.

Wersjonowanie

Na początek kluczowe jest, abyśmy znali wersję kodu, która jest aktualnie wdrożona w środowisku produkcyjnym. Może być zawarty w jakimś pliku lub przynajmniej w nazwie folderu lub pliku. Jeśli chodzi o nazewnictwo, jeśli zastosujemy się do standardowej praktyki wersjonowania semantycznego, możemy zawrzeć w nim więcej informacji niż tylko pojedynczą liczbę.

Przyglądając się dwóm różnym wydaniom, te dodane informacje mogą pomóc nam w łatwym zrozumieniu charakteru zmian wprowadzanych między nimi.

Obraz przedstawiający wyjaśnienie wersji semantycznej.

Wersjonowanie wydania rozpoczyna się od systemu kontroli wersji, takiego jak Git. Powiedzmy, że przygotowaliśmy wydanie do wdrożenia, na przykład w wersji 1.0.3. Jeśli chodzi o organizowanie tych wydań i przepływów kodu, istnieją różne style programistyczne, takie jak programowanie oparte na trunku i przepływ Git, które można wybrać lub mieszać w oparciu o preferencje zespołu i specyfikę projektu. W końcu najprawdopodobniej skończymy z naszymi wydaniami oznaczonymi odpowiednio w naszej głównej gałęzi.

Po zatwierdzeniu możemy stworzyć prosty tag, taki jak ten:

git tag v1.0.3

A następnie dołączamy tagi podczas wykonywania polecenia push:

git push <origin> <branch> --tags

Możemy również dodawać tagi do starych zatwierdzeń za pomocą ich skrótów.

Pobieranie plików zwolnienia do miejsca docelowego

Wdrożenie Laravela wymaga czasu, nawet jeśli jest to zwykłe kopiowanie plików. Jednak nawet jeśli nie potrwa to zbyt długo, naszym celem jest osiągnięcie zerowych przestojów .

Dlatego powinniśmy unikać instalowania aktualizacji na miejscu i nie zmieniać plików, które są udostępniane na żywo. Zamiast tego powinniśmy wdrożyć do innego katalogu i dokonać przełączenia dopiero po zakończeniu instalacji.

Właściwie istnieją różne narzędzia i usługi, które mogą nam pomóc we wdrożeniach, takie jak Envoyer.io (autorstwa Laravel.com, Jacka McDade), Capistrano, Deployer itp. Nie używałem jeszcze wszystkich w produkcji, więc nie mogę sporządź rekomendacje lub napisz obszerne porównanie, ale pozwól, że przedstawię ideę, która kryje się za tymi produktami. Jeśli niektóre (lub wszystkie) z nich nie spełniają Twoich wymagań, zawsze możesz utworzyć własne skrypty, aby zautomatyzować proces w najlepszy dla siebie sposób.

Na potrzeby tej demonstracji załóżmy, że nasza aplikacja Laravel jest obsługiwana przez serwer Nginx z następującej ścieżki:

/var/www/demo/public

Po pierwsze, potrzebujemy katalogu do umieszczania plików wydań za każdym razem, gdy wykonujemy wdrożenie. Potrzebujemy również dowiązania symbolicznego, które będzie wskazywać na bieżące wydanie robocze. W tym przypadku /var/www/demo będzie służyć jako nasze dowiązanie symboliczne. Ponowne przypisanie wskaźnika pozwoli nam na szybką zmianę wersji.

Obsługa plików wdrożenia Laravel

W przypadku, gdy mamy do czynienia z serwerem Apache, być może będziemy musieli zezwolić na następujące dowiązania symboliczne w konfiguracji:

Options +FollowSymLinks

Nasza struktura może wyglądać mniej więcej tak:

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

Mogą istnieć pliki, które musimy zachować w różnych wdrożeniach, np. pliki dziennika (oczywiście, jeśli nie używamy Logstash). W przypadku wdrożenia Laravela możemy chcieć zachować katalog przechowywania i plik konfiguracyjny .env. Możemy je oddzielić od innych plików i zamiast tego używać ich dowiązań symbolicznych.

Aby pobrać nasze pliki wydań z repozytorium Git, możemy użyć poleceń klonowania lub archiwizowania. Niektórzy używają git clone, ale nie możesz sklonować konkretnego zatwierdzenia lub tagu. Oznacza to, że pobierane jest całe repozytorium, a następnie wybierany jest określony tag. Gdy repozytorium zawiera wiele gałęzi lub dużą historię, jego rozmiar jest znacznie większy niż archiwum wydania. Tak więc, jeśli nie potrzebujesz konkretnie repozytorium git w środowisku produkcyjnym, możesz użyć git archive . To pozwala nam pobrać tylko archiwum plików według określonego tagu. Kolejną zaletą korzystania z tego ostatniego jest to, że możemy zignorować niektóre pliki i foldery, których nie powinno być w środowisku produkcyjnym, np. testy. W tym celu wystarczy ustawić właściwość export-ignore w .gitattributes file . W liście kontrolnej OWASP Secure Coding Practices Checklist można znaleźć następujące zalecenie: „Przed wdrożeniem usuń kod testowy lub jakąkolwiek funkcjonalność, która nie jest przeznaczona do użytku produkcyjnego”.

Jeśli pobieramy wydanie z systemu kontroli wersji źródłowych, archiwum git i export-ignore mogą nam pomóc w spełnieniu tego wymogu.

Rzućmy okiem na uproszczony skrypt (wymagałby lepszej obsługi błędów w produkcji):

wdrożyć.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

Aby wdrożyć nasze wydanie, moglibyśmy po prostu wykonać następujące czynności:

deploy.sh v1.0.3

Uwaga: w tym przykładzie v1.0.3 jest tagiem git naszego wydania.

Kompozytor na produkcji?

Być może zauważyłeś, że skrypt wywołuje Composer w celu zainstalowania zależności. Chociaż widzisz to w wielu artykułach, mogą wystąpić pewne problemy z tym podejściem. Ogólnie najlepszą praktyką jest utworzenie kompletnej kompilacji aplikacji i przejście jej przez różne środowiska testowe Twojej infrastruktury. W końcu będziesz miał dokładnie przetestowaną kompilację, którą można bezpiecznie wdrożyć do produkcji. Nawet jeśli każda kompilacja powinna być odtwarzalna od zera, nie oznacza to, że powinniśmy przebudowywać aplikację na różnych etapach. Kiedy każemy zainstalować kompozytora w produkcji, nie jest to naprawdę ta sama wersja, co testowana, a oto, co może się nie udać:

  • Błąd sieci może przerwać pobieranie zależności.
  • Dostawca biblioteki może nie zawsze postępować zgodnie z SemVer.

Błąd sieci można łatwo zauważyć. Nasz skrypt przestałby się nawet wykonywać z błędem. Ale przełomowa zmiana w bibliotece może być bardzo trudna do ustalenia bez uruchamiania testów, czego nie można zrobić w środowisku produkcyjnym. Podczas instalowania zależności Composer, npm i inne podobne narzędzia polegają na wersji semantycznej – major.minor.patch. Jeśli widzisz ~1.0.2 w composer.json, oznacza to zainstalowanie wersji 1.0.2 lub najnowszej wersji łaty, takiej jak 1.0.4. Jeśli widzisz ^1.0.2, oznacza to zainstalowanie wersji 1.0.2 lub najnowszej wersji pomocniczej lub poprawki, takiej jak 1.1.0. Ufamy dostawcy biblioteki, że podbije główną liczbę, gdy zostanie wprowadzona jakakolwiek przełomowa zmiana, ale czasami ten wymóg jest pomijany lub nie jest przestrzegany. Zdarzały się takie przypadki w przeszłości. Nawet jeśli umieścisz stałe wersje w swoim composer.json, twoje zależności mogą mieć ~ i ^ w ich composer.json.

Jeśli jest dostępny, moim zdaniem lepszym sposobem byłoby użycie repozytorium artefaktów (Nexus, JFrog itp.). Wersja wydania, zawierająca wszystkie niezbędne zależności, zostanie początkowo utworzona raz. Ten artefakt byłby przechowywany w repozytorium i stamtąd pobierany do różnych etapów testowania. Byłaby to również kompilacja do wdrożenia w środowisku produkcyjnym, zamiast przebudowywania aplikacji z Git.

Utrzymywanie zgodności kodu i bazy danych

Powodem, dla którego zakochałem się w Laravelu od pierwszego wejrzenia, było to, że jego autor przywiązywał dużą wagę do szczegółów, myślał o wygodzie programistów, a także włączył do frameworka wiele dobrych praktyk, takich jak migracje baz danych.

Migracje baz danych pozwalają nam zsynchronizować naszą bazę danych i kod. Obie ich zmiany mogą być zawarte w jednym zatwierdzeniu, stąd pojedyncze wydanie. Nie oznacza to jednak, że każdą zmianę można wdrożyć bez przestojów. W pewnym momencie wdrożenia będą działać różne wersje aplikacji i bazy danych. W razie problemów ten punkt może nawet zamienić się w kropkę. Zawsze powinniśmy starać się, aby oba były kompatybilne z poprzednimi wersjami ich towarzyszy: stara baza danych – nowa aplikacja, nowa baza danych – stara aplikacja.

Załóżmy na przykład, że mamy kolumnę address i musimy podzielić ją na address1 i address2 . Aby wszystko było zgodne, możemy potrzebować kilku wydań.

  1. Dodaj dwie nowe kolumny w bazie danych.
  2. Zmodyfikuj aplikację, aby w miarę możliwości korzystała z nowych pól.
  3. Przenieś dane address do nowych kolumn i upuść je.

Ten przypadek jest również dobrym przykładem tego, jak małe zmiany są znacznie lepsze we wdrożeniu. Ich wycofywanie jest również łatwiejsze. Jeśli zmieniamy bazę kodu i bazę danych przez kilka tygodni lub miesięcy, aktualizacja systemu produkcyjnego może być niemożliwa bez przestoju.

Trochę niesamowitości Kubernetes

Mimo że skala naszej aplikacji może nie wymagać chmur, węzłów i Kubernetes, nadal chciałbym wspomnieć, jak wyglądają wdrożenia w K8s. W tym przypadku nie wprowadzamy zmian w systemie, a raczej deklarujemy, co chcielibyśmy osiągnąć i co powinno działać na ilu replikach. Następnie Kubernetes upewnia się, że rzeczywisty stan odpowiada żądanemu.

Zawsze, gdy mamy gotowe nowe wydanie, budujemy obraz z nowymi plikami, oznaczamy obraz nową wersją i przekazujemy go do K8s. Ten ostatni szybko rozkręci nasz obraz wewnątrz gromady. Poczeka, aż aplikacja będzie gotowa na podstawie dostarczonego przez nas sprawdzenia gotowości, a następnie niezauważalnie przekieruje ruch do nowej aplikacji i zabije starą. Możemy bardzo łatwo uruchomić kilka wersji naszej aplikacji, które pozwolą nam wykonać wdrożenia niebieskie/zielone lub kanaryjskie za pomocą zaledwie kilku poleceń.

Jeśli jesteś zainteresowany, istnieje kilka imponujących demonstracji w prelekcji „9 Steps to Awesome with Kubernetes by Burr Sutter”.

Powiązane: Pełne uwierzytelnianie użytkownika i kontrola dostępu – samouczek dotyczący paszportu Laravel, Pt. 1