Disclosure: This article may contain affiliate links. We may earn a commission if you purchase through these links, at no extra cost to you. We only recommend products we believe in.

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:

  1. Aktywne konto GCP z włączonym billingiem ( Cloud Build wymaga projektu z Cloud Build API)
  2. Konto GitLab (Free, Premium lub Self-Managed w wersji 14.0+) z uprawnieniami Owner lub Maintainer w przynajmniej jednym projekcie
  3. gcloud CLI zainstalowane lokalnie (wersja 400.0.0+) i skonfigurowane na projekcie
  4. Uprawnienia:
    • W GCP: roles/cloudbuild.builds.editor, roles/iam.serviceAccountUser
    • W GitLab: Maintainer lub Owner
  5. 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?

  1. E2_HIGHCPU_8 zamiast N1_HIGHCPU_8 — ta sama moc, 20% taniej ($0.40/h vs $0.50/h)
  2. Caching Docker layers — redukcja o 60-80% czasu buildu
  3. Build only changed packages — monorepo? Uruchamiaj tylko affected services
  4. Parallel test execution — pytest-xdist, go test -p
  5. 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:

  1. Workload Identity zamiast service account keys — bezpieczeństwo bez kompromisów
  2. cloudbuild.yaml jako single source of truth dla pipeline'u — wersjonowany, review'owany, powtarzalny
  3. Substitucje zamiast duplikacji — jeden plik, wiele środowisk
  4. Timeout'y i caching — optymalizacja kosztów i czasu
  5. 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

Leave a comment