End-to-End CI/CD: GitHub Actions → Cloud Build → Cloud Run
Many teams use GitHub Actions for CI but struggle to integrate it cleanly with GCP deployments. This guide shows two approaches — pure GitHub Actions deployment and a hybrid model where GitHub Actions triggers Cloud Build — with secure Workload Identity Federation so you never store GCP service account keys.
GitHub Actions is where most development teams live. Cloud Build is Google's native CI system with deep GCP integration. Cloud Run is the easiest way to deploy containerized workloads on GCP. Getting all three working together cleanly requires solving two problems: how to authenticate GitHub Actions to GCP securely (without service account key files), and which tasks should run in GitHub Actions vs. Cloud Build.
This guide covers both approaches — using GitHub Actions to deploy directly to Cloud Run, and using GitHub Actions to trigger Cloud Build which handles the GCP side. We'll use Workload Identity Federation throughout, which eliminates the need for long-lived service account keys in GitHub secrets.
Setting Up Workload Identity Federation
Workload Identity Federation lets external systems (like GitHub Actions) authenticate to GCP using short-lived tokens instead of static key files. When a GitHub Actions workflow runs, it presents a JWT from GitHub's OIDC endpoint. GCP exchanges that JWT for a GCP access token, scoped to a specific service account.
Creating the Workload Identity Pool
# Enable required APIs
gcloud services enable iamcredentials.googleapis.com
gcloud services enable sts.googleapis.com
# Create the Workload Identity Pool
gcloud iam workload-identity-pools create github-actions-pool --location=global --display-name="GitHub Actions Pool" --description="Pool for GitHub Actions workflows"
# Create the OIDC Provider within the pool
gcloud iam workload-identity-pools providers create-oidc github-actions-provider --location=global --workload-identity-pool=github-actions-pool --display-name="GitHub Actions Provider" --issuer-uri="https://token.actions.githubusercontent.com" --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" --attribute-condition="assertion.repository=='my-org/my-repo'"
Creating the Service Account and Binding
# Create a service account for GitHub Actions to use
gcloud iam service-accounts create github-actions-sa --display-name="GitHub Actions Service Account" --project=my-project
# Get the pool resource name
POOL_ID=$(gcloud iam workload-identity-pools describe github-actions-pool --location=global --format='value(name)')
# Allow GitHub Actions to impersonate this service account
gcloud iam service-accounts add-iam-policy-binding github-actions-sa@my-project.iam.gserviceaccount.com --role=roles/iam.workloadIdentityUser --member="principalSet://iam.googleapis.com/${POOL_ID}/attribute.repository/my-org/my-repo"
# Grant the service account permission to deploy Cloud Run
gcloud projects add-iam-policy-binding my-project --member="serviceAccount:github-actions-sa@my-project.iam.gserviceaccount.com" --role="roles/run.admin"
# Grant permission to push to Artifact Registry
gcloud projects add-iam-policy-binding my-project --member="serviceAccount:github-actions-sa@my-project.iam.gserviceaccount.com" --role="roles/artifactregistry.writer"
# Grant permission to act as the Cloud Run service's service account
gcloud iam service-accounts add-iam-policy-binding cloud-run-sa@my-project.iam.gserviceaccount.com --member="serviceAccount:github-actions-sa@my-project.iam.gserviceaccount.com" --role="roles/iam.serviceAccountUser"
Get the Provider Resource Name for GitHub
# You'll need this for the GitHub Actions workflow
gcloud iam workload-identity-pools providers describe github-actions-provider --workload-identity-pool=github-actions-pool --location=global --format='value(name)'
# Output: projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
Approach 1: Pure GitHub Actions Deployment
For simpler setups, deploy directly from GitHub Actions without involving Cloud Build. This works well for Cloud Run services where you want GitHub Actions to own the full pipeline.
# .github/workflows/deploy.yml
name: Build and Deploy to Cloud Run
on:
push:
branches: [main]
env:
PROJECT_ID: my-project
REGION: europe-west4
SERVICE: payment-api
REGISTRY: europe-west4-docker.pkg.dev
REPOSITORY: my-app
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: pip install -r requirements-test.txt
- name: Run unit tests
run: pytest tests/unit/ -v --tb=short --junitxml=junit.xml
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: junit.xml
build-deploy:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for Workload Identity Federation
steps:
- uses: actions/checkout@v4
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider'
service_account: 'github-actions-sa@my-project.iam.gserviceaccount.com'
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Configure Docker
run: gcloud auth configure-docker ${{ env.REGISTRY }} --quiet
- name: Build Docker image
run: |
docker build --tag "${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}" --tag "${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:latest" --cache-from "${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:latest" .
- name: Push to Artifact Registry
run: |
docker push "${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}"
docker push "${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:latest"
- name: Deploy to Cloud Run
uses: google-github-actions/deploy-cloudrun@v2
with:
service: ${{ env.SERVICE }}
region: ${{ env.REGION }}
image: '${{ env.REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}'
flags: |
--service-account=cloud-run-sa@my-project.iam.gserviceaccount.com
--max-instances=20
--min-instances=1
--cpu=2
--memory=1Gi
--concurrency=80
--port=8080
- name: Post deployment URL
run: |
SERVICE_URL=$(gcloud run services describe ${{ env.SERVICE }} --region=${{ env.REGION }} --format='value(status.url)')
echo "Deployed to: $SERVICE_URL"
echo "SERVICE_URL=$SERVICE_URL" >> $GITHUB_ENV
Approach 2: GitHub Actions Triggers Cloud Build
For more complex workflows — multi-service deployments, GKE deployments, or when you want Cloud Build's native GCP integration — use GitHub Actions to trigger Cloud Build:
# .github/workflows/trigger-build.yml
name: Trigger Cloud Build
on:
push:
branches: [main]
jobs:
trigger:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider'
service_account: 'github-actions-sa@my-project.iam.gserviceaccount.com'
- name: Trigger Cloud Build
run: |
gcloud builds submit --no-source --config=- --region=europe-west4 <<'EOF'
steps:
- name: 'gcr.io/cloud-builders/git'
args: ['clone', '--branch=main', 'https://github.com/my-org/my-repo.git', '.']
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'europe-west4-docker.pkg.dev/$PROJECT_ID/my-app/api:$COMMIT_SHA', '.']
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'europe-west4-docker.pkg.dev/$PROJECT_ID/my-app/api:$COMMIT_SHA']
EOF
- name: Wait for and monitor build
run: |
# Get the latest build ID
BUILD_ID=$(gcloud builds list --region=europe-west4 --limit=1 --format='value(id)')
echo "Watching build: $BUILD_ID"
# Stream the build logs
gcloud builds log $BUILD_ID --region=europe-west4 --stream
Cloud Run Deployment with Traffic Splitting
For zero-downtime deployments to Cloud Run, use traffic splitting to gradually shift traffic to the new revision:
# Deploy new revision without sending traffic to it
gcloud run deploy payment-api --image=europe-west4-docker.pkg.dev/my-project/my-app/api:$COMMIT_SHA --region=europe-west4 --no-traffic --tag=canary
# Send 10% of traffic to the new revision
gcloud run services update-traffic payment-api --region=europe-west4 --to-tags=canary=10
# After validating metrics, shift to 100%
gcloud run services update-traffic payment-api --region=europe-west4 --to-latest
GKE Deployments from GitHub Actions
For GKE deployments, get cluster credentials and apply manifests:
- name: Get GKE credentials
uses: google-github-actions/get-gke-credentials@v2
with:
cluster_name: production
location: europe-west4
- name: Deploy to GKE
run: |
# Update the image in the deployment
kubectl set image deployment/payment-api api=europe-west4-docker.pkg.dev/my-project/my-app/api:${{ github.sha }} -n production
# Wait for rollout to complete
kubectl rollout status deployment/payment-api -n production --timeout=5m
# Verify pods are healthy
kubectl get pods -n production -l app=payment-api
Environment-Specific Deployments
Use GitHub Environments for deployment protection:
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging # Links to GitHub Environment settings
permissions:
contents: read
id-token: write
steps:
# ... deploy to staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requires manual approval in GitHub
permissions:
contents: read
id-token: write
steps:
# ... deploy to production
In GitHub repository settings, configure the production environment to require reviewers before any deployment. This creates a manual approval gate without needing Cloud Deploy.
Security Best Practices
Never store GCP credentials in GitHub Secrets: Workload Identity Federation eliminates this need entirely. If you inherited a repository with a GCP_SA_KEY secret, remove it and migrate to WIF.
Scope service account permissions narrowly: The github-actions-sa should only have the minimum permissions needed. For Cloud Run deployments: roles/run.admin + roles/artifactregistry.writer + roles/iam.serviceAccountUser (to use the Cloud Run service's SA).
Use repository-level attribute conditions: The attribute-condition="assertion.repository=='my-org/my-repo'" in the WIF provider ensures only your specific repo can authenticate. Without this, anyone in your organization could use the pool.
For container security scanning integrated into this pipeline, see our GKE security hardening guide. For the Cloud Build-native approach to CD with Cloud Deploy, see our GCP DevOps guide.