Kurs NestJS · Moduł 12: Konteneryzacja i CI/CD
Zaawansowane CI/CD - matrix builds, artifacts i cache
W tej lekcji6
Testy przechodzą u Ciebie. U kolegi nie - bo ma inną wersję Node'a. Na serwerze też nie, bo ktoś zapomniał przebudować obrazu. Każda z tych trzech sytuacji kosztuje pół dnia, a wszystkie mają wspólną przyczynę: sprawdzenie zależy od tego, kto i gdzie je uruchomił.
Continuous Integration usuwa tę zależność: to automatyczne budowanie i testowanie kodu po każdym push. Nie ręczne wdrażanie raz w tygodniu, nie monitorowanie pamięci i nie kopie zapasowe bazy - tylko jedno sprawdzenie, zawsze tak samo, na maszynie, której nikt nie skonfigurował po swojemu.
Kiedy uruchomić
Workflow zaczyna się od wskazania zdarzeń, które go budzą:
1name: CI
2
3on:
4 push:
5 branches: [main]
6 pull_request:on: pull_request uruchamia workflow przy tworzeniu pull requesta - i to jest ten moment, o który chodzi najbardziej, bo sprawdzenie odbywa się przed wejściem zmiany do gałęzi głównej.
on: push reaguje na wypchnięcie kodu; oba stosuje się zwykle razem. Istnieją też inne wyzwalacze, ale robią co innego: on: schedule uruchamia workflow o wyznaczonej porze, a on: deployment - przy zdarzeniu wdrożenia. Żaden z nich nie zadziała, gdy ktoś otworzy pull request.
Kolejność zadań
Pipeline dzieli się na joby, a ich kolejność nie jest dowolna:
- Lint - sprawdzenie formatowania.
- Build - kompilacja TypeScriptu.
- Test - testy jednostkowe.
- Docker build & push - zbudowanie i wysłanie obrazu.
Reguła porządkująca brzmi: najtańsze i najszybsze najpierw. Lint kończy się w kilka sekund, więc nie ma sensu czekać pięciu minut na testy, żeby dowiedzieć się o brakującym średniku. Kompilacja poprzedza testy, bo test nieskompilowanego kodu i tak nie ruszy. Obraz Dockera budujemy na końcu - to najdroższy krok i nie ma po co go zaczynać, jeśli cokolwiek wcześniej padło.
Krok po kroku
Każdy job składa się z kroków, a pierwszym jest zawsze pobranie kodu:
1jobs:
2 test:
3 runs-on: ubuntu-latest
4 steps:
5 - name: Checkout kodu
6 uses: actions/checkout@v4
7
8 - name: Setup Node
9 uses: actions/setup-node@v4
10 with:
11 node-version: 20
12
13 - name: Instalacja zaleznosci
14 run: yarn install --frozen-lockfile
15
16 - name: Testy
17 run: yarn test --coverageBudowa kroku jest stała: - name: Checkout kodu, potem uses:, dalej actions/checkout@v4. name to opis widoczny w interfejsie, uses wskazuje gotową akcję, a run - polecenie do wykonania w powłoce.
Kolejność w jobie testowym też jest niezmienna: actions/checkout@v4 pobiera kod, actions/setup-node@v4 instaluje Node'a, yarn install --frozen-lockfile wciąga zależności, a na końcu yarn test --coverage uruchamia testy.
Flaga --frozen-lockfile zasługuje na uwagę: każe zainstalować dokładnie wersje z pliku blokady i przerwać, gdyby się nie zgadzały. Bez niej pipeline mógłby cicho zainstalować nowszą wersję zależności i przetestować coś innego, niż uruchomisz na produkcji.
Sekrety
Pipeline potrzebuje haseł - do rejestru obrazów, do serwera, do bazy testowej. Przechowujemy je w Settings > Secrets and variables > Actions, w ustawieniach repozytorium.
Nie w pliku .env w repozytorium, nie w komentarzu do commita, nie w README.md - wszystkie te miejsca są publiczne dla każdego, kto ma dostęp do kodu, a historii Gita nie da się tak po prostu wyczyścić.
Zapisany sekret wstawiasz do workflow przez odwołanie, nigdy wprost:
1 - name: Logowanie do rejestru
2 run: docker login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_TOKEN }}Wartość zostanie podstawiona przy uruchomieniu, a w logach pojawi się w postaci zamaskowanej.
Trzy usprawnienia
Na koniec trzy rzeczy, które odróżniają pipeline działający od pipeline'u, na który da się czekać.
Matrix uruchamia ten sam job na kilku konfiguracjach naraz:
1 strategy:
2 matrix:
3 node-version: [18, 20, 22]Trzy wersje Node'a, trzy równoległe przebiegi, jeden opis. To odpowiedź na problem z początku lekcji - „u kolegi nie działa" wychodzi teraz przy pull requeście, a nie po tygodniu.
Cache zapamiętuje pobrane zależności między przebiegami. Bez niego każdy przebieg ściąga te same paczki od nowa; z nim instalacja skraca się z minut do sekund.
Artifacts to pliki, które job zostawia po sobie - raport pokrycia, zbudowana paczka, zrzuty ekranu z testów. Przeżywają zakończenie przebiegu i można je pobrać z interfejsu, co bywa jedynym sposobem, by zrozumieć, dlaczego coś padło tylko na maszynie CI.
Podsumowanie
Sprawdzenie przestało zależeć od tego, kto je uruchamia:
- CI to automatyczne budowanie i testowanie kodu po każdym push - nie ręczne wdrażanie, nie monitoring pamięci, nie kopie zapasowe,
on: pull_requesturuchamia workflow przy tworzeniu pull requesta;on: scheduledziała o wyznaczonej porze,on: deploymentprzy wdrożeniu,- kolejność jobów: Lint → Build → Test → Docker build & push, według zasady „najtańsze najpierw",
- budowa kroku:
- name:, potemuses:, potem nazwa akcji;run:wykonuje polecenie powłoki, - job testowy w kolejności:
actions/checkout@v4,actions/setup-node@v4,yarn install --frozen-lockfile,yarn test --coverage, --frozen-lockfileinstaluje dokładnie wersje z pliku blokady i przerywa przy niezgodności,- sekrety trzymamy w Settings > Secrets and variables > Actions - nie w
.env, nie w komentarzu, nie wREADME.md, - matrix uruchamia job na kilku konfiguracjach równolegle, cache skraca instalację zależności, artifacts zachowują pliki po zakończeniu przebiegu.
W następnej lekcji zejdziemy z poziomu liczb do pojedynczego żądania - poznasz distributed tracing. A na razie zapamiętaj: pipeline to jedno sprawdzenie, wykonywane zawsze tak samo - a matrix sprawia, że „u mnie działa" przestaje być argumentem.
Kod do tej lekcji: src/github-actions.ts
1// CI/CD z GitHub Actions - Cursus Publicus
2console.log("=== CI/CD PIPELINE ===\n");
3
4interface PipelineJob {
5 name: string;
6 description: string;
7 steps: string[];
8 runsAfter?: string;
9}
10
11const pipeline: PipelineJob[] = [
12 {
13 name: 'lint',
14 description: 'Inspekcja kodu',
15 steps: ['checkout', 'setup-node', 'yarn install', 'yarn lint'],
16 },
17 {
18 name: 'test',
19 description: 'Testy jednostkowe',
20 steps: ['checkout', 'setup-node', 'yarn install', 'yarn test --coverage'],
21 runsAfter: 'lint',
22 },
23 {
24 name: 'build',
25 description: 'Budowanie aplikacji',
26 steps: ['checkout', 'setup-node', 'yarn install', 'yarn build'],
27 runsAfter: 'test',
28 },
29 {
30 name: 'docker',
31 description: 'Docker image build & push',
32 steps: ['checkout', 'docker login', 'docker build & push'],
33 runsAfter: 'build',
34 },
35];
36
37pipeline.forEach((job, i) => {
38 const arrow = job.runsAfter ? ` (po: ${job.runsAfter})` : ' (start)';
39 console.log(`Job ${i + 1}: ${job.name}${arrow}`);
40 console.log(` Opis: ${job.description}`);
41 job.steps.forEach((s, j) => {
42 console.log(` Krok ${j + 1}: ${s}`);
43 });
44 console.log();
45});
46
47// Triggery
48console.log("=== TRIGGERY WORKFLOW ===\n");
49console.log("push na main -> pelny pipeline + deploy");
50console.log("push na develop -> CI bez deploy");
51console.log("pull_request -> CI (lint + test + build)\n");
52
53// Sekrety
54console.log("=== GITHUB SECRETS ===\n");
55console.log("Hasla i klucze API przechowuj w:");
56console.log(" Settings > Secrets > Actions");
57console.log(" Nigdy w kodzie zrodlowym!");
58Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co oznacza CI (Continuous Integration)?
2. Który trigger uruchamia workflow GitHub Actions przy tworzeniu pull requesta?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj joby pipeline CI/CD od pierwszego do ostatniego:
- Edytor kodu
Zdefiniuj kroki dla trzech jobów: lint (checkout, setup-node, install, lint), test (+ coverage), build (+ build)
- Układanie w poziomie
Ułóż elementy kroku checkout w GitHub Actions w prawidłowej kolejności:
- Klikanie w kolejności
Ułóż kroki joba 'test' w GitHub Actions w prawidłowej kolejności:
- Edytor kodu
Skonfiguruj helmet, compression, CORS, ValidationPipe i ThrottlerGuard w odpowiedniej kolejności