Jak bezpiecznie wdrażać aplikację na produkcję 

“Jedyny słuszny język programowania”, “jedyny słuszny framework”, “jedyne słuszne narzędzie do wdrażania aplikacji”. W mojej opinii, nie ma czegoś takiego. Bez zamknięcia tego w kontekście wyzwania, z którym mamy się zmierzyć, taka generalizacja nie ma sensu. Dlatego przedstawię swoje doświadczenia związane z wdrażaniem kodu, które było na dany moment oparte o (moim zdaniem) najlepsze dostępne rozwiązania. Były to rozwiązania, które spełniały swoje założenia, czyli dzięki nim dostarczaliśmy kod na produkcję. Czy były bez wad? Nie. Czy widzieliśmy możliwość usprawnień? Zdecydowanie tak.

Inne spojrzenie na shadow deployment

Jeśli odejdziemy trochę od książkowego podejścia, to można powiedzieć, że strategie: canary, a/b testing, shadow posiadają pewną również trochę inną interpretację. Jeśli chodzi o samą zasadę to pozostaje ona bez zmian. Przestajemy jednak mówić o instancjach, a skupiamy się na “ficzerach”, czyli nowych funkcjonalnościach, które chcemy udostępnić użytkownikom i przetestować w środowisku produkcyjnym. Sam kod jest dostarczany wtedy na produkcję dzięki strategiom ramped lub blue/green (o recreate nie wspominam z wiadomych względów). 

Finalnie wszystkie instancje obsługujące naszą aplikację będą oparte na tej samej wersji kodu. Jednak to nie znaczy, że każdy ma dostęp do wszystkich funkcjonalności. Jeśli mówimy o cannary i a/b tsting to przywilej korzystania z nich ma tylko pewna część wybrańców, którzy spełniają określone “warunki” (np. pochodzą z danego kraju).

Gdy omawiałem strategie wdrożenia na podstawie instancji, propagacja ruchu była realizowana poza aplikacją na warstwie infrastruktury (load balancer). Jednak w tym przypadku, kiedy udostępniamy tylko poszczególne funkcjonalności, to już aplikacja musi zadbać o to, w jaki sposób będą one udostępniane. Bardzo użyteczne w tym przypadku jest zaimplementowanie mechanizmu “feature flags”, dzięki któremu jesteśmy w stanie sterować naszą aplikacją i po części użytkownikami, którzy w niej korzystają. Pisząc “sterować”, mam na myśli włączanie/przełączanie/wyłączanie konkretnych funkcjonalności dla użytkowników, którzy spełniają określone reguły np. lokalizacja, posiadają konkretne ciastko, itp. 

Inaczej ma się sprawa, gdy mówimy o strategii “shadow”, w tym przypadku użytkownik musi zostać przeprowadzony starą ścieżką, jeśli chodzi o dany proces np. dodanie produktu do systemu, jak również w tle (w cieniu) musi zostać przeprocesowana nowa ścieżka. Najlepiej, żeby to nie miało wpływu na użytkownika, zarówno jeśli chodzi o stan aplikacji (czyli nie może nastąpić sytuacja, że dodał dwa takie same produkty), jak i o kwestie wydajnościowe. Jednak nie zwalnia nas to z obowiązku zarejestrowania, jak działa funkcja uruchomiona w tle. 

Finalnie chcemy przeanalizować jej działanie i porównać dane w stosunku do jej starszej wersji. W tym podejściu jest więcej pułapek, na które musimy uważać. Bardzo dużo zależy od tego, co chcemy wdrażać i nakład pracy poświęcony na odpowiednie przeprocesowanie użytkownika jest znaczący. 

Czy ta strategia jest najlepsza, ze wszystkich możliwych? Nie jest. Nie jest też podejściem często przeze mnie wybieranym ponieważ wdrożeń gdzie jest sens ją stosować wbrew pozorom nie jest dużo. Nasuwa się więc pytanie czy jest sens poświęcać tej strategii więcej uwagi? Moim zdaniem jest i to nie podlega dyskusji.  

Historia pewnej porażki

Wyobraź sobie sytuację, że musisz wraz z zespołem refaktoryzować jakąś kluczową część systemu. Przykładem może być system do naliczania prowizji dla użytkownika. Co teraz robisz? Zapewne wydzieracie tą część systemu z monolitu, który pamięta jeszcze czasy, gdy zarywaliśmy nocki grając w Quake 3 Arena (jeśli nie wiesz co to, to już w samo w sobie znaczy, że to coś starego). Tworzycie bazylion mikroserwisów (lub nie), rozpraszacie wszystko, co tylko możliwie i gdzie to tylko możliwe, aż finalnie zbliża się godzina zero.

Czas na wdrożenie, uruchomienie i zebranie pochwał. Wybieracie okienko czasowe, gdzie ruch jest najmniejszy, czyli pewnie gdzieś w nocy, zamawiacie pizze, wdrażacie kod i… wszystko działa, prowizje się naliczają, wypłaty są realizowane. 

Rano poklepujecie się po plecach, zbierasz gratulację od szefostwa. Z niecierpliwością zaczynasz wyczekiwać końca miesiąca z myślą o bonusie za udane wdrożenie strategicznego projektu. Aby zabić czas zabierasz się za naprawianie kolejnej części aplikacji. Następuje chwila spokoju, a Ty oddajesz się nowemu projektowi. Po pewnym czasie zaczynają jednak napływać niepokojące wieści z działu obsługi klienta. 

Coś komuś się nie naliczyło. Ktoś miał problem z wypłatą. Co gorsza to nie są generyczne przypadki. Problem pojawia się wybiórczo i nie jesteś w stanie określić skali osób nim dotkniętych. Zaczynają się niewygodnie pytania w stylu: co poszło nie tak? Liczba zgłoszeń rośnie wprost proporcjonalnie do niezadowolenia użytkowników z serwisu. 

Grunt coraz bardziej zaczyna palić się wam pod stopami, nie jesteście w stanie odtworzyć prawidłowego stanu systemu, zaczyna się szukanie winnego, premie zostaną cofnięte… i tak naprawdę jesteście w cz….. d…. .

Jesteś pewien, że chcesz coś takiego przeżywać? Ja przeżyłem i uwierz mi, nie jest to nic miłego. 

Oczywiście trochę przejaskrawiłem, jednak to dobrze obrazuje pewien przypadek, gdzie jak najbardziej sensowne jest użycie podejścia “shadow”, czyli tak zwanego równoległego uruchomienia starej i nowej wersji systemu. 

W sytuacjach gdy:

  • potrzebujemy monitorować działanie usługi w dłuższym okresie, 
  • potrzebujemy sprawdzić zachowanie usługi przy obciążeniu produkcyjnym,
  • wdrażamy kluczowe części systemu,
  • refaktorujemy serwis i chcemy mieć pewność, że z biznesowego punktu widzenia nic się nie zmieniło,
  • wszystkie lub dowolna kombinacja powyższych,

… jak najbardziej uzasadnione jest poświęcenie czasu na obsługę shadow deployment.

Wdrożenie to nie tylko wgranie kodu na produkcję

Wdrożenie aplikacji nie kończy się w chwili wrzuceniu kodu na produkcję. Cały proces rozkłada się w czasie na konkretne etapy, a podział na etapy dotyczy zarówno kontekstu instancji, jak i ficzerów:

1. Równoległe uruchomienie dwóch wersji aplikacji/ficzerów (zapewne pamiętasz, że jedna ma działać w tle).

2. Monitoring obu wersji. To czas gdy zbieramy dane. Porównujemy rezultaty operacji jakie zostały przeprowadzone przez starą wersję i nową wersję. Przykładowo jeśli dokonujemy “tylko” rafaktoryzacji to oczekujemy, że naliczone zostaną identyczne prowizje zarówno przez stary system jak również przez jego nową implementację. Dzięki temu, że trzymamy te dane osobno możemy je ze sobą zestawić. Przeszukać pod kątem anomalii, porównać wygenerowane raporty itp.

Dodatkowo jesteśmy również w stanie stwierdzić czy nowa wersja działa stabilnie przy produkcyjnym obciążeniu. Ile powinny trwać tego rodzaju testy? Nie ma reguły, to zależy w dużej mierze od tego, co jest wdrażane i od czasu potrzebnego do nabrania pewność że zebrane dane i przeprowadzone analizy dają jednoznaczną odpowiedź na pytanie czy aplikacja działa poprawnie. Na tym etapie, gdy wyjdą jakiekolwiek błędy to jesteśmy w stanie albo bezboleśnie je załatać i wgrać poprawkę albo jeśli to jest coś poważniejszego wycofać całe wdrożenie (jednak to już jakiś ekstremalny przypadek). Czy użytkownik będzie miał świadomość, że coś skaszanilśmy, absolutnie nie. On nawet nie będzie wiedział, że gdzieś działała jakaś inna wersja kodu. 

ZOBACZ TEŻ:  Code Review bez przemocy, czyli 5 sposobów empatycznej komunikacji developera

3. Finalnie jeśli wszystkie znaki na niebie, ziemi i nawet w /dev/null wskazują, że wdrożone zmiany działają poprawnie możemy przełączyć wersję kodu obsługujące użytkownika. Podkreślam – przełączyć. Nowa wersja zaczyna obsługiwać żądania od użytkownika, a stara wersja przechodzi do podziemia i zaczyna działać w tle. Zauważ, że nie musimy dokonywać żadnych migracji danych itp, ponieważ wcześniej aplikacja działająca w tle cały czas odkładała i zapisywała dane, a Ty te dane zweryfikowałeś i stwierdziłeś że są poprawne. Ostatni raz już wspomnę, że pozostawienie poprzedniej wersji kodu działającej w tle jest bardzo istotne na tym etapie. Dlaczego? Dowiesz się w następnym punkcie. 

4. Następuje kolejny proces weryfikacji. Pewnie ciśnie Ci się na usta pytanie: “kurcze, ile to razy można weryfikować aplikację, robiliśmy to podczas developmentu, procesu akceptacyjnego, przeszły automaty, podczas działania w tle i teraz kolejny raz, daj pan żyć, ile jeszcze?”

Zauważ, jedną istotną różnicę. Teraz nowa wersja aplikacji jest dostępna publicznie i używana przez użytkownika, a poniekąd wiadomo, że nie ma lepszego testera aplikacji niż użytkownik końcowy i jest spore prawdopodobieństwo, że zgłosi nam napotkany przez siebie błąd, a wręcz pewność, jeśli na tym błędzie jest w jakiś sposób stratny. Jeśli w firmie, w której pracujesz jest dział obsługi klienta, to jest to najlepszy moment, aby go poinformować o wdrożeniu nowej wersji usługi i w razie jakichkolwiek zgłoszeń powinien bezpośrednio Ciebie (zespół) informować. 

Dzięki takiemu podejściu jesteśmy w stanie szybko dowiedzieć się o potencjalnych problemach, których nie udało Ci się wychwycić a przytrafiły się użytkownikom. W przypadku gdyby pojawiło się coś poważnego, czego w odpowiednio krótkim czasie nie będziesz w stanie naprawić, to ponownie przełączamy wersje aplikacji. Ruch użytkowników ponownie jest procedowany przez starszą wersję i to bez żadnych migracji danych, ponieważ cały czas działała w tle i przeprowadzałą równolegle procesy biznesowe utrwalając ich stan. Ty w tym czasie na spokojnie i bez stresu (powiedzmy) tworzysz łatki do nowego kodu.

5. Z czasem nabierzesz pewności co do działającego kodu i nadchodzi chwila wyłączenia oraz usunięcia z kodu zbędnej już starej wersji aplikacji. Dopiero na tym etapie można powiedzieć, że wdrożenie kodu się zakończyło. 

Dla osób, które przytłoczyła ilość tekstu napiszę bardziej zwięzłą wersję.

  1. Równoległe uruchomienie dwóch wersji aplikacji/ficzerów (zapewne pamiętasz, że jedna ma działać w tle).
  2. Monitoring obu wersji, porównanie rezultatów ich działania.
  3. W przypadku gdy rezultaty są zgodne z oczekiwanymi przełączamy użytkownika na nową wersję a stara zaczyna działać w tle.
  4. Ponowny monitoring, analizujemy również sygnały napływające od użytkowników.
  5. Finalnie usuwamy kod powiązany ze starą wersją aplikacji.

Mam nadzieję, że teraz dostrzegasz potencjał tego rodzaju deploymentu i masz świadomość kiedy jest sens go stosować. Ostatnim razem, gdy użyłem tej strategii, było wdrożenie nowej wersji generowania miniatur prac (grafik) serwowanych użytkownikowi. 

Przykład z życia

Displate to serwis, który opiera się na grafikach. Użytkownik szuka plakatu, my go drukujemy na metalu i dzięki temu nasz produkt staje się w pewien sposób unikalny. Na pewnym etapie przygniotła nas skala grafik do obsłużenia sięgająca już nie tysięcy, ale milionów prac. 

Stare rozwiązanie nie udźwignęło tematu, a nieskończone skalowanie serwerów w górę pożerało budżet. Finalnie zaimplementowaliśmy rozwiązanie oparte na Cloudflare + AWS Lambda + S3 + PHP, jednak z perspektywy artykułu istotny jest sposób wdrożenia. 

Ze względu na to, że rozproszyliśmy nasz system, praktycznie całą implementację mogliśmy uruchomić “na boku” produkcyjnie i testować niezależnie, z racji tego, że to były nowe usługi. Nie byliśmy jednak w stanie sprawdzić jakości wszystkich grafik, tym bardziej, że konkretne miniatury były generowane na żądanie, a nie podczas wgrywania pliku do systemu. 

Dodatkowo musieliśmy zmienić adresację do nowego miejsca, gdzie zapisywaliśmy wygenerowane pliki miniatur. Nie mogliśmy sobie pozwolić na sytuację, w której uruchamiamy nowy system na produkcji i nagle zasypują nas informacje o błędach przy generowaniu plików, lub użytkownicy będą wysyłać screeny błędnymi grafikami lub ich brakiem. Doszliśmy więc do wniosku, że najsensowniejszym rozwiązaniem na wdrożenie będzie shadow deployment. 

W pierwszym etapie wdrożenia przy każdym żądaniu o grafikę w tle nowy system generował nową. Już na tym etapie okazało się, że podjęliśmy słuszną decyzję. Wgrywane grafiki okazały się na tyle różnorodne, że część z nich wygenerowała problemy, do których po rozwiązanie trzeba było zapuścić się przynajmniej na drugą stronę z wynikami wyszukiwania w googlu. 

Dokonfigurowanie lambdy zajęło nam około dwóch tygodni. Podczas gdy my ciężko pracowaliśmy w tym okresie, użytkownicy – na tyle na ile pozwalał stary system – bez problemu używali bieżącej wersji serwisu. Po przepięciu systemu na nowy udało się już uniknąć zgłoszeń od użytkowników, a to, co się pojawiło, to były już problemy z samymi wgrywanymi plikami (niepoprawna kompresja, itp), a nie z systemem. Tak więc z perspektywy czasu mogę powiedzieć, że wybór okazał się słuszny i dał nam przestrzeń do działania, gdy pojawiły się problemy. Dodatkowo system zmierzył się z obciążeniem produkcyjnym, co utwierdziło nas w przekonaniu, że nie będzie z nim problemów pod tym kątem po udostępnieniu go użytkownikom.

Parę słów na koniec

Jeśli jesteś w tym miejscu to jestem pod wrażeniem, że wytrwałeś i dziękuję Ci za to.

Możesz się poczuć trochę przytłoczony tym wszystkim, w szczególności gdy wcześniej nie miałeś z tym tematem do czynienia. Jednak nie przejmuj się, z czasem wszystko się zacznie układać w jedną całość. Które z tych rozwiązań jest najlepsze? Moja odpowiedź: “to zależy”. 

Dla mnie nie ma w tym przypadku jedynej słusznej drogi. Zdecydowanie nie polecam “Recreate” przy wdrożeniach produkcyjnych. Jeśli chodzi o środowiska developerskie/akceptacyjne to ten rodzaj wdrażania jest jeszcze do przyjęcia, jednak jeśli chodzi o wdrożenie produkcyjne, to nigdy przenigdy go nie stosuj. Jeśli w tej chwili tak wygląda Twój sposób wdrażania nowych wersji, to proponuję poświęcić trochę czasu temu aspektowi i zmienić ten sposób na dowolny inny.

Na zakończenie daję Ci taką oto złotą myśl: “nie myśl tylko happy path’em, przygotuj sobie też plan na wypadek, gdyby coś poszło nie tak”. Czasami może to być szybkie wycofanie zmian, czasami po prostu wyświetlanie użytkownikowi stosownej informacji o niedostępności usługi. Ważne, aby ten plan był, dzięki temu będziesz miał więcej czasu na reakcję w przypadku ewentualnych problemów.

Obyś miał jak najmniej nieudanych wdrożeń!

teleturniej programista100k

Zdjęcie główne artykuły pochodzi z unsplash.com.

Zapraszamy do dyskusji

Patronujemy

 
 
More Stories
Robot wielkości karalucha? Tak, a do tego jest wytrzymały