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

Projekt główny - konteneryzacja i CI/CD Pipeline

5 min czytania
W tej lekcji7

Cały moduł składał się z osobnych narzędzi: obraz, Compose, pipeline, sekrety, metryki, tracing, plan infrastruktury. Teraz zbierzemy je w jedną konfigurację - i przy okazji przypomnimy sobie, dlaczego każda decyzja wygląda tak, a nie inaczej. Bo to właśnie te uzasadnienia odróżniają konfigurację skopiowaną od zrozumianej.

Obraz - dwa etapy

1FROM node:20-alpine AS builder
2WORKDIR /app
3COPY package.json yarn.lock ./
4RUN yarn install --frozen-lockfile
5COPY . .
6RUN yarn build
7
8FROM node:20-alpine AS production
9WORKDIR /app
10RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
11COPY --from=builder /app/dist ./dist
12COPY --from=builder /app/node_modules ./node_modules
13USER nestjs
14EXPOSE 4000
15HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
16  CMD curl -f http://localhost:4000/health || exit 1
17CMD ["node", "dist/main"]

Trzy decyzje w tym pliku warto umieć uzasadnić.

COPY --from=builder przenosi wynik z pierwszego etapu do drugiego. Kolejność członów jest stała: COPY, --from=builder, /app/dist (skąd), ./dist (dokąd). Dzięki temu w obrazie produkcyjnym nie ma kompilatora, źródeł ani zależności deweloperskich - zostaje sam wynik.

USER nestjs - użytkownika bez uprawnień root tworzymy, aby ograniczyć potencjalne szkody w przypadku włamania. Nie po to, żeby kontener startował szybciej, żeby obraz był mniejszy ani żeby umożliwić wielowątkowość. Proces działający jako root wewnątrz kontenera, po wyrwaniu się z izolacji, jest rootem także na hoście.

HEALTHCHECK mówi Dockerowi, jak sprawdzić, czy aplikacja żyje naprawdę, a nie tylko czy proces istnieje. Kolejność: healthcheck:, test: curl -f http://localhost:4000/health, interval: 30s, retries: 3.

Compose - trzy serwisy

W pliku Compose sprawdź dwie rzeczy, które najczęściej się myli.

Kontener api łączy się z bazą przez nazwę serwisu: mongodb:27017 - nie przez adres IP kontenera, nie przez localhost:27017 i nie przez zewnętrzny DNS. localhost wewnątrz kontenera to ten sam kontener, więc wskazywałby na API, nie na bazę.

Flaga -v w docker compose down -v usuwa wolumeny wraz z danymi. Nie włącza trybu szczegółowych logów, nie waliduje pliku i nie wypisuje wersji. To polecenie kasuje bazę - używaj go świadomie.

Wdrożenie - kolejność

Pełna droga zmiany na produkcję ma cztery etapy:

  1. Push kodu do gałęzi main.
  2. CI: lint, test, build.
  3. Docker build & push image.
  4. Smoke tests i monitoring po wdrożeniu.

Czwarty etap bywa pomijany, a jest tym, który odróżnia „wdrożone" od „działa". Smoke tests to garść sprawdzeń najważniejszych ścieżek zaraz po wdrożeniu - czy strona się otwiera, czy logowanie przechodzi, czy zdrowie odpowiada. Pipeline mógł przejść na zielono, a aplikacja i tak nie wstać, bo zabrakło zmiennej środowiskowej, której na CI nikt nie potrzebował.

Kubernetes - dwa polecenia

Gdy obraz jest gotowy, klaster przyjmuje opis zasobów z pliku:

1kubectl apply -f deployment.yaml

Kolejność członów: kubectl, apply, -f, deployment.yaml. Flaga -f wskazuje plik z opisem; apply doprowadza klaster do stanu w nim zapisanego - tworzy, czego brakuje, i zmienia, co się różni.

Skalowanie to osobne polecenie:

1kubectl scale deployment roman-api --replicas=5

Tu kolejność brzmi: kubectl scale, deployment roman-api, --replicas=5. Pięć replik znaczy pięć równoległych kopii aplikacji, między które klaster rozdzieli ruch.

Terraform - moduły

Ostatnia rzecz z warstwy infrastruktury. Moduły w Terraform to reużywalne bloki infrastruktury, które można parametryzować - nie pluginy do komunikacji z chmurą (tym są providery), nie testy jednostkowe i nie pliki logów.

Sens jest ten sam co przy funkcji w kodzie: opisujesz raz „serwer aplikacyjny z bazą i siecią", a potem wywołujesz to trzy razy dla środowiska testowego, przedprodukcyjnego i produkcyjnego, zmieniając tylko parametry.

Checklista projektu

Zbierz komplet i sprawdź każdy punkt:

  • Docker - obraz wieloetapowy, użytkownik bez roota, HEALTHCHECK,
  • Docker Compose - trzy serwisy, łączenie po nazwach, wolumen na dane bazy, depends_on z service_healthy,
  • CI/CD - workflow na push i pull_request, joby w kolejności lint → build → test → docker, sekrety w ustawieniach repozytorium,
  • Sekrety - żadnego hasła w repozytorium; .env tylko lokalnie,
  • Observability - endpoint /metrics, metryki RED, tracing z span.end() w finally,
  • Infrastructure as Code - plan w plikach .tf, stan w backendzie zdalnym, nie w Gicie.

Podsumowanie

Kampania gotowa do wymarszu:

  • COPY --from=builder przenosi wynik budowania: COPY, --from=builder, źródło, cel,
  • użytkownika bez roota tworzymy, aby ograniczyć szkody przy włamaniu - nie dla szybkości ani rozmiaru obrazu,
  • HEALTHCHECK w kolejności: healthcheck:, test:, interval:, retries:,
  • kontenery łączą się przez nazwę serwisu (mongodb:27017), nie przez IP ani localhost,
  • docker compose down -v usuwa wolumeny wraz z danymi,
  • kolejność wdrożenia: push → CI (lint, test, build) → docker build & push → smoke tests i monitoring,
  • kubectl apply -f deployment.yaml wprowadza opis do klastra,
  • kubectl scale deployment nazwa --replicas=5 zmienia liczbę kopii,
  • moduły Terraform to reużywalne, parametryzowalne bloki infrastruktury - nie providery, nie testy, nie logi.

To ostatnia lekcja modułu. Umiesz teraz zbudować obraz, opisać środowisko, przepuścić zmianę przez pipeline, ukryć sekrety, zobaczyć, co dzieje się na produkcji, i odtworzyć całą infrastrukturę z pliku. A na razie zapamiętaj: każdy element tej konfiguracji ma uzasadnienie - i to jego znajomość, a nie sam plik, przenosi się na następny projekt.

Kod do tej lekcji: src/deployment-pipeline.ts
1// Projekt Glowny - Kompletny Deployment Pipeline
2console.log("=== DROGI IMPERIUM - DEPLOYMENT PIPELINE ===\n");
3
4interface ChecklistItem {
5  category: string;
6  items: { name: string; status: boolean }[];
7}
8
9const checklist: ChecklistItem[] = [
10  {
11    category: 'Docker',
12    items: [
13      { name: 'Multi-stage Dockerfile', status: true },
14      { name: 'Non-root user', status: true },
15      { name: '.dockerignore', status: true },
16      { name: 'HEALTHCHECK w Dockerfile', status: true },
17    ],
18  },
19  {
20    category: 'Docker Compose',
21    items: [
22      { name: 'API + MongoDB + Redis', status: true },
23      { name: 'Health checks na serwisach', status: true },
24      { name: 'Wolumeny dla danych', status: true },
25      { name: 'Izolowana siec', status: true },
26    ],
27  },
28  {
29    category: 'CI/CD',
30    items: [
31      { name: 'Lint + Test + Build', status: true },
32      { name: 'Docker build & push', status: true },
33      { name: 'GitHub Secrets', status: true },
34    ],
35  },
36  {
37    category: 'Security',
38    items: [
39      { name: 'Helmet', status: true },
40      { name: 'CORS', status: true },
41      { name: 'Rate limiting', status: true },
42      { name: 'ValidationPipe', status: true },
43      { name: 'Compression', status: true },
44    ],
45  },
46  {
47    category: 'Monitoring',
48    items: [
49      { name: 'Health check endpoint', status: true },
50      { name: 'Structured logging', status: true },
51      { name: 'Log rotation', status: true },
52    ],
53  },
54];
55
56checklist.forEach(cat => {
57  console.log(`\n${cat.category}:`);
58  cat.items.forEach(item => {
59    const icon = item.status ? 'DONE' : 'TODO';
60    console.log(`  [${icon}] ${item.name}`);
61  });
62});
63
64const total = checklist.reduce((s, c) => s + c.items.length, 0);
65const done = checklist.reduce(
66  (s, c) => s + c.items.filter(i => i.status).length, 0
67);
68console.log(`\nStatus: ${done}/${total} elementow gotowych`);
69console.log("\nDrogi Imperium gotowe - od kodu do produkcji!");
70

Widzisz błąd w tej lekcji?

Przydatne artykuły