Przeniesienie domeny i CI/CD z Github Actions
Przeniesienie domeny i CI/CD z Github Actions
Zdarzają się sytuacje, dla których trudno znaleźć jeden kurs online czy tutorial, bo wymagają połączenia wiedzy z wielu z nich. Przykładem może być przeniesienie domeny oraz skonfigurowanie środowiska developerskiego i produkcyjnego. Pomimo tego, że każdy z tych elementów uwzględnia powszechnie stosowane dobre praktyki, nie można zastosować ich wprost w praktyce, ze względu na specyfikę konkretnego projektu. W tym wydaniu Full-stack newsletter podzielę się z Tobą moimi doświadczeniami z projektu easy_. Zadanie polegało na przeniesieniu domeny oraz ustawieniu całego workflow CI/CD w oparciu o Github Actions.
Kilka słów o projekcie
Tworzymy zestaw narzędzi, ułatwiające sprzedaż produktów cyfrowych w Internecie. Do tej pory znajdowały się w domenie easytools.pl (tutaj była strona Webflow) oraz subdomenie app.easytools.pl (tutaj były aplikacje). Sam projekt podzielony jest na dwa repozytoria — front-end w Vue i back-end w NestJS. W teorii przeniesienie aplikacji na nową domenę, nie jest dużym wyzwaniem. Ustawienie łatwego deploymentu z Github Actions również. W praktyce wygląda to zupełnie inaczej.
Kształt projektów zmieniał się na przestrzeni ostatnich dwóch lat. Przykładowo EasyCart było rozwijane całkowicie niezależnie. EasyTimer przez jakiś czas był jednym plikiem node.js. Szybko zaczęły dochodzić kolejne narzędzia i oficjalnie jest ich 7 a nieoficjalnie .. więcej. Oznacza to, że nawet proces budowania aplikacji przestał był w porządku i opublikowanie aplikacji trwało nawet kilka minut. Nadszedł dobry czas aby to wszystko poukładać i dostosować cały projekt do szybkich zmian oraz przyrostu liczby narzędzi. Pojawiła się także jeszcze jedna zmienna — większy zespół związany z operacyjnym połączeniem EasyCart i EasyTools.
W skrócie:
- kilka aplikacji łączy się w jedną domenę easy.tools
- kilka małych, zewnętrznych aplikacji zostaje dołączona do jednej z dwóch głównych aplikacji
- powiększa się zespół
- rośnie liczba narzędzi easy (np. EasyPricing)
- konieczne są środowiska dla testów oraz środowisko developerskie
Do tego dochodzi obsługa wielu języków aplikacji (PL i EN) oraz poprawki wydajności w dwóch pobocznych narzędziach, które funkcjonowały w porządku do tej pory ale teraz konieczna była refaktoryzacja. No i ostatecznie — wszystko musi się wydarzyć z możliwie minimalnym brakiem dostępności narzędzi, ponieważ aplikacja posiada użytkowników (tutaj mowa o EasyTools. EasyCart pozostało bez zmian).
Gdy patrzyłem na to wszystko, wiedziałem że czeka nas sporo pracy i najlepsze co mogliśmy zrobić, to podzielić się pracą i zacząć.
Miesiąc później
Przygotowanie aplikacji do przeniesienia trwało lekko ponad miesiąc i powodem były detale, których zwykle nie bierze się pod uwagę na etapie wczesnego developmentu. Nawet konfiguracja zmiennych środowiskowych zaczęła być problematyczna. Do tej pory sam traktowałem je dość luźno i stosowałem wyłącznie dla kluczy i pojedynczych ustawień aplikacji.
Tutaj konieczne było także zadbanie o chociażby katalogi w których przechowywane są pliki tymczasowe czy pliki wgrywane przez użytkowników. Te ostatnie muszą jeszcze uwzględniać kopie zapasowe, ponieważ oczywiście nie są częścią repozytorium albo nawet znajdują się poza katalogiem projektu i w procesie budowania nowej wersji aplikacji są "linkowane" z docelowym miejscem. Dzięki temu nie ma potrzeby aby je przenosić za każdym razem do nowej wersji projektu.
Przykładowo jedna z aplikacji posiada funkcję generowania kodu iframe do osadzenia na stronie użytkownika. Bez ustawienia domeny aplikacji w zmiennych środowiskowych, funkcja przestałaby działać lokalnie na komputerze czy na serwerze developerskim.
Zmienne środowiskowe odgrywają istotną rolę nie tylko przy generowaniu takich linków, ale także w kontekście bezpieczeństwa (np. ustawień CORS) i dopuszczonych domen, które mogą komunikować się z konkretnym API.
Zarządzanie tak rozbudowanym plikiem ze zmiennymi środowiskowymi ułatwia plik env.d.ts opisującym typy pliku .env.
Zmienne środowiskowe okazały się też szczególnie przydatne na potrzeby logów. NestJS posiada wbudowany Logger z którego w tym przypadku korzystamy zarówno po to aby zapisywać informacje o błędach ale także o niektórych z kluczowych akcji:
W momencie gdy cokolwiek dzieje się nie tak z aplikacją, zwykle pierwszym miejscem do którego sięgamy, są logi. Wykorzystanie zmiennych środowiskowych znacznie zwiększa ich użyteczność i czytelność. Jest to kolejny przykład pokazujący, że wartości w pliku .env odgrywają bardzo istotną rolę.
Pomimo tego, że mówimy tutaj o detalach, spędziliśmy na nich wiele godzin, upewniając się, że niczego nie pominęliśmy. Było to także miejsce na przygotowanie miejsca na testy z Jest, których do tej pory nie mieliśmy.
Reszta wprowadzonych zmian uwzględniała naprawę mniejszych i większych błędów oraz modyfikacje wymagane do przejścia na pełen proces CI/CD z Github Actions. Tutaj sytuacja skomplikowała się o konieczność skonfigurowania nowego serwera w Digital Ocean, przeznaczonego wyłącznie na potrzeby developerskie. Na szczęście skonfigurowanie plików opisujących workflow w GA jest stosunkowo proste ale i tak zajęło mnóstwo czasu bo uwzględniało scenariusze dla środowisk: prod (produkcja), dev (nasze potrzeby), stage (tutaj trafiają zmiany gotowe do publikacji), demo (udostępnione innym), e2e (tu odbywają się testy automatyczne).
Określiliśmy także zasady wykorzystywania każdego z nich ale myślę że nazwy są wystarczająco opisowe. Przykładowo na prod i stage mogą trafiać wyłącznie takie Pull Requesty, które przeszły review przynajmniej jednej dodatkowej osoby. Co więcej te ustalenia odzwierciedlone są w ustawieniach ochrony poszczególnych branch'y w repozytorium:
Przyłapaliśmy się jednak na tym, że włączanie zasad ochrony nie sprawdza się na wczesnym etapie konfiguracji workflow, ponieważ wtedy dopuszczalne było akceptowanie własnych zmian bo z tej wersji aplikacji i tak jeszcze nie korzystali użytkownicy.
Po zakończeniu konfiguracji, wprowadzanie zmian w projekcie przechodzi przez kilka etapów, które zapewniają wystarczający poziom zabezpieczeń przed publikacją czegokolwiek, co zawiera błędy. Oczywiście zdarzają się sytuacje gdy coś przestaje działać, ale wykorzystanie TypeScript i pokrywanie kluczowych funkcji testami, daje coraz lepsze efekty.
Przeniesienie domeny
Technicznie rzecz biorąc w naszym przypadku chodziło o przekierowanie, ponieważ poprzednia domena dalej funkcjonuje, jednak z pomocą proxy_pass przekierowuje użytkownika na nową domenę. Przykładowo adres: https://app.easytools.pl/easylove
przekierowuje użytkownika na https://app.easy.tools/love
. Co ciekawe, zapytania z app.easy.tools
również nie są obsługiwane przez docelowy serwer, lecz przez Server Proxy. Powodem takiej konfiguracji jest fakt, że łączymy nie jedną aplikację a dwie. Muszę przyznać, że wygląda to dość skomplikowanie ale to najlepsza opcja na jaką wpadliśmy.
W pierwszej kolejności, jeszcze przed przekierowaniem domeny, upewniliśmy się, że aplikacja i jej wszystkie funkcje działają poprawnie na nowej domenie. Gdy tak się stało, był to sygnał do tego, że jesteśmy gotowi na przekierowanie.
Przeniesienie domeny uwzględniało także konieczność przeniesienia plików wgrywanych przez użytkowników do innego katalogu, podłączonego do nowej aplikacji. Tutaj należało zwrócić uwagę na wymagany downtime aplikacji. W trakcie wyłączenia mającego trwać kilka minut, uniemożliwiliśmy wgrywanie czegokolwiek aby uniknąć sytuacji w której zgubimy jakieś pliki. Oczywiście można byłoby to załatwić bez wyłączania aplikacji ale na tym etapie rozwoju projektu było to nieuzasadnione. Sam downtime trwał jednak nieco dłużej i obejmował tylko wybrane funkcje, które z różnych przyczyn przestały działać a główną z nich była konfiguracja NGINX.
Ciekawostka: w szybkim rozwiązaniu problemów dotyczących konfiguracji pomogło nam GPT-3, które bardzo precyzyjnie wskazywało na przyczyny problemów i ułatwiało nam znalezienie rozwiązań. Jeden z przykładów:
Nie mam wątpliwości, że gdybyśmy nie mieli wtedy dostępu do GPT-3, rozwiązanie wszystkich problemów zajęłoby nam znacznie więcej czasu.
Doszliśmy więc do momentu w którym aplikacja zaczęła działać na nowej domenie. Niezaprzeczalną zaletą całego zadania jest fakt, że po drodze przygotowaliśmy szereg narzędzi i zmieniliśmy cały workflow tak, aby nie tylko był dostosowany do rozwoju obecnych narzędzi EasyTools ale także pozwalał na dodawanie kolejnych. Jest to jedna z praktyk, którą stosuję w każdym z moich projektów. Polega na sprawianiu aby większość podejmowanych działań przekładała się na efekty większe niż bezpośredni efekt tego działania. W tym przypadku przekierowanie domeny przygotowało nasze całe środowisko developerskie i produkcyjne.
Podsumowując, w tej chwili aplikacje działają łącznie na 5 serwerach, nie licząc backupów (EasyCart Prod & Dev, EasyTools Prod & Dev, Proxy) a niebawem dojdzie szósty. No i w ramach serwerów developerskich istnieje kilka, wspomnianych środowisk. Do zarządzenia taką strukturą konieczne jest automatyczne zarządzanie procesem deploymentu, wliczając w to migracje baz danych oraz monitoring aplikacji.
Co dalej?
Minął miesiąc od wdrożenia opisanych zmian. W praktyce sprawdziły się bardzo dobrze i znacząco wpłynęły na mniejszą liczbę nowych błędów. Oczywiście nie pozbyliśmy się ich wszystkich i mamy na swojej liście przynajmniej kilka o których wiemy a których nie udało nam się jeszcze rozwiązać.
Jest kilka punktów, które chciałbym podkreślić z tego tekstu:
- wczesny rozwój aplikacji musi być szybki. Nie rzadko jakość kodu jest wtedy niższa ale nie jest to usprawiedliwienie. Warto mieć na uwadze przyszłe scenariusze oraz fakt, że nie każdy z nich uda nam się przewidzieć lub zaadresować (u mnie była to np. obsługa wielu języków)
- zmienne środowiskowe stają się kluczowe a ich mądre wykorzystanie wpływa na elastyczność projektu w różnych konfiguracjach serwerów
- narzędzia takie jak GPT-3 już teraz są w stanie skutecznie pomagać w rozwiązywaniu problemów
- jakość logów w aplikacji przenosi proces debugowania na kolejny poziom. Trzeba tylko skonfigurować je tak, aby nie zapisywać w nich jakichkolwiek wrażliwych danych
- Github Actions jest niezwykle łatwe w konfiguracji, jednak miało problemy z budowaniem aplikacji na serwerze z pamięcią na poziomie 1GB RAM. Same runnery zajmują także sporo miejsca i warto wykupić większy dysk
Jako ciekawostkę dodam fakt, że część z nas pracuje w VSC a część w IntelliJ. W IntelliJ regularnie zdarza nam się wychwytywać różne błędy, których nie wychwytuje VSC. Oczywiście może to być wyłącznie kwestia konfiguracji, jednak już wielokrotnie przekonałem się, że narzędzia JetBrains w praktyce sprawdzają się znacznie lepiej. Na jednym z moich stories na @_overment pisałem także o tym, że zwiększenie pamięci w ustawieniach IntelliJ znacząco przyspiesza jego działanie. Co nie zmienia faktu, że VSC to znacznie szybszy edytor (ale nie tak szybki jak Lapce, aczkolwiek ten nie sprawdza się jeszcze tak dobrze i ma nadal bardzo małe community wokół siebie. Warto jednak przynajmniej go zobaczyć).
Na koniec chciałbym jeszcze podkreślić coś, co raczej nie wybrzmiało dostatecznie. Przy rozwoju moich aplikacji, nie rzadko zdarza mi się iść na kompromisy lub popełniać błędy. Przykładem kompromisu i bardzo szybkiego "poruszania się" był EasyTimer, który jako jeden plik index.js zdobył pierwsze miejsce na Product Hunt rok temu i pracował w tej formie dla mnie przez kolejne 12 miesięcy. Dziś został przepisany, zoptymalizowany i w nowej formie wcielony do narzędzi easy_
Mam nadzieję, że udało mi się wskazać przynajmniej kilka punktów, które możesz wziąć pod uwagę w swoich aplikacjach, aby uniknąć moich błędów lub wykorzystać praktyki które sprawdziły się w moich projektach. Oczywiście w tak krótkim tekście nie miałem możliwości wejść w duże szczegóły i jeżeli jakiś temat szczególnie zwrócił Twoją uwagę, daj mi proszę znać a wezmę go pod uwagę w kolejnych wpisach.