GitLab pipelines
Ostatnimi czasy bardzo popularne jest podejście CI/CD – chcemy jak najszybciej wypuszczać i wdrażać nasze zmiany. Dzięki mikroserwisom i dzieleniu projektów na mniejsze, niezależne części jest to znacznie łatwiejsze. GitLab pipelines sprawia, że możemy mieć wszystko w jednym miejscu, a także budować, testować, wdrażać itp. W tym artykule pokażę, jak działają pipeline’y na przykładzie prostego projektu Spring Boot.
Przewidywania dla projektu
Stworzę prosty projekt Spring Boot, aby zobaczyć, jak działają GitLab pipelines. Będzie miał on kilka testów, co pozwoli nam zobaczyć, jak wszystko jest procesowane.
- Projekt jest dostępny na stronie: https://gitlab.com/michaltomasznowak/spring_rest_application/). Tam też znajdziesz wszystkie artykuły i przykłady.
- Jeśli nie korzystasz z GitLab, możesz założyć darmowe konto pomocne w nauce. Darmowy plan daje wystarczającą liczbę funkcji do prowadzenia ćwiczeń i zrozumienia CI/CD.
Aplikacja Spring Boot Rest
Mam prostą aplikację Maven. Teraz chcę przygotować plik jar i uruchomić unit test. Lokalnie na urządzeniu można to zrobić za pomocą polecenia maven mvn install.
Co jednak, jeśli chcę mieć zautomatyzowany proces budowania, testowania i wdrażania aplikacji? Wtedy mogę utworzyć strukturę CI/CD przy użyciu GitLab pipelines.
- Pipeline to najwyższy poziom komponentu ciągłej integracji, dostarczania i wdrażania.
- Pipeline’y obejmują:
- Joby – definiują, co należy zrobić, np. kompilujące lub testujące kod.
- Stages – definiują, kiedy uruchamiać joby, np. uruchamiające testy po etapie kompilacji kodu.
- Każdy pipeline, który będzie działał, potrzebuje środowiska zwanego
runner
.Runner
jest pewnego rodzaju systemem, w którym można uruchamiać nasze pipeline’y. Oczywiście zależy to od typu aplikacji (np. Java, Terraform, Nodejs). Potrzebujemy różnych środowisk, dlatego w tym celu używamy obrazówDocker
.
Runner
to wirtualny system operacyjny, np. rodzaj Linuksa, na którym zainstalowałem Dockera, a teraz, kiedy uruchamiam swoje pipeline’y, mogę użyć każdego obrazu Dockera do ich uruchomienia.
Oto prosta struktura:
Oczywiście możemy mieć więcej niż jednego runnera. Dzięki temu możemy uruchamiać wiele pipeline’ów w tym samym czasie i równolegle jobów na tym samym etapie (jeśli nie ma między nimi zależności).
Cała konfiguracja naszych potoków jest zdefiniowana w pliku .gitlab-ci.yml
.
Zobacz przykładowy pipeline .gitlab-ci.yml
:
stages:
- build
maven-build-jdk-11:
image: maven:3-jdk-11
stage: build
script: "mvn package -B"
artifacts:
paths:
- target/*.jar
W tym przypadku mam stage build
z jednym jobem maven-build-jdk-11
. Uruchamiam więc ten pipeline na runnerze. Następnie runner pobierze obraz Dockera z Javą
i Mavenem
:
image: maven:3-jdk-11
Co dalej? Na uruchomionym kontenerze uruchamiam polecenie…
script: "mvn package -B"
…i używam paths
, aby połączyć się z miejscem docelowym dockera na zewnątrz (podobnie jak w przypadku docker volume).
artifacts:
paths:
- target/*.jar
Robię to, ponieważ chcę, aby inne joby miały dostęp do utworzonego pliku jar, ponieważ po zakończeniu joba kontener dokera traci wszystkie dane.
Cały proces możesz śledzić w konsoli GitLab:
Jeśli klikniesz na job (2)
, zobaczysz wszystkie joby:
Jeśli teraz klikniesz na job
, zobaczysz cały proces:
Wiele jobów w jednym stage’u
Teraz pokażę następny przykład z wieloma jobami w jednym stage’u. Aby go uprościć, joby będą łatwymi poleceniami. Spójrz:
stages:
- build
maven-build-jdk-11:
image: maven:3-jdk-11
stage: build
script: "mvn package -B"
artifacts:
paths:
- target/*.jar
job_2:
stage: build
script: "ls -l"
W tym kodzie joby były uruchamiane równolegle, ponieważ nie było między nimi zależności.
Wynik job2
:
Pora, aby rozważyć podobny przypadek z zależnością. Jak to zrobić? Dodaj do job2
słowo kluczowe, czyli needs
:
job_2:
stage: build
script: "ls -l"
needs:
- maven-build-jdk-11
Teraz job czeka na wystartowanie, dopóki maven-build-jdk-11
nie zostanie ukończony.
Zobacz wynik. Ponieważ czekamy na maven-build-jdk-11
, mamy folder docelowy:
Stages
Przyszedł moment, aby przenieść job2
do innego stage’u. Na tym etapie wrzucę utworzony plik jar z poprzedniego etapu build
do bucketa AWS S3. Wcześniej muszę skonfigurować zmienne CI z uprawnieniami do mojego AWS. Aby to osiągnąć, trzeba najpierw wprowadzić ustawienia zmiennych CI:
Muszę dodać trzy zmienne:
- AWS_ACCESS_KEY_ID;
- AWS_SECRET_ACCESS_KEY;
- AWS_DEFAULT_REGION.
Wartości dla tych zmiennych zostaną nadane przez IAM w AWS.
Zobacz teraz, jak wygląda nowy stage job:
stages:
- build
- deploy
maven-build-jdk-11:
image: maven:3-jdk-11
stage: build
script: "mvn package -B"
artifacts:
paths:
- target/*.jar
job_2:
image:
name: amazon/aws-cli
entrypoint: [""]
stage: deploy
before_script:
- export AWS_ACCESS_KEY_ID=$TEST_AWS_ACCESS_KEY_ID
- export AWS_SECRET_ACCESS_KEY=$TEST_AWS_SECRET_ACCESS_KEY
- export AWS_DEFAULT_REGION=$TEST_AWS_DEFAULT_REGION
script:
- aws --version
- aws s3 cp foo.bar s3://bucketmn/foo.txt
W tym przypadku w job_2
użyję obrazu docker amazon/aws-cli
, aby mieć AWS cli. W tej części kodu będzie to wyglądać następująco:
before_script:
- export AWS_ACCESS_KEY_ID=$TEST_AWS_ACCESS_KEY_ID
- export AWS_SECRET_ACCESS_KEY=$TEST_AWS_SECRET_ACCESS_KEY
- export AWS_DEFAULT_REGION=$TEST_AWS_DEFAULT_REGION
Musiałem skopiować poprzedni zestaw zmiennych do kontenera docker. Następnie wywołuję polecenia związane z AWS cli:
- aws --version
- aws s3 cp foo.bar s3://bucketmn/foo.txt
Dwa etapy:
Jak widać, mam teraz dwa stage. Wynik uruchomionego joba to:
Skopiowałem jar wygenerowany w poprzednim jobie do bucketa s3 z nową nazwą spring.jar
.
Zmienne
Wcześniej ustawiłem wartości uwierzytelniania AWS jako zmienne. Tego typu zmienne są ustawione na stałe i uzyskuje się do nich dostęp za każdym razem, gdy w projekcie uruchamiany jest pipeline. Można jednak ręcznie ustawić inną zmienną przed każdym uruchomieniem pipeline’u. Jak to zrobić? Wystarczy kliknąć Run Pipeline
:
Tutaj wybieram branch, na którym chcę uruchomić pipeline i ustawiam zmienną:
Aby uzyskać wartość tej zmiennej, używam $
. Tę zmienną można wykorzystać np. do uruchomienia lub pominięcia niektórych jobów. Zobacz przykład:
.go_release:
rules:
- if: '$release != "true"'
when: never
job_3:
stage: release
script:
- echo "$release"
rules:
- !reference [.go_release, rules]
W tym przypadku użyłem nowej funkcji GitLab o nazwie !reference [.go_release, rules]
. Dzięki niej można oddzielać i łączyć warunki, co czyni kod znacznie łatwiejszym i czytelniejszym. Stworzyłem warunek:
.go_release:
rules:
- if: '$release != "true"'
when: never
- if: '$release == "true"'
Sprawdza on poprzednio ustawioną zmienną release
. Jeśli nie jest ona ustawiona, ten job nigdy nie zostanie uruchomiony. Gdy ustawię ją na true
, job zostanie uruchomiony i pojawi się wynik:
Ciekawi Cię wynik tego joba? Oto on:
GitLab API
Kolejną świetną rzeczą, którą chcę pokazać w tym artykule, jest możliwość wywoływania jobów przez API. Dzięki temu można użyć innej aplikacji do komunikacji z GitLab pipeline’ami. Kiedy się to przydaje? Gdy zmieniamy coś w konfiguracji i wymaga to wdrożenia nowej wersji aplikacji lub gdy chcemy mieć np. zewnętrzny system do zarządzania pipeline’ami.
Tworzenie API jest bardzo proste. Można to zrobić, konfigurując trigger token w ten sposób:
Tutaj masz dokładny opis tego, jak wywołać pipeline:
Spróbuję wywołać pipeline przez curl. W tym przypadku TOKEN
to nasz wygenerowany token, a REF_NAME
to nazwa brancha.
curl -X POST --fail -F token=3017969a1f58913a35b3c37af8b162 -F ref=main https://gitlab.com/api/v4/projects/32464279/
W odpowiedzi otrzymuję:
{
"id":440317423,
"iid":24,
"project_id":32464279,
"sha":"43730518e4fb78e1fc66b58c0c447c36ba6a685b",
"ref":"main",
"status":"created",
"source":"trigger",
"created_at":"2022-01-03T10:01:35.475Z",
"updated_at":"2022-01-03T10:01:35.475Z",
"web_url":"https://gitlab.com/michaltomasznowak/spring_rest_application/-/pipelines/440317423",
"before_sha":"0000000000000000000000000000000000000000",
"tag":false,
"yaml_errors":null,
"user":{
"id":10528456,
"username":"michaltomasznowak",
"name":"michaltomasznowak",
"state":"active",
"avatar_url":"https://secure.gravatar.com/avatar/300316e59189a08aa73f74298eb5a1ea?s=80\u0026d=identicon",
"web_url":"https://gitlab.com/michaltomasznowak"
},
"started_at":null,
"finished_at":null,
"committed_at":null,
"duration":null,
"queued_duration":null,
"coverage":null,
"detailed_status":{
"icon":"status_created",
"text":"created",
"label":"created",
"group":"created",
"tooltip":"created",
"has_details":true,
"details_path":"/michaltomasznowak/spring_rest_application/-/pipelines/440317423",
"illustration":null,
"favicon":"/assets/ci_favicons/favicon_status_created-4b975aa976d24e5a3ea7cd9a5713e6ce2cd9afd08b910415e96675de35f64955.png"
}
}
Z tego komunikatu można odczytać "status": "created"
. Oznacza to, że job został uruchomiony.
Uruchamianie pipeline’u między wieloma projektami
Czasami zdarzają się sytuacje, w których podczas jednego wywołania pipeline’u chcemy zainicjować pipeline w innych projektach. W pipeline możemy to łatwo zrobić za pomocą słowa kluczowego trigger
. Na potrzeby tego testu stworzyłem kolejny projekt spring-rest-application-to-trigger
(projekt jest dostępny tutaj: https://gitlab.com/michaltomasznowak/spring-rest-application-to-trigger). Z projektu spring-rest-application chcę wywołać projekt spring-rest-application
. Aby to zrobić, muszę dodać kod:
trigger:
stage: trigger
trigger:
project: michaltomasznowak/spring-rest-application-to-trigger
branch: main
W GUI można zobaczyć wywołany pipeline z innego projektu:
Podsumowanie
W tym artykule pokazałem, jak utworzyć prosty pipeline dla projektu. Przedstawiłem też kilka podstawowych sposobów pracy ze stages
i jobs
, a także jak używać zmiennych i połączyć się z AWS. Ta wiedza powinna pozwolić Ci zrozumieć związek między runnerami a dockerami. W wyniku kodu napisanego jako nasz pipeline możesz wykonywać polecenia na runnerach. Przykład? Dzięki słowu kluczowemu script
możesz wywołać i uruchomić wszystkie polecenia obsługiwane przez pobrany obraz Dockera na runnerze.