Implementacja CI/CD na Google Cloud Platform z Cloud Build i GitLab. Krok po kroku: konfiguracja, triggery, automatyzacja wdrożeń i najlepsze praktyki DevOps.
Wprowadzenie: Problem, który zna każdy zespół DevOps
Wyobraź sobie zespół 8 developerów pracujących nad mikroserwisową aplikacją e-commerce na GCP. Każdy commit wymaga ręcznego deploymentu. Deployment trwa 45 minut, w tym 30 minut na SSH, pull, build, test i restart kontenerów. Efekt? Developerzy omijają testy, mergują po cichu "hotfixy" bez review, a w piątek o 16:00 produkcja pada przez niedziałający cache po niepełnej aktualizacji. Brzmi znajomo?
Statystyki są bezlitosne: Gartner szacuje, że 68% incydentów produkcyjnych wynika z błędów konfiguracji lub deploymentu, a DORA (DevOps Research and Assessment) raportuje, że zespoły z dojrzałym CI/CD wdrażają 208 razy częściej niż te bez automatyzacji. Różnica w mean time to recovery (MTTR)? Z godziny do 5 minut.
Jeśli pracujesz na Google Cloud Platform i masz GitLab jako system kontroli wersji — masz już wszystkie narzędzia. Cloud Build kosztuje grosze (pierwsze 120 minut miesięcznie za darmo, dalej $0.0034/minutę przy standardowych maszynach), a integracja z GitLab jest dobrze udokumentowana. Czas zamienić chaos w powtarzalny, audytowalny pipeline.
Co to jest CI/CD na Google Cloud Platform?
CI/CD na GCP to zestaw usług zarządzających automatycznym budowaniem, testowaniem i wdrażaniem kodu. W ekosystemie Google kluczową rolę odgrywa Cloud Build — w pełni zarządzana platforma serverless do orkiestracji buildów, która zastępuje tradycyjne serwery CI (Jenkins, GitLab CI Runner).
W odróżnieniu od self-hosted runnerów GitLab CI, Cloud Build:
- Skaluje się automatycznie — nie musisz zarządzać pulą maszyn do buildów
- Jest zintegrowany z GCP IAM — pełna kontrola uprawnień na poziomie usługi
- Korzysta z Google VPC — buildy działają wewnątrz infrastruktury Google, bez ruchu przez zewnętrzne sieci
- Oferuje natywne integracje — Artifact Registry, Cloud Run, GKE, Cloud Functions — jeden pipeline do wszystkiego
Minus? Cloud Build nie ma wbudowanego interfejsu repozytorium kodu (do tego służy GitLab) i wymaga konfiguracji triggerów, co dla zespołów przyzwyczajonych do prostego .gitlab-ci.yml może wyglądać na overengineering.
Wymagania wstępne
Zanim zaczniemy, upewnij się, że masz:
- Aktywne konto GCP z włączonym billingiem ( Cloud Build wymaga projektu z Cloud Build API)
- Konto GitLab (Free, Premium lub Self-Managed w wersji 14.0+) z uprawnieniami Owner lub Maintainer w przynajmniej jednym projekcie
- gcloud CLI zainstalowane lokalnie (wersja 400.0.0+) i skonfigurowane na projekcie
- Uprawnienia:
- W GCP: roles/cloudbuild.builds.editor, roles/iam.serviceAccountUser
- W GitLab: Maintainer lub Owner
- Repozytorium z kodem aplikacji — może być Dockerizowana appka, funkcja Cloud Run, konfiguracja Kubernetes, lub nawet plain Go/Python/Node z buildem przez Gradle/Maven/npm
Krok 1: Przygotowanie konta service w GCP
Cloud Build wymaga dedykowanego service account do wykonywania operacji (pushing do Artifact Registry, deployment do Cloud Run/GKE, itp.). Tworzenie go poprzez UI jest ryzykowne — łatwo zapomnieć o jakiejś roli.
gcloud iam service-accounts create cloudbuild-deployer \
--display-name="Cloud Build Deployer" \
--project=YOUR_PROJECT_ID
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:cloudbuild-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/run.admin"
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:cloudbuild-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer"
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:cloudbuild-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountUser"
Dla GKE dodaj dodatkowo roles/container.developer. Dla Cloud Functions — roles/functions.developer. Zasada: service account dostaje tylko role niezbędne do wykonania pipeline'u, zgodnie z zasadą least privilege.
Krok 2: Integracja GitLab z Cloud Build przez Workload Identity
Tu zaczyna się magia. Zamiast tworzyć service account key (JSON, który wyleciał z mody po incydencie z Conti ransomware), używamy Workload Identity Federation — bezpieczniejszego rozwiązania opartego na tokenach OIDC.
2.1 Tworzenie Workload Identity Pool
gcloud iam workload-identity-pools create gitlab-pool \
--location="global" \
--display-name="GitLab Workload Pool"
gcloud iam workload-identity-pools providers create oidc-gitlab \
--location="global" \
--workload-identity-pool="gitlab-pool" \
--display-name="GitLab OIDC Provider" \
--issuer-uri="https://gitlab.com" \
--attribute-mapping="google.subject=subject,attribute.actor=actor.username,attribute.repository=attribute.repository"
2.2 Konfiguracja w GitLab
W GitLab przejdź do Settings → CI/CD → Pipeline triggers i wygeneruj trigger token. Następnie przejdź do Settings → Access Tokens i stwórz Personal Access Token z scopes: read_api, write_repository, create_runner_registration_tokens (tylko dla self-hosted GitLab; dla gitlab.com pomiń runner tokens).
W GCP:
PROVIDER=$(gcloud iam workload-identity-pools providers describe oidc-gitlab \
--location="global" \
--workload-identity-pool="gitlab-pool" \
--format="value(name)")
gcloud iam service-accounts add-iam-policy-binding cloudbuild-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://${PROVIDER}/attribute.repository/YOUR_GROUP/YOUR_REPO"
To ostatnie polecenie oznacza, że tylko konkretne repozytorium może impersonate'ować service account Cloud Build. Zabezpieczenie przed eskalacją uprawnień przez malicious fork.
2.3 Konfiguracja GitLab CI/CD variables
W GitLab przejdź do Settings → CI/CD → Variables i dodaj:
| Variable | Value | Protected | Masked |
|---|---|---|---|
| GCP_PROJECT_ID | your-project-id | ✓ | ✗ |
| GCP_REGION | europe-west3 | ✓ | ✗ |
| GAR_LOCATION | europe-west3-docker.pkg.dev | ✓ | ✗ |
| SERVICE_ACCOUNT_EMAIL | cloudbuild-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com | ✓ | ✗ |
| WORKLOAD_IDENTITY_PROVIDER | projects/123456789/locations/global/workloadIdentityPools/gitlab-pool/providers/oidc-gitlab | ✓ | ✗ |
Krok 3: Tworzenie pipeline'u cloudbuild.yaml
Tu definiujesz logikę CI/CD. cloudbuild.yaml to sekwencyjna lista kroków (steps), gdzie każdy krok to kontener wykonujący określone polecenie. Kroki dzielą workspace — zmiany w jednym kroku są widoczne w następnym.
3.1 Podstawowa struktura
steps:
# BUILD: Budowanie obrazu Docker
- name: 'gcr.io/cloud-builders/docker'
id: 'build-image'
args:
- 'build'
- '-t'
- '${_GAR_LOCATION}/${PROJECT_ID}/${_SERVICE_NAME}:${SHORT_SHA}'
- '-t'
- '${_GAR_LOCATION}/${PROJECT_ID}/${_SERVICE_NAME}:latest'
- '.'
env:
- 'BUILD_VERSION=${SHORT_SHA}'
waitFor: ['-'] # Startuje natychmiast
# TEST: Uruchomienie testów jednostkowych
- name: 'gcr.io/cloud-builders/docker'
id: 'test'
entrypoint: 'bash'
args:
- '-c'
- |
docker run --rm ${_GAR_LOCATION}/${PROJECT_ID}/${_SERVICE_NAME}:${SHORT_SHA} pytest tests/ -v --junitxml=/tmp/results.xml
waitFor: ['build-image']
# PUSH: Wypychanie do Artifact Registry
- name: 'gcr.io/cloud-builders/docker'
id: 'push'
args:
- 'push'
- '--all-tags'
- '${_GAR_LOCATION}/${PROJECT_ID}/${_SERVICE_NAME}'
env:
- 'PROJECT_ID=${PROJECT_ID}'
waitFor: ['test']
# DEPLOY: Wdrożenie do Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
id: 'deploy-cloudrun'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- '${_SERVICE_NAME}'
- '--image=${_GAR_LOCATION}/${PROJECT_ID}/${_SERVICE_NAME}:${SHORT_SHA}'
- '--region=${_GCP_REGION}'
- '--platform=managed'
- '--allow-unauthenticated'
- '--tag=${_ENVIRONMENT}'
waitFor: ['push']
# Substitucje (domyślne wartości)
substitutions:
_SERVICE_NAME: my-app
_GAR_LOCATION: europe-west3-docker.pkg.dev
_GCP_REGION: europe-west3
_ENVIRONMENT: staging
# Opcje
options:
logging: CLOUD_LOGGING_ONLY
machineType: 'E2_HIGHCPU_8'
dynamicSubstitutions: true
# Timeout (maksymalnie 3600s = 1h)
timeout: '1200s'
3.2 Zaawansowane wzorce: Build matrix
Jeśli budujesz wieloplatformową aplikację (x86 + ARM), Cloud Build oferuje Build triggers z matrix:
# Definiujesz w Cloud Console:
# dimension: platform=linux/amd64,platform=linux/arm64
# Cloud Build automatycznie uruchomi dwa buildy równolegle
W praktyce oznacza to 2x szybszy build przy natywnych obrazach ARM dla Raspberry Pi lub Apple Silicon developerów.
Krok 4: Konfiguracja triggerów w Cloud Build
4.1 Trigger z GitLab webhook
gcloud builds triggers create github-push-trigger \
--project=YOUR_PROJECT_ID \
--repo-name=YOUR_REPO \
--repo-owner=YOUR_GROUP \
--branch-pattern="^main$" \
--build-config="cloudbuild.yaml" \
--description="Deploy to production on main merge" \
--service-account="cloudbuild-deployer@YOUR_PROJECT_ID.iam.gserviceaccount.com"
Dla GitLab w wersji self-hosted (nie gitlab.com) dodaj flagę --inline-config lub użyj triggerów REST API z webhook secret.
4.2 Złożony trigger z warunkami
Często chcesz:
- Deploy do staging na każdy merge request
- Deploy do production tylko na tagged release
gcloud builds triggers create merge-request-trigger \
--repo-name=YOUR_REPO \
--repo-owner=YOUR_GROUP \
--branch-pattern="^feature/.*$" \
--build-config="cloudbuild-staging.yaml" \
-- substitutions=_ENVIRONMENT=staging
gcloud builds triggers create release-trigger \
--repo-name=YOUR_REPO \
--repo-owner=YOUR_GROUP \
--tag-pattern="^v[0-9]+\\.[0-9]+\\.[0-9]+$" \
--build-config="cloudbuild.yaml" \
-- substitutions=_ENVIRONMENT=production
Krok 5: GitLab CI wrapper (opcjonalny, ale warty uwagi)
Wiele zespołów woli zarządzać pipeline'em w .gitlab-ci.yml i tylko delegować do Cloud Build. Oto wzorzec:
# .gitlab-ci.yml
stages:
- trigger-cloudbuild
cloudbuild-staging:
stage: trigger-cloudbuild
image: google/cloud-sdk:slim
variables:
GCP_PROJECT_ID: $GCP_PROJECT_ID
script:
- echo "$GCP_WORKLOAD_IDENTITY_TOKEN" > /tmp/gcp_creds
- gcloud auth activate-workload-identity --project=$GCP_PROJECT_ID /tmp/gcp_creds
- gcloud builds submit --config=cloudbuild.yaml --substitutions=SHORT_SHA=$CI_COMMIT_SHORT_SHA,_ENVIRONMENT=staging
trigger:
strategy: depend
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
cloudbuild-production:
stage: trigger-cloudbuild
image: google/cloud-sdk:slim
script:
- gcloud auth activate-workload-identity --project=$GCP_PROJECT_ID
- gcloud builds submit --config=cloudbuild.yaml --substitutions=SHORT_SHA=$CI_COMMIT_SHORT_SHA,_ENVIRONMENT=production
rules:
- if: '$CI_COMMIT_TAG =~ /^v[0-9]+\\.[0-9]+\\.[0-9]+$/'
To podejście ma sens gdy:
- Masz już istniejący .gitlab-ci.yml i nie chcesz przepisywać wszystkiego
- Potrzebujesz dodatkowych stages w GitLab (np. security scanning z GitLab Advanced Security)
- Zespół woli jeden plik konfiguracyjny w repozytorium
Najlepsze praktyki CI/CD na GCP
Po roku wdrożeń i setkach buildów, oto co działa w produkcji:
1. Cache'uj zależności
- name: 'gcr.io/cloud-builders/docker'
id: 'build-with-cache'
args:
- 'build'
- '--cache-from=${_GAR_LOCATION}/${PROJECT_ID}/${_SERVICE_NAME}:latest'
- '-t=${_GAR_LOCATION}/${PROJECT_ID}/${_SERVICE_NAME}:${SHORT_SHA}'
- '.'
Typowy build Node.js ze scarowaniem node_modules z poprzedniego obrazu: 4 minuty zamiast 12 minut. Zwrot z inwestycji: natychmiastowy.
2. Ustawiaj timeout'y realistycznie
Buildując Go z complikacją 800+ pakietów, Docker build z wieloma warstwami, i testami integracyjnymi — potrzebujesz buffer. Standard:
- Build prostego API REST: 5-8 minut
- Build frontend z webpack: 8-12 minut
- Build pełnego monorepo: 15-25 minut
Ustawiaj timeout z 20% marginesem. Zbyt krótki timeout = fałszywy fail na produkcji.
3. Warianty środowisk przez substitucje
Unikaj duplikacji cloudbuild.yaml. Zamiast trzech plików (cloudbuild-dev.yaml, cloudbuild-staging.yaml, cloudbuild-prod.yaml), używaj jednego z dynamicznymi substitucjami:
env:
- 'DATABASE_URL=${_DATABASE_URL}'
- 'REDIS_URL=${_REDIS_URL}'
# W triggerze definiujesz:
# _DATABASE_URL: postgresql://prod-db/app (dla production)
# _DATABASE_URL: postgresql://staging-db/app (dla staging)
4. Blue-green deployments z Cloud Run
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
entrypoint: bash
args:
- '-c'
- |
gcloud run deploy ${_SERVICE_NAME} \
--image=${_GAR_LOCATION}/${PROJECT_ID}/${_SERVICE_NAME}:${SHORT_SHA} \
--tag=blue \
--no-traffic
REVISION=$(gcloud run services describe ${_SERVICE_NAME} --region=${_GCP_REGION} --format="value(status.traffic[0].latestCreatedRevisionName)")
sleep 30 # Health check grace period
gcloud run services update-traffic ${_SERVICE_NAME} --to-revisions=${REVISION}=100 --region=${_GCP_REGION}
5. Canary releases z Traffic Director (dla GKE)
Jeśli deployujesz do Kubernetes, używaj Argo Rollouts lub Flagger z Cloud Build triggers. Konfiguracja wykracza poza ten artykuł, ale wynik: automatyczny canary 5% → 25% → 50% → 100% z automatycznym rollbackiem przy błędach.
Typowe pułapki i jak ich unikać
Pułapka #1: Niejawne uprawnienia service account
Cloud Build service account (123456789@cloudbuild.gserviceaccount.com) ma domyślnie rolę Editor w projekcie. To opens door do wszystkiego. Zawsze: nigdy nie używaj domyślnego service account. Stwórz własny z minimalnymi rolami (zasada z Krok 1).
Pułapka #2: Brak secret management
Nie wkładaj haseł do zmiennych GitLab CI/CD jako zwykłe Variables. Używaj GitLab CI/CD Masked Variables (tylko dla wartości pasujących do regex /\A[*\w@/-]{8,}$/) lub, lepiej, Google Secret Manager:
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
entrypoint: bash
args:
- '-c'
- |
gcloud secrets versions access latest --secret="DB_PASSWORD" > /tmp/db_pass
docker build --build-arg DB_PASSWORD=$(cat /tmp/db_pass) .
Pułapka #3: Zbyt duże obrazy Docker
Każdy push do Artifact Registry to transfer danych. Obraz 2GB vs 200MB:
- Transfer: $0.12/GB vs $0.012/GB
- Build time: 8 min vs 3 min
- Cold start Cloud Run: 4s vs 0.5s
Używaj multi-stage builds:
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -ldflags="-s -w" -o main
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]
Finalny obraz: ~15MB zamiast 800MB.
Pułapka #4: Brak build reproducibility
Hardcoded wersje obrazów bazowych to tickets do supportu za 3 miesiące. Pin'uj konkretne wersje:
FROM python:3.12.1-slim-bookworm # Pin do konkretnej wersji, nie :3.12 czy :latest
Pułapka #5: Niedziałające buildy na develop branch
Zespół pushuje do main, wszystko działa. Potem ktoś otwiera merge request z feature branchem i build pada, bo nie ma jeszcze triggerów dla /feature/.*. Konfiguruj triggery dla wszystkich branchy z prefiksem.
Optymalizacja kosztów Cloud Build
Cloud Build pricing jest proste, ale łatwo przekroczyć budżet:
| Build time | Cost (120 free min included) |
|---|---|
| 0-120 min/miesiąc | $0 |
| 500 min/miesiąc | ~$1.29 |
| 2000 min/miesiąc | ~$6.39 |
| 10000 min/miesiąc | ~$33.59 |
Jak zmniejszyć zużycie?
- E2_HIGHCPU_8 zamiast N1_HIGHCPU_8 — ta sama moc, 20% taniej ($0.40/h vs $0.50/h)
- Caching Docker layers — redukcja o 60-80% czasu buildu
- Build only changed packages — monorepo? Uruchamiaj tylko affected services
- Parallel test execution — pytest-xdist, go test -p
- Skip builds for docs/changelog — filtrowanie po ścieżce pliku w triggerze:
--included-files="src/**,cloudbuild.yaml,Dockerfile"
Dla zespołów z budżetem Enterprise: rozważ Cloud Build Enterprise (w preview), oferujący dedykowane build workers bez oversubscription, co przy ciężkich buildach (>30 min) może być szybsze i bardziej przewidywalne.
Kiedy NIE używać Cloud Build
Cloud Build nie jest uniwersalnym rozwiązaniem. Rozważ alternatywy gdy:
- Masz 50+ buildów dziennie na jednym projekcie — GitLab CI Runner (self-hosted) z autoskalowaniem na GKE może być tańszy
- Potrzebujesz Windows builds — Cloud Build nie obsługuje natywnych buildów Windows containers
- Twoje testy trwają >1h — timeout Cloud Build to maksymalnie 1h (dla standard), a cena rośnie liniowo
- Masz strict data residency — Cloud Build domyślnie używa regionu build, ale nie gwarantuje, że build nie opuści regionu (dla regulacji typu GDPR/art. 28 RODO rozważ self-hosted runner z Cloud SQL metadata)
Alternatywy: GitLab CI (native), GitHub Actions (jeśli GitHub), Argo Workflows (dla Kubernetes-native), Tekton (jeśli chcesz K8s-native pipeline jako code).
Podsumowanie
CI/CD na GCP z Cloud Build i GitLab to produkcyjnie dojrzałe rozwiązanie, które wdrożysz w jeden dzień, a które zwróci się w tygodniach. Kluczowe elementy:
- Workload Identity zamiast service account keys — bezpieczeństwo bez kompromisów
- cloudbuild.yaml jako single source of truth dla pipeline'u — wersjonowany, review'owany, powtarzalny
- Substitucje zamiast duplikacji — jeden plik, wiele środowisk
- Timeout'y i caching — optymalizacja kosztów i czasu
- Secret Manager — hasła poza kodem, nawet w zmiennych CI/CD
Wynik? Deployment w 3 minuty, zamiast 45. Zero ręcznych błędów w godzinach pracy. Audyt trail dostępny w Cloud Logging przez 30 dni (lub dłużej z Log Router sink do Cloud Storage). Developer experience poprawia się diametralnie — zespoły, które miały opory przed mergowaniem w piątek, nagle deployują bez obaw.
Jeśli utkniesz na konfiguracji, sprawdź oficjalną dokumentację Cloud Build lub wypróbuj automatyczny generator triggerów w Cloud Console. Ciro Cloud regularnie publikuje deep-dive'y na temat konkretnych scenariuszy — od multi-region deployments po integrację z Vertex AI.
Masz pytania lub własne doświadczenia z CI/CD na GCP? Zostaw komentarz — chętnie dyskutujemy o trade-offach przy konkretnych przypadkach użycia.
Weekly cloud insights — free
Practical guides on cloud costs, security and strategy. No spam, ever.
Comments