DevOpsil
GCP
92%
Needs Review

Google Cloud Build: CI/CD Pipelines for GCP-Native Teams

Riku TanakaRiku Tanaka6 min read

Google Cloud Build: CI/CD Pipelines for GCP-Native Teams

If your stack runs on GCP — GKE, Cloud Run, Artifact Registry — Cloud Build is worth a serious look before you reach for GitHub Actions or Jenkins. It's serverless, tightly integrated with GCP IAM, and has no infrastructure to manage. You write a cloudbuild.yaml, and Google runs it.

This guide shows how to build a production-quality CI/CD pipeline with Cloud Build, including container builds, automated testing, Artifact Registry pushes, and GKE deployments.

How Cloud Build Works

A Cloud Build pipeline is a series of steps, each running in a Docker container. Steps share a workspace volume. You can use any Docker image as a step — Google provides official "cloud builders" for common tools, and you can bring your own.

Trigger (push/PR/tag)
  → Cloud Build picks up cloudbuild.yaml
  → Runs steps sequentially (or in parallel)
  → Pushes artifacts
  → Notifies (Pub/Sub, Cloud Logging)

Basic cloudbuild.yaml Structure

# cloudbuild.yaml
steps:
  # Step 1: Install dependencies and run tests
  - name: node:20
    entrypoint: npm
    args: ['ci']
    id: install

  - name: node:20
    entrypoint: npm
    args: ['test']
    id: test
    waitFor: ['install']

  # Step 2: Build Docker image
  - name: gcr.io/cloud-builders/docker
    args:
      - build
      - -t
      - $_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO/$_SERVICE:$SHORT_SHA
      - -t
      - $_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO/$_SERVICE:latest
      - .
    id: build-image
    waitFor: ['test']

  # Step 3: Push to Artifact Registry
  - name: gcr.io/cloud-builders/docker
    args:
      - push
      - --all-tags
      - $_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO/$_SERVICE
    id: push-image
    waitFor: ['build-image']

  # Step 4: Deploy to GKE
  - name: gcr.io/cloud-builders/kubectl
    args:
      - set
      - image
      - deployment/$_SERVICE
      - $_SERVICE=$_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO/$_SERVICE:$SHORT_SHA
      - -n
      - production
    env:
      - CLOUDSDK_COMPUTE_REGION=$_REGION
      - CLOUDSDK_CONTAINER_CLUSTER=$_GKE_CLUSTER
    id: deploy
    waitFor: ['push-image']

images:
  - $_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO/$_SERVICE:$SHORT_SHA

substitutions:
  _REGION: us-central1
  _REPO: app-images
  _SERVICE: api-service
  _GKE_CLUSTER: prod-cluster

options:
  logging: CLOUD_LOGGING_ONLY
  machineType: E2_HIGHCPU_8    # Bigger machine for faster builds

$PROJECT_ID and $SHORT_SHA are built-in substitutions. The $_ variables are user-defined and can be overridden per trigger.

Setting Up Artifact Registry

# Create a Docker repository in Artifact Registry
gcloud artifacts repositories create app-images \
  --repository-format docker \
  --location us-central1 \
  --description "Application container images"

# Configure Docker auth for local dev
gcloud auth configure-docker us-central1-docker.pkg.dev

# Grant Cloud Build service account access
PROJECT_NUMBER=$(gcloud projects describe YOUR_PROJECT_ID --format='value(projectNumber)')

gcloud artifacts repositories add-iam-policy-binding app-images \
  --location us-central1 \
  --member "serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" \
  --role roles/artifactregistry.writer

Configure Build Triggers

# Trigger on push to main branch
gcloud builds triggers create github \
  --name "push-to-main" \
  --repo-name YOUR_REPO \
  --repo-owner YOUR_GITHUB_ORG \
  --branch-pattern "^main$" \
  --build-config cloudbuild.yaml \
  --substitutions _SERVICE=api-service,_GKE_CLUSTER=prod-cluster

# Trigger on pull requests (run tests only, no deploy)
gcloud builds triggers create github \
  --name "pull-request-ci" \
  --repo-name YOUR_REPO \
  --repo-owner YOUR_GITHUB_ORG \
  --pull-request-pattern "^main$" \
  --build-config cloudbuild-pr.yaml \
  --comment-control COMMENTS_ENABLED_FOR_EXTERNAL_CONTRIBUTORS_ONLY

For PRs, use a separate cloudbuild-pr.yaml that runs tests and a security scan but skips the deploy:

# cloudbuild-pr.yaml
steps:
  - name: node:20
    entrypoint: npm
    args: ['ci']

  - name: node:20
    entrypoint: npm
    args: ['run', 'test:coverage']

  - name: node:20
    entrypoint: npm
    args: ['run', 'lint']

  - name: gcr.io/cloud-builders/docker
    args: ['build', '-t', 'app:pr-$SHORT_SHA', '.']

  - name: aquasec/trivy
    args:
      - image
      - --exit-code
      - '1'
      - --severity
      - HIGH,CRITICAL
      - app:pr-$SHORT_SHA

options:
  logging: CLOUD_LOGGING_ONLY

Parallel Steps for Faster Pipelines

By default, steps run sequentially. Use waitFor to run independent steps in parallel:

steps:
  - name: node:20
    entrypoint: npm
    args: ['ci']
    id: install

  # These three run in parallel after install
  - name: node:20
    entrypoint: npm
    args: ['run', 'test:unit']
    id: unit-tests
    waitFor: ['install']

  - name: node:20
    entrypoint: npm
    args: ['run', 'lint']
    id: lint
    waitFor: ['install']

  - name: node:20
    entrypoint: npm
    args: ['run', 'type-check']
    id: type-check
    waitFor: ['install']

  # Build only after all checks pass
  - name: gcr.io/cloud-builders/docker
    args: ['build', '-t', 'gcr.io/$PROJECT_ID/app:$SHORT_SHA', '.']
    id: build
    waitFor: ['unit-tests', 'lint', 'type-check']

Using Cloud Build with Secret Manager

Never put secrets in cloudbuild.yaml. Reference them from Secret Manager:

steps:
  - name: node:20
    entrypoint: npm
    args: ['run', 'integration-test']
    secretEnv: ['DATABASE_URL', 'API_KEY']

availableSecrets:
  secretManager:
    - versionName: projects/$PROJECT_ID/secrets/database-url/versions/latest
      env: DATABASE_URL
    - versionName: projects/$PROJECT_ID/secrets/api-key/versions/latest
      env: API_KEY
# Grant Cloud Build SA access to secrets
gcloud secrets add-iam-policy-binding database-url \
  --member "serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" \
  --role roles/secretmanager.secretAccessor

Private Pools for Compliance

By default, Cloud Build runs on shared Google infrastructure. For compliance requirements (accessing private VPC resources, no public internet), use a private pool:

# Create a private pool in your VPC
gcloud builds worker-pools create prod-pool \
  --region us-central1 \
  --peered-network projects/YOUR_PROJECT_ID/global/networks/default \
  --worker-machine-type e2-standard-4 \
  --worker-disk-size 100

# Reference in cloudbuild.yaml
# options:
#   pool:
#     name: projects/YOUR_PROJECT_ID/locations/us-central1/workerPools/prod-pool

Private pools can reach resources in your VPC — Cloud SQL, private GKE API servers, internal services — without exposing them to the internet.

Build Notifications via Pub/Sub

# Cloud Build publishes build status to Pub/Sub by default
# Subscribe to get notified on failures

gcloud pubsub subscriptions create build-notifications \
  --topic cloud-builds \
  --push-endpoint https://your-webhook.example.com/cloud-build \
  --ack-deadline 60

# Or use Cloud Build Notifiers (pre-built)
# https://github.com/GoogleCloudPlatform/cloud-build-notifiers
# Available: Slack, email, GitHub commit status, HTTP

Build Caching

Cloud Build builds are stateless. Without caching, npm install or pip install runs from scratch every time:

steps:
  # Pull cache image (fails silently if not found)
  - name: gcr.io/cloud-builders/docker
    entrypoint: bash
    args:
      - -c
      - docker pull $_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO/$_SERVICE:cache || true

  - name: gcr.io/cloud-builders/docker
    args:
      - build
      - --cache-from
      - $_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO/$_SERVICE:cache
      - -t
      - $_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO/$_SERVICE:$SHORT_SHA
      - -t
      - $_REGION-docker.pkg.dev/$PROJECT_ID/$_REPO/$_SERVICE:cache
      - .

This pulls the previous image as a cache layer. Docker's layer caching then skips unchanged layers — npm install becomes fast when package-lock.json hasn't changed.

Pricing at a Glance

Machine TypevCPUsRAMCost/min
e2-medium (default)14GB$0.003
E2_HIGHCPU_888GB$0.016
E2_HIGHCPU_323232GB$0.064
N1_HIGHCPU_887.2GB$0.034

The first 120 build-minutes per day are free. A typical Node.js build on E2_HIGHCPU_8 takes 3-5 minutes and costs $0.05-0.08. For a team of 10 developers pushing 20 times a day, that's roughly $20-30/month — cheaper than a Jenkins server.

Share:

Was this article helpful?

Riku Tanaka
Riku Tanaka

SRE & Observability Engineer

If it's not measured, it doesn't exist. SLO-driven, metrics-obsessed, and the person who gets paged at 3 AM so you don't have to. Observability isn't optional.

Related Articles

Discussion