Kurs NestJS · Moduł 12: Konteneryzacja i CI/CD

Zaawansowane CI/CD - matrix builds, artifacts i cache

5 min czytania
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:

  1. Lint - sprawdzenie formatowania.
  2. Build - kompilacja TypeScriptu.
  3. Test - testy jednostkowe.
  4. 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 --coverage

Budowa 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_request uruchamia workflow przy tworzeniu pull requesta; on: schedule działa o wyznaczonej porze, on: deployment przy wdrożeniu,
  • kolejność jobów: Lint → Build → Test → Docker build & push, według zasady „najtańsze najpierw",
  • budowa kroku: - name:, potem uses:, 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-lockfile instaluje 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 w README.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!");
58

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co oznacza CI (Continuous Integration)?

  2. 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

Przydatne artykuły