GCP DevOps: Cloud Build, Cloud Deploy, and Artifact Registry in Practice

Google Cloud's native CI/CD stack — Cloud Build for continuous integration, Artifact Registry for container and artifact management, and Cloud Deploy for progressive delivery — integrates deeply with GKE and Cloud Run. This guide shows you how to wire them together into a production-grade delivery pipeline.

Building a CI/CD pipeline on Google Cloud used to mean stitching together Jenkins, Nexus, Spinnaker, and a half-dozen shell scripts. Today, the native GCP stack — Cloud Build, Cloud Deploy, and Artifact Registry — covers the full delivery pipeline without reaching for third-party tools. These services are fully managed, deeply integrated with GKE and Cloud Run, and significantly cheaper to operate than self-hosted alternatives.

This guide walks through a realistic production setup: Cloud Build running tests and building container images, Artifact Registry storing images with vulnerability scanning, and Cloud Deploy managing progressive rollouts to staging and production GKE clusters.

Architecture Overview

Source Code (GitHub/GitLab/Cloud Source Repos)
        ↓ push / PR trigger
Cloud Build (CI)
  - Unit tests
  - Docker build
  - Push to Artifact Registry
        ↓ successful build
Artifact Registry
  - Image storage
  - Vulnerability scanning
  - SBOM generation
        ↓ release created
Cloud Deploy (CD)
  - Delivery pipeline
  - Staging deployment → approval gate → production deployment
        ↓ deploys to
GKE Clusters (staging, production)

Setting Up Artifact Registry

Artifact Registry is the successor to Container Registry. It supports Docker images, Helm charts, Maven, npm, Python packages, and more — all in one service with IAM-controlled access.

# Enable the API
gcloud services enable artifactregistry.googleapis.com

# Create a Docker repository
gcloud artifacts repositories create my-app   --repository-format=docker   --location=europe-west4   --description="Production Docker images for my-app"

# Configure Docker to use Artifact Registry
gcloud auth configure-docker europe-west4-docker.pkg.dev

# Verify
gcloud artifacts repositories list --location=europe-west4

Enable Vulnerability Scanning

# Enable Container Analysis and Artifact Registry scanning
gcloud services enable containeranalysis.googleapis.com
gcloud services enable ondemandscanning.googleapis.com

# Enable automatic vulnerability scanning on the repository
gcloud artifacts repositories update my-app   --location=europe-west4   --update-labels=scan=enabled

# View vulnerability scan results after pushing an image
gcloud artifacts docker images list   europe-west4-docker.pkg.dev/my-project/my-app   --show-occurrences   --occurrence-filter="kind=VULNERABILITY"

Terraform for Artifact Registry

resource "google_artifact_registry_repository" "app" {
  location      = "europe-west4"
  repository_id = "my-app"
  format        = "DOCKER"
  description   = "Production Docker images"

  vulnerability_scanning_config {
    enablement_config = "INHERITED"  # Uses project-level setting
  }

  cleanup_policies {
    id     = "keep-last-10"
    action = "KEEP"
    most_recent_versions {
      keep_count = 10
    }
  }

  cleanup_policies {
    id     = "delete-old"
    action = "DELETE"
    condition {
      older_than = "2592000s"  # 30 days
    }
  }
}

# Grant GKE node pool permission to pull images
resource "google_artifact_registry_repository_iam_member" "gke_pull" {
  location   = google_artifact_registry_repository.app.location
  repository = google_artifact_registry_repository.app.name
  role       = "roles/artifactregistry.reader"
  member     = "serviceAccount:${google_service_account.gke_nodes.email}"
}

Cloud Build: CI Pipeline Configuration

Cloud Build runs builds in containers. Each step in your cloudbuild.yaml is a container that executes a command. Steps share a workspace via /workspace.

Basic cloudbuild.yaml Structure

# cloudbuild.yaml
steps:
  # Step 1: Run unit tests
  - name: 'python:3.11-slim'
    id: unit-tests
    entrypoint: bash
    args:
      - -c
      - |
        pip install -r requirements-test.txt --quiet
        python -m pytest tests/unit/ -v --tb=short

  # Step 2: Build Docker image
  - name: 'gcr.io/cloud-builders/docker'
    id: build-image
    args:
      - build
      - -t
      - '$_AR_HOSTNAME/$PROJECT_ID/$_REPOSITORY/$_IMAGE:$COMMIT_SHA'
      - -t
      - '$_AR_HOSTNAME/$PROJECT_ID/$_REPOSITORY/$_IMAGE:latest'
      - --cache-from
      - '$_AR_HOSTNAME/$PROJECT_ID/$_REPOSITORY/$_IMAGE:latest'
      - .
    waitFor: [unit-tests]

  # Step 3: Push to Artifact Registry
  - name: 'gcr.io/cloud-builders/docker'
    id: push-image
    args:
      - push
      - --all-tags
      - '$_AR_HOSTNAME/$PROJECT_ID/$_REPOSITORY/$_IMAGE'
    waitFor: [build-image]

  # Step 4: Create Cloud Deploy release
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk:slim'
    id: create-release
    entrypoint: gcloud
    args:
      - deploy
      - releases
      - create
      - 'release-$COMMIT_SHA'
      - --project=$PROJECT_ID
      - --region=$_REGION
      - --delivery-pipeline=$_PIPELINE
      - --images=app=$_AR_HOSTNAME/$PROJECT_ID/$_REPOSITORY/$_IMAGE:$COMMIT_SHA
    waitFor: [push-image]

substitutions:
  _AR_HOSTNAME: europe-west4-docker.pkg.dev
  _REPOSITORY: my-app
  _IMAGE: api
  _REGION: europe-west4
  _PIPELINE: my-app-delivery-pipeline

options:
  logging: CLOUD_LOGGING_ONLY
  machineType: E2_HIGHCPU_8

# Artifact Registry images built in this pipeline
images:
  - '$_AR_HOSTNAME/$PROJECT_ID/$_REPOSITORY/$_IMAGE:$COMMIT_SHA'

Cloud Build Triggers

# Create a trigger for main branch pushes
gcloud builds triggers create github   --name="main-push-trigger"   --repository-owner=my-org   --repository-name=my-app   --branch-pattern="^main$"   --build-config=cloudbuild.yaml   --region=europe-west4

# Create a trigger for pull requests (run tests only, don't deploy)
gcloud builds triggers create github   --name="pr-test-trigger"   --repository-owner=my-org   --repository-name=my-app   --pull-request-pattern="^main$"   --build-config=cloudbuild-pr.yaml   --region=europe-west4

Cloud Build IAM Setup

The Cloud Build service account needs specific permissions:

# Get the Cloud Build service account
PROJECT_NUMBER=$(gcloud projects describe my-project --format='value(projectNumber)')
BUILD_SA="${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com"

# Grant Artifact Registry write permission
gcloud projects add-iam-policy-binding my-project   --member="serviceAccount:${BUILD_SA}"   --role="roles/artifactregistry.writer"

# Grant Cloud Deploy release creator permission
gcloud projects add-iam-policy-binding my-project   --member="serviceAccount:${BUILD_SA}"   --role="roles/clouddeploy.releaser"

# Grant GKE deploy permission (for Cloud Deploy)
gcloud projects add-iam-policy-binding my-project   --member="serviceAccount:${BUILD_SA}"   --role="roles/container.developer"

Cloud Deploy: Progressive Delivery

Cloud Deploy manages the promotion of releases through delivery pipelines. It integrates with GKE (via Helm or kubectl), Cloud Run, and Anthos clusters.

Delivery Pipeline Definition

# clouddeploy.yaml
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
  name: my-app-delivery-pipeline
  annotations:
    description: "My app delivery pipeline"
serialPipeline:
  stages:
  - targetId: staging
    profiles: [staging]
    strategy:
      standard:
        verify: true  # Run verification tests after deploy
  - targetId: production
    profiles: [production]
    strategy:
      canary:
        runtimeConfig:
          kubernetes:
            gatewayServiceMesh:
              httpRoute: my-app-route
              service: my-app-svc
              deployment: my-app
        canaryDeployment:
          percentages: [25, 50]
          verify: true
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
  name: staging
spec:
  gke:
    cluster: projects/my-project/locations/europe-west4/clusters/staging-cluster
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
  name: production
spec:
  requireApproval: true  # Manual approval gate before production
  gke:
    cluster: projects/my-project/locations/europe-west4/clusters/prod-cluster
# Apply the delivery pipeline configuration
gcloud deploy apply   --file=clouddeploy.yaml   --region=europe-west4   --project=my-project

Skaffold for Kubernetes Manifests

Cloud Deploy uses Skaffold to render and apply Kubernetes manifests. Your repository needs a skaffold.yaml:

# skaffold.yaml
apiVersion: skaffold/v4beta7
kind: Config
metadata:
  name: my-app
build:
  artifacts:
  - image: app
    docker:
      dockerfile: Dockerfile

profiles:
- name: staging
  deploy:
    kubectl:
      manifests:
        - k8s/staging/*.yaml
- name: production
  deploy:
    helm:
      releases:
      - name: my-app
        chartPath: charts/my-app
        valuesFiles:
          - charts/my-app/values-prod.yaml
        setValues:
          image.tag: "{{.IMAGE_TAG}}"

Approving Production Releases

# List pending rollouts awaiting approval
gcloud deploy rollouts list   --delivery-pipeline=my-app-delivery-pipeline   --region=europe-west4   --filter="approvalState=NEEDS_APPROVAL"

# Approve a specific rollout
gcloud deploy rollouts approve   projects/my-project/locations/europe-west4/deliveryPipelines/my-app-delivery-pipeline/releases/release-abc123/rollouts/rollout-to-production-001   --region=europe-west4

# Or reject it
gcloud deploy rollouts reject   projects/my-project/locations/europe-west4/deliveryPipelines/my-app-delivery-pipeline/releases/release-abc123/rollouts/rollout-to-production-001   --region=europe-west4

Rollback Procedures

# List recent releases for the pipeline
gcloud deploy releases list   --delivery-pipeline=my-app-delivery-pipeline   --region=europe-west4

# Promote a previous stable release to roll back
gcloud deploy releases promote   --release=release-abc111   --delivery-pipeline=my-app-delivery-pipeline   --region=europe-west4   --to-target=production

Notification and Audit Integration

Cloud Deploy integrates with Pub/Sub for notifications on deployment events:

# Create a Pub/Sub topic for deploy events
gcloud pubsub topics create clouddeploy-events

# Cloud Deploy automatically publishes to this topic when:
# - A release is created
# - A rollout starts or completes
# - Approval is needed
# - A deployment fails

# Subscribe a Cloud Function to notify Slack on failures
gcloud pubsub subscriptions create deploy-slack-notify   --topic=clouddeploy-events   --push-endpoint=https://europe-west4-my-project.cloudfunctions.net/deploy-notify

Terraform for the Complete Stack

resource "google_cloudbuild_trigger" "main_push" {
  name     = "main-push-trigger"
  location = "europe-west4"
  project  = var.project_id

  github {
    owner = var.github_owner
    name  = var.github_repo
    push {
      branch = "^main$"
    }
  }

  filename = "cloudbuild.yaml"

  substitutions = {
    _AR_HOSTNAME = "${var.region}-docker.pkg.dev"
    _REPOSITORY  = google_artifact_registry_repository.app.repository_id
    _IMAGE       = "api"
    _REGION      = var.region
    _PIPELINE    = "my-app-delivery-pipeline"
  }
}

Cost Considerations

Cloud Build is priced per minute of build time. Optimize costs by:

  1. Using build caches: Artifact Registry supports Docker layer caching. Pull the latest image as a cache source (as shown in the cloudbuild.yaml above)
  2. Choosing appropriate machine types: Default (e2-medium equivalent) is cheapest. Only use E2_HIGHCPU_8 or N1_HIGHCPU_8 for CPU-intensive builds like large Go or C++ compilation
  3. Using private pools for builds that need access to private VPC resources (avoids Cloud NAT charges)

Cloud Deploy has no charge for the service itself — you only pay for the underlying GKE or Cloud Run resources during deployment.

For the GKE clusters that receive these deployments, see our GKE production guide. For security around image signing and admission control, see our GKE security hardening guide.