530x244
W procesie tworzenia aplikacji internetowych możemy wskazać grupę czynności, które cyklicznie powtarzamy po zakończeniu fazy implementacji. Są to m.in.:


- budowanie aplikacji
- uruchomienie testów jednostkowych
- zbudowanie obrazu Dockera
- wdrożenie aplikacji na serwer testowy
 
Oczywiście wszystkie z powyższych jesteśmy w stanie wykonać ręcznie, jednak problem pojawia się gdy nasza aplikacja oparta jest na architekturze rozproszonej (mikroserwisy czy actor model) i składa się z kilku bądź kilkunastu małych projektów. Wtedy cały proces należałoby niezależnie zaaplikować na każdy z nich co skutkowałoby spędzeniem wielu godzin przed monitorem. Rozwiązaniem problemu oczywiście istnieje, a jest nim automatyzacja. Problem w tym, że stosunkowo duża grupa programistów nie jest przychylna do nauki tego typu narzędzi, ponieważ operują na językach dziedzinowych (DSL), których programiści nie wykorzystują na codzień w swojej pracy. Próg wejścia jest zatem dużo większy, niż nauka np. nowej biblioteki do C#. W tym artykule postaram się pokazać, że droga do upragnionej automatyzacji może być jednak dużo prostrza, niż się to Wam wydawało. 

 

Przegląd naszego projektu

Zacznijmy od szybkiego przeglądu naszego projektu tak, abyśmy wiedzieli co poddamy procesowi automatyzacji. Solucja zawiera dwa projekty:

 
Projekt o nazwie SomeProjekt jest domyślnym projektem ASP.NET Core 2.0 wygenerowanym poprzez dotnet CLI (komendą dotnet new webapi). Dokonałem tutaj jednak dwóch modyfikacji. Pierwsza z nich to utworzenie serwisu domenowego o nazwie ValuesService, którego implementacja wygląda następująco:
 
public interface IValuesService
{
    string[] GetValues();
}
	
public class ValuesService : IValuesService
{
    public string[] GetValues()
        => new[] {"Value1", "Value2"};
}​
 
Serwis został utworzony, aby testy jednostkowe nie były wykonywane bezpośrednio na klasie ValuesController. Oczywiście zmiany można zauważyć także w samym kontrolerze:
 
[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly IValuesService _valuesService;
        
    public ValuesController(IValuesService valuesService)
    {
        _valuesService = valuesService;
    }

    // GET api/values
    [HttpGet]
    public IEnumerable<string> Get()
        => _valuesService.GetValues();
}
 
Jak widać, serwis został wstrzyknięty do konstruktora klasy ValuesConttroller, a następnie jego metoda wywoływana jest już bezpośrednio w akcji Get. Druga zmiana, której dokonałem w projekcie to utworzenie pliku .Dockerfile, którego struktura wygląda następująco:
 
FROM microsoft/dotnet:latest
COPY . /app
WORKDIR  /app
 
RUN dotnet restore
RUN dotnet build
 
EXPOSE 5000/tcp
ENTRYPOINT dotnet run
 
Sam plik nie jest rozbudowany, a jego kolejne kroki to:
  1. Pobierz ostatni obraz .NET Core
  2. Skopiuj projekt do folderu app
  3. Pobierz zależności do projektu komendą dotnet restore
  4. Zbuduj aplikację komendą dotnet build
  5. Wystaw domyślny port Kestrela  (5000)
  6. Ustaw punkt wejścia do aplikacji komendą dotnet run
 
Przejdźmy teraz do drugiego projektu o nazwie SampleProject.Tests. Zawiera on jedynie jeden test jednostkowy dla naszego serwisu domenowego:
 
public class values_service_tests
{
    private readonly IValuesService _valuesService;
        
    public values_service_tests()
    {
        _valuesService = new ValuesService();
    }   

    [Fact]
    public void getValues_returns_two_values()
    {
        var values = _valuesService.GetValues();
        Assert.Equal(values.Length, 2);
    }
}

 

Omówienie procesu budowania aplikacji

Wiedząc już jak wygląda nasz projekt musimy określić jakie czynności chcielibyśmy wykonać przy każdorazowym wypchnięciu naszych zmian do repozytorium. Na potrzeby artykułu, będą to proste kroki, ale jednocześnie najbardziej popularne:
 
  1. Pobranie zależności dla solucji
  2. Zbudowanie solucji
  3. Uruchomienie testów jednostkowych
  4. Zbudowanie obrazu dockera i wypchnięcie go do DockerHub
  5. Wysłanie notyfikacji na Slack o poprawnym zbudowaniu aplikacji
 
Aby osiągnąć zamierzony cel skorzystamy z dwóch narzędzi, a pierwszym z nim jest Cake.

 

Cake

Cake jest cross-platformowym narzędziem do automatyzacji procesu budowania aplikacji. Jego największą zaletą jest fakt, że jego DSL bardzo przypomina kod C#. Dzięki temu każdy programista, który na codzień pracuje z tym językiem nie będzie miał problemu ze zrozumieniem kodu, a także z jego napisaniem. Aby dołączyć Cake do naszej solucji sugerowałbym uruchomienie projektu w edytorze Visual Studio Code, ponieważ zawiera on bardzo przydany plugin do integracji. Po uruchomieniu edytora przechodzimy do View -> Command Palette, a następnie wpisujemy słowo “Cake”:
  
 
Mamy możliwość zainstalowania dwóch rzeczy: pliku konfiguracyjnego, oraz bootstrappera czyli skryptu, który będzie odpowiedzialny za uruchomienie naszego procesu budowania aplikacji. Oba elementy będą nam niezbędne do poprawnego działania, dlatego klikamy w “Install a configuration file”, a następnie “Install a bootstrapper” wybierając rozszerzenie zgodne z naszym systemem operacyjnym (ps1 dla Widnows, sh dla Unix). Mamy zatem serce naszego mechanizmu do automatyzacji. Potrzebujemy jeszcze definicji kolejnych kroków budowania aplikacji. W tym celu na poziomie pozostałych plików cake musimy utworzyć plik o nazwie buid.cake. W nim umieścimy nasze kroki:
 
#tool "nuget:?package=xunit.runner.console"

var target = Argument("target", "Default");
var configuration = Argument("configuration", "Release");

Task("dotnet-restore")
	.Does(() => 
	{
		DotNetCoreRestore("./SomeProject.sln");
	});


Task("dotnet-build")
	.IsDependentOn("dotnet-restore")	
	.Does(() => 
	{
		DotNetCoreBuild("./SomeProject.sln", new DotNetCoreBuildSettings 
		{
			Configuration = configuration
		});
	});

Task("run-xunit-tests")	
	.IsDependentOn("dotnet-build")
	.Does(() => 
	{
		DotNetCoreTest("./tests/SomeProject.Tests.csproj", new DotNetCoreTestSettings
		{
			Configuration = configuration
		});
	});	

Task("Default")
	.IsDependentOn("run-xunit-tests");

RunTarget(target);
 
Nasz skrypt jest bardzo prosty, i zmieścił się w 35 liniach. Omówmy sobie co się po kolei dzieje. Pierwsza linijka określa narzędzia, które będą nam niezbędne do uruchomienia całego pipeline-u. Warto zwrócić uwagę, że znajdują się tu jedynie pozycje, których możemy nie posiadać na komputerze. Przykładowo nie umieściłem tutaj MSBuild, ponieważ mam zainstalowane Visual Studio, które dostarcza mi go po zainstalowaniu. W przypadku Xunit sprawa wygląda inaczej dlatego, aby mieć pewność, że będę w stanie uruchomić testy jednostkowe dodałem deklaracje dla Cake. Kolejne dwie linijki to zmienne, które definiują moje środowisko uruchomieniowe (Debug, Release itp.) oraz domyślny krok, od którego rozpocznie się cały proces budowania naszej aplikacji. Jak widać mamy także możliwość określania domyślnych wartości dla argumentów jeśli te nie zostaną dołączone przy uruchomieniu bootstrappera. W naszym przykładzie domyślnym zadaniem, które zostanie wykonane jest “Default”, a projekt będzie budowany i testowany w Release. Dalsza część kodu do deklaracje kolejnych kroków czyli pobranie zależności, zbudowanie aplikacji w zadanej konfiguracji oraz uruchomienie testów jednostkowych. Należy zwrócić uwagę na jeden istotny fragment kodu - metodę IsDepentOn. Dzięki niej możemy łatwo określić, konkretną sekwencję kroków dzięki czemu będziemy mieć pewność, że np. testy jednostkowe zostaną uruchomione dopiero wtedy kiedy aplikacji się zbuduje. Ostatnia linijka skryptu odpowiada za uruchomienie zadanego kroku (domyślnie “Default”).
 
Mamy zatem definicję trzech z pięciu czynności, które sobie założyliśmy. Pytanie, które naturalnie się nasuwa to w jaki sposób możemy sprawdzić, czy to co napisaliśmy w ogóle działa? W tym celu należy otworzyć Terminal/Powershell na poziomie naszej solucji z projektami, a następnie wywołać skrypt build.sh lub build.ps1. Wynik po kilku chwilach powinien być następujący:

 
Jeżeli wszystko zrobiliśmy dobrze powinniśmy uzyskać efekt przedstawiony na powyższym zdjęciu. Możemy teraz śmiało wypchnąć zmiany na repozytorium (w moim przypadku Bitbucket) i przejść do drugie narzędzia, które po pierwsze wykona nasz skrypt Cake, a następnie zbuduje zawarty w projekcie obraz Dockera, wyśle go do DockerHuba i poinformuje nas o zakończeniu pracy na Slacku. Poznajcie Buddy!

 

Buddy

Buddy to system Continuous Delivery, którego niewątpliwą zaletą jest jedna rzecz - jest nieziemsko prosty do opanowania. W odróżnieniu od konkurencji (Travis CI, AppVayor itp.) Buddy wyróżniaja dwie istotne cechy. Po pierwsze jest w całości oparty na Dockerze, co oznacza, że naszą aplikację możemy budować/testować zarówno na Windowsie jak i systemach z rodziny Unix. Po drugie, oprócz typowej konfiguracji naszego pipeline-u przy użyciu pliku YAML, możemy alternatywnie wszystko “wyklikać” w aplikacji internetowej, która działa bardzo sprawnie, a co najważniejsze jest prosta i intuicyjna. Aby rozpocząć pracę z Buddy, należy udać się na oficjalną stronę, a następnie utworzyć konto. Mamy tutaj kilka opcji rejestracji, ale zdecydowanie najszybszą z nich jest użycie naszego konta Bitbucket lub GitHub. Po zalogowaniu naszym oczom powinien pojawić się następujący widok:
 
 
W kolejnym kroku wskazujemy providera, z którego chcemy pobrać nasz kod. Tak jak wspomniałem w poprzednim akapicie, w moim przypadku jest to Bitbucket. Następnie wskazujemy projekt, który nas interesuje i klikamy “Create new project”. Po krótkiej chwili nasz kod powinien zostać pomyślnie zsynchronizowany, a my możemy śmiało przejść do utworzenia naszego pipeline-u:
 
 
Jak widać, mamy dwie możliwości kreacji: utworzenie całego procesu z poziomu aplikacji bądź dodając plik yml . Ponieważ chcemy, aby wszystko było maksymalnie proste wybieramy pierwszą opcję, a naszym oczom powinien ukazać się następujący widok:
 
 
W tym miejscu mamy możliwość wskazania gałęzi, którą Buddy będzie budował. W moim przypadku jest to master. Możemy także wskazać tryb uruchomienia całego pipeline-u z trzech dostępnych opcji:

  • ręczne uruchomienie
  • automatyczne uruchomienie po wykryciu zmian na gałęzi
  • cykliczne uruchomienie
 
Po wybraniu wszystkich opcji zostaniemy proszeni o wybranie pierwszego kroku dla naszego pipeline-u. Nas interesuje uruchomienie skryptu Cake, dlatego wybieramy ikonę “Local Shell”:
 
 
W tym miejscu musimy dokonać dwóch rzeczy. Po pierwsze umieścić odpowiednią komendę, która zostanie uruchomiona w terminalu. Tu sprawa jest bardzo prosta, ponieważ wpisujemy dokładnie to samo co na naszym komputerze. Po drugie musimy wskazać obraz Dockera, który zbuduje nam kontener. Ponieważ nasza aplikacja jest napisana w .NET Core a Cake do działania wymaga Mono, wybrałem obraz który dostarcza mi wszystkich niezbędnych zależności (docker-dotnet-mono-node). Oczywiście jeśli nie chcecie korzystać z gotowych obrazów, możecie stworzyć własny i umieścić go na DockerHub. Kiedy wszystko jest gotowe możemy dodać naszą akcję. Nasz ekran powinien prezentować się następująco:

 
Jak dobrze pamiętamy, nasz krok 4 w założeniach miał budować i wysyłać obraz Dockera na repozytorium w DockerHub. Klikamy w plusik poniżej naszej pierwszej akcji i z listy dostepnych w sekcji Devops wybieramy “Docker Image”:
 
 
Tutaj sprawa również jest bardzo prosta. Musimy wskazać plik .Dockerfile, który chcemy zbudować i wysłać. Następnie podajamy login/hasło do DockerHub, wskazujemy rezpozytorium na które chcemy wysłać nasz obraz i tag z jakim ma on zostać umieszczony. Następnie możemy zapisać akcję. Jako nasz ostatni krok dodajmy notyfikację na Slacku. Z listy dostepnych kroków wybieramy “Slack”. Poproszeni zostaniemy o dostęp do aplikacji, a następnie przejdziemy konfiguracji naszej wiadomości:

Jeżeli nie potrzebujemy wielu szczegółów, możemy pozstawić domyślną strukturę notyfikacji. Pamiętajmy także o wskazaniu odpowiedniego kanału na Slacku. Jeżeli wszsytko zostało już ustawione możemy przejść do testowania.

 

Testowanie

Aby przetestować czy cały nasz proces budowania aplikacji działa, wystarczy abyśmy wypchnęli zmiany do repozytorium na wskazanej w Buddy gałęzi. Po krótkiej chwili nasz pipeline powinien zostać uruchomiony:

 
A po kilku minutach wszystko powinno zakończyć się powodzeniem:
 

 
Nasze repozytorium na DockerHub powinno mieć najnowszy obraz aplikacji:

 
A my otrzymamy stosowną notyfikację na Slacku:

 
Jak widać proces automatyzacji budowania naszych aplikacji nie musi być taki straszny jak się nam wydaje. Należy jednak odpowiedzieć jeszcze na jedno pytanie: czy nie dało się wszystkiego zrobić w Buddy bez użycia Cake? Dało się, jednak należy pamiętać, że kiedyś możemy chcieć zmienić nasz system CI. W takim przypadku cały nasz pipeline, który w nim zdefiniowaliśmy musimy przepisać i dostosować dla innego providera. Cake daje nam jedną, bardzo istotną przewagę. Definicja kolejnych kroków znajduje się razem z naszym kodem i jest ona transparentna względem systemu CI. Wszystko co musimy zrobić to wywołać nasz skrypt bootstrappera.
 

Darek Pawlukiewicz - programista pasjonat. Prowadzi blog Forever Frame - foreverframe.pl oraz podcast programistyczny DevReview. Uwielbia poznawać nowe, ciekawe technologie i nie boi się używać ich w swoich projektach. Fan TypeScript oraz frameworku Aurelia. Entuzjasta DDD, CQRS i Event Sourcingu. Na co dzień pracuje, jako Full Stack Developer w firmie Connectis_.