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:
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:
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:
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:
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 exactmain.^(main|release/.*)$matchmainvà mọi branchrelease/. - 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
# 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
# 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:
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 đâyWorker 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
# 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=200Hoặc via config file (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: 200gcloud builds worker-pools create my-pool \
--config-from-file=private-pool-config.yaml \
--project=my-project \
--region=us-central1Key parameters:
peeredNetwork: VPC resource URL để peer vớipeeredNetworkIpRange: CIDR range cho worker VMs. Phải/29hoặc lớn hơn./29= 6 usable IPs = 6 concurrent workers tối đaegressOption: NO_PUBLIC_EGRESS: Disable internet access. Workers chỉ có thể communicate qua VPC peering hoặc Google APIs qua Private Google AccessmachineType:e2,n2d,c3families. 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
# 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 VPCLưu ý: Khi NO_PUBLIC_EGRESS, build không thể pull images từ Docker Hub hay public registries. Phải:
- Mirror images cần thiết vào Artifact Registry trước
- Hoặc dùng remote repositories trong Artifact Registry (cache Docker Hub)
- 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:
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ế:
- Kaniko build từng layer của Dockerfile
- Sau mỗi layer, compute hash của layer content
- Check Artifact Registry xem hash đó đã tồn tại chưa (cache hit)
- Cache hit: skip rebuild layer, pull từ Artifact Registry
- 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
# 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):
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.gzGCS 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:
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:
| Variable | Giá trị |
|---|---|
$PROJECT_ID | GCP Project ID |
$BUILD_ID | ID của build hiện tại |
$PROJECT_NUMBER | Project number |
$LOCATION | Region của build |
$COMMIT_SHA | Full commit SHA (chỉ trigger) |
$SHORT_SHA | 7 ký tự đầu của commit SHA |
$BRANCH_NAME | Git branch name |
$TAG_NAME | Git tag name |
$REPO_NAME | Repository name |
$TRIGGER_NAME | Tên trigger |
$SERVICE_ACCOUNT_EMAIL | SA đang dùng |
Custom Substitutions
Phải bắt đầu với _ và viết hoa:
# 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:
gcloud builds submit --no-source \
--config=cloudbuild.yaml \
--substitutions=_ENVIRONMENT=staging,_REGION=us-east1Dynamic Substitutions
Với dynamicSubstitutions: true, substitutions có thể reference nhau:
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
options:
automapSubstitutions: true
steps:
- name: 'bash'
script: |
echo "Environment: $ENVIRONMENT" # Từ _ENVIRONMENT substitution
echo "Project: $PROJECT_ID" # Built-inautomapSubstitutions: 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:
# 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 imagesroles/logging.logWriter: write build logsroles/storage.objectAdmintrê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...)# 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:
# 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.