
Na początek trochę teorii. Czym jest ?
Mówiąc prosto, jest to dokument mający na celu uproszczenie opracowywania aplikacji SaaS, pomagający tym, że informuje programistów i inżynierów DevOps o problemach oraz praktykach, które najczęściej występowały w opracowywaniu nowoczesnych aplikacji.
Dokument ten został opracowany przez twórców platformy Heroku.
Metodyka dwunastu czynników (The Twelve-Factor App) może być stosowana w aplikacjach napisanych w każdym języku programowania i wykorzystujących dowolne kombinacje usług zewnętrznych (backing services) (bazy danych, kolejki komunikatów, pamięci podręczne itp.).
Krótko o samych czynnikach, na których oparta jest ta metodyka:
- Baza kodu – Jedna baza kodu, monitorowana w systemie kontroli wersji – wiele wdrożeń
- Zależności – Wyraźnie deklaruj i izoluj zależności
- Konfiguracja – Przechowuj konfigurację w środowisku wykonawczym
- Usługi zewnętrzne (Backing Services) – Traktuj usługi zewnętrzne (backing services) jako zasoby do podłączenia
- Budowanie, wydanie, wykonanie – Ściśle oddzielaj etapy budowy i wykonania
- Procesy – Uruchamiaj aplikację jako jeden lub kilka procesów bez stanów wewnętrznych (stateless)
- Przypisanie portu (Port binding) – Eksportuj usługi poprzez przypisanie portów
- Równoległość – Skaluj aplikację za pomocą procesów
- Usuwalność (Disposability) – Maksymalizuj niezawodność dzięki szybkiemu uruchamianiu i prawidłowemu zakończeniu pracy
- Paralelizm rozwijania/pracy aplikacji – Utrzymuj środowiska rozwoju, wdrożenia wstępnego (staging) oraz produkcji maksymalnie zbliżone do siebie
- Logi (Logs) – Traktuj logi jako strumień zdarzeń
- Zadania administracyjne – Wykonuj zadania administracyjne/zarządzające za pomocą jednorazowych procesów
Więcej informacji o 12 czynnikach możesz znaleźć w następujących zasobach:
- — obowiązkowe do przeczytania
- — oficjalne tłumaczenie
- — świeże spojrzenie na 12 czynników w celu ich udoskonalenia.
Czym jest wdrożenie Blue-Green?
Wdrożenie Blue-Green to sposób dostarczania aplikacji na production w taki sposób, że klient końcowy nie zauważa żadnych zmian z jego strony. Innymi słowy, wdrażanie aplikacji bez przestojów downtime.
Klasyczny schemat BG Deploy wygląda tak, jak pokazano na poniższym obrazku.

- Na początku są 2 fizyczne serwery z absolutnie identycznym kodem, aplikacją i projektem oraz router (balanser).
- Router początkowo kieruje wszystkie żądania na jeden z serwerów (alfa).
- W momencie, gdy trzeba znów przeprowadzić wydanie, cały projekt jest aktualizowany na drugim serwerze (niebieski), który w danym momencie nie obsługuje żadnych żądań.
- Po tym, jak kod na niebieskim serwerze zostanie całkowicie zaktualizowany, router otrzymuje polecenie, aby przełączyć się na zielony na niebieski serwer.
- Teraz wszyscy klienci widzą efekty pracy kodu z niebieskiego .
- Przez jakiś czas alfa serwer służy jako kopia zapasowa na wypadek nieudanej aktualizacji na niebieski serwer i w przypadku niepowodzenia i błędów, router przełącza strumień użytkowników z powrotem na alfa serwer ze starą stabilną wersją, a nowy kod idzie do poprawki i testowania.
- Na końcu procesu, w ten sam sposób aktualizowany jest alfa serwer. A po jego aktualizacji, router przełącza strumień żądań z powrotem na alfa serwer.
To wszystko wygląda bardzo dobrze i na pierwszy rzut oka nie powinno być z tym żadnych problemów.
Jednak ponieważ żyjemy w nowoczesnym świecie, opcja z fizycznym przełącznikiem, jak przedstawiono w klasycznym schemacie, nam nie odpowiada. Na razie zapisz tę informację, wrócimy do niej później.
Złe i dobre porady
Zrzeczenie się odpowiedzialności: W poniższych przykładach podano narzędzia/metodologie, które ja stosuję, możesz używać absolutnie dowolnych alternatyw o podobnych funkcjach.
Większość przykładów będzie w ten czy inny sposób łączyć się z web developmentem (to niespodzianka), z PHP i Dockerem.
W punktach poniżej znajduje się proste praktyczne opisanie zastosowania czynników na konkretnych przykładach, jeśli chcesz uzyskać więcej teorii na ten temat, zajrzyj do źródeł podlinkowanych powyżej.
1. Baza kodowa
Używaj FTP i FileZilla, przesyłając pliki na serwery po jednym, nie przechowuj kodu nigdzie poza serwerem produkcyjnym.
W projekcie zawsze powinna być jednolita baza kodowa, czyli cały kod pochodzi z jednego miejsca. Gitem repozytoriów. Serwery (produkcja, staging, test1, test2 …) korzystają z kodu z gałęzi jednego wspólnego repozytorium. Dzięki temu osiągamy spójność kodu.
2. Zależności
Pobieraj wszystkie biblioteki w folderach bezpośrednio do katalogu głównego projektu. Aktualizacje wykonuj po prostu przenosząc nowy kod do folderu z aktualną wersją biblioteki. Instaluj wszystkie niezbędne narzędzia bezpośrednio na serwerze hostującym, na którym działa jeszcze 20 usług.
Projekt zawsze powinien mieć wyraźnie określoną listę zależności (pod zależnościami rozumiem również środowisko). Wszystkie zależności powinny być wyraźnie zdefiniowane i izolowane.
Na przykład weźmy Composer i Docker.
Composer — menedżer pakietów, który umożliwia instalację bibliotek PHP. Composer pozwala na jednoznaczne lub elastyczne określenie wersji i ich jednoznaczną definicję. Na serwerze może znajdować się 20 różnych projektów, a każdy z nich będzie miał swoją własną listę pakietów i bibliotek, niezależną od innych.
Docker — narzędzie, które pozwala definiować i izolować środowisko, w którym będzie działać aplikacja. W związku z tym, podobnie jak w przypadku composera, ale w bardziej dogłębny sposób, możemy określić, na czym działa aplikacja. Wybrać konkretną wersję PHP, zainstalować tylko niezbędne pakiety do działania projektu, nie dodając nic zbędnego. A co najważniejsze, nie kolidując z pakietami i środowiskiem maszyny hostującej i innych projektów. To znaczy, wszystkie projekty na serwerze działające przez Docker mogą korzystać z absolutnie dowolnego zestawu pakietów i z zupełnie różnych środowisk.
3. Konfiguracja
Przechowuj konfiguracje jako stałe bezpośrednio w kodzie. Oddzielne stałe dla serwera testowego, oddzielne dla produkcji. Powiąż działanie aplikacji z zależnością od środowiska bezpośrednio w logice biznesowej projektu, używając konstrukcji if else.
Konfiguracje — to jedyna rzecz, która powinna różnić wdrożenia projektu (deployment). W idealnym przypadku konfiguracje powinny być przekazywane za pomocą zmiennych środowiskowych (env vars).
Oznacza to, że nawet jeśli będziesz przechowywał kilka plików konfiguracyjnych .config.prod, .config.local i zmieniał ich nazwy w momencie wdrażania na .config (główny plik konfiguracyjny, z którego aplikacja odczytuje dane) — to będzie niewłaściwe podejście, gdyż w takim przypadku informacje z konfiguracji będą ogólnie dostępne wszystkim programistom aplikacji, a dane z serwera produkcyjnego będą skompromitowane. Wszystkie konfiguracje powinny być przechowywane bezpośrednio w systemie wdrażania (CI/CD) i generowane dla różnych środowisk z różnymi wartościami potrzebnymi dla konkretnego środowiska właśnie w momencie wdrożenia.
4. Zewnętrzne usługi (Backing Services)
Mocno związuj się ze środowiskiem, używaj różnych połączeń dla tych samych usług w określonych środowiskach.
W rzeczywistości ten punkt mocno pokrywa się z punktem o konfiguracjach, ponieważ bez tego punktu nie da się stworzyć normalnych danych konfiguracyjnych, a w ogóle możliwość konfigurowania zaniknie.
Wszystkie połączenia z zewnętrznymi usługami, takimi jak serwery kolejki, bazy danych, usługi cache muszą być jednorodne zarówno dla lokalnego środowiska, jak i zewnętrznego / produkcyjnego. Innymi słowy, mogę w każdej chwili, zmieniając ciąg połączenia, zastąpić odwołania do bazy #1 na bazę #2 bez zmiany kodu aplikacji. Albo przewidując przykład, że podczas skalowania usługi, nie będziesz musiał podawać połączenia w specjalny sposób dla dodatkowego serwera cache.
5. Budowanie, wydanie, wykonanie
Miej na serwerze tylko ostateczną wersję kodu, bez możliwości cofnięcia wydania. Nie należy zapełniać przestrzeni dyskowej. Kto myśli, że może wypuścić kod na produkcję z błędem, jest złym programistą!
Wszystkie etapy wdrożenia powinny być od siebie oddzielone.
Miej możliwość powrotu. Wydawaj wersje z zachowaniem starych kopii aplikacji (już skompilowanych i gotowych do użycia) w szybkim dostępie, aby w przypadku błędów móc przywrócić wcześniejszą wersję. To znaczy, że jest folder releases i folder prąd, a po pomyślnym wdrożeniu i kompilacji folder prąd jest powiązany symboliczny link z nowym wydaniem, które leży w środku releases z umowną nazwą numeru wydania.
Tutaj przypominamy sobie o Blue-Green deployment, który umożliwia nie tylko przełączanie między kodem, ale także przełączanie między wszystkimi zasobami, a nawet środowiskami z możliwością przywrócenia wszystkiego do poprzedniego stanu.
6. Procesy
Przechowuj dane o stanach aplikacji bezpośrednio w samej aplikacji. Używaj sesji w pamięci operacyjnej samej aplikacji. Używaj jak najwięcej wspólnych zewnętrznych usług. Uzależniaj to od faktu, że aplikacja może mieć tylko jeden proces i nie dopuszczaj możliwości skalowania.
Jeśli chodzi o sesje, przechowuj dane wyłącznie w pamięci podręcznej kontrolowanej przez zewnętrzne usługi (memcached, redis), w ten sposób nawet jeśli masz uruchomionych 20 procesów aplikacji, to każda z nich, zwracając się do pamięci podręcznej, może kontynuować pracę z klientem w tym samym stanie, w jakim użytkownik był pracując z aplikacją w innym procesie. Przy takim podejściu okazuje się, że niezależnie od tego, ile kopii zewnętrznych usług używasz, wszystko będzie działać poprawnie i bez problemów z dostępem do danych.
7. Wiązanie portów (Port binding)
Tylko serwer WWW powinien wiedzieć, jak pracować z zewnętrznymi usługami. A najlepiej byłoby uruchomić zewnętrzne usługi bezpośrednio w serwerze webowym. Na przykład jako moduł PHP w Apache.
Wszystkie twoje usługi powinny być dostępne dla siebie nawzajem poprzez odwołanie do jakiegoś adresu i portu (localgost:5432, localhost:3000, nginx:80, php-fpm:9000), to znaczy z nginx mogę uzyskać dostęp zarówno do php-fpm, jak i do postgres, a z php-fpm do postgres i nginx, i właściwie z każdej usługi mogę uzyskać dostęp do innej usługi. W ten sposób żywotność jednej usługi nie jest uzależniona od żywotności innej usługi.
8. Równoległość
Pracuj z jednym procesem, bo nagle kilka procesów może nie umieć ze sobą współpracować!
Pozostaw możliwość skalowania. Świetnie do tego nadaje się docker swarm.
Docker Swarm to narzędzie do tworzenia i zarządzania klastrami kontenerów zarówno między różnymi maszynami, jak i setkami kontenerów na jednej maszynie.
Dzięki swarmowi mogę określić, ile zasobów przeznaczam na każdy proces oraz ile procesów tej samej usługi będę uruchamiać. Wewnętrzny balansujący, przyjmując dane na przypisanym porcie, automatycznie przekieruje je do procesów. W ten sposób, w przypadku wzrostu obciążenia serwera, mogę dodać więcej procesów, tym samym zmniejszając obciążenie poszczególnych z nich.
9. Utylizowalność (Disposability)
Nie używaj kolejek do pracy z procesami i danymi. Zatrzymanie jednego procesu powinno wpływać na działanie całej aplikacji. Jeśli jedna usługa spadnie — spada wszystko.
Każdy proces i usługa mogą być wyłączone w każdym momencie, a to nie powinno wpływać na inne usługi (mowa oczywiście nie o tym, że usługa będzie niedostępna dla innej, ale o tym, że inna usługa nie wyłączy się w ślad za tą). Wszystkie procesy muszą kończyć się łagodnie, tak aby przy ich zakończeniu dane nie ucierpiały, a przy następnym uruchomieniu system działał poprawnie. To znaczy, że nawet w przypadku awaryjnego zakończenia, dane nie powinny zostać uszkodzone (tu sprawdzi się mechanizm transakcji, zapytania do bazy danych działają tylko w grupach, i jeśli którekolwiek zapytanie z grupy nie zostało wykonane lub wykonano je z błędem, żadne inne zapytanie z grupy również w rzeczywistości nie jest realizowane).
10. Parzystość rozwoju / pracy aplikacji
Produkcja, staging i lokalna wersja aplikacji powinny być różne. Na produkcji mamy framework Yii Lite, a lokalnie Yii, aby na produkcji działało szybciej!
W rzeczywistości wszystkie wdrożenia i praca z kodem powinny odbywać się w niemal identycznym środowisku (nie mówię tutaj o fizycznym sprzęcie). Również kod na produkcji powinien być w razie potrzeby wdrożony przez każdego pracownika zespołu deweloperskiego, a nie przez specjalnie przeszkolony dział devops, który tylko dzięki szczególnej sile może uruchomić aplikację w produkcji.
W tym również pomaga nam Docker. Przy przestrzeganiu wszystkich poprzednich punktów, użycie dockera sprawi, że proces wdrażania środowiska zarówno na produkcji, jak i na lokalnej maszynie sprowadzi się do wprowadzenia jednej lub dwóch komend.
11. Rejestrowanie (Logs)
Logi zapisujemy w plikach i bazie danych! Pliki i baza danych logów nie są czyszczone. Po prostu kupimy dysk twardy na 9000 Petabajtów i będzie ok.
Wszystkie logi należy traktować jako strumień zdarzeń. Sama aplikacja nie powinna zajmować się przetwarzaniem logów. Logi powinny być wysyłane do stdout lub przesyłane protokołem takim jak udp, aby praca aplikacji z logami nie stwarzała żadnych problemów. Do tego celu doskonale nadaje się graylog. Graylog, przyjmując wszystkie logi przez udp (w tym protokole nie jest wymagane oczekiwanie na potwierdzenie odbioru pakietu), nie zakłóca w żaden sposób działania aplikacji i zajmuje się tylko strukturyzacją i przetwarzaniem logów. Logika aplikacji nie zmienia się przy użyciu takich podejść.
12. Zadania administracyjne
Aby zaktualizować dane, bazę danych itp., należy użyć osobno utworzonego endpointa w API. Wykonanie go dwa razy z rzędu może spowodować, że wszystko się zdubluje. Ale wy przecież nie jesteście głupcami, więc nie naciśniecie dwa razy, a migracje są nam niepotrzebne.
Wszystkie zadania administracyjne powinny być wykonywane w tym samym środowisku, co cały kod, na poziomie wersji. To znaczy, że jeśli musimy zmienić strukturę bazy danych, nie będziemy tego robić ręcznie, zmieniając nazwy kolumn i dodając nowe przez jakieś wizualne narzędzia do zarządzania bazą danych. Na takie rzeczy tworzymy osobne skrypty — migracje, które są wykonywane wszędzie i na wszystkich środowiskach w sposób jednolity i zrozumiały. Dla wszystkich innych zadań, takich jak wypełnianie projektu danymi, należy stosować podobne metodologie.
Przykład implementacji w PHP, Laravel, Laradock, Docker-Compose
P.S. Wszystkie przykłady zostały zrobione na MacOS. Większość z nich jest odpowiednia również dla Linuxa. Użytkownicy Windows, przepraszam, ale od lat nie pracowałem z Windowsem.
Wyobraźmy sobie sytuację, w której na naszym PC nie jest zainstalowana żadna wersja PHP ani nic innego.
Instalujemy najnowsze wersje dockera i docker-compose. (można to znaleźć w internecie)
docker -v &&
docker-compose -v

1. Instalujemy
git clone https://github.com/Laradock/laradock.git &&
ls

Jeśli chodzi o Laradock, to mogę powiedzieć, że to bardzo fajna rzecz, w której zebrano wiele kontenerów i pomocniczych narzędzi. Jednakże używanie Laradock bez modyfikacji na produkcji — nie polecałbym ze względu na jego nadmiarowość. Lepiej stworzyć własne kontenery opierając się na przykładach z Laradock, ponieważ będzie to bardziej optymalne, gdyż nikt nie potrzebuje wszystkiego, co tam jest, jednocześnie.
2. Konfigurujemy Laradock do działania naszej aplikacji.
cd laradock &&
cp env-example .env

2.1. Otwieramy katalog habr (główna folder, w którym sklonowano laradock) w dowolnym edytorze. (W moim przypadku PHPStorm)
Na tym etapie nadajemy tylko nazwę projektowi.

2.2. Uruchamiamy obraz workspace. (W twoim przypadku obrazy będą się budować przez jakiś czas)
Workspace to specjalnie przygotowany obraz do pracy z frameworkiem w imieniu programisty.
Przechodzimy do kontenera za pomocą
docker-compose up -d workspace &&
docker-compose exec workspace bash

2.3. Instalujemy Laravel
composer create-project --prefer-dist laravel/laravel application 
2.4. Po instalacji sprawdzamy, czy utworzono katalog z projektem, i zamykamy compose.
ls
exit
docker-compose down

2.5. Wracamy do PHPStorm i ustawiamy poprawną ścieżkę do naszego aplikacji laravel w pliku .env.

3. Dodajemy cały kod do Gita.
W tym celu tworzymy repozytorium na Githubie (lub gdziekolwiek indziej). Przechodzimy w terminalu do katalogu habr i wykonujemy następujący kod.
echo "# habr-12factor" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:nzulfigarov/habr-12factor.git # tutaj będzie link do twojego repo
git push -u origin master
git status
Sprawdzamy, czy wszystko jest w porządku.

Dla wygody polecam używać jakiegoś wizualnego interfejsu do Gita, w moim przypadku jest to . (tutaj link referencyjny)
4. Uruchamiamy!
Przed uruchomieniem upewnij się, że na porcie 80 i 443 nic nie jest zajęte.
docker-compose up -d nginx php-fpm 
W ten sposób nasz projekt składa się z 3 oddzielnych usług:
- nginx - serwer WWW
- php-fpm - PHP do obsługi żądań z serwera WWW
- workspace - PHP dla programisty
Na chwilę obecną osiągnęliśmy to, że stworzyliśmy aplikację, która spełnia już 4 punkty z 12, a mianowicie:
1. Baza kodu — cały kod znajduje się w jednym repozytorium (drobna uwaga: być może lepiej byłoby umieścić dockera wewnątrz projektu laravel, ale to nie jest zasadnicze).
2. Zależności — Wszystkie nasze zależności są wyraźnie zapisane w application/composer.json i w każdym Dockerfile każdego kontenera.
3. Usługi zewnętrzne (Backing Services) — Każda z usług (php-fpm, nginx, workspace) działa swoim życiem i jest podłączona z zewnątrz, a podczas pracy z jedną usługą, druga nie będzie dotknięta.
4. Procesy — każda usługa to jeden proces. Każda z usług nie przechowuje wewnętrznego stanu.
5. Przypisanie portu (Port binding)
docker ps

Jak widzimy, każda usługa działa na swoim własnym porcie i jest dostępna dla wszystkich innych usług.
6. Równoległość
Docker pozwala nam uruchamiać wiele procesów tych samych usług z automatycznym balansowaniem obciążenia między nimi.
Zatrzymajmy kontenery i uruchommy je z flagą —scale
docker-compose down &&
docker-compose up -d --scale php-fpm=3 nginx php-fpm

Jak widzimy, kontener php-fpm stworzył kopie. Nie musimy nic zmieniać w pracy z tym kontenerem. Nadal będziemy się do niego zwracać przez port 9000, a Docker reguluje obciążenie między kontenerami.
7. Usuwalność (Disposability) — każdy kontener można zabić bez szkody dla innych. Zatrzymanie lub ponowne uruchomienie kontenera nie wpłynie na działanie aplikacji przy kolejnych uruchomieniach. Każdy kontener można również uruchomić w dowolnym momencie.
8. Paralelizm rozwijania/pracy aplikacji — wszystkie nasze środowiska są identyczne. Uruchamiając system na serwerze w produkcji, nie będziesz musiał nic zmieniać w swoich poleceniach. Wszystko będzie działać dokładnie tak samo na bazie Docker.
9. Logi (Logs) — wszystkie logi w tych kontenerach wypływają na standardowe wyjście i są widoczne w konsoli Docker. (w tym przypadku, w rzeczywistości, przy innych stworzonych kontenerach może być inaczej, jeśli o to nie zadbasz)
docker-compose logs -f 
Jednak pojawia się problem, że wartości domyślne w PHP i Nginx również zapisują logi do pliku. Aby dostosować się do 12 zasad, konieczne jest słowo kluczowe debugger; na bieżącej stronie. zapis logów do pliku w konfiguracjach każdego kontenera osobno.
Docker daje również możliwość kierowania logów nie tylko do stdout, ale także do takich narzędzi jak graylog, o którym wspomniałem wcześniej. A w graylog możemy operować logami jak tylko chcemy, a nasza aplikacja nie będzie tego zauważać.
10. Zadania administracyjne — wszystkie zadania administracyjne są rozwiązywane przez laravel dzięki narzędziu artisan dokładnie tak, jak życzyliby sobie twórcy aplikacji 12-faktorowej.
Na przykład pokażę, jak wykonują się niektóre polecenia.
Wchodzimy do kontenera.
docker-compose exec workspace bash
php artisan list

Teraz możemy skorzystać z dowolnego polecenia. (zwróć uwagę, że nie konfigurowaliśmy bazy danych ani pamięci podręcznej, dlatego połowa poleceń nie wykona się poprawnie, ponieważ są one przeznaczone do pracy z pamięcią podręczną i bazą danych).

11. Konfiguracje i 12. Budowanie, wydanie, wykonanie
Chciałem poświęcić tę część Blue-Green Deployment, ale okazało się to zbyt rozbudowane na ten artykuł. O tym napiszę oddzielny artykuł.
Krótko mówiąc, koncepcja opiera się na systemach CI/CD takich jak Jenkins i Gitlab CI. W obu można ustawiać zmienne środowiskowe związane z konkretnym środowiskiem. W związku z tym w tej sytuacji wykonywany będzie punkt dotyczący Konfiguracji.
A punkt dotyczący Budowanie, wydanie, wykonanie jest rozwiązywany przez wbudowane w obie narzędzia funkcje o nazwie Pipeline.
Pipeline umożliwia podział procesu wdrażania na wiele etapów, wyodrębniając etapy budowy, wydania i wykonywania. W Pipeline będziesz mógł także tworzyć kopie zapasowe, a w zasadzie cokolwiek innego. To narzędzie ma nieograniczony potencjał.
Kod aplikacji znajduje się na .
Nie zapomnij zainicjować submodułu przy klonowaniu tego repozytorium.
P.S.: Wszystkie te podejścia można zastosować z dowolnymi innymi narzędziami i językami programowania. Najważniejsze, aby zasada się nie różniła.
Źródło: habr.com
