CI/CD - dlaczego wdrożenie zmian nie powinno wymagać modlitwy i nocnej zmiany
Sobota, 22:00. Okno serwisowe. Tomek loguje się na serwer produkcyjny przez SSH. Wpisuje git pull. Czeka. Restartuje usługę. Czeka. Otwiera przeglądarkę, klika po systemie. Wygląda OK. Wychodzi z SSH. Idzie spać.
Niedziela, 7:00. Telefon. System nie działa. Tomek nie ściągnął jednej zależności. Albo migracja bazy się nie wykonała. Albo nowa wersja wymaga zmiennej środowiskowej, której Tomek nie ustawił, bo zapomniał. Albo wszystko działało na jego komputerze, ale serwer ma inną wersję Javy.
Brzmi znajomo? W większości polskich firm wdrożenie oprogramowania to ręczny, stresujący, podatny na błędy rytuał, który wymaga obecności jednej konkretnej osoby, najczęściej poza godzinami pracy, i który regularnie się nie udaje.
Jest inaczej. W nowoczesnym stacku wdrożenie wygląda tak: programista klika "Merge" na GitHubie. 5 minut później nowa wersja działa na produkcji. Bez SSH. Bez ręcznego restartu. Bez modlitwy. I bez sekundy przestoju dla użytkowników.
Czym jest CI/CD
CI/CD to skrót od dwóch praktyk, które razem automatyzują drogę kodu od laptopa programisty do serwera produkcyjnego:
CI - Continuous Integration (ciągła integracja)
Każda zmiana w kodzie jest automatycznie:
- Pobrana z repozytorium
- Skompilowana
- Przetestowana (testy automatyczne)
- Sprawdzona pod kątem jakości (formatowanie, bezpieczeństwo zależności)
Jeśli którykolwiek krok się nie uda - programista dostaje informację w ciągu minut. Kod nie przechodzi dalej.
CD - Continuous Deployment (ciągłe wdrażanie)
Jeśli CI przeszło pomyślnie, kod jest automatycznie:
- Spakowany do artefaktu (kontener Docker, release Elixira)
- Wdrożony na serwer staging (testowy)
- Po akceptacji - wdrożony na produkcję
- Zmonitorowany (czy nowa wersja nie powoduje błędów)
Cały proces trwa 3-10 minut. Bez udziału człowieka po kliknięciu "Merge".
graph TD
PUSH["Programista pushuje kod"]
subgraph CI["CI"]
C1["1. Kompilacja [15s]"]
C2["2. Testy (237 testów) [4s]"]
C3["3. Analiza kodu [10s]"]
C4["4. Audit bezpieczeństwa [5s]"]
C1 --> C2 --> C3 --> C4
end
OK{"Wszystko OK?"}
subgraph CD["CD"]
B["Build"] --> D["Deploy"] --> M["Monitor"]
end
STOP["STOP
Email do programisty
z informacją co jest źle"]
PROD["Produkcja działa z nową wersją
Zero downtime'u"]
PUSH --> CI --> OK
OK -- "TAK" --> CD --> PROD
OK -- "NIE" --> STOP
Jak wygląda wdrożenie bez CI/CD
Spiszmy co robi Tomek przy każdym wdrożeniu:
Czeka na okno serwisowe - bo wdrożenie wymaga restartu, a restart oznacza przestój. Piątek wieczór albo sobota rano.
Loguje się na serwer przez SSH. Wpisuje hasło (albo szuka klucza SSH, który jest gdzieś na jego laptopie).
Ściąga nowy kod -
git pull origin master. Modli się, że nie ma konfliktów merge'a na produkcji.Instaluje zależności -
pip install -r requirements.txt/npm install/bundle install. Modli się, że serwer ma tę samą wersję Pythona/Node'a/Ruby co jego laptop.Migruje bazę danych -
python manage.py migrate/mix ecto.migrate. Modli się, że migracja się wykona i nie zepsuje danych.Restartuje usługę -
systemctl restart myapp. System jest niedostępny. Użytkownicy widzą błąd.Testuje ręcznie - otwiera przeglądarkę, klika po systemie. Sprawdza 5-10 scenariuszy. Nie sprawdza 200 pozostałych.
Modli się - że nic nie zepsuł, że wszystko działa, że telefon nie zadzwoni w nocy.
Czas: 30-60 minut. Ryzyko: wysokie. Stres: kosmiczny. Powtarzalność: zerowa (Tomek robi to trochę inaczej za każdym razem).
Koszty tego podejścia
Okno serwisowe - system nie działa 15-60 minut. Przy systemie e-commerce to utracone zamówienia. Przy systemie ERP używanym przez 3 zmiany - pracownicy czekają.
Czas Tomka - 1-2 godziny w weekend na jedno wdrożenie. Przy wdrożeniu co tydzień - 100 godzin rocznie. 12 500 PLN rocznie na klikanie po SSH.
Ryzyko błędu - ręczne wdrożenie ma ~10% szans na problem. Przy 50 wdrożeniach rocznie - 5 incydentów. Każdy kosztuje godziny naprawiania.
Strach przed wdrożeniem - bo wdrożenie jest ryzykowne, firma wdraża rzadko. Zmiany kumulują się przez tygodnie. Im więcej zmian naraz, tym większe ryzyko. Błędne koło.
Bus factor - tylko Tomek umie wdrażać. Tomek jest chory? Wdrożenie czeka. Tomek na urlopie? Krytyczna poprawka czeka, aż wróci.
Jak wygląda wdrożenie z CI/CD
Krok 1: Programista tworzy Pull Request
Programista skończył pracę nad nową funkcją. Tworzy Pull Request (PR) na GitHubie. To jest propozycja zmiany - "chcę dodać ten kod do systemu".
Krok 2: CI uruchamia się automatycznie
W ciągu sekund od utworzenia PR, CI zaczyna pracę:
# .github/workflows/ci.yml - konfiguracja pipeline'a
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
elixir-version: '1.17'
otp-version: '27'
- run: mix deps.get
- run: mix compile --warnings-as-errors
- run: mix test
- run: mix format --check-formatted
- run: mix deps.auditCo się dzieje:
- Czysty serwer - każdy build startuje na świeżym serwerze. Nie ma "u mnie działa" - bo "u niego" to identyczne środowisko jak produkcja.
- Kompilacja - kod się kompiluje. Jeśli jest błąd składni - wiadomo natychmiast.
- Testy - 237 testów sprawdza, czy nic nie zostało zepsute. 4 sekundy.
- Formatowanie - kod jest spójnie sformatowany. Żadnych wojen o spacje vs taby.
- Audit bezpieczeństwa - zależności nie mają znanych podatności.
Wynik jest widoczny w PR: zielony checkmark (wszystko OK) lub czerwony X (coś nie tak, z dokładnym opisem co).
Krok 3: Code review
Inny programista przegląda kod. Widzi, że CI przeszło (testy OK, bezpieczeństwo OK). Sprawdza logikę, architekturę, czytelność. Akceptuje PR.
Krok 4: Merge i automatyczny deploy
Programista klika "Merge". Od tego momentu człowiek nie bierze udziału:
Build artefaktu - system buduje release Elixira (skompilowany, zoptymalizowany pakiet gotowy do uruchomienia) lub kontener Docker.
Deploy na staging - nowa wersja jest wdrażana na serwer testowy. Automatyczne smoke testy sprawdzają, czy aplikacja startuje i odpowiada.
Deploy na produkcję - nowa wersja jest wdrażana na serwer produkcyjny. Bez restartu. Bez downtime'u.
Health checks - system monitoruje nową wersję. Czy odpowiada? Czy nie rzuca błędów? Czy czas odpowiedzi jest normalny?
Automatyczny rollback - jeśli health checki wykryją problem, system automatycznie wraca do poprzedniej wersji. Bez udziału człowieka. Bez budzenia Tomka.
Krok 5: Powiadomienie
Programista dostaje powiadomienie na Slacku: "Deploy #347 na produkcję zakończony sukcesem. Wersja: v1.23.0. Czas: 4 min 12s."
Koniec. Żadnego SSH. Żadnego okna serwisowego. Żadnej modlitwy.
Zero downtime deployment - jak to technicznie działa
Tradycyjne wdrożenie: zatrzymaj starą wersję → uruchom nową → modli się. W tym momencie system nie działa. Przy 5-sekundowym restarcie - nie dramatycznie. Przy 30-sekundowym - użytkownicy widzą błąd. Przy 5-minutowym (Java z cold startem) - to jest downtime.
Blue-green deployment
Masz dwa identyczne środowiska: Blue (aktualne) i Green (nowe).
Load Balancer
/ \
Blue (v1.22) Green (v1.23)
[aktywne] [nowa wersja]- Nowa wersja jest wdrażana na Green
- Green jest testowany (health checks)
- Load balancer przełącza ruch z Blue na Green
- Użytkownicy są teraz na Green (v1.23)
- Blue stoi jako backup - jeśli coś pójdzie nie tak, przełączenie z powrotem trwa sekundy
Czas przestoju: zero. Przełączenie load balancera to operacja na poziomie milisekund. Użytkownik, który kliknął w trakcie przełączenia, dostaje odpowiedź od nowej wersji. Nie widzi różnicy.
Rolling deployment
Masz 3 serwery za load balancerem:
Krok 1: [v1.22] [v1.22] [v1.22] ← wszystkie na starej wersji
Krok 2: [v1.23] [v1.22] [v1.22] ← pierwszy zaktualizowany
Krok 3: [v1.23] [v1.23] [v1.22] ← drugi zaktualizowany
Krok 4: [v1.23] [v1.23] [v1.23] ← wszystkie na nowej wersjiKażdy serwer jest aktualizowany po kolei. Load balancer kieruje ruch tylko na działające serwery. W żadnym momencie system nie jest niedostępny.
Hot code reload (BEAM)
Erlang/Elixir ma unikalne rozwiązanie, którego nie ma żaden inny ekosystem: hot code reload - wymiana kodu w działającym systemie bez restartu.
BEAM ładuje nowy moduł do pamięci obok starego. Nowe wywołania funkcji idą do nowego modułu. Stare wywołania (w trakcie) dokończą się na starym module. Gdy stary moduł nie jest już używany - jest usuwany z pamięci.
Efekt: wdrożenie bez restartu procesu, bez utraty połączeń WebSocket, bez utraty stanu sesji. Systemy telekomunikacyjne Ericssona (na Erlangu) osiągają 99.9999999% uptime dzięki temu mechanizmowi - to 31 milisekund downtime na rok.
Migracja bazy danych bez przestoju
Największy strach przy wdrożeniach: "migracja bazy zepsuje dane". W CI/CD migracje bazy danych są:
Wersjonowane
Każda zmiana w strukturze bazy to plik migracji z timestampem:
priv/repo/migrations/
├── 20260101120000_create_klienci.exs
├── 20260115090000_add_nip_to_klienci.exs
├── 20260201140000_create_zamowienia.exs
├── 20260210100000_add_priorytet_to_zamowienia.exs
└── 20260221080000_add_index_on_zamowienia_data.exsKażdy programista w zespole widzi historię zmian bazy. Każda zmiana jest w code review. Żadnych "Tomek dodał kolumnę ręcznie na serwerze".
Testowane
Migracja uruchamia się w CI na czystej bazie danych. Jeśli migracja się nie wykona - CI nie przejdzie. Dowiadujesz się o problemie przed wdrożeniem, nie po.
Odwracalne
Dobre migracje mają rollback:
defmodule MyApp.Repo.Migrations.AddPriorytetToZamowienia do
use Ecto.Migration
def up do
alter table(:zamowienia) do
add :priorytet, :integer, default: 0
end
end
def down do
alter table(:zamowienia) do
remove :priorytet
end
end
endJeśli coś pójdzie nie tak - rollback przywraca bazę do poprzedniego stanu.
Online (bez blokowania)
PostgreSQL obsługuje większość zmian schematu bez blokowania tabeli:
ADD COLUMNzDEFAULT- nie blokuje odczytów ani zapisówCREATE INDEX CONCURRENTLY- tworzenie indeksu bez blokowania tabeliDROP COLUMN- natychmiastowe (PostgreSQL oznacza kolumnę jako niewidoczną)
W praktyce 95% migracji nie wymaga downtime'u bazy danych. Pozostałe 5% (np. zmiana typu kolumny na dużej tabeli) planuje się w oknie serwisowym - ale to wyjątek, nie reguła.
Monitorowanie po wdrożeniu
CI/CD nie kończy się na deployu. Po wdrożeniu system monitoruje sam siebie:
Health checks
# Endpoint /health sprawdzany co 10 sekund
get "/health" do
checks = %{
database: check_database(),
memory: check_memory(),
disk: check_disk(),
queue: check_oban_queue()
}
if Enum.all?(checks, fn {_, v} -> v == :ok end) do
json(conn, %{status: "healthy", checks: checks})
else
conn |> put_status(503) |> json(%{status: "unhealthy", checks: checks})
end
endJeśli health check zwraca 503 po wdrożeniu - automatyczny rollback. System wraca do poprzedniej wersji, zanim ktokolwiek zauważy problem.
Metryki BEAM
BEAM daje wgląd w stan systemu, którego nie oferuje żaden inny runtime:
- Liczba procesów - gwałtowny wzrost = wyciek procesów
- Zużycie pamięci per proces - jeden proces zjada RAM = bug
- Długość kolejek mailbox - procesy nie nadążają z przetwarzaniem = bottleneck
- Czas GC - garbage collector zabiera za dużo czasu = problem z alokacją
- Scheduler utilization - ile pracy mają schedulery = obciążenie systemu
# Telemetry - automatyczne zbieranie metryk
:telemetry.execute(
[:myapp, :request],
%{duration: duration},
%{path: path, status: status}
)Dashboard z metrykami (Grafana, LiveDashboard) pokazuje stan systemu w czasie rzeczywistym. Anomalia po wdrożeniu jest widoczna natychmiast.
Alerty
[ALERT] Czas odpowiedzi /api/zamowienia wzrósł z 50ms do 800ms
po deploy #347. Ostatnia zmiana: optymalizacja_zapytan.
Automatyczny rollback za 60 sekund jeśli nie potwierdzisz.System mówi Ci, że coś jest nie tak, wskazuje prawdopodobną przyczynę i daje Ci czas na decyzję. Jeśli nie zareagujesz - sam się naprawi (rollback).
Ile kosztuje CI/CD vs ile kosztuje jego brak
Koszt wdrożenia CI/CD
| Element | Koszt | Częstotliwość |
|---|---|---|
| Konfiguracja pipeline'a | 8 000-15 000 PLN | Jednorazowo |
| GitHub Actions / GitLab CI | 0-2 000 PLN/mies. | Miesięcznie |
| Serwer staging | 200-500 PLN/mies. | Miesięcznie |
| Monitoring (Grafana/Sentry) | 0-500 PLN/mies. | Miesięcznie |
| Razem rok 1 | 16 000-30 000 PLN | |
| Razem kolejne lata | 5 000-15 000 PLN/rok |
Koszt braku CI/CD
| Element | Koszt | Częstotliwość |
|---|---|---|
| Czas Tomka na ręczne wdrożenia | 12 500 PLN/rok | 100h × 125 PLN/h |
| Okna serwisowe (downtime) | 20 000-50 000 PLN/rok | Utracone zamówienia |
| Incydenty po wdrożeniach | 25 000-75 000 PLN/rok | 5 incydentów × naprawy |
| Opóźnione wdrożenia (strach) | Niepoliczalne | Nowe funkcje czekają tygodnie |
| Bus factor (Tomek) | Niepoliczalne | Ryzyko kadrowe |
| Razem | 57 000-137 000 PLN/rok |
CI/CD zwraca się w 3-6 miesięcy. Potem oszczędza dziesiątki tysięcy rocznie.
Ukryty zysk: częstsze wdrożenia
Bez CI/CD firma wdraża raz na 2-4 tygodnie (bo to ryzykowne i czasochłonne). Z CI/CD - kilka razy dziennie.
Co to zmienia?
Szybszy feedback - nowa funkcja jest u klienta w dniu napisania, nie za 3 tygodnie. Klient mówi "fajne, ale zmień X" tego samego dnia. Bez CI/CD ta informacja przychodzi po 3 tygodniach, gdy programista już nie pamięta kontekstu.
Mniejsze ryzyko - wdrożenie 5 zmian jest bezpieczniejsze niż wdrożenie 50. Jeśli coś się zepsuje, wiesz która z 5 zmian to zrobiła. Przy 50 zmianach - powodzenia w szukaniu.
Szybsze naprawy - krytyczny bug na produkcji. Z CI/CD: fix → push → 5 minut → poprawka na produkcji. Bez CI/CD: fix → czekaj na Tomka → czekaj na okno serwisowe → ręczne wdrożenie → godziny do dni.
CI/CD w praktyce - nasz stack
Nasz typowy pipeline dla projektu Phoenix:
Push do repozytorium
↓
┌─── CI (3-5 minut) ────────────────────┐
│ │
│ mix deps.get (zależności) │
│ mix compile --warnings-as-errors │
│ mix test (237 testów) │
│ mix format --check (formatowanie) │
│ mix deps.audit (bezpieczeństwo)│
│ mix dialyzer (typy) │
│ │
└────────────────┬───────────────────────┘
↓
Wszystko zielone?
↓ TAK
┌─── CD (2-3 minuty) ───────────────────┐
│ │
│ docker build (kontener) │
│ docker push (rejestr) │
│ deploy staging (test) │
│ smoke tests (health check) │
│ deploy production (blue-green) │
│ health checks (monitoring) │
│ │
└────────────────┬───────────────────────┘
↓
Produkcja zaktualizowana
Powiadomienie na Slack
Zero downtime'uCały proces: 5-8 minut od push do produkcji. Bez udziału człowieka po kliknięciu "Merge".
Jak zacząć - nawet z legacy systemem
Nie musisz od razu mieć pełnego CI/CD. Zacznij od małych kroków:
Krok 1: Repozytorium kodu
Jeśli Twój kod nie jest w Git - to jest pierwszy krok. Każda zmiana zapisana, z historią, z możliwością cofnięcia. Koszt: 0 PLN (GitHub/GitLab darmowe plany).
Krok 2: Jeden test, jeden pipeline
Nie 237 testów. Jeden. "Czy aplikacja startuje?" CI uruchamia ten jeden test przy każdym push. Jeśli aplikacja nie startuje - wiesz zanim wdrożysz.
Krok 3: Automatyczny deploy na staging
Każdy push → automatyczne wdrożenie na serwer testowy. Tomek nie robi nic ręcznie. Może klikać po staging i weryfikować, ale deploy jest automatyczny.
Krok 4: Więcej testów, więcej pewności
Stopniowo dodawaj testy dla krytycznych funkcji (faktury, zamówienia, uprawnienia). Każdy test to kolejna linia obrony.
Krok 5: Automatyczny deploy na produkcję
Gdy masz wystarczająco testów, żeby ufać pipeline'owi - włącz automatyczny deploy na produkcję. To jest moment, w którym Tomek przestaje być potrzebny w piątek wieczorem.
Podsumowanie
| Aspekt | Ręczne wdrożenie | CI/CD |
|---|---|---|
| Kto wdraża | Tomek (i tylko Tomek) | Maszyna (każdy programista może triggerować) |
| Kiedy | Okno serwisowe (weekend, noc) | W dowolnym momencie dnia pracy |
| Czas wdrożenia | 30-60 minut | 5-8 minut |
| Downtime | 5-30 minut | Zero |
| Ryzyko błędu | ~10% (czynnik ludzki) | ~0.1% (automatyczne testy + rollback) |
| Częstotliwość | Raz na 2-4 tygodnie | Kilka razy dziennie |
| Rollback | Ręczny, stresujący, 30-60 min | Automatyczny, 30 sekund |
| Koszt roczny | 57 000-137 000 PLN | 5 000-15 000 PLN |
| Bus factor | 1 (Tomek) | 0 (maszyna) |
Chcesz, żeby Twoje wdrożenia przestały być nocnym rytuałem? Porozmawiajmy - pokażemy, jak wdrożyć CI/CD dla Twojego systemu i ile czasu i pieniędzy zaoszczędzisz w pierwszym roku.