Skip to content

Cloud Build Internals — Triggers, Worker Pools, Caching

Tại sao hiểu Cloud Build Internal quan trọng

Cloud Build thường được xem là "CI tool run YAML file". Nhưng trong production IaC và container pipelines, nhiều vấn đề không hiểu rõ dẫn đến:

  • Build worker cần truy cập private GKE cluster hoặc Cloud SQL private IP nhưng không thể → cần private worker pool
  • Docker layer cache không hoạt động vì mỗi build là VM mới → cần hiểu caching mechanisms
  • Substitution variable không đúng syntax → build fail với cryptic error
  • Build SA có quá nhiều quyền → security risk; quá ít → build fail

Hiểu execution model của Cloud Build là điều kiện tiên quyết để thiết kế pipeline đúng.

Cloud Build Execution Model

Ephemeral Worker Lifecycle

Mỗi Cloud Build invocation:

1. Build request đến Cloud Build API

2. Cloud Build allocate một worker VM (mới, clean state)

3. Source code được copy vào /workspace
   (từ Cloud Source Repos, GitHub, GCS, hoặc upload trực tiếp)

4. Build steps chạy tuần tự (hoặc parallel với waitFor)
   - Mỗi step là một Docker container
   - Tất cả steps share /workspace volume

5. Artifacts được upload (nếu cấu hình)

6. Worker VM bị destroy

Điểm quan trọng: Worker VM là ephemeral. Không có filesystem state nào persist giữa các builds. Mọi thứ trên VM ngoài /workspace (mà bạn explicitly copy vào) sẽ mất sau build.

Hệ quả cho caching: bạn không thể cache node_modules hay Docker layers trên disk như Jenkins agent tái sử dụng. Phải explicitly push/pull cache từ external storage (GCS hoặc Artifact Registry).

Build Step Execution

Mỗi step trong cloudbuild.yaml là một Docker container chạy với /workspace được mount:

yaml
steps:
  # Step 1: download dependencies
  - name: 'node:20-alpine'
    entrypoint: 'npm'
    args: ['install']
    dir: '/workspace/app'  # CWD trong workspace
    
  # Step 2: run tests (chạy sau step 1)
  - name: 'node:20-alpine'
    entrypoint: 'npm'
    args: ['test']
    dir: '/workspace/app'
    
  # Step 3: build Docker image (chạy sau step 2)
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', 'us-docker.pkg.dev/$PROJECT_ID/myapp/server:$COMMIT_SHA', '.']

waitFor cho phép parallel execution:

yaml
steps:
  # Download backend deps
  - name: 'node:20'
    id: 'backend-deps'
    dir: '/workspace/backend'
    args: ['npm', 'install']
    waitFor: ['-']  # Bắt đầu ngay (không đợi step nào)

  # Download frontend deps (song song với backend-deps)
  - name: 'node:20'
    id: 'frontend-deps'
    dir: '/workspace/frontend'
    args: ['npm', 'install']
    waitFor: ['-']  # Bắt đầu ngay (song song)

  # Test backend (đợi backend-deps)
  - name: 'node:20'
    id: 'backend-test'
    dir: '/workspace/backend'
    args: ['npm', 'test']
    waitFor: ['backend-deps']

  # Build image (đợi cả hai test xong)
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', 'us-docker.pkg.dev/$PROJECT_ID/app/server:$SHORT_SHA', '.']
    waitFor: ['backend-test', 'frontend-deps']

waitFor: ['-'] là special value nghĩa là "không đợi step nào" — chạy ngay khi build bắt đầu.

Trigger Types và Filter Mechanics

Cloud Build supports nhiều loại triggers với different event sources:

Cloud Source Repositories

Trigger được tạo trực tiếp trong GCP, không cần webhook setup:

bash
gcloud builds triggers create cloud-source-repositories \
  --name="my-trigger" \
  --repo="my-repo" \
  --branch-pattern="^main$" \
  --build-config="cloudbuild.yaml"

Khi developer push lên main branch, Cloud Build tự động nhận event qua internal pub/sub channel.

GitHub và GitLab

Yêu cầu kết nối repository qua Cloud Build GitHub App hoặc GitLab integration. Sau khi kết nối, Cloud Build nhận webhook events từ GitHub/GitLab:

bash
gcloud builds triggers create github \
  --name="github-trigger" \
  --repository="projects/my-project/locations/us-central1/connections/github-conn/repositories/my-repo" \
  --branch-pattern="^main$" \
  --build-config="cloudbuild.yaml"

Filter mechanics: Trigger evaluation xảy ra ở Cloud Build API level trước khi build được schedule:

  • Branch pattern: RE2 regex. ^main$ chỉ match exact main. ^(main|release/.*)$ match main và mọi branch release/.
  • Included files: Chỉ trigger nếu file trong pattern bị thay đổi. VD: ["**/*.go", "go.mod"]
  • Ignored files: Ngược lại — skip trigger nếu chỉ files này thay đổi. Hữu ích để skip README changes.
  • Tag pattern: Trigger trên git tags thay vì branches
yaml
# cloudbuild-trigger.yaml (trigger configuration)
name: production-deploy
description: Deploy to production on main push
github:
  owner: my-org
  name: my-repo
  push:
    branch: ^main$
includedFiles:
  - 'src/**'
  - 'terraform/**'
  - 'cloudbuild.yaml'
ignoredFiles:
  - '**.md'
  - 'docs/**'

Nếu commit chỉ thay đổi README.md — trigger không fire dù push lên main. Nếu commit thay đổi cả src/main.go lẫn README.md — trigger fire.

Manual và Scheduled Triggers

bash
# Trigger manually
gcloud builds triggers run my-trigger --branch=main

# Schedule qua Cloud Scheduler
gcloud scheduler jobs create http terraform-plan \
  --schedule="0 9 * * 1-5" \
  --uri="https://cloudbuild.googleapis.com/v1/projects/my-project/triggers/TRIGGER_ID:run" \
  --message-body='{"branchName":"main"}' \
  --oauth-service-account-email="scheduler@my-project.iam.gserviceaccount.com"

Pub/Sub Trigger

Trigger khi có message đến Pub/Sub topic — cho phép event-driven builds:

bash
gcloud builds triggers create pubsub \
  --name="pubsub-trigger" \
  --topic="projects/my-project/topics/my-topic" \
  --build-config="cloudbuild.yaml" \
  --subscription-filter='attributes.action = "deploy"'

Private Worker Pools

Tại sao cần Private Worker Pool

Default Cloud Build workers chạy trên Google-managed infrastructure, không kết nối trực tiếp vào VPC của bạn. Điều này có nghĩa:

  • Không thể SSH vào private GKE cluster (private endpoint)
  • Không thể connect tới Cloud SQL với chỉ private IP
  • Không thể truy cập resources trong VPC không có public IP

Private Worker Pool (PWP) giải quyết bằng cách chạy workers trong VPC riêng được peer với VPC của bạn.

Kiến trúc PWP

Your VPC (us-central1)
  ├── Subnets (10.0.0.0/20)
  └── VPC Peering ←→ Service Producer VPC (Google managed)
                           └── Worker VMs (private IPs, 10.1.0.0/24)
                                   ├── /workspace (source code)
                                   └── Build steps chạy tại đây

Worker VMs nằm trong "Service Producer VPC" — đây là VPC được Google quản lý và tự động peer với VPC của bạn khi tạo PWP. Worker chỉ có private IPs. Traffic từ worker đến resources private trong VPC của bạn đi qua VPC peering — không ra internet.

Tạo và Cấu Hình PWP

bash
# Tạo Private Worker Pool với VPC peering
gcloud builds worker-pools create my-private-pool \
  --project=my-project \
  --region=us-central1 \
  --peered-network="projects/my-project/global/networks/main-vpc" \
  --peered-network-ip-range="192.168.10.0/24" \
  --no-public-egress \
  --worker-machine-type=e2-standard-4 \
  --worker-disk-size=200

Hoặc via config file (YAML):

yaml
# private-pool-config.yaml
privatePoolV1Config:
  networkConfig:
    peeredNetwork: projects/my-project/global/networks/main-vpc
    peeredNetworkIpRange: 192.168.10.0/24
    egressOption: NO_PUBLIC_EGRESS
  workerConfig:
    machineType: e2-standard-4
    diskSizeGb: 200
bash
gcloud builds worker-pools create my-pool \
  --config-from-file=private-pool-config.yaml \
  --project=my-project \
  --region=us-central1

Key parameters:

  • peeredNetwork: VPC resource URL để peer với
  • peeredNetworkIpRange: CIDR range cho worker VMs. Phải /29 hoặc lớn hơn. /29 = 6 usable IPs = 6 concurrent workers tối đa
  • egressOption: NO_PUBLIC_EGRESS: Disable internet access. Workers chỉ có thể communicate qua VPC peering hoặc Google APIs qua Private Google Access
  • machineType: e2, n2d, c3 families. Chọn theo workload (Terraform plan cần ít CPU, Docker build nhiều CPU)
  • diskSizeGb: 100-4000GB. Mỗi build step có thể cần disk cho Docker layers, dependencies

Sử dụng PWP trong Build

yaml
# cloudbuild.yaml
options:
  pool:
    name: 'projects/my-project/locations/us-central1/workerPools/my-private-pool'

steps:
  - name: 'hashicorp/terraform:latest'
    args: ['apply', '-auto-approve']
    # Bây giờ có thể access resources private trong VPC

Lưu ý: Khi NO_PUBLIC_EGRESS, build không thể pull images từ Docker Hub hay public registries. Phải:

  1. Mirror images cần thiết vào Artifact Registry trước
  2. Hoặc dùng remote repositories trong Artifact Registry (cache Docker Hub)
  3. Hoặc dùng gcr.io/cloud-builders/* images (Google-managed, accessible qua Google APIs không cần internet)

Build Caching Strategies

Vấn đề: Ephemeral VM không cache

Mỗi build là VM mới. npm install hay pip install phải download packages từ đầu mỗi lần. Docker build rebuild tất cả layers. Build chậm.

Kaniko Layer Caching

Kaniko là Docker image builder chạy trong container (không cần Docker daemon). Nó hỗ trợ caching Docker layers trong Artifact Registry:

yaml
steps:
  - name: 'gcr.io/kaniko-project/executor:latest'
    args:
      - '--destination=us-docker.pkg.dev/$PROJECT_ID/my-repo/myapp:$COMMIT_SHA'
      - '--cache=true'
      - '--cache-ttl=168h'  # Cache expire sau 7 ngày
      - '--cache-repo=us-docker.pkg.dev/$PROJECT_ID/my-repo/cache'
    env:
      - 'DOCKER_CONFIG=/kaniko/.docker'

Cơ chế:

  1. Kaniko build từng layer của Dockerfile
  2. Sau mỗi layer, compute hash của layer content
  3. Check Artifact Registry xem hash đó đã tồn tại chưa (cache hit)
  4. Cache hit: skip rebuild layer, pull từ Artifact Registry
  5. Cache miss: build layer, push lên Artifact Registry cho lần sau

Layer sau trong Dockerfile thường bị cache miss vì phụ thuộc layer trước. Để maximize cache hit rate:

  • Đặt các instructions ít thay đổi (base image, system deps) lên đầu Dockerfile
  • Đặt COPY . . và các instructions phụ thuộc code xuống cuối
dockerfile
# Tối ưu cho layer caching
FROM python:3.12-slim

# Layer 1: System deps - hiếm khi thay đổi → cache hit thường xuyên
RUN apt-get update && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*

# Layer 2: Python deps - thay đổi ít hơn code
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Layer 3: Application code - thay đổi mỗi commit → luôn rebuild
COPY . .

CMD ["python", "main.py"]

GCS Custom Cache

Cho dependencies không phải Docker layers (node_modules, pip packages, Gradle cache):

yaml
steps:
  # Step 1: Restore cache từ GCS
  - name: 'gcr.io/cloud-builders/gsutil'
    entrypoint: 'sh'
    args:
      - '-c'
      - |
        gsutil cp gs://my-build-cache/node_modules.tar.gz /workspace/cache.tar.gz 2>/dev/null || echo "No cache found"
        if [ -f /workspace/cache.tar.gz ]; then
          tar -xzf /workspace/cache.tar.gz -C /workspace
        fi

  # Step 2: Install dependencies (cache hit = nhanh hơn nhiều)
  - name: 'node:20'
    args: ['npm', 'install']
    dir: '/workspace'

  # Step 3: Run build
  - name: 'node:20'
    args: ['npm', 'run', 'build']
    dir: '/workspace'

  # Step 4: Save updated cache
  - name: 'gcr.io/cloud-builders/gsutil'
    entrypoint: 'sh'
    args:
      - '-c'
      - |
        tar -czf /workspace/cache.tar.gz -C /workspace node_modules
        gsutil cp /workspace/cache.tar.gz gs://my-build-cache/node_modules.tar.gz

GCS cache không giảm network traffic (vẫn phải download từ GCS), nhưng GCS download trong GCP region thường nhanh hơn nhiều so với npm registry hay PyPI từ internet — đặc biệt khi dùng private worker pool với NO_PUBLIC_EGRESS.

Machine Type as Performance Lever

Đôi khi tăng CPU/RAM nhanh hơn là optimize caching phức tạp:

yaml
options:
  machineType: 'N1_HIGHCPU_32'  # 32 vCPU cho heavy Docker builds
  # Hoặc:
  machineType: 'E2_HIGHCPU_32'

Với Docker multi-stage builds, nhiều stages có thể được parallelize — CPU nhiều hơn = build nhanh hơn.

Substitution Variables

Built-in Substitutions

Luôn có sẵn, không cần declare:

VariableGiá trị
$PROJECT_IDGCP Project ID
$BUILD_IDID của build hiện tại
$PROJECT_NUMBERProject number
$LOCATIONRegion của build
$COMMIT_SHAFull commit SHA (chỉ trigger)
$SHORT_SHA7 ký tự đầu của commit SHA
$BRANCH_NAMEGit branch name
$TAG_NAMEGit tag name
$REPO_NAMERepository name
$TRIGGER_NAMETên trigger
$SERVICE_ACCOUNT_EMAILSA đang dùng

Custom Substitutions

Phải bắt đầu với _ và viết hoa:

yaml
# cloudbuild.yaml
substitutions:
  _ENVIRONMENT: 'prod'
  _APP_VERSION: '1.0.0'
  _REGION: 'us-central1'

steps:
  - name: 'hashicorp/terraform:latest'
    args: ['apply', '-var', 'environment=${_ENVIRONMENT}', '-var', 'region=${_REGION}']

Override khi chạy manual:

bash
gcloud builds submit --no-source \
  --config=cloudbuild.yaml \
  --substitutions=_ENVIRONMENT=staging,_REGION=us-east1

Dynamic Substitutions

Với dynamicSubstitutions: true, substitutions có thể reference nhau:

yaml
substitutions:
  _REPO: 'us-docker.pkg.dev/$PROJECT_ID/myapp'
  _IMAGE: '${_REPO}/server:$COMMIT_SHA'

options:
  dynamicSubstitutions: true

steps:
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', '${_IMAGE}', '.']
  
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', '${_IMAGE}']

Không cần dynamicSubstitutions cho trigger builds (mặc định bật). Cần bật explicit cho manual builds.

automapSubstitutions — Env Vars Tự Động

yaml
options:
  automapSubstitutions: true
  
steps:
  - name: 'bash'
    script: |
      echo "Environment: $ENVIRONMENT"  # Từ _ENVIRONMENT substitution
      echo "Project: $PROJECT_ID"        # Built-in

automapSubstitutions: true tự động expose tất cả substitutions như environment variables trong steps. Custom subs _FOO được expose không có underscore: FOO.

Service Accounts và IAM

Default Cloud Build Service Account

Default SA: PROJECT_NUMBER@cloudbuild.gserviceaccount.com

Permissions mặc định (roles/cloudbuild.builds.builder):

  • Đọc/ghi Cloud Storage
  • Push images lên Container Registry (legacy)
  • Đọc Secret Manager secrets
  • Đọc Cloud Source Repositories

Đây là quyền quá rộng cho production. Nên dùng custom SA:

yaml
# cloudbuild.yaml
serviceAccount: 'projects/my-project/serviceAccounts/cloudbuild@my-project.iam.gserviceaccount.com'

steps:
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', 'us-docker.pkg.dev/my-project/myapp/server:$COMMIT_SHA', '.']

Custom SA cần:

  • roles/artifactregistry.writer: push images
  • roles/logging.logWriter: write build logs
  • roles/storage.objectAdmin trên artifact bucket: nếu dùng GCS artifacts
  • Permissions specific cho gì build cần làm (Terraform: thêm GCP resource permissions)

Terraform trong Cloud Build — SA Chain

Cloud Build Job
    ↓ (authenticate)
Cloud Build SA (PROJECT_NUMBER@cloudbuild.gserviceaccount.com)
    ↓ (impersonate via roles/iam.serviceAccountTokenCreator)
Terraform SA (terraform@my-project.iam.gserviceaccount.com)
    ↓ (apply với quyền Terraform SA)
GCP Resources (GKE, VPC, IAM...)
yaml
# cloudbuild.yaml cho Terraform
steps:
  - name: 'hashicorp/terraform:1.7'
    entrypoint: 'sh'
    args:
      - '-c'
      - |
        terraform init
        terraform plan -out=plan.tfplan
        terraform apply plan.tfplan
    env:
      - 'GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=terraform@my-project.iam.gserviceaccount.com'
      - 'TF_VAR_project_id=$PROJECT_ID'

Org Policy: allowedIntegrations

constraints/cloudbuild.allowedIntegrations kiểm soát source providers nào được phép kết nối với Cloud Build trong organization:

hcl
# Chỉ cho phép GitHub
resource "google_org_policy_policy" "cloud_build_integrations" {
  name   = "organizations/${var.org_id}/policies/cloudbuild.allowedIntegrations"
  parent = "organizations/${var.org_id}"
  
  spec {
    rules {
      values {
        allowed_values = ["github.com"]
      }
    }
  }
}

Nếu không set, mọi supported provider (GitHub, GitLab, Bitbucket, GitHub Enterprise) đều cho phép. Trong môi trường regulated, nên restrict để chỉ cho phép source provider được approved.

References