Fake it... ale nie tak jak myślisz - NServiceBus Web Host
Posty z tej serii:
- Fake it… ale nie tak jak myślisz - NServiceBus Web Host
- Fake it… ale nie tak jak myślisz - ASP.NET Web Host
- Fake it… ale nie tak jak myślisz - NServiceBus Windows Service Host
- Fake it… ale nie tak jak myślisz - Build - Run Unit tests - Publish
Jakie jest twoje pierwsze skojarzenie z frazę Fake it w kontekście wytwarzania oprogramowania? U mnie jest to/było Fakeowanie zależności w celu jednostkowego przetestowania konkretnego kawałka funkcjonalności. Przykłady zależności? Warstwa dostępu do bazy danych, zależność do zewnętrznego serwisu lub do systemu plików. W skrócie, każda zależność do zasobu, który trzeba przygotować przed uruchomieniem testu. Fakeując taki zasób, symulujemy jego istnienie, wykorzystując do tego pamięć operacyjną maszyny na której uruchamiany jest test. Z takim podejściem związana jest cała filozofia, która definiuje pojęcia, nazwy, sposób zastosowania, itd. W wersji uproszczonej, chodzi o możliwość jednostkowego testowania logiki biznesowej w oderwaniu od zewnętrznych zależności. A czy istnieją biblioteki, wspomagające w realizacji takiego podejścia? Tak? A jakie? A proszę bardzo:
Od momentu, kiedy natrafiłem na narzędzie FAKE fraza Fake it nabrała dla mnie innego znaczenia, a mianowicie, przygotowanie napisanego kodu do wdrożenia oraz jego wdrożenie:
FAKE - A DSL for build tasks and more The power of F# - anywhere - anytime (https://fake.build/)
FAKE pozwala pisać skrypty wdrożeniowe w języku F#. Samo narzędzie również napisane jest w języku F#.
Artykuł ten jest pierwszym z serii, w której pokażę Ci, w jaki sposób wykorzystałem FAKEa do buildowania oraz wdrażania systemu komentarzy używanego na niniejszym blogu. Do tej pory używałem skryptów napisanych w PowerShellu. O szczegółach implementacji możesz przeczytać w jednym z moich poprzednich artykułów.
Zacznijmy od pewnej nowości. Właścicielem agregatu CommentPolicy jest Endpoint o nazwie BlogComments. Framework NServiceBus umożliwia hostowanie Endpointów na wiele sposobów. Jednym z nich jest możliwość wykorzystania któregoś z frameworków webowych. Dzięki temu możemy wysyłać wiadomości z aplikacji webowej za pomocą Endpointa typu Send-only. Jeśli używamy technologii kolejkowej opartej o model centralnego Message Brokera, to nic nie stoi na przeszkodzie, aby w procesie aplikacji webowej hostować pełny Endpoint, który oprócz wysyłania wiadomości potrafi je również przetwarzać. W wersji 2.1.0-beta.1 systemu komentarzy na blogu przeszedłem z hostingu Windows Service na hosting Web Application.
W dalszej części artykułu przejdziemy przez funkcjonalności wdrażania aplikacji webowej, której jedynym zadaniem jest hostowanie Endpointa NServiceBus.
Design
Wdrożenie nowej wersji Endpointa składa się z minimum czterech operacji:
- Zatrzymanie uruchomionego Endpointa
- Usunięcie artefaktów z aktualnymi funkcjonalnościami
- Wgranie artefaktów z nowymi funkcjonalnościami
- Wystartowanie Endpointa
Powyższe kroki pokrywają się 1 do 1 z krokami wdrażania nowych wersji hostowanych jako Windows Service, w których mechanizm wdrażający ma uprawnienia do zatrzymywania oraz uruchamiania usług Windows. Jeśli mechanizm nie ma takich uprawnień to hmm…nie spotkałem się z tego typu rozwiązaniami.
Jeśli chodzi o aplikacje webowe, mając odpowiednie uprawnienia na serwerze webowym, możemy wykonać takie same kroki. Jedna ważna uwaga. Pamiętaj, że wdrażamy Endpoint NServiceBusa, co oznacza, że używamy mechanizmu kolejkowego, co z kolei oznacza, że zatrzymanie aplikacji webowej nie wstrzymuje
użytkowników przed dalszym korzystaniem z systemu. Wszystkie wygenerowane dane w trakcie wdrażania nowej wersji dodawane są do kolejki i zostaną przetworzone, jak tylko pojawi się nowa wersja aplikacji. Jest to odwrotne zachowanie w stosunku do aplikacji webowych przetwarzających żądania HTTP, gdzie zatrzymanie aplikacji powoduje niemożliwość
korzystania z systemu. W takich przypadkach można np. stosować podejście Blue-Green-Deployment. Więcej o takim podejściu przeczytasz we wcześniej wspomnianym artykule.
No dobrze, a co jeśli mechanizm wdrażający nie ma uprawnień zatrzymywania oraz startowania aplikacji na serwerze webowym? W takim przypadku trzeba przedefiniować pojęcia Startu i Stopu. Wystartować aplikację możemy poprzez wywołanie dowolnego żądania HTTP. Jeśli używamy np. frameworka ASP.NET to wystarczy, że mamy jeden Controller z jedną metodą typu GET, którą zawołamy w ostatnim kroku wdrożeniowym. Sposób zatrzymywania aplikacji zależny jest od dostawcy. Może to być np. wgranie na serwer pliku z odpowiednim rozszerzeniem, a następnie wywołanie żądania HTTP.
Do powyższego schematu możemy dodawać elementy opcjonalne np. backup aktualnej wersji, wysyłanie powiadomień o pojawieniu się nowej wersji itp. Schemat, którego ja używam, przy wdrażaniu nowej wersji systemu komentarzy na blogu zawiera kroki:
- Zatrzymać uruchomiony Endpoint
- Zrobić backup aktualnej wersji
- Usunąć artefakty z aktualnymi funkcjonalnościami
- Wgrać artefakty z nowymi funkcjonalnościami
- Wystartować Endpoint
Develop
Pisząc w języku F# mamy możliwość uruchamiania kodu w tzw. trybie interaktywnym. Oznacza to, że nie musimy tworzyć projektów *.csproj. W Visual Studio kod możemy pisać oraz uruchamiać w oknie F# Interactive. Możemy też dodać implementację do pliku z rozszerzeniem .fsx, a następnie uruchomić program z wiersza poleceń za pomocą narzędzia fsi.exe.
FAKE umożliwia uruchamianie kodu napisanego w plikach .fsx. Zobaczmy, w jaki sposób możemy zakodować prototyp skryptu wdrożeniowego np. w pliku o nazwie host_ftp_deploy.fsx:
Kroki do uruchomienia definiuje się w tzw. Targetach. Następnie za pomocą operatora ==>
definiuje się kolejność wykonywania poszczególnych Targetów. Ostatnim krokiem jest uruchomienie Targetu rozpoczynającego proces wdrażania. W naszym przykładzie uruchamiamy Target Deploy Endpoint
. FAKE rozpozna zależność do Targetu Backup Endpoint
, a następnie do Targetu Stop Endpoint
, co spowoduje uruchamianie Targetów w kolejności:
- Stop Endpoint
- Backup Endpoint
- Deploy Endpoint
Po uruchomieniu skryptu dzieją się ciekawe rzeczy. Kod napisany jest w języku F#, także mamy pełne wsparcie kompilatora! Jeśli implementacja jest niepoprawna, to nic się nie uruchomi. FAKE zwróci błąd kompilacji. Przykładowo, jeśli zastąpisz, któreś z wywołań Trace.trace
na Trace.trac
, to uruchamiając skrypt, otrzymasz komunikat:
Drugą pomocną rzeczą jest pełne wsparcie Intellisense! Otwierając skrypt w Visual Studio zobaczysz kolorowanie składki, podpowiedzi dostępnych funkcji, błędy kompilacji itd.
Trzecim elementem jest wsparcie dla Nugeta. Pierwsze uruchomienie skryptu spowoduje pobranie zależności zdefiniowanych na początku pliku.
Moją ulubioną cechą w programowaniu funkcyjnym jest możliwość dzielenia kodu na mniejsze kawałki, poprzez definiowanie kolejnych funkcji. W programowaniu obiektowym tymi kawałkami są klasy, natomiast mam takie odczucie, że proces decyzyjny w przypadku funkcji jest prostszy niż w przypadku klas. Funkcje mogą być zarówno proste, jak i złożone. Sama funkcja jest oddzielnym elementem. W przypadku klas trzeba się trochę bardziej nagimnastykować, aby dobrze zdefiniować jej dobrą odpowiedzialność, dobrać odpowiednie pola, metody, itp. W przypadku FAKEa możemy określić, że pojedynczą odpowiedzialnością jest sam skrypt wdrożeniowy. W naszym przypadku jest to wdrożenie Endpointa hostowanego w aplikacji webowej. Poszczególne Targety są funkcjami, które wyznaczają kroki wdrożeniowe. Każdy krok może być w całości zakodowany w Targecie lub podzielony na mniejsze elementy - funkcje.
Przejdźmy teraz do szczegółów. Skrypt powinien być uniwersalny tak, aby można go było wykorzystać do wdrażania na różne środowiska. Pierwszym krokiem jest wydzielenie kodu pobierającego parametry wdrożeniowe:
FAKE posiada wbudowane moduły oraz funkcje, których możemy używać. Jedna z nich umożliwia odczytanie parametrów wejściowych skryptu wewnątrz Targetu - Environment.environVarOrFail
. Opakujmy wbudowaną funkcję naszą funkcją, której będziemy używać we wszystkich Targetach:
FAKE w piątej wersji nie posiada modułu pozwalającego wykonywać operacje na protokole FTP. Z tego względu musimy taką funkcjonalność, zakodować sami używając np. WinSCP. Trzy podstawowe kroki wykonania żądania FTP to:
- Otwórz sesję
- Wykonaj operację
- Zamknij sesję
Idealna funkcjonalność do zamknięcia w osobnej funkcji:
A teraz dwa magiczne elementy języka F#:
-
Słowo kluczowe
use
użyte przy tworzeniu obiektuSession
sprawi, że po zakończeniu cyklu życia obiektu automatycznie zostanie zawołana metodaDispose
. -
Parametr
action
funkcjimakeFtpAction
jest również funkcją, która jako parametr bierze obiektSession
i zwraca generyczną wartość. Oznacza to, że do funkcjimakeFtpAction
możemy przekazać dowolną operację zgodną z sygnaturą funkcjiaction
.
Zobaczmy, jak można wykorzystać funkcję makeFtpAction
w kroku zatrzymywania Endpointa:
Przejdźmy przez powyższy kod, zaczynając analizę od końca. Najpierw sprawdzamy stan Endpointa poprzez wywołanie zakodowanej lokalnej funkcji getEndpointState
. Jeśli Endpoint nie istnieje lub jest już zatrzymany, to nie robimy nic. Jeśli jest uruchomiony, to wywołujemy zakodowaną lokalną funkcję stopEndpoint
, która zatrzyma Endpoint. W lokalnych funkcjach używany wcześniej zdefiniowanej funkcji makeFtpAction
. Na przykład:
makeFtpAction (fun ftp -> ftp.EnumerateRemoteFiles(ftpEndpointPath, null, EnumerationOptions.None
- pobranie listy plików ze zdalnego zasobu
makeFtpAction (fun ftp -> ftp.FileExists(online))
- sprawdzenie, czy plik istnieje na zdalnym zasobie
makeFtpAction (fun ftp -> ftp.MoveFile(offline, online))
- zmiana nazwy pliku na zdalnym zasobie
Do podejmowania decyzji o kolejnych krokach programu, bazujących na wartościach zwracanych przez funkcje, używamy konstrukcji języka F# zwanej Pattern Matching.
A jak może wyglądać implementacja backupu aktualnej wersji?
Funkcjonalność zakodowana jest wprost, zgodnie z algorytmem:
- Wyczyść lokalny zasób
- Pobierz do lokalnego zasobu aktualną wersję z zasobu zdalnego
- Wyczyść zdalny zasób przeznaczony na backup
- Wgraj na zdalny zasób wersję z zasobu lokalnego
Do operacji na zdalnym zasobie ponownie używamy funkcji makeFtpAction
Ostatnim krokiem w procesie Deploy’u jest wgranie nowej wersji na zdalny zasób:
Ponownie kod odzwierciedla poszczególne kroki algorytmu:
- Wyczyszczenie lokalnego zasobu
- Przegranie do lokalnego zasobu skompilowanych artefaktów wraz z plikami konfiguracji
- Wyczyszczenie zasobu zdalnego
- Wgranie nowej wersji z zasobu lokalnego na zasób zdalny
- Wystartowanie Endpointa poprzez wywołanie żądania HTTP
Jeśli w którymkolwiek Targecie wystąpi wyjątek, którego nie obsłużyliśmy, dostaniemy pełną informację w kliencie uruchamiającym skrypt np. oknie konsoli.
Implementację całej funkcjonalności możesz podejrzeć na moim GitHubie.
Test & Deploy
Skrypty możemy uruchamiać poleceniem fake run nazwa_pliku.fsx
. Do przekazywania parametrów, które można odczytać w Targetach służy parametr -e
. Przykład wywołania:
fake run host_ftp_deploy.fsx -e p1 -e p2
W przypadku gdy liczba parametrów jest spora, możemy skorzystać z narzędzia PowerShell i napisać skrypt .ps1, który uruchomi skrypt napisany w FAKE:
Konwencja nazewnictwa parametrów rozpoznawanych przez FAKE to key=value
.
Dzięki parametrom możemy użyć tego samego skryptu przy wdrożeniach na różne środowiska, zarówno testowe, jak i produkcyjne. Całość sprowadza się do utworzenia osobnego skryptu PowerShell z odpowiednimi parametrami dla każdego środowiska. Ja posiadam dwa takie skrypty:
- run_deploy_test.ps1
- run_deploy_production.ps1
Kiedy natrafiłem na FAKEa, od razu zainteresowałem się jego możliwościami. Pisanie kodu dla funkcjonalności oraz kodu wdrażającego w tym samym języku z użyciem tych samych narzędzi developerskich mocno upraszcza cały proces wytwarzania oprogramowania. Podejście funkcyjne, kompilacja kodu, Intellisense, obsługa Nugeta itp. znacząco zwiększa komfort pracy nad skryptem. Dodatkowym bonusem jest frajda z pisania kodu w F#, a następnie obserwowanie jak napisany kod działa i robi to, co ma robić :)
W następnym artykule przejdziemy przez funkcjonalność wdrażania aplikacji webowej obsługującej żądania HTTP.
=