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:
- Using build caches: Artifact Registry supports Docker layer caching. Pull the latest image as a cache source (as shown in the cloudbuild.yaml above)
- 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
- 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.