Pełna ścieżka CI/CD w chmurze: od commita do produkcji z użyciem GitHub Actions, GitLab i natywnych narzędzi

0
67
2.5/5 - (2 votes)

Nawigacja:

Scenka z życia: kiedy ręczne wdrożenia przestają dawać radę

Zespół duszący się w ręcznych deployach

Mały zespół produktowy startuje z prostą aplikacją. Pierwsze wdrożenie to paczka ZIP wrzucona przez SSH na jedynego VPS-a, szybkie npm install i restart procesu. Po kilku tygodniach pojawiają się nowe funkcje, hotfixy, pierwszy klient biznesowy – a sposób wdrażania nadal wygląda tak samo.

Dochodzi piątek, jest 22:30. Release miał być „na chwilę”, ale ktoś zapomniał zaktualizować zmienną środowiskową, inna osoba nie zbackupowała bazy danych, a trzecia osoba loguje się na produkcję z pociągu. Nagle nikt nie ma pewności, jaka wersja aplikacji faktycznie działa. Pojawiają się komentarze w stylu „u mnie działało”, „chyba czegoś nie skopiowałem z testu” i „kto wgrał ten plik na serwer?”.

W reakcji na rosnący chaos rodzą się pierwsze spontaniczne skrypty: trochę Bash, trochę Ansible, kilka plików .sh używanych tylko przez jedną osobę. Każdy projekt ma inną metodę wdrożenia, każde środowisko jest trochę inne. Zaczyna brakować jednego, spójnego sposobu, żeby przejść drogę od commita do produkcji tak, aby każdy członek zespołu wiedział, co się dzieje i mógł temu zaufać.

W tym momencie naturalnie pojawia się potrzeba pełnej, zautomatyzowanej ścieżki CI/CD w chmurze. Potrzebny jest proces, który:

  • startuje automatycznie po commicie lub merge requestcie,
  • buduje aplikację w powtarzalny sposób,
  • uruchamia automatyczne testy i weryfikacje jakości,
  • pakuje wynik w artefakt gotowy do wdrożenia,
  • w przewidywalny sposób wypycha nową wersję na środowiska chmurowe.

Jeśli ten obrazek brzmi znajomo, oznacza to, że organizacja dojrzała do zaprojektowania kompletnej ścieżki CI/CD w chmurze z wykorzystaniem GitHub Actions, GitLab CI oraz natywnych usług dostawców chmury.

Fundamenty CI/CD w chmurze – o co tak naprawdę chodzi

Od commitów do wartości dla użytkownika

CI/CD bywa traktowane jak zbiór narzędzi, ale w praktyce jest to sposób myślenia o tym, jak kod zmienia się w wartość dla użytkownika. Sednem jest przepływ: commit → integracja → testy → artefakt → wdrożenie → monitorowanie. Narzędzia są tylko środkiem do tego celu.

Warto rozróżnić trzy często mylone pojęcia:

  • Continuous Integration (CI) – każdy commit trafia jak najszybciej do wspólnej gałęzi i jest automatycznie testowany. Chodzi o to, by integracja zmian następowała często i w małych porcjach.
  • Continuous Delivery (CD – wariant 1) – system jest w każdej chwili gotowy do wdrożenia na produkcję, ale samo wdrożenie może wymagać ręcznego zatwierdzenia.
  • Continuous Deployment (CD – wariant 2) – każdy zielony build przechodzi automatycznie na produkcję, bez dodatkowego „kliknięcia” człowieka (po spełnieniu określonych warunków jakości i bezpieczeństwa).

Wszystkie te kroki spięte są wokół repozytorium z kodem. Gdy repo (GitHub, GitLab) staje się centrum wszechświata, każde zdarzenie Git – push, pull request, merge request, tag, release – jest okazją, żeby automatycznie uruchomić pipeline CI/CD. Zamiast ręcznie wchodzić na serwer, uruchamia się predefiniowany workflow, który wykonuje zawsze te same, sprawdzone kroki.

Rola chmury w procesie CI/CD

Chmura dodaje do układanki kilka kluczowych elementów: elastyczną infrastrukturę, bogate API i natywne integracje. Dzięki temu pipeline CI/CD nie kończy się na zbudowaniu binarki – może samodzielnie:

  • tworzyć i aktualizować infrastrukturę (Infrastructure as Code),
  • budować i pushować obrazy Docker do rejestrów (ECR, ACR, GCR, GHCR, GitLab Registry),
  • skalować klastry Kubernetes i wdrażać na nie nowe wersje,
  • wdrażać funkcje serverless (AWS Lambda, Azure Functions, Cloud Functions),
  • reagować na metryki i alarmy z systemów monitoringu.

Dzięki API-first każde z tych zadań można zautomatyzować w pipeline, bez gryzienia się z ręczną konfiguracją paneli. GitHub Actions i GitLab CI mają oficjalne akcje/integracje do natywnych usług chmurowych, co drastycznie upraszcza całą ścieżkę.

Jakość pipeline’u a tempo i bezpieczeństwo zmian

Tempo dostarczania nowych funkcji zależy bezpośrednio od dwóch rzeczy: jakości kodu oraz jakości pipeline’u. Jeśli pipeline jest niestabilny, wolny i pełen „magicznych” kroków, zespół przestaje mu ufać. Zaczynają się obejścia: „uruchomię deploy ręcznie, bo pipeline znowu nie doszedł”. To prosta droga do powrotu do chaosu.

Stabilny pipeline CI/CD oznacza, że:

  • każda zmiana jest testowana w identycznym środowisku,
  • pipeline jest na tyle szybki, że można go uruchamiać wielokrotnie w ciągu dnia,
  • wyniki są jednoznaczne: albo zielono, albo czerwono, bez „półśrodków”,
  • wymogi bezpieczeństwa (skanowanie podatności, skan SAST/DAST) są wbudowane w flow, a nie realizowane ad hoc.

Jeżeli pipeline odzwierciedla świadomie zaprojektowany proces zespołu, to każdy wie, jakie warunki musi spełnić kod, aby znaleźć się na produkcji. Jeśli proces jest niejasny, nawet najlepsze narzędzia nie rozwiążą problemu – tylko go przykryją.

Duże industrialne rury pod dramatycznym, pochmurnym niebem w Nowej Zelandii
Źródło: Pexels | Autor: Ray Bran

Wybór narzędzi: GitHub Actions, GitLab CI i natywne usługi chmurowe

Kiedy które narzędzie ma sens

GitHub Actions i GitLab CI realizują te same podstawowe funkcje: definiowanie pipeline’ów jako kodu, uruchamianie jobów na runnerach, integrację z repozytorium i zarządzanie sekretami. Różnią się jednak ekosystemem i charakterem.

GitHub Actions sprawdza się świetnie, gdy:

  • kod jest już w GitHubie i nie ma powodu go przenosić,
  • chcesz korzystać z gotowych akcji ze społeczności (Marketplace),
  • kluczowa jest łatwość startu i dobra dokumentacja,
  • część procesów chcesz powiązać z innymi usługami GitHub (Issues, Releases, Packages).

GitLab CI jest mocny, gdy:

  • używasz GitLaba jako pełnej platformy DevOps (repo, CI/CD, registry, wiki, issues),
  • potrzebujesz hostować instancję on-premise lub w prywatnej chmurze,
  • ważna jest rozbudowana konfiguracja pipeline’ów (multi-project pipelines, parent-child),
  • chcesz mieć w jednym miejscu zarówno kod, jak i natywny registry kontenerów.

Pod kątem vendor lock-in różnica jest subtelna: zarówno workflowy GitHub Actions, jak i .gitlab-ci.yml są specyficzne dla danej platformy. Jednak logika kroków (bash, docker, terraform, kubectl) jest zwykle przenośna – zmienia się „klej” między krokami. Decyzja o wyborze narzędzia powinna więc uwzględniać nie tylko funkcje CI/CD, ale cały proces pracy zespołu.

Natywne usługi chmurowe i ich miejsce w układance

Dostawcy chmury oferują własne narzędzia CI/CD:

  • AWS – CodeCommit, CodeBuild, CodePipeline, CodeDeploy,
  • Azure – Azure DevOps, GitHub (własność Microsoftu), Azure Pipelines,
  • GCP – Cloud Build, Cloud Deploy, Cloud Source Repositories.

Narzędzia te dobrze współpracują z resztą usług chmurowych, mają natywne uprawnienia IAM/Role i często mniejsze opóźnienia między pipeline’em a infrastrukturą. Często spotykany wzór to jednak: repozytorium i główny pipeline w GitHub/GitLab, a natywne usługi jako „silnik wykonawczy” lub wyspecjalizowany element (np. Cloud Build do budowy obrazów w GCP, CodeDeploy do rolling update’ów w EC2).

Przykładowe scenariusze mieszane:

  • Repozytoria na GitHubie, a infrastruktura w AWS. GitHub Actions wywołuje AWS CodeBuild lub bezpośrednio AWS CLI/Terraform, aby budować i wdrażać.
  • Repozytoria w GitLabie i Kubernetes w GKE. GitLab CI buduje obraz, publikuje go do GitLab Registry, a następnie używa kubectl/Helm do wdrożeń do GKE.
  • Repozytoria w GitHubie, infrastruktura w Azure, natywne buildy w Azure Pipelines dla konkretnych projektów .NET.

Orchestrator versus executor – kto rządzi pipeline’em

Dla zdrowego procesu potrzebne jest jedno miejsce, w którym widać cały przepływ od commita do produkcji. Najrozsądniej, jeśli to właśnie platforma VCS (GitHub/GitLab) jest „mózgiem” – jedynym źródłem prawdy o pipeline’ach. Natywne narzędzia chmury wykonują konkretne zadania, ale decyzje, co, kiedy i na jakim kodzie się dzieje, wynikają z definicji pipeline’u przy kodzie.

Dobry wzorzec organizacyjny:

  • GitHub Actions / GitLab CI jako orchestrator – decyduje o etapach, środowiskach, warunkach przejścia, approvals.
  • Usługi chmurowe jako executory – budują obrazy, tworzą zasoby IaC, wdrażają na klastry, raportują status.

Taki podział porządkuje krajobraz narzędzi. Zamiast patchworku skryptów rozrzuconych po różnych repozytoriach i konsolach, zespół ma jedno główne źródło prawdy – pliki YAML z definicjami pipeline’ów.

Projekt architektury ścieżki CI/CD – od gałęzi po środowiska

Środowiska: od dev po produkcję

Spójna ścieżka CI/CD zaczyna się od jasnego podziału na środowiska. Typowy zestaw:

  • dev – środowisko deweloperskie, często współdzielone lub per-branch, używane do wczesnych testów funkcjonalnych,
  • test/stage – bardziej stabilne środowisko, gdzie łączą się większe zmiany i gdzie dział QA / biznes testuje releasy,
  • prod – środowisko produkcyjne, z konserwatywnymi zasadami dostępu i rygorystycznymi wymaganiami dotyczącymi zmian.

Czasem pojawiają się dodatkowe poziomy: UAT (User Acceptance Testing) lub pre-prod (prawie produkcja, ale z danymi testowymi). Im więcej środowisk, tym ważniejszy staje się automatyczny provisioning (IaC) i spójność konfiguracji. Niewielkie zespoły zwykle zaczynają od test/stage i prod, dołączając dev per-branch później.

Każde środowisko powinno mieć wyraźnie określone:

  • kiedy i w jaki sposób następuje wdrożenie (trigger pipeline’u),
  • jaki poziom testów musi przejść kod (od unitów po testy end-to-end),
  • jakie dane są używane (syntetyczne, zanonimizowane, produkcyjne),
  • kto może zatwierdzić przejście do kolejnego poziomu.

Modele pracy z gałęziami a CI/CD

Pipeline CI/CD musi odzwierciedlać przyjęty model pracy z git. Kilka popularnych podejść:

  • Trunk-based development – jedna główna gałąź (main/master), krótkie feature branche, szybkie merge’e, częste releasy. Idealny pod pełne CI/CD i continuous deployment.
  • GitHub Flow – main + krótkie gałęzie feature, obowiązkowe pull requesty, często wdrażanie z main bez dodatkowych gałęzi release.
  • GitFlow – rozbudowany model z gałęziami develop, release, hotfix. Wymaga starannego zaprojektowania pipeline’u, łatwo w nim o chaos, jeśli zespół jest mały.

Dla większości małych i średnich zespołów najpraktyczniejszy jest trunk-based lub GitHub Flow. Pozwalają skupić wysiłek na automatyzacji i testach, zamiast walki ze skomplikowanym drzewem gałęzi. GitFlow ma sens głównie tam, gdzie wymagania procesowe (np. compliance) narzucają skomplikowane cykle release’owe.

Mapowanie zdarzeń Git na pipeline CI/CD

GitHub Actions i GitLab CI mogą reagować na różne zdarzenia z repozytorium. Sensowny schemat dla pełnej ścieżki CI/CD w chmurze wygląda na przykład tak:

  • push na feature branch – uruchamia szybki CI: linting, testy jednostkowe, podstawowy build, bez wdrożeń,
  • pull/merge request do main – uruchamia pełniejszy CI: testy jednostkowe, integracyjne, statyczna analiza, build artefaktów,
  • merge do main – wyzwala pipeline CD na środowisko test/stage,
  • tag/release – startuje pipeline wdrożeniowy na produkcję z dodatkowymi zabezpieczeniami (manualne approve, smoke tests, blue-green/canary).

Każde z tych zdarzeń jest czytelnie widoczne w interfejsie GitHub/GitLab, więc zespół widzi, w którym miejscu ścieżki znajduje się dana zmiana. Z czasem można dodać bardziej zaawansowane warunki, np. automatyczne wdrożenia na środowiska „preview” dla konkretnych pull requestów.

Zasady jakości przed produkcją

Standard jakości jako kontrakt zespołu

Wyobraź sobie deploy na produkcję tuż przed końcem dnia. Ktoś „na szybko” wyłącza testy e2e, bo „i tak przechodzą”, a godzinę później support zasypują zgłoszenia. Źródło problemu nie leży tylko w jednym commicie – to brak wspólnego kontraktu jakości.

Ścieżka CI/CD porządkuje ten kontrakt. Zamiast nieformalnych zasad typu „zawsze odpalaj testy lokalnie”, pojawiają się twarde reguły zakodowane w pipeline’ach. Przykładowy zestaw minimalnych wymagań przed wejściem na produkcję może wyglądać tak:

  • pipeline CI zakończony sukcesem dla commita wchodzącego na produkcję,
  • pokrycie testami jednostkowymi na ustalonym poziomie (np. sprawdzane przez narzędzia typu Jest, pytest + raport coverage),
  • statyczna analiza kodu (SAST) bez krytycznych błędów,
  • skan podatności dependency (np. GitHub Dependabot, GitLab Dependency Scanning) i brak blokujących CVE,
  • przejście przynajmniej podstawowych testów end-to-end na środowisku stage,
  • przegląd kodu (code review) przez minimum jedną osobę techniczną z zespołu.

Te zasady trzeba spisać w formie „Definition of Done” dla zmian, ale także odwzorować w pipeline’ach. Inaczej szybko zamienią się w życzeniową checklistę.

Techniczne bramki jakości w pipeline’ach

Najbardziej skuteczne są takie bramki, które nie wymagają dyskusji – działają automatycznie i konsekwentnie. Na poziomie GitHub Actions lub GitLab CI można wdrożyć m.in.:

  • status checks wymagane do merge’a – bez zielonego pipeline’u merge request/pull request nie może zostać scalony,
  • quality gate z narzędzi typu SonarQube – pipeline failuje, jeśli jakość kodu spada poniżej zadanych progów,
  • policy dla test coverage – skrypt w CI porównuje pokrycie z poprzednim buildem i nie dopuszcza dużych spadków,
  • blokady na krytyczne podatności – skan SCA/SAST oznaczony jako „blocking” dla poziomu „critical/high”.

W GitHubie konfiguruje się to głównie przez branch protection rules i „required status checks”. W GitLabie – przez wymagane pipeline’y i zasady merge (np. „Pipelines must succeed”). Taka automatyczna dyscyplina zdejmuje presję z pojedynczych osób; „zły glina” siedzi w YAML-u.

Manualne approve’y tam, gdzie to ma sens

Nie każda bramka powinna być automatyczna. Wdrożenia na produkcję często wymagają świadomej decyzji: „tak, to jest dobry moment, biznes jest gotowy”. Tutaj sprawdzają się manualne kroki w pipeline’ach:

  • w GitHub Actions – jobs z environment: production i włączonymi required reviewers,
  • w GitLab CI – manual jobs typu „Deploy to prod” z przypisanymi uprawnieniami do ich uruchamiania.

Dobrą praktyką jest, aby manualny approve był potrzebny tylko wtedy, gdy zmiana wychodzi poza uzgodnioną „strefę komfortu” (np. duże migracje schematu bazy). Mniejsze iteracje można wdrażać automatycznie – inaczej zespół szybko zacznie obchodzić proces.

Przemysłowe rurociągi w saudyjskiej fabryce, widok z bliska
Źródło: Pexels | Autor: Mumtaz Niazi

Podstawowy pipeline CI: od commita do artefaktów gotowych do wdrożenia

Minimalny zestaw jobów w pipeline CI

Dobry podstawowy pipeline CI składa się z kilku jasno odseparowanych etapów. Jeden z realnych przykładów z małego zespołu produktowego:

  1. Lint & format – szybkie sprawdzenie jakości kodu (ESLint, Prettier, flake8, gofmt),
  2. Testy jednostkowe – uruchamiane równolegle dla backendu i frontendu,
  3. Testy integracyjne – z użyciem lokalnych usług (np. Testcontainers, docker-compose),
  4. Build aplikacji – kompilacja, bundling, przygotowanie paczek,
  5. Tworzenie artefaktów – publikacja buildów (np. obraz kontenera, paczka Helm, archiwum ZIP) do registry.

Kluczem jest szybkość feedbacku. Lint i testy jednostkowe powinny kończyć się w minutach, nie w dziesiątkach minut. Dlatego często dzieli się pipeline na „fast lane” (dla każdego pusha) i „full CI” (dla merge requestów, nocnych buildów).

Przykładowa konfiguracja GitHub Actions

Niewielki projekt Node.js + Docker może mieć w repo plik .github/workflows/ci.yml w stylu:

name: ci

on:
  push:
    branches: [ main, develop, "feature/**" ]
  pull_request:
    branches: [ main, develop ]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --ci --reporters=default

  build:
    needs: lint-and-test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - run: docker build -t myapp:${GITHUB_SHA} .

Taki pipeline robi dwie rzeczy: broni repo przed łamaniem builda oraz buduje obraz kontenera dla każdego zielonego commita. Brakuje w nim jeszcze pushu do registry i tagowania – tym zajmie się kolejny etap.

Przykładowa konfiguracja GitLab CI

Analogiczny pipeline w GitLab CI wymaga pliku .gitlab-ci.yml w repo:

stages:
  - test
  - build

variables:
  DOCKER_TLS_CERTDIR: ""  # dla docker:dind

lint_and_test:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm run lint
    - npm test -- --ci --reporters=default
  only:
    - merge_requests
    - main
    - develop

build_image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t registry.gitlab.com/group/project/myapp:$CI_COMMIT_SHA .
  only:
    - main
    - tags

Tu też widać separację: testy są odpalane szerzej (także na merge requestach), a budowa obrazu jest zarezerwowana dla zmian na głównych gałęziach i tagach.

Artefakty: co tak naprawdę wychodzi z CI

W pełnej ścieżce CI/CD produktem pipeline’u CI nie jest „zielony ptaszek”, tylko konkretne artefakty. Najczęściej są to:

  • obrazy kontenerów – wersjonowane i przechowywane w registry (GitHub Container Registry, GitLab Registry, ECR, GCR, ACR),
  • paczki aplikacyjne – np. archiwa ZIP dla Lambd, pliki JAR/WAR, paczki NuGet/Maven/npm,
  • artefakty infrastruktury – paczki Helm chartów, moduły Terraform, gotowe manifesty K8s.

Dobrą regułą jest: ten sam artefakt przechodzi przez wszystkie środowiska. Nie buduje się oddzielnego obrazu dla stage i prod – zmienia się tylko konfiguracja (np. wartości w Helm chart), nie binaria.

Integracja z chmurą: budowanie i wersjonowanie obrazów, zarządzanie infrastrukturą

Strategia tagowania obrazów kontenerów

Karuzela zaczyna się wtedy, gdy ktoś próbuje debugować błąd na produkcji i pyta: „jaki obraz jest tam teraz wdrożony?”. Jeśli odpowiedź brzmi „latest”, to znaczy, że system wersjonowania nie istnieje.

Praktyczny schemat tagowania obrazów w CI:

  • app:<commit_sha> – unikalny, techniczny tag, zawsze generowany w pipeline (źródło prawdy),
  • app:<branch-name> – wygodny tag dla środowisk nieprodukcyjnych (np. dev, feature),
  • app:<semver> (np. 1.4.0) – tag powiązany z wydaniem (np. git tag lub release),
  • app:latest – opcjonalny shortcut, odsyłający do najnowszej stabilnej wersji (ale nie używany jako jedyne źródło prawdy).

W GitHub Actions może to wyglądać tak:

- name: Build and push image
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: |
      ghcr.io/org/app:${{ github.sha }}
      ghcr.io/org/app:${{ github.ref_name }}
    labels: |
      org.opencontainers.image.revision=${{ github.sha }}
      org.opencontainers.image.source=${{ github.repository }}

W ten sposób każdy build jest odtwarzalny: z SHA commita można dojść do konkretnego obrazu i odwrotnie.

Wybór registry: GitHub/GitLab vs natywne chmurowe

Decyzja „gdzie trzymać obrazy” ma wpływ na bezpieczeństwo, koszty transferu i wygodę. Dwa najczęstsze warianty:

  • Registry przy VCS – GitHub Container Registry, GitLab Container Registry. Zaletą jest prostota (jeden login, jeden zestaw uprawnień, łatwe powiązanie z repo).
  • Registry w chmurze – ECR (AWS), GCR/Artifact Registry (GCP), ACR (Azure). Tu plusem są integracje IAM, mniejsze koszty i brak dodatkowego „skoku” sieciowego do chmury.

W praktyce wiele zespołów trzyma obrazy produkcyjne w registry dostawcy chmury, a te „robocze” (feature, preview) w registry GitHub/GitLab. Pipeline orchestrujący może wypychać ten sam obraz w dwa miejsca, o ile bilans kosztów i złożoności tego nie zabija.

Autoryzacja CI do chmury: sekrety, tokeny, role

Najczęstszy antywzorzec: klucze dostępowe do chmury wpisane na sztywno w YAML lub trzymane w plaintext w zmiennych środowisk runnera. Żeby pipeline mógł bezpiecznie budować i wdrażać, potrzebuje wygodnego, ale bezpiecznego kanału autoryzacji.

Trzy sprawdzone mechanizmy:

  • OIDC (OpenID Connect) z GitHub/GitLab do chmury – pipeline uzyskuje krótkotrwały token na podstawie tożsamości joba, bez stałych kluczy w sekretnych zmiennych,
  • role przypisane runnerom – w przypadku self-hosted runnerów w tej samej chmurze (np. EC2 z rolą IAM),
  • sekrety rotowane automatycznie – jeśli już trzeba używać kluczy, to z mechanizmem regularnej rotacji (np. AWS Secrets Manager, HashiCorp Vault).

GitHub ma natywne wsparcie OIDC z AWS, Azure i GCP, co pozwala w YAML-u użyć krótkich akcji typu aws-actions/configure-aws-credentials bez wklejania kluczy. GitLab z kolei udostępnia JWT w zmiennych CI, które można mapować na role w chmurze.

Infrastruktura jako kod w pipeline’ach

Ręczne klikanie zasobów w konsoli chmurowej jest do zaakceptowania w fazie eksperymentów, ale nie przy stabilnym produkcie. Infrastruktura powinna podlegać tym samym zasadom co aplikacja: wersjonowaniu, code review, testom i kontrolowanemu wdrażaniu.

Najczęściej spotykany stack:

  • Terraform / Pulumi – do warstwy IaaS/PaaS (VPC, bazy, kolejki, klastry K8s, konta serwisowe),
  • Helm / Kustomize – do zarządzania manifestami Kubernetes,
  • CloudFormation / ARM / Deployment Manager – jeśli zespół preferuje natywne narzędzia jednego dostawcy.

Dobrym wzorcem jest wydzielenie osobnych repo dla infrastruktury (np. infra-aws-prod, infra-shared) i spięcie ich pipeline’ami CI. Aplikacje wtedy jedynie „deklarują” swoje wymagania (np. przestrzeń w namespace K8s), a decyzja o faktycznym utworzeniu zasobów zapada w repo infra.

Pipeline Terraform w GitHub Actions – przykład

Przykładowa sekwencja kroków dla zarządzania infrastrukturą w AWS:

name: infra

on:
  push:
    branches: [ main ]
    paths:
      - "terraform/**"

jobs:
  plan:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gh-oidc-terraform
          aws-region: eu-central-1
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.8.0
      - run: |
          cd terraform/prod
          terraform init -input=false
          terraform plan -input=false -out=tfplan
      - name: Upload plan
        uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: terraform/prod/tfplan

  apply:
    needs: plan
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment:
      name: prod
      url: https://console.aws.amazon.com/
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gh-oidc-terraform
          aws-region: eu-central-1
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.8.

Łączenie pipelines aplikacyjnych i infrastrukturalnych

Typowy moment spięcia następuje, gdy aplikacja wymaga nowego zasobu w chmurze: kolejki, topica albo dodatkowego bucketa. Developer dodaje użycie w kodzie, pipeline aplikacyjny się wywraca i zaczyna się ręczne klikanie w konsoli. To najkrótsza droga do rozjazdu między tym, co jest w repo, a tym, co stoi w chmurze.

Zdrowsze podejście opiera się na dwóch osobnych, ale ściśle powiązanych ścieżkach:

  • pipeline infra – zakłada i zarządza zasobami, ma szybkie feedbacki, ale wymaga review i zwykle mocniejszego nadzoru,
  • pipeline aplikacyjny – korzysta z już istniejącej infrastruktury, a jedynie deklaruje zależności (np. nazwy kolejek, sekrety, configmapy).

Spina się to przez kontrakt. W prostym wydaniu kontraktem jest plik konfiguracyjny w repo aplikacji, z którego pipeline infra generuje część deklaracji Terraform/Helm. Przykładowo, w repo aplikacji ląduje plik infra-requirements.yaml:

queues:
  - name: orders-events
    retention_days: 7
  - name: billing-notifications
    retention_days: 3

buckets:
  - name: invoices-archive
    lifecycle_days: 365

Pipeline infra, odpalany na zmianę w takim pliku (np. przez GitHub Actions on: pull_request z filtrem ścieżek), generuje lub aktualizuje odpowiednie moduły Terraform/Helm i robi plan. Reviewer widzi w MR-ze aplikacyjnym zarówno zmiany w kodzie, jak i planowane zmiany w infrastrukturze.

Mini-wniosek: wdrożenie aplikacji nie powinno cicho „zakładać” zasobów w chmurze. Albo używa istniejących, albo jasno komunikuje, że potrzebuje migracji infra, która przejdzie osobny obieg review.

Ścieżka delivery: od obrazów do działającego środowiska

Gdy artefakty są zbudowane i zepchnięte do registry, zaczyna się właściwe CD. W wielu zespołach ten etap jest najbardziej „ręczny”: ktoś loguje się do klastra, robi kubectl set image albo odpala playbooka Ansible z laptopa. Działa – dopóki nie trzeba cofnąć zmian albo zrozumieć, co działo się tydzień temu o 2:34 w nocy.

Pełna ścieżka CD w chmurze ma kilka stałych punktów:

  1. wybór wersji artefaktu (tag obrazu, release aplikacji),
  2. modyfikacja deklaratywnego opisu wdrożenia (np. wartości Helm, manifesty K8s),
  3. uruchomienie narzędzia deploymentowego (Helm, Argo CD, GitLab Deployments),
  4. walidacja i ewentualny rollback.

Pierwszy i drugi krok często są automatyzowane w tym samym pipeline’ie, który budował obraz, ale coraz częściej rozdziela się je na osobny pipeline CD. Zamiast „build + deploy” przy każdym pushu, pipeline CI buduje obraz, a pipeline CD reaguje na zmianę konfiguracji środowiska (np. commit w repo env-prod zmieniający tag obrazu na nowy SHA).

GitOps jako model sterowania wdrożeniami

Duży zespół, kilka mikroserwisów, kilka środowisk. Ktoś pyta, co dokładnie jest wdrożone na stage. Jeden z developerów przegląda historię jobów na GitLabie, ktoś inny patrzy w Argo, trzeci wchodzi na serwer. Po godzinie wiadomo mniej więcej tyle, że „coś koło wersji 1.7”. To klasyczny sygnał, że czas na GitOps.

GitOps wprowadza prostą zasadę: źródłem prawdy o stanie środowiska jest repozytorium Git, a nie „to, co jest w klastrze”. Ktoś/Coś (Argo CD, Flux) stale porównuje definicje z Gita ze stanem klastra i doprowadza je do zgodności. Zmiana wdrożenia to po prostu commit.

Typowa architektura GitOps dla Kubernetes:

  • repozytorium z kodem aplikacji (service-orders),
  • repozytorium z definicją środowiska (env-prod), zawierające HelmRelease/manifesty z konkretnymi tagami obrazów i konfiguracją,
  • Argo CD/Flux zainstalowany w klastrze, wskazujący na env-prod, z włączonym automatycznym sync.

Pipeline CI, po zbudowaniu obrazu ghcr.io/org/orders:abc1234, robi merge request do env-prod, który zmienia tag obrazu dla danego serwisu. Po akceptacji MR-a Argo wykrywa zmianę i robi rollout. Cała historia wdrożeń jest w Git: kto, kiedy i na jaką wersję.

# fragment values.yaml dla Helm chartu
image:
  repository: ghcr.io/org/orders
  tag: "abc1234"
  pullPolicy: IfNotPresent

resources:
  limits:
    cpu: "500m"
    memory: "512Mi"

Mini-wniosek: GitOps nie jest kolejnym modnym słowem, tylko sposobem na to, żeby „co jest na produkcji” dało się sprawdzić jednym git log, a nie serią SSH-ów.

Strategie wdrożeń: blue/green, canary, rolling

Przez pewien czas standardem było „deploy && pray”. Jedno środowisko, jeden deployment, kilkusekundowe okno niedostępności. Dopiero gdy zaczynają się poważne SLA i weekendowe dyżury, rośnie apetyt na bardziej wyrafinowane strategie.

Najczęściej spotykane scenariusze w chmurze:

  • rolling update – domyślny w Kubernetes, stopniowo zamienia stare pody na nowe; mało spektakularny, ale w wielu przypadkach wystarczający,
  • blue/green – dwa równoległe środowiska (blue i green), przełączanie ruchu przez load balancer/ingress; ułatwia natychmiastowy rollback,
  • canary – część ruchu (np. 5%, 20%, 50%) trafia do nowej wersji, reszta do starej; ruch zwiększa się stopniowo, o ile metryki są w normie.

W Kubernetes konfiguracja blue/green/canary zwykle dzieje się w warstwie ingress/servicemesh. Przykładowo, w AWS można użyć ALB z target groupami; w GCP – Internal HTTP(S) LB; w bardziej złożonych setupach – Istio/Linkerd/Traefik. Pipeline CD nie „klika” tych zmian ręcznie, tylko modyfikuje deklaracje w repo env.

Przykład prostego canary z Istio (procentowanie ruchu po wersjach):

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: orders
spec:
  hosts:
    - orders.example.com
  http:
    - route:
        - destination:
            host: orders
            subset: v1
          weight: 80
        - destination:
            host: orders
            subset: v2
          weight: 20

Pipeline CD może w kilku krokach zwiększać wagę nowej wersji (np. 5% → 25% → 50% → 100%), przy każdym kroku sprawdzając metryki z Prometheusa lub logi z Elastic/Cloud Logging. W razie problemów wystarczy commit przywracający stare proporcje.

Automatyczne walidacje po wdrożeniu

Fakt, że deployment się wykonał, nie znaczy jeszcze, że system działa poprawnie. Na produkcji często wychodzą na jaw rzeczy, których nie widać w testach: zbyt małe limity, brak uprawnień do nowego zasobu, różnice w danych.

Dlatego obok testów przed wdrożeniem pojawiają się testy po wdrożeniu (smoke/health checks), uruchamiane z poziomu pipeline’u CD albo narzędzia typu Argo Rollouts. Mogą to być:

  • proste wywołania HTTP do kluczowych endpointów (status 200 + sprawdzenie fragmentu treści),
  • krótkie scenariusze end-to-end (np. z Cypress lub Playwright) odpalane na małym zestawie danych,
  • walidacja metryk (sprawdzenie, czy error rate nie przekracza progu w ciągu X minut).

W GitHub Actions można to zorganizować jako osobny job po wdrożeniu, blokujący oznaczenie release’u jako „stable”:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # ... rollout helm / argo ...

  post_deploy_checks:
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - name: Check health endpoint
        run: |
          for i in {1..10}; do
            STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health)
            if [ "$STATUS" = "200" ]; then
              echo "Service is healthy"
              exit 0
            fi
            echo "Health check failed with $STATUS, retrying..."
            sleep 10
          done
          echo "Health check failed after retries"
          exit 1

Niewinny skrypt z pętlą może oszczędzić nocnych telefonów: deployment uznaje się za zakończony dopiero wtedy, gdy aplikacja naprawdę odpowiada zgodnie z oczekiwaniami.

Środowiska tymczasowe (preview environments)

W pewnym momencie developer przestaje wierzyć testom lokalnym. „U mnie działa” przestaje cokolwiek znaczyć, bo produkcja ma inne integracje, inne dane, inne limity. Zaczyna się potrzeba „zobaczenia” konkretnej gałęzi w prawie produkcyjnym układzie – zanim trafi do maina.

Tu wchodzą środowiska tymczasowe (preview). Za każdym poważniejszym merge requestem pipeline tworzy osobne środowisko: namespace w K8s, osobny stack w Terraform/CloudFormation, dynamiczny URL. Po zamknięciu MR-a środowisko się usuwa.

Dla Kubernetes można to zorganizować tak:

  • konwencja nazewnicza namespace’ów, np. preview-<mr_number>,
  • osobny chart Helm lub values-y z domyślnymi zasobami i ingress-em,
  • job CI, który przy otwarciu MR-a tworzy namespace i deployuje tam aplikację, a przy zamknięciu – sprząta.
# GitLab CI – przykład
preview:
  stage: deploy
  image: bitnami/kubectl:1.30
  script:
    - export NS=preview-$CI_MERGE_REQUEST_IID
    - kubectl create namespace $NS || true
    - helm upgrade --install orders ./helm/orders 
        --namespace $NS 
        --set image.tag=$CI_COMMIT_SHA 
        --set ingress.host=mr$CI_MERGE_REQUEST_IID.preview.example.com
  only:
    - merge_requests

cleanup_preview:
  stage: cleanup
  image: bitnami/kubectl:1.30
  script:
    - export NS=preview-$CI_MERGE_REQUEST_IID
    - kubectl delete namespace $NS --ignore-not-found=true
  when: manual
  only:
    - merge_requests

Efekt uboczny jest bardzo korzystny: tester, product owner czy „biznes” mogą kliknąć w link w MR-ze i zobaczyć funkcjonalność w realnym środowisku. Feedback przychodzi wcześniej, a liczba poprawek po wdrożeniu na produkcję spada.

Kontrola dostępu i approvals w ścieżce CD

Gdy tylko pipeline zaczyna dotykać produkcji, na scenę wchodzą compliance, audyty i różne „czy możemy zablokować auto-deploy na prod?”. Z jednej strony: chcemy mieć automatyzację. Z drugiej: ktoś musi świadomie podjąć decyzję „tak, ta zmiana może iść na produkcję”.

Rozwiązaniem są środowiskowe approvals i granularne uprawnienia. GitHub Actions, GitLab i narzędzia chmurowe pozwalają powiązać environment z zestawem zasad, np.:

  • deploy na dev – automatyczny, bez approvals,
  • deploy na stage – wymaga jednego approvala z zespołu,
  • deploy na prod – wymaga dwóch approvals, przynajmniej jednego spoza autorów MRa.

W GitHub Actions definiuje się to w sekcji environment joba oraz w ustawieniach repo/organizacji:

jobs:
  deploy_prod:
    runs-on: ubuntu-latest
    needs: build
    environment:
      name: prod
      url: https://app.example.com
    steps:
      # ... kroki wdrożenia ...

W ustawieniach środowiska prod można wymusić approvals i ograniczyć, kto może je udzielać. GitLab robi to analogicznie przez Protected Environments. Dobrze skonfigurowane approvals dają kompromis: automat robi swoje, ale człowiek ma ostatnie słowo przy krytycznych zmianach.

Obserwowalność i feedback do pipeline’ów

Bez sensownych metryk każdy pipeline CD jest w praktyce ślepy. Wdrożenie się udało, status 0, wszystko zielone – a pięć minut później support ma kolejkę zgłoszeń. Brakuje sprzężenia zwrotnego: „czy po rolloutcie system zachowuje się normalnie?”.

W praktyce przydaje się kilka prostych integracji:

  • po każdym wdrożeniu – zapis tagu obrazu i timestampu w systemie monitoringowym (np. jako annotation w Prometheusie lub event w DataDog),
  • linki z jobów CI/CD do dashboardów (SLO, error rate, latency),
  • alerty powiązane z konkretnym deploymentem (np. „error rate > X% w 15 minut po rolloutcie wersji Y”).

W Kubernetes adnotacje w Deployment/ReplicaSet potrafią być bardzo pomocne. Pipeline CD może dodawać tam SHA commita, nazwę joba czy link do MR-a:

metadata:
  annotations:
    app.kubernetes.io/version: "abc1234"
    ci.job.url: "https://gitlab.com/group/project/-/jobs/12345"
    ci.pipeline.url: "https://gitlab.com/group/project/-/pipelines/67890"

Narzędzia obserwowalności (np. Grafana) mogą te adnotacje odczytywać i nanosić na wykresy jako markery. Analiza typu „po którym wdrożeniu zaczęły się błędy 500” przestaje być polowaniem z latarką.

Najczęściej zadawane pytania (FAQ)

Po czym poznać, że mój zespół jest gotowy na pełną ścieżkę CI/CD w chmurze?

Jeśli każde wdrożenie kończy się nocnym dyżurem, serwer pamięta logowania połowy firmy, a nikt nie wie, jaka wersja właśnie działa na produkcji – to już jest sygnał alarmowy. Kolejny znak to rosnąca liczba „magicznych” skryptów .sh, które rozumie tylko jedna osoba w zespole.

Gotowość na CI/CD objawia się tym, że:

  • wdrożenia są częste, ale bolesne i podatne na pomyłki,
  • różne projekty mają różne, nieudokumentowane sposoby deployu,
  • po każdym hotfixie trzeba ręcznie odtwarzać kroki, bo nigdzie nie są spisane.
  • Jeśli tak wygląda codzienność, pora zastąpić „chaosowy” deploy jednym, spójnym pipeline’em w chmurze.

Co to jest CI, Continuous Delivery i Continuous Deployment – i czym się różnią w praktyce?

Typowy zespół zaczyna od CI: każdy commit trafia do wspólnej gałęzi i od razu jest budowany oraz testowany. Znika etap „u mnie działa”, bo weryfikacja odbywa się w tym samym, powtarzalnym środowisku.

Continuous Delivery oznacza, że po przejściu pipeline’u system jest zawsze gotowy do wdrożenia, ale ktoś jeszcze klika „deploy” albo akceptuje release. Continuous Deployment idzie krok dalej – każdy zielony build, po spełnieniu kryteriów jakości i bezpieczeństwa, automatycznie ląduje na produkcji. Różnica nie tkwi w narzędziach, lecz w poziomie zaufania do testów i odwagi organizacji.

Kiedy lepiej wybrać GitHub Actions, a kiedy GitLab CI do budowy pipeline’u CI/CD?

Często wybór wynika z tego, gdzie już leży kod. Jeśli repozytoria są w GitHubie, start z GitHub Actions jest naturalny: łatwe początki, bogaty Marketplace z gotowymi akcjami i dobra integracja z Issues, Releases czy GitHub Packages. To dobry wybór, gdy chcesz szybko zautomatyzować typowe scenariusze i korzystać z gotowych klocków społeczności.

GitLab CI wygrywa tam, gdzie GitLab jest traktowany jako cała platforma DevOps: repo, CI/CD, kontenery, wiki, zarządzanie zadaniami – wszystko w jednym miejscu. Jest też sensowny, gdy potrzebujesz instalacji on‑premise lub prywatnej chmury i bardziej zaawansowanych układów pipeline’ów (multi-project, parent-child). W obu przypadkach logika kroków (bash, docker, terraform, kubectl) pozostaje podobna; różni się głównie „klej” i integracja z ekosystemem.

Jak połączyć GitHub Actions lub GitLab CI z natywnymi usługami chmurowymi (AWS, Azure, GCP)?

Typowy schemat wygląda tak: GitHub/GitLab jest „mózgiem”, a chmura – „rękami”. Pipeline buduje aplikację, odpala testy, a następnie przez API/CLI odwołuje się do usług chmurowych. Dzięki temu te same definicje pipeline’u mogą tworzyć infrastrukturę, budować obrazy i wdrażać nową wersję bez wchodzenia do panelu dostawcy chmury.

W praktyce często stosuje się układy:

  • GitHub Actions + AWS: budowanie obrazu Dockera, wysyłka do ECR, wdrożenie na ECS/EKS z użyciem AWS CLI lub CodeDeploy,
  • GitLab CI + GCP: build, push do GitLab Registry, a potem kubectl/Helm na GKE,
  • GitHub + Azure: pipeline wyzwalany w GitHub Actions, a ciężkie buildy .NET i release’y obsługiwane przez Azure Pipelines lub Azure Web Apps Deployment.
  • Im więcej robisz „as code” (Terraform, ARM/Bicep, Cloud Deployment Manager), tym łatwiej spiąć wszystko w jeden przewidywalny przepływ.

Jak zbudować stabilny i szybki pipeline CI/CD, któremu zespół będzie ufał?

Pierwszy krok to pozbycie się „magii”. Każdy ręczny krok, który dziś wykonuje ktoś na produkcji (kopiowanie plików, migracje bazy, restarty usług), powinien trafić do pipeline’u. Drugi – dbałość o powtarzalne środowisko: ten sam obraz Dockera lub ta sama konfiguracja runnera dla wszystkich buildów, bez „specjalnych” maszyn.

W praktyce warto:

  • trzymać pipeline krótki i modularny (osobno build, testy, bezpieczeństwo, deploy),
  • budować tylko raz, a potem używać tego samego artefaktu w kolejnych etapach,
  • wymusić jasne rezultaty: albo zielono, albo czerwono – bez ręcznych korekt i „dogrywania” na serwerze,
  • dołożyć automatyczne skany bezpieczeństwa (SAST/DAST, skan obrazów) jako zwykłe kroki, a nie osobny, „opcjonalny” proces.
  • Gdy każdy widzi ten sam, przejrzysty pipeline i rozumie warunki przejścia na produkcję, zaufanie do automatyzacji rośnie bardzo szybko.

Czy używanie narzędzi CI/CD dostawcy chmury (CodeBuild, Cloud Build, Azure Pipelines) nie zamyka mnie na jednego vendora?

Największe ryzyko lock‑inu zwykle nie tkwi w samym silniku CI/CD, lecz w tym, jak bardzo uzależnisz swoją infrastrukturę i aplikację od specyficznych usług danego dostawcy. Nawet jeśli pipeline miesza GitHub/GitLab z CodeBuild czy Cloud Build, to dopóki kluczowe kroki są realizowane za pomocą standardowych narzędzi (docker, terraform, kubectl, ansible), przeniesienie procesu jest stosunkowo proste.

Dobrą praktyką jest traktowanie chmurowego CI/CD bardziej jako „executor” konkretnych zadań niż miejsce, w którym zaszyty jest całościowy proces biznesowy. Orkiestracja (definicja, co po czym następuje i jakie są bramki jakości) stoi wtedy po stronie GitHub Actions lub GitLab CI, a usługi chmurowe wykonują wyspecjalizowane, wymienialne klocki.

Jak zacząć migrację z ręcznych wdrożeń na pełny pipeline CI/CD w chmurze bez paraliżowania pracy?

Zespoły często potykają się, próbując „zrobić wszystko naraz”. Rozsądniejsze podejście to małe kroki: najpierw zautomatyzuj sam build i testy przy każdym commicie, ale jeszcze bez deployu. Potem dołóż automatyczne budowanie artefaktów (paczek, obrazów Dockera) i wdrażanie na środowisko testowe.

Następny etap to przeniesienie procesu produkcyjnego do pipeline’u – początkowo z ręczną akceptacją (Continuous Delivery). Gdy taki flow zadziała bez dramatów przez kilka iteracji, można rozważać przejście na bardziej automatyczne wdrożenia. Kluczowe jest, by każdy nowy krok w pipeline’ie zastępował istniejący, ręczny fragment procesu, a nie tylko go dublował.

Co warto zapamiętać

  • Ręczne wdrożenia szybko zamieniają się w chaos – różne skrypty, różne nawyki i „magia na serwerze” sprawiają, że nikt nie ma pewności, jaka wersja działa na produkcji i kto co zmienił.
  • Pełna ścieżka CI/CD zaczyna się już od commita i prowadzi przez integrację, testy, budowę artefaktu i automatyczne wdrożenie aż do monitoringu, a repozytorium z kodem staje się centralnym punktem całego procesu.
  • Continuous Integration, Continuous Delivery i Continuous Deployment to trzy różne poziomy dojrzałości: od częstej integracji i testowania commitów, przez ciągłą gotowość do wdrożenia, po całkowicie automatyczne wypuszczanie zmian na produkcję.
  • Chmura pozwala domknąć pipeline: z tego samego procesu można tworzyć infrastrukturę jako kod, budować obrazy Docker, wdrażać aplikacje na Kubernetes lub funkcje serverless i spinać to wszystko z monitoringiem przez API.
  • Jakość i stabilność pipeline’u wprost wpływają na tempo i bezpieczeństwo zmian – powtarzalne środowisko, szybkie i jednoznaczne wyniki testów oraz automatyczne skany bezpieczeństwa eliminują „obejścia” i ręczne deploye.
  • GitHub Actions i GitLab CI dostarczają podobne funkcje techniczne, lecz różnią się ekosystemem: GitHub wygrywa łatwością startu i bogatym Marketplace, a GitLab sprawdza się jako kompletna, zintegrowana platforma DevOps.
  • Kluczem nie są same narzędzia, lecz świadomie zaprojektowany proces – jeśli zespół jasno definiuje, jakie kroki musi przejść kod, by trafić na produkcję, GitHub Actions, GitLab CI i usługi chmurowe stają się przewidywalną „taśmą produkcyjną” zmian.