GCP for DevOps — Container CI/CD
Container CI/CD
This page ties together Cloud Build, Artifact Registry, and Cloud Deploy into a complete container CI/CD pipeline for GKE and Cloud Run.
End-to-End Pipeline
┌──────────────────────────────────────────────────────────┐
│ Container CI/CD Pipeline │
├──────────────────────────────────────────────────────────┤
│ │
│ 1. Push Code 2. Cloud Build 3. Artifact Reg │
│ ┌──────┐ ┌────────────┐ ┌────────────┐ │
│ │ Git ├────────►│ Build ├──────►│ Docker │ │
│ │ Push │ trigger│ Test │ push │ Image │ │
│ └──────┘ │ Scan │ └─────┬──────┘ │
│ └────────────┘ │ │
│ │ │
│ 4. Cloud Deploy 5. GKE / Cloud Run │ │
│ ┌────────────┐ ┌────────────┐ │ │
│ │ Release │◄───┤ Deploy │◄────────────┘ │
│ │ Promote │ │ Verify │ │
│ │ Approve │ │ Monitor │ │
│ └────────────┘ └────────────┘ │
└──────────────────────────────────────────────────────────┘
Complete cloudbuild.yaml for GKE
steps:
# Run unit tests
- name: 'golang:1.22'
entrypoint: 'go'
args: ['test', './...']
id: 'test'
# Build Docker image
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- 'us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/api:$SHORT_SHA'
- '-t'
- 'us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/api:latest'
- '--cache-from'
- 'us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/api:latest'
- '.'
id: 'build'
waitFor: ['test']
# Push to Artifact Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', '--all-tags', 'us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/api']
id: 'push'
waitFor: ['build']
# Create Cloud Deploy release
- name: 'gcr.io/cloud-builders/gcloud'
args:
- 'deploy'
- 'releases'
- 'create'
- 'release-$SHORT_SHA'
- '--delivery-pipeline=api-pipeline'
- '--region=us-central1'
- '--images=api=us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/api:$SHORT_SHA'
id: 'release'
waitFor: ['push']
images:
- 'us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/api:$SHORT_SHA'
- 'us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/api:latest'
options:
machineType: 'E2_HIGHCPU_8'
Complete cloudbuild.yaml for Cloud Run
steps:
# Build
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- 'us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/web:$SHORT_SHA'
- '.'
id: 'build'
# Push
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/web:$SHORT_SHA']
id: 'push'
waitFor: ['build']
# Deploy to Cloud Run
- name: 'gcr.io/cloud-builders/gcloud'
args:
- 'run'
- 'deploy'
- 'web'
- '--image=us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/web:$SHORT_SHA'
- '--region=us-central1'
- '--platform=managed'
- '--min-instances=1'
- '--max-instances=50'
waitFor: ['push']
Binary Authorization
Enforce that only signed, verified images are deployed to GKE.
# Enable Binary Authorization
$ gcloud services enable binaryauthorization.googleapis.com
# Create an attestor
$ gcloud container binauthz attestors create build-attestor \
--attestation-authority-note=build-note \
--attestation-authority-note-project=my-project
# Set the default policy to require attestation
$ gcloud container binauthz policy export > policy.yaml
# policy.yaml — require attestation
admissionWhitelistPatterns:
- namePattern: "gcr.io/google_containers/*"
defaultAdmissionRule:
evaluationMode: REQUIRE_ATTESTATION
enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG
requireAttestationsBy:
- projects/my-project/attestors/build-attestor
$ gcloud container binauthz policy import policy.yaml
Vulnerability Scanning in CI/CD
# Add scanning step to cloudbuild.yaml
steps:
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', '$_IMAGE', '.']
- name: 'gcr.io/cloud-builders/docker'
args: ['push', '$_IMAGE']
# Wait for scan results
- name: 'gcr.io/cloud-builders/gcloud'
entrypoint: 'bash'
args:
- '-c'
- |
# Wait for vulnerability scan to complete
sleep 30
VULNS=$(gcloud artifacts docker images describe $_IMAGE \
--show-package-vulnerability \
--format='value(package_vulnerability_summary.severity_count)' 2>/dev/null)
echo "Vulnerabilities: $VULNS"
# Fail build if critical vulnerabilities found
CRITICAL=$(echo "$VULNS" | grep -o 'CRITICAL:[0-9]*' | cut -d: -f2)
if [ "$CRITICAL" -gt "0" ] 2>/dev/null; then
echo "CRITICAL vulnerabilities found! Failing build."
exit 1
fi
substitutions:
_IMAGE: 'us-central1-docker.pkg.dev/${PROJECT_ID}/my-repo/api:${SHORT_SHA}'
Multi-Architecture Builds
# Build for both amd64 and arm64
steps:
- name: 'gcr.io/cloud-builders/docker'
args: ['buildx', 'create', '--use']
- name: 'gcr.io/cloud-builders/docker'
args:
- 'buildx'
- 'build'
- '--platform=linux/amd64,linux/arm64'
- '-t'
- 'us-central1-docker.pkg.dev/$PROJECT_ID/my-repo/api:$SHORT_SHA'
- '--push'
- '.'
Common Mistakes
| Mistake | Why It’s Wrong | Fix |
|---|---|---|
| No Docker layer caching | Slow builds | Use --cache-from with latest tag |
| Skipping vulnerability scan | Security risk | Add scanning step in pipeline |
| No Binary Authorization | Untrusted images deployed | Enforce attestation policy |
| Building in production cluster | Resource contention | Use Cloud Build (off-cluster) |
| No image tag pinning | Inconsistent deploys | Use $SHORT_SHA, never just latest |
Quick Reference
| Pattern | Tool |
|---|---|
| Build container | Cloud Build + cloudbuild.yaml |
| Store image | Artifact Registry |
| Scan image | Container Analysis (automatic) |
| Deploy to GKE | Cloud Deploy or kubectl in Cloud Build |
| Deploy to Cloud Run | gcloud run deploy in Cloud Build |
| Enforce trusted images | Binary Authorization |
Next → Container Exercises