DevOps w małej firmie: tani i bezpieczny pipeline CI/CD na GitHub Actions krok po kroku

1
30
Rate this post

Nawigacja:

Po co małej firmie DevOps i pipeline CI/CD na GitHub Actions

Od „zipa na serwer” do powtarzalnej automatyzacji

Typowy start w małej firmie wygląda podobnie: programista kończy funkcję, pakuje pliki w archiwum, łączy się przez FTP lub SSH, wrzuca zmiany na serwer i liczy, że nic się nie rozsypie. Działa to, dopóki:

  • zespół jest jednoosobowy lub dwuosobowy,
  • aplikacja nie jest krytyczna biznesowo,
  • zmiany wchodzą raz na kilka tygodni.

W momencie gdy zmian przybywa, a kodu dotyka kilka osób, zaczynają się klasyczne problemy: „u mnie działa”, deploy w piątek kończący się cofnięciem zmian, brak pewności co aktualnie jest na produkcji, stres przy każdym wdrożeniu. Pipeline CI/CD na GitHub Actions rozwiązuje te kłopoty, nie przez magię, tylko przez standaryzację i automatyzację powtarzalnych kroków: testy, build, deploy.

Częstotliwość zmian i wielkość zespołu a sens CI/CD

W małej firmie kluczowe pytanie nie brzmi „czy potrzebujemy CI/CD?”, ale „jak dużo automatyzować na tym etapie?”. Kilka prostych wskaźników pomaga zdecydować:

  • Liczba osób dotykających kodu – od 2–3 osób w górę manualne wdrożenia zaczynają się rozjeżdżać. Pipeline CI, odpalany na pull requestach, szybko wychwytuje błędy integracji.
  • Częstotliwość deployów – jeśli produkcja jest aktualizowana rzadziej niż raz na miesiąc, można pozostać przy prostym CI bez pełnego CD. Przy deployach tygodniowych i częstszych zautomatyzowanie wdrożeń daje ogromny zwrot z inwestycji.
  • Rodzaj aplikacji – systemy B2C, SaaS, produkty wystawione do klientów szybciej uzasadniają inwestycję w CI/CD niż jednorazowy wewnętrzny intranet.

Im częściej pojawiają się zmiany i im więcej rąk je dotyka, tym większy sens ma ciągła integracja (CI) i krok po kroku rozszerzany ciągły deployment (CD). Nawet w zespole trzyosobowym pipeline na GitHub Actions, uruchamiany przy każdym pull requeście, potrafi wychwycić większość regresji przed połączeniem kodu.

Gdzie DevOps w małej firmie daje największy zwrot

W małych zespołach DevOps nie ma robić wrażenia na prezentacjach, tylko zdejmować z barków powtarzalne, ryzykowne czynności. Najszybsze korzyści:

  • Mniej awaryjnych deployów – każdy merge do głównej gałęzi przechodzi te same testy i build co produkcja. Znika pokusa „zdeployujmy na szybko, bo klient czeka”.
  • Szybszy feedback – błędy stylu, testy jednostkowe, testy integracyjne odpalają się automatycznie. Programista dostaje wynik kilka minut po wrzuceniu kodu, a nie po kilku godzinach na serwerze klienta.
  • Dokumentacja ukryta w automatyzacji – zamiast „tajemnej wiedzy admina”, pipeline w YAML opisuje, jak zbudować, przetestować i wdrożyć aplikację. Nowa osoba w zespole zobaczy kroki wprost w repozytorium.
  • Mniej kontekstów – build i deploy nie wymagają ręcznego klikania. Zespół może skupić się na kodzie, a nie na odtwarzaniu kroków z pamięci.

Paradoksalnie to w małych firmach, gdzie każda godzina jest droga, sensowny pipeline CI/CD szybciej się „zwraca” niż w korporacjach z rozlanymi kosztami.

Kiedy CI/CD może być przerostem formy nad treścią

Popularna rada „automatyzuj wszystko” potrafi być zabójcza dla małego zespołu. Kilka przykładów, kiedy pełny, rozbudowany CI/CD bardziej szkodzi niż pomaga:

  • Jedna mała aplikacja, rzadkie zmiany – jeśli produkt jest stabilny, zmiany pojawiają się kilka razy w roku, a zespół to jedna osoba, rozbudowany pipeline z wieloma środowiskami może być zwyczajnie zbędny.
  • Zespół bez elementarnego porządku w repozytorium – gdy nie ma code review, branchy, podstawowych testów, zaczynanie od „zaawansowanego” CD kończy się frustracją. Najpierw minimalny porządek w Git, potem automatyzacja.
  • Brak właściciela pipeline’u – jeśli nikt nie czuje się odpowiedzialny za utrzymanie CI/CD, każdy błąd w pipeline’ie staje się „czyimś problemem”. W małym zespole lepiej mieć prosty, ale czytelny workflow niż skomplikowaną orkiestrę kroków, której nikt nie dotyka.

Sensowne podejście w małej firmie to minimum, które realnie odciąża zespół w najbliższych miesiącach, a nie budowanie kopii korporacyjnego procesu. Najczęściej oznacza to: CI na pull requestach, prosty build, najważniejsze testy, manualny (ale powtarzalny) deploy na produkcję.

Dobór poziomu „DevOpsowości” do małego zespołu

Trzy poziomy dojrzałości w praktyce

Zamiast „albo nic, albo full enterprise CI/CD”, lepiej podzielić wdrażanie DevOps na trzy poziomy. Dzięki temu łatwiej dobrać ambicje do realnych zasobów.

Poziom 1: CI tylko na pull requestach

Najprostszy, a często najbardziej opłacalny krok:

  • Każdy pull request do głównej gałęzi (np. main) uruchamia workflow CI.
  • Pipeline odpala lint, szybkie testy jednostkowe, ewentualnie podstawowy build.
  • Brak automatycznego deployu – produkcja nadal jest wdrażana świadomie, ręcznie (np. przez SSH lub prosty skrypt).

Taki poziom rozwiązuje 70% problemów jakościowych i nie wymaga skomplikowanego setupu środowisk. To dobry cel na pierwszy miesiąc w małej firmie.

Poziom 2: CI + ręczny deploy przez workflow

Kolejny krok to zrzucenie z zespołu manualnych deployów, ale z zachowaniem kontroli:

  • CI działa jak wcześniej, na pull requestach.
  • Po zmerge’owaniu do main można ręcznie uruchomić workflow CD (np. workflow_dispatch), który wykona powtarzalny deploy na produkcję lub staging.
  • Pipeline CD korzysta z tych samych kroków build, co CI – mamy spójność środowisk.

To rozwiązanie jest dobrym kompromisem, gdy zespół nie ufa jeszcze w pełni automatycznym wdrożeniom albo produkt jest wrażliwy biznesowo.

Poziom 3: Pełny CI/CD z automatycznym wdrożeniem

Najwyższy poziom, w którym merge do określonej gałęzi sam wyzwala deploy:

  • CI uruchamia się na każdym pull requeście.
  • Merge do main automatycznie wywołuje workflow CD i wdraża aplikację na produkcję (często z dodatkowymi zabezpieczeniami, jak required approvals w GitHub Environments).
  • Wersjonowanie, tagi, release’y generowane są automatycznie.

W małej firmie ten poziom ma sens dopiero wtedy, gdy pipeline jest stabilny, a zespół ma do niego zaufanie. W przeciwnym wypadku każdy błąd w CD blokuje produkcję.

„Automatyzuj wszystko” – kiedy to zabija mały zespół

Popularne hasło „automatyzuj wszystko, co się da” brzmi atrakcyjnie, ale w praktyce:

  • każda automatyzacja musi być utrzymywana,
  • każda warstwa złożoności generuje nowe punkty awarii,
  • czasem taniej jest zrobić coś ręcznie raz na miesiąc niż utrzymywać wyrafinowany pipeline.

Przykład, gdzie automatyzacja szkodzi: mała firma z jedną aplikacją, niewielkim ruchem, decyduje się na rozbudowany mechanizm blue-green deploy, automatyczny rollback, smoke testy po wdrożeniu i dynamiczne skalowanie. Miesiąc później infrastruktura wygląda jak lab korporacyjny, a nikt nie rozumie, co się dzieje, gdy pipeline się wywali. W tym scenariuszu prosta strategia „zdeployuj nową wersję, zostaw starą na drugim serwerze jako awaryjną” byłaby znacznie zdrowsza.

Minimalny cel na trzy miesiące

Dla małej firmy rozsądny cel na najbliższy kwartał to:

  • CI uruchamiany na pull requestach do głównej gałęzi,
  • zestaw szybkich testów i lint, które kończą się w kilka minut,
  • półautomatyczny deploy (workflow wyzwalany ręcznie) na przynajmniej jedno środowisko, np. staging lub produkcję.

Zamiast planować „idealny” pipeline na rok do przodu, lepiej zainwestować energię w stabilny fundament, który zespół zrozumie. Po 2–3 miesiącach można świadomie zdecydować, co dalej: automatyczny deploy na produkcję, dodatkowe środowisko, testy e2e, skanowanie bezpieczeństwa.

Kto utrzymuje pipeline, gdy nie ma działu DevOps

W małej firmie „dział DevOps” to zazwyczaj ktoś, kto po prostu „lubi takie rzeczy”. Jeśli rola nie zostanie nazwana, pipeline staje się niczyj. Kilka praktycznych zasad:

  • Wyznacz właściciela pipeline’u – nie musi być jednym DevOpsem. Często jest to senior developer lub tech lead, który akceptuje zmiany w workflowach.
  • Daj zespołowi prawo do drobnych zmian – proste poprawki (np. wersje akcji, timeouty) może aktualizować każdy, ale większe zmiany przechodzą przez code review właściciela.
  • Traktuj YAML jak kod produktu – workflow także przechodzi przez pull requesty, ma opis i komentarze. Brak „wrzucania na szybko” bez recenzji.

Bez świadomego właściciela pipeline stanie się z czasem „czarną skrzynką”, której każdy się boi. W małym zespole lepiej mieć jeden prosty, zadbany workflow niż trzy złożone, których nikt nie chce dotknąć.

Zespół developerów przy komputerach w nowoczesnym biurze tech
Źródło: Pexels | Autor: cottonbro studio

GitHub Actions: co jest za darmo, a gdzie zaczynają się koszty

Podstawowe pojęcia: workflow, job, runner

GitHub Actions to nic innego jak uruchamianie skryptów w reakcji na zdarzenia w repozytorium. Ważne pojęcia:

  • Workflow – plik YAML zdefiniowany w .github/workflows. Określa, kiedy i co ma się uruchomić.
  • Job – zestaw kroków wykonywanych na jednym runnerze (maszynie). Workflow może mieć kilka jobów, uruchamianych równolegle lub sekwencyjnie.
  • Runner – maszyna, na której wykonują się kroki (np. ubuntu-latest, windows-latest). GitHub udostępnia hostowane runnery, można też użyć własnych.

Pipeline CI/CD to w praktyce jeden lub kilka workflowów z jobami, które wykonują testy, buildy, publikację artefaktów i deploy. Skoro każda minuta na runnerze kosztuje (poza darmowym limitem), kluczowe jest świadome planowanie.

Darmowy plan GitHub i limity

Na planie Free i Team GitHub daje pulę darmowych minut na GitHub Actions (inaczej dla publicznych i prywatnych repozytoriów). W uproszczeniu:

  • Publiczne repozytoria mają zwykle niemal nieograniczone darmowe minuty – Actions traktowane są jako wkład w open source.
  • Prywatne repozytoria mają limit darmowych minut, po którego przekroczeniu minuty są płatne.

Nie trzeba znać wszystkich szczegółów cennika na pamięć, ale trzeba wiedzieć, że:

  • minuty na windows-latest i macos-latest są liczone drożej niż na ubuntu-latest,
  • storage artefaktów (np. paczki buildów) ma swój limit pojemności – przechowywanie dużej liczby starych artefaktów bywa kosztowne,
  • ruch ponad limit naliczany jest automatycznie w rozliczeniu konta organizacji lub użytkownika.
ElementPubliczne repozytoriaPrywatne repozytoria
Minuty ActionsPraktycznie bez limituOgraniczona pula darmowa, potem płatne
Storage artefaktówDarmowy w szerokim zakresieLimit GB, nadmiar płatny
Typ runneraDowolny, ale koszt liczony według typuDowolny, koszt liczony po przekroczeniu darmowego limitu

W małej firmie najprostsze oszczędności wynikają z:

  • ograniczenia liczby triggerów (np. nie odpalanie pełnego pipeline’u na każdy push do branchy developerskich),
  • preferowania ubuntu zamiast Windows lub macOS, gdy to możliwe,
  • czyszczenia i ograniczania artefaktów do tego, co naprawdę potrzebne.

Gdzie kończy się „tanie” GitHub Actions

Większość małych zespołów zużywa minuty Actions nie na faktyczne testy, tylko na „szum”: niepotrzebne triggery, zbyt długie buildy i nadmiernie częste pipeline’y. Rachunek rośnie niepostrzeżenie.

Kilka miejsc, w których koszty zaczynają się wymykać spod kontroli:

  • „Full CI” na każdą gałąź i każdy push – przy kilku osobach commitujących co chwilę kończy się to kaskadą równoległych runów, z których połowa i tak zostanie nadpisana kolejnym pushem.
  • Testy e2e w każdym pipeline’ie – długie, ciężkie scenariusze selenium/cypress potrafią zająć więcej czasu niż wszystkie testy jednostkowe razem.
  • Budowanie frontu i back-endu osobno dla każdego joba – powielanie pracy bez cache’u i współdzielonych artefaktów szybko przepala minuty.
  • Przypadkowe użycie macOS lub Windows – bo „tak było w przykładzie z dokumentacji”, a każdy run jest wielokrotnie droższy niż na Ubuntu.

Popularna rada „odpalaj cały zestaw testów na każdym pushu” działa w projektach z bardzo małym ruchem commitów. Gdy zespół zaczyna commitować częściej, lepszą strategią jest:

  • lite CI na każdy push do feature brancha (lint + szybkie testy jednostkowe),
  • pełny zestaw testów (w tym e2e) tylko na pull request i przed merge do main.

Dla małej firmy to znacznie zdrowszy balans niż ślepe „więcej CI = lepiej”.

Oszczędzanie minut bez cięcia jakości

Zespół nie powinien walczyć z pipeline’em o każdą minutę, ale kilka prostych reguł sprawia, że rachunek stoi w miejscu, a nie rośnie wraz z liczbą developerów.

Sprawdzone dźwignie:

  • Używaj workflow_run – zamiast dublować kroki w wielu workflowach, uruchamiaj jeden „główny” workflow po zakończeniu innego (np. CD po przejściu CI). Mniej powtórek, bardziej przejrzysty graf.
  • Gating po labelach – workflow pełnych testów e2e może uruchamiać się tylko, gdy PR ma konkretną etykietę (np. needs-e2e). Drobne zmiany tekstowe nie muszą przepalać zasobów.
  • Fail fast – pierwsze kroki to lint i szybkie testy. Dopiero później build, a na końcu ciężkie testy. Nie ma sensu odpalać 15-minutowego e2e, gdy lintersy wywalają się w 30 sekundzie.
  • Reużywanie artefaktów – build z joba build powinien być przekazywany do joba deploy, zamiast budować aplikację od zera na każdym etapie.

Paradoksalnie, oszczędne używanie Actions często poprawia też jakość – zmusza do lepszego podziału kroków, czyszczenia testów i rozsądnego doboru triggerów.

Projekt startowy – co będzie automatyzowane

Mała aplikacja, realny pipeline

Zamiast teoretyzować na temat „dowolnego stacku”, lepiej oprzeć się na modelowym, ale realistycznym przykładzie, który mała firma może wdrożyć niemal 1:1.

Załóżmy aplikację:

  • Back-end: Node.js (Express / Nest lub podobny) z testami w Jest.
  • Front-end: React/Vue/Next – ale na początek pipeline obejmie tylko back-end, front może dostać osobny workflow później.
  • Baza: PostgreSQL (lokalnie w CI jako kontener Docker).
  • Hosting: mały VPS (np. na Hetznerze) lub tani PaaS (np. Railway, Render, fly.io). W przykładach skupimy się na VPS, bo jest częsty w małych firmach.

Środowiska: nie zaczynaj od trzech

Popularna rada mówi o pełnym zestawie dev → test → staging → prod. Dla 5-osobowego zespołu to często sztuczny luksus. Im więcej środowisk, tym więcej konfiguracji, driftu i niespójności.

Przy ograniczonych zasobach rozsądny układ to:

  • lokalne środowisko deweloperskie – każdy odpala aplikację u siebie (Docker optional, nie dogmat),
  • jedno środowisko „zdalne” – staging albo produkcja, zależnie od etapu projektu.

Dla nowego produktu, w fazie intensywnego rozwoju i braku dużego ruchu, staging często jest zbędny. Lepszy wzorzec:

  • deploy na produkcję,
  • zachowanie poprzedniej wersji aplikacji (np. drugi katalog / kontener) jako awaryjnego rollbacku,
  • może prosty feature flag do ukrywania eksperymentalnych funkcji, zamiast osobnego środowiska.

Staging nabiera sensu dopiero wtedy, gdy:

  • na produkcji jest realny ruch i nie można „przeklikać” nowej wersji na żywo,
  • biznes wymaga akceptacji zmian przed wdrożeniem (UAT),
  • pipeline jest już stabilny i dodatkowe środowisko nie zamieni się w kolejną „egzotyczną konfigurację”.

Zakres automatyzacji na start

Aby pipeline był sensowny, a nie przytłaczający, dobrze jest ustalić, czego nie robimy w pierwszej iteracji:

  • brak automatycznego rollbacku – rollback to ręczne wywołanie prostego skryptu (np. przełączający symlink current na poprzednią wersję).
  • brak automatycznego skalowania – jeden serwer, jedna instancja, ewentualnie prosty proces manager (pm2, systemd).
  • brak pełnego e2e w CI – na początek lint + testy jednostkowe + ewentualnie kilka szybkich testów integracyjnych.

Automatyzujemy za to:

  • lint + testy jednostkowe na pull requestach,
  • build aplikacji w spójnym środowisku (Node.js w tej samej wersji co na serwerze),
  • ręczny deploy na jeden serwer z poziomu GitHub Actions (Poziom 2 z wcześniejszego podziału).

Struktura repozytorium i konfiguracja GitHub Actions

Minimalny układ katalogów

Przy Node.js/TypeScript prosty i czytelny układ może wyglądać tak:

.
├── src/
│   ├── index.ts
│   └── ...
├── tests/
│   └── ...
├── package.json
├── tsconfig.json
├── jest.config.js
└── .github/
    └── workflows/
        ├── ci.yml
        └── deploy.yml

To nie jest jedyny poprawny wariant, ale ma tę zaletę, że GitHub Actions „wie, gdzie szukać” workflowów. Każdy plik w .github/workflows to osobny workflow, który może odpowiadać za inny etap: CI, deploy, release, housekeeping (np. czyszczenie artefaktów).

Podstawy pliku workflow

Szablon najprostszego workflow CI może wyglądać tak:

name: CI

on:
  pull_request:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm test

To już jest działający CI na pull requestach. Kluczowe elementy, które często są ignorowane:

  • pinowanie wersji akcji (np. @v4) – ogranicza ryzyko, że nagła zmiana w akcji zablokuje pipeline,
  • cache menedżera pakietów (cache: "npm") – drastycznie skraca czas kolejnych runów.

Oddzielne workflowy czy jeden “monolit”

Popularne jest łączenie wszystkiego w jeden workflow: testy, build, deploy, wydawanie release’ów, smoke testy. Przy małym zespole taki „potwór” szybko staje się nieedytowalny.

Bardziej praktyczne podejście:

  • jeden workflow ci.yml – lint + testy + ewentualnie build,
  • jeden workflow deploy.yml – ręczny deploy (workflow_dispatch), korzystający z artefaktów przygotowanych w ci.yml lub budujący na nowo.

Monolit jest uzasadniony dopiero wtedy, gdy:

  • deploy zawsze ma się wykonać po CI, bez wyjątków,
  • logika jest na tyle prosta, że jeden plik pozostaje czytelny (kilkadziesiąt linii, nie kilkaset).
Dwóch programistów omawia kod DevOps przy monitorze w biurze
Źródło: Pexels | Autor: Mikhail Nilov

Budowa taniego i sensownego pipeline’u CI

Podział jobów i równoległość

Mała firma rzadko potrzebuje orkiestracji w stylu „10 jobów na macOS i Windows”. Ale proste rozdzielenie na 2–3 joby potrafi znacznie przyspieszyć feedback bez dużego wzrostu kosztów.

Przykładowy podział:

  • lint_and_unit – lint + testy jednostkowe,
  • integration – testy integracyjne z bazą (PostgreSQL w kontenerze),
  • build – opcjonalny build (np. TypeScript → JS), tylko jeśli wcześniejsze joby przeszły.

Konfiguracja w YAML może wyglądać tak:

name: CI

on:
  pull_request:
    branches: [main]

jobs:
  lint_and_unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"

      - run: npm ci

      - run: npm run lint

      - run: npm test -- --runInBand

  integration:
    runs-on: ubuntu-latest
    needs: lint_and_unit
    services:
      postgres:
        image: postgres:16
        ports:
          - 5432:5432
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: app_test
        options: >-
          --health-cmd="pg_isready -U test" 
          --health-interval=10s 
          --health-timeout=5s 
          --health-retries=5
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"

      - run: npm ci

      - name: Run integration tests
        env:
          DATABASE_URL: postgres://test:test@localhost:5432/app_test
        run: npm run test:integration

  build:
    runs-on: ubuntu-latest
    needs: [lint_and_unit, integration]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"

      - run: npm ci

      - run: npm run build

Różnica w stosunku do jednego joba jest subtelna, ale cenna: przyczynę problemu widać od razu (np. fail w integracji nie zaciemnia informacji o lint), a build startuje dopiero, gdy testy faktycznie przeszły.

Cache – tani booster prędkości

Najczęstszy błąd to brak cache’u i powtarzanie pełnego instalowania zależności w każdym jobie. Przy Node.js i podobnych stackach GitHub daje wbudowane wsparcie przez actions/setup-node, ale można też użyć ogólnego actions/cache.

- name: Cache node_modules
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

Przy małym zespole nie ma sensu optymalizować cache’u do granic możliwości. Lepiej trzymać się prostego klucza opartego na lockfile i zaakceptować, że czasem cache nie trafi – wciąż będzie to szybsze i tańsze niż brak cache’u w ogóle.

Kiedy pipeline jest „za wolny”

Wiele zespołów próbuje wymusić, aby cały CI kończył się w 30 sekund. Przy realnym projekcie to rzadko wykonalne i niepotrzebne. Z drugiej strony 20–30 minut to już często bariera psychologiczna – nikt nie chce czekać z merge’em.

Sensowny target dla małej firmy:

  • quick feedback < 3 min – lint + testy jednostkowe, odpalane zawsze,
  • pełny CI < 10–12 min – włączając testy integracyjne, ewentualnie szybki build.

Jeśli pipeline notorycznie przekracza te czasy, warto zadać dwa niewygodne pytania:

  1. czy część testów nie jest tak wolna i niestabilna, że powinna zostać przeniesiona do osobnego, rzadziej uruchamianego workflow (np. nocny zestaw regresyjny),
  2. czy obecny poziom testowania pasuje do skali produktu, czy jest to „miniaturka korporacyjnego QA” kopiowana bezrefleksyjnie.

Bezpieczeństwo w GitHub Actions

Secrets – klucze poza kodem

Nawet najprostszy deploy wymaga dostępu do serwera, bazy danych czy zewnętrznych API. Trzymanie haseł w repozytorium to proszenie się o kłopoty, nawet w prywatnym projekcie. GitHub daje trzy główne miejsca na tajne dane:

Rodzaje secrets i zasięg – organizacja, repozytorium, środowisko

GitHub udostępnia kilka poziomów przechowywania secrets, które różnią się zasięgiem i tym, kto może je nadpisywać:

  • Organization secrets – widoczne dla wielu repozytoriów w organizacji. Przydatne dla wspólnych integracji (np. token do narzędzia analitycznego). W małej firmie często są nadużywane – łatwo wtedy „przegrzać” uprawnienia.
  • Repository secrets – przypisane do konkretnego repo. To najrozsądniejszy domyślny wybór: klucze produkcyjne aplikacji app-backend są tylko w jej repo, a nie w całej organizacji.
  • Environment secrets – powiązane z environments (np. staging, production). To poziom, na którym da się najczyściej rozdzielić „kto ma dostęp do czego” i wymusić review przy deployu na produkcję.

Popularny schemat „wrzućmy wszystko jako repo secret” jest wygodny na start. Przestaje działać, gdy wchodzi drugie środowisko albo osobny zespół Dev/QA. Od tego momentu brak environment secrets powoduje dziwne obejścia: klucze stagingu i produkcji upchane w jednym stringu JSON czy w nazwach zmiennych typu PROD_DATABASE_URL, STAGING_DATABASE_URL.

Prostsza alternatywa:

  • wspólne integracje (np. Sentry) – organization secrets,
  • specyficzne klucze aplikacji (np. klucz JWT) – repository secrets,
  • parametry środowiskowe (np. DATABASE_URL, SSH_KEY) – environment secrets dla staging i production.

Ograniczanie uprawnień w workflow – permissions jako bezpieczny default

Domyślnie GitHub Actions daje workflowowi dość szerokie uprawnienia do API. W małym projekcie zwykle nikt tego nie rusza – „działa, to po co grzebać”. Problem pojawia się, gdy workflow zaczyna używać zewnętrznych akcji, które dostają dostęp do tokena z prawem zapisu do repozytorium.

Minimalistyczna konfiguracja, która robi różnicę, to jawne ustawienie sekcji permissions:

permissions:
  contents: read
  pull-requests: write

W workflowach CI przeważnie wystarczy samo contents: read. Zapis do PR-ów jest potrzebny tylko wtedy, gdy pipeline ma komentować wyniki (np. raport coverage). Gdy któryś job ma faktycznie modyfikować repo (np. automatyczny bump wersji), lepiej zawęzić uprawnienia tylko do niego:

jobs:
  ci:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      # testy, lint itd.

  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      # generowanie tagów / release notes

Standardowa rada „ustawiaj wszędzie permissions: read-all” jest szybka na POC, ale przeciwskuteczna w projekcie, w którym rośnie liczba akcji stron trzecich. W pewnym momencie trudno ogarnąć, kto tak naprawdę może pisać do repozytorium.

Zewnętrzne akcje – jak nie ściągnąć sobie problemu do pipeline’u

Marketplace jest pełen akcji typu „zrób wszystko za mnie”: deploy, release, automatyczny changelog. Kuszące, szczególnie gdy brakuje czasu. Jednocześnie to kod, który wykonuje się z uprawnieniami twojego repozytorium.

Zanim dołączysz kolejną akcję, warto sprawdzić kilka bazowych rzeczy:

  • pinowanie po commit SHA, a nie tylko po wersji tagowanej – wersję typu @v1 da się podmienić; commit SHA jest stabilny.
  • historia projektu – kiedy ostatnio były commity, czy są zgłoszone CVE, czy projekt jest w ogóle utrzymywany.
  • minimalne uprawnienia – jeśli akcja wymaga contents: write, a jedyne, co robi, to odczyt artefaktów, coś jest nie tak.

Przykładowy bezpieczniejszy zapis użycia akcji:

- name: Deploy via rsync
  uses: some-org/rsync-deploy@4f2a7b3c9d8e1f0a...
  with:
    # parametry

Popularna rada „używaj tylko oficjalnych akcji GitHub” brzmi bezpiecznie, ale w praktyce bywa ograniczająca. Nieduży, dobrze utrzymywany projekt open source często będzie lepszy niż skomplikowany, „oficjalny” kombajn. Warunek: trzeba go przypiąć do konkretnego commita i nie nadawać mu zbyt szerokich uprawnień.

Przekazywanie secrets do jobów i kroków

Najprostszy sposób użycia secretu to zmienna środowiskowa na poziomie joba:

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_KEY }}
    steps:
      - name: Use SSH key
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

Nadawanie sekretu globalnie bywa jednak zbyt szerokie. Przy bardziej wrażliwych danych sensowniejsze jest ograniczenie ich do pojedynczego kroku:

- name: Run DB migration
  env:
    DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
  run: npm run migrate

To drobna różnica, ale w przypadku debugowania logów czy generowania artefaktów bezpieczeństwo wzrasta. Mniej miejsc, w których secret może przypadkowo „wypłynąć”.

Blokady na środowiskach i zasada dwóch par oczu

GitHub Environments pozwalają zdefiniować nie tylko secrets, ale też ochronę środowisk. Dla małej firmy najprostszy i bardzo skuteczny mechanizm to „wymagany review przed deployem na produkcję”.

Konfiguracja od strony workflow może wyglądać tak:

name: Deploy

on:
  workflow_dispatch:
    inputs:
      environment:
        description: "Target environment"
        required: true
        type: choice
        options:
          - staging
          - production

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ github.event.inputs.environment }}
    steps:
      # kroki deployu

Po stronie GitHuba środowisko production ma ustawionych required reviewers – np. jedną osobę spoza zespołu, który właśnie wypycha zmianę. Przy zespole 3–5 osobowym to zwykle nie jest biurokracja, tylko tania „poduszka bezpieczeństwa”.

Najczęstszy kontrargument: „jesteśmy zbyt mali na takie procesy”. Realny problem pojawia się jednak nie przy trzech osobach, tylko gdy dołączają kolejne dwie i nikt już nie pamięta, kto co wypchnął. Jedno kliknięcie review w GitHubie jest tańsze niż diagnozowanie nocnego downtime’u.

Krok po kroku: prosty pipeline CI/CD na GitHub Actions

Założenia dla przykładowej aplikacji

Przykładowy scenariusz, który dobrze odwzorowuje realia małej firmy:

  • backend w Node.js/TypeScript, REST API, baza PostgreSQL,
  • jeden serwer VPS (np. w Hetzner/OVH), dostęp przez SSH,
  • deploy bez Dockera – zwykły build TS → JS i pm2 / systemd na serwerze,
  • branch main traktowany jako „staging”, osobny branch/tag dla produkcji.

Pipeline ma robić trzy rzeczy:

  1. przetestować każdą zmianę na pull requeście,
  2. po merge’u do main zbudować aplikację i przygotować artefakt,
  3. na ręczne wywołanie – wdrożyć konkretną wersję na serwer przez SSH.

Workflow CI – testy i build na main

Na początek workflow .github/workflows/ci.yml, który obsługuje zarówno pull requesty, jak i push na main:

name: CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  lint_and_unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"

      - run: npm ci

      - run: npm run lint

      - run: npm test -- --runInBand

  build:
    runs-on: ubuntu-latest
    needs: lint_and_unit
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"

      - run: npm ci

      - run: npm run build

      - name: Archive build
        run: |
          mkdir -p dist_artifact
          cp -R dist package.json package-lock.json dist_artifact/
        shell: bash

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: app-build
          path: dist_artifact
          retention-days: 3

Kilka decyzji małych, ale praktycznych:

  • build odpala się tylko na push do main – nie marnuje minut na każdy PR, a jednocześnie gwarantuje, że gałąź główna jest zawsze „zbudowalna”,
  • build jest pakowany do prostego artefaktu zamiast publikować obraz Dockera,
  • retencja artefaktów ustawiona na kilka dni – przy małym projekcie dłuższe trzymanie rzadko ma sens, a czyści koszty i bałagan.

Przygotowanie serwera pod prosty deploy

Po stronie serwera wystarczy skromna konfiguracja:

/
├── /var/www/my-app/
│   ├── releases/
│   │   ├── 2024-04-10_12-30/
│   │   └── 2024-04-11_09-15/
│   └── current -> /var/www/my-app/releases/2024-04-11_09-15/
└── /etc/systemd/system/my-app.service

Minimalny serwis systemd może wyglądać następująco:

[Unit]
Description=My Node.js App
After=network.target

[Service]
Environment=NODE_ENV=production
WorkingDirectory=/var/www/my-app/current
ExecStart=/usr/bin/node dist/index.js
Restart=always
RestartSec=5
User=www-data
Group=www-data

[Install]
WantedBy=multi-user.target

Ten układ jest mniej modny niż „wszystko w Dockerze”, ale w realiach jednego VPS-a często wygrywa prostotą. Mniej ruchomych części oznacza mniej miejsc, w których coś trzeba łatać i monitorować.

SSH key jako secret i konfiguracja środowiska

Po stronie GitHuba konfigurujemy środowisko, np. production, i dokładamy secrets:

  • PROD_SSH_HOST – adres serwera (np. 123.45.67.89 lub hostname),
  • PROD_SSH_USER – użytkownik do deployu (np. deploy),
  • PROD_SSH_KEY – klucz prywatny w formacie PEM,
  • PROD_APP_DIR – katalog aplikacji (np. /var/www/my-app).

Klucz prywatny to ten sam, który został dodany do ~/.ssh/authorized_keys użytkownika deploy na serwerze. Nie wrzucamy haseł root ani kluczy używanych lokalnie przez developerów – osobny klucz tylko do deployu jest prościej odwołać, gdy coś pójdzie nie tak.

Workflow deploy – ręczne wywołanie i prosty symlink release’ów

Teraz workflow .github/workflows/deploy.yml, który odbierze artefakt z CI i wdroży go na produkcję:

name: Deploy

on:
  workflow_dispatch:
    inputs:
      environment:
        description: "Target environment"
        required: true
        type: choice
        options:
          - production
      build_run_id:
        description: "Run ID z CI (opcjonalne, domyślnie ostatni)"
        required: false
        type: string

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ github.event.inputs.environment }}

    env:
      SSH_HOST: ${{ secrets.PROD_SSH_HOST }}
      SSH_USER: ${{ secrets.PROD_SSH_USER }}
      SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
      APP_DIR: ${{ secrets.PROD_APP_DIR }}

    steps:
      - name: Prepare SSH key
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H "$SSH_HOST" >> ~/.ssh/known_hosts

      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: app-build
          # jeśli podano build_run_id, można go użyć przez API lub osobny krok

      - name: Create release directory on server
        run: |
          RELEASE_NAME=$(date +"%Y-%m-%d_%H-%M-%S")
          echo "RELEASE_NAME=$RELEASE_NAME" >> $GITHUB_ENV
          ssh $SSH_USER@$SSH_HOST "mkdir -p $APP_DIR/releases/$RELEASE_NAME"

      - name: Upload files via rsync
        run: |
          rsync -az --delete dist_artifact/ $SSH_USER@$SSH_HOST:$APP_DIR/releases/$RELEASE_NAME/

      - name: Update current symlink
        run: |
          ssh $SSH_USER@$SSH_HOST "ln -sfn $APP_DIR/releases/$RELEASE_NAME $APP_DIR/current"

      - name: Restart service
        run: |
          ssh $SSH_USER@$SSH_HOST "sudo systemctl restart my-app.service"

Jest to celowo prosty pipeline, który spełnia kilka istotnych wymogów:

  • deploy jest zawsze ręczny – odpala go świadoma osoba, nie każdy push,
  • Najczęściej zadawane pytania (FAQ)

    Czy mała firma naprawdę potrzebuje DevOps i CI/CD na GitHub Actions?

    Dla jednoosobowego zespołu, który wypuszcza aktualizację raz na kwartał, rozbudowany pipeline faktycznie może być przerostem formy nad treścią. Jednak gdy nad kodem pracują już 2–3 osoby i pojawia się kilka wdrożeń w miesiącu, ręczne „zip na serwer” zaczyna generować realne koszty: cofane deploye, stres, gaszenie pożarów u klienta.

    GitHub Actions pozwala wdrożyć najprostszy poziom DevOps: automatyczne testy i build na pull requestach. To często wystarczy, żeby zredukować 70% problemów jakościowych bez budowania „korporacyjnego” potwora z wielu środowisk i setek kroków w pipeline.

    Od ilu osób w zespole i jakiej częstotliwości zmian ma sens CI/CD?

    Praktyczna granica zaczyna się od 2–3 osób pracujących na jednym repozytorium. Wtedy konflikty zmian, „u mnie działa” i ręczne wdrożenia stają się codziennością, a pipeline CI na pull requestach szybko wychwytuje błędy integracji zanim trafią na main.

    Jeśli deploy na produkcję robisz częściej niż raz w miesiącu, opłaca się co najmniej:

  • wdrożyć CI na pull requestach (lint + szybkie testy),
  • zastanowić się nad prostym, ręcznie wyzwalanym workflow do powtarzalnego deployu.

Przy kilku deployach tygodniowo ręczne klikanie i logowanie się na serwer zazwyczaj przegrywa z kosztowo zautomatyzowanym CD.

Jaki jest minimalny, sensowny pipeline CI/CD dla małej firmy?

Rozsądne minimum na pierwsze 3 miesiące to:

  • CI uruchamiany na każdym pull requeście do głównej gałęzi,
  • lint i szybkie testy, które kończą się w kilka minut,
  • półautomatyczny deploy – workflow wyzwalany ręcznie z GitHuba, który wykonuje powtarzalne kroki wdrożenia.

Taki setup daje powtarzalność i bezpieczeństwo, a jednocześnie nie zamienia małego repo w poligon doświadczalny z dziesiątkami jobów i środowisk.

Pełne, automatyczne wdrożenie na produkcję po każdym merge do main ma sens dopiero wtedy, gdy ten prosty fundament działa stabilnie, a zespół ufa, że pipeline nie wywróci się przy byle zmianie.

Kiedy automatyzacja i rozbudowany CI/CD bardziej szkodzą małemu zespołowi?

Popularna rada „automatyzuj wszystko” nie bierze pod uwagę kosztu utrzymania tej automatyzacji. W małej firmie rozbudowany pipeline z wieloma środowiskami, blue-green deploy, automatycznym rollbackiem i masą testów e2e potrafi po kilku tygodniach stać się czarną skrzynką, której nikt nie rozumie.

Jeśli:

  • masz jedną małą aplikację z rzadkimi zmianami,
  • zespół nie ma jeszcze podstawowego porządku w Git (branching, code review, testy),
  • nikt nie czuje się właścicielem pipeline’u,

to lepsze będzie proste CI i manualny, ale opisany skryptem deploy. Rozbudowę automatyzacji opłaca się robić dopiero wtedy, gdy obecny poziom naprawdę przestaje wystarczać.

Jak dobrać poziom „DevOpsowości” do etapu rozwoju firmy?

Praktyczne podejście to trzy poziomy zamiast skoku na „full enterprise CI/CD”:

  • Poziom 1: CI na pull requestach (lint, testy, ewentualny build), bez automatycznego deployu.
  • Poziom 2: CI jak wyżej + ręcznie wyzwalany workflow do deployu na produkcję lub staging.
  • Poziom 3: pełny CI/CD – merge do main automatycznie uruchamia deploy na produkcję z dodatkowymi zabezpieczeniami.

Mała firma zwykle powinna zatrzymać się na poziomie 1 lub 2 na kilka miesięcy i dopiero po zebraniu doświadczeń przechodzić wyżej, jeśli tempo zmian i krytyczność produktu rzeczywiście tego wymagają.

Kto powinien utrzymywać pipeline CI/CD w małej firmie bez działu DevOps?

W małym zespole „dział DevOps” zazwyczaj nie istnieje. Pipeline najczęściej utrzymuje:

  • programista, który ma najwięcej doświadczenia z infrastrukturą,
  • lub osoba techniczna, która „lubi takie rzeczy” i naturalnie przejmuje tę rolę.

Kluczowe jest, żeby pipeline miał jasno wskazanego właściciela, a workflow był na tyle prosty i opisany (komentarze w YAML, README), by inni członkowie zespołu rozumieli przynajmniej podstawowe kroki.

Lepszy prosty, czytelny pipeline, który dwie–trzy osoby potrafią naprawić, niż wyrafinowana orkiestra kroków, którą rozumie tylko autor – szczególnie gdy ten autor właśnie odszedł z firmy.

Czy GitHub Actions to dobry wybór na tani pipeline CI/CD dla małej firmy?

Dla zespołów pracujących już na GitHubie Actions są naturalnym wyborem: nie trzeba stawiać własnego serwera CI, integracja z repozytorium jest wbudowana, a darmowy limit minut dla małych projektów zwykle wystarcza. To realnie obniża próg wejścia – zarówno kosztowy, jak i organizacyjny.

Alternatywy (Jenkins, GitLab CI, własne rozwiązania) mają sens głównie wtedy, gdy:

  • nie możesz trzymać kodu na GitHubie,
  • masz specyficzne wymagania infrastrukturalne,
  • albo zespół ma już duże doświadczenie z innym narzędziem.

Dla typowej małej firmy, która po prostu chce przestać „rzucać zipy na serwer” i mieć powtarzalne, tanie wdrożenia, GitHub Actions jest najczęściej wystarczający.

1 KOMENTARZ

  1. Bardzo ciekawy artykuł! Właśnie zacząłem zgłębiać tematykę DevOps i ten przewodnik po pipeline CI/CD na GitHub Actions okazał się niezwykle pomocny. Krok po kroku wyjaśnione kroki oraz instrukcje sprawiły, że nawet osoba początkująca jak ja jest w stanie zacząć budować własne rozwiązanie bez większych problemów. Dzięki temu artykułowi udało mi się zrozumieć, jak skonfigurować cały proces w małej firmie w sposób tani i bezpieczny. Gorąco polecam wszystkim zainteresowanym tematyką DevOps!

Komentarze są dostępne tylko po zalogowaniu.