Deploying to Cloud Run with GitHub Actions and Workload Identity Federation
I set up a secure pipeline that builds my website into a container image, stores it in Artifact Registry, and deploys to Cloud Run generating a unique preview URL for each revision.
Why Workload Identity Federation?
Traditionally, authenticating GitHub Actions to Google Cloud required creating and storing service account keys as secrets. This approach has several drawbacks:
- Long-lived credentials that need rotation
- Risk of key exposure if not properly managed
- Additional operational overhead
Workload Identity Federation solves these problems by allowing GitHub Actions to authenticate using short-lived tokens instead of static keys. No credentials to manage, rotate, or worry about leaking.
How Authentication Works
Here's the authentication flow when GitHub Actions authenticates to GCP using Workload Identity Federation:
Architecture Overview
Our CI/CD pipeline does the following:
- Build on every branch - Docker images are built and tagged with the commit SHA
- Push to Artifact Registry - All images are stored for auditability
- Deploy to Cloud Run - Main branch deploys to production, other branches create preview URLs
Pipeline Workflow
The diagram below shows the complete CI/CD pipeline flow, including the conditional logic for main vs feature branch deployments:
Note: Enterprise applications typically separate Continuous Integration (CI) and Continuous Deployment (CD) by building once and promoting the same image across environments. For my personal site, combining build and deploy in one workflow is simpler and sufficient.
Prerequisites
Before setting up the pipeline, ensure you have:
- A Google Cloud project with billing enabled
- The gcloud CLI installed and authenticated
- A GitHub repository for your code
Enable Required APIs
These APIs must be enabled before setting up Workload Identity Federation:
# Enable required APIs
gcloud services enable iamcredentials.googleapis.com \
artifactregistry.googleapis.com \
run.googleapis.com \
--project=${PROJECT_ID}Important: The IAM Service Account Credentials API (iamcredentials.googleapis.com) is required for Workload Identity Federation. If this isn't enabled, you'll get authentication errors when GitHub Actions tries to push images.
Create Artifact Registry Repository
gcloud artifacts repositories create personal-website \
--repository-format=docker \
--location=us-west1 \
--project=${PROJECT_ID} \
--description="Container images for personal website"Setting Up Workload Identity Federation
1. Create a Workload Identity Pool
export PROJECT_ID="your-project-id"
export GITHUB_REPO="your-org/your-repo"
# Create the identity pool
gcloud iam workload-identity-pools create "github-pool" \
--project="${PROJECT_ID}" \
--location="global" \
--display-name="GitHub Actions Pool"2. Create the OIDC Provider
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
--project="${PROJECT_ID}" \
--location="global" \
--workload-identity-pool="github-pool" \
--display-name="GitHub Provider" \
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
--attribute-condition="assertion.repository_owner == '$(echo $GITHUB_REPO | cut -d'/' -f1)'" \
--issuer-uri="https://token.actions.githubusercontent.com"3. Create and Configure Service Account
# Create service account
gcloud iam service-accounts create github-actions \
--project="${PROJECT_ID}" \
--display-name="GitHub Actions"
# Grant Artifact Registry Writer permission
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:github-actions@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer"
# Grant Cloud Run Admin permission
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:github-actions@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/run.admin"
# Grant Service Account User permission
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:github-actions@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountUser"4. Allow GitHub to Impersonate Service Account
gcloud iam service-accounts add-iam-policy-binding \
"github-actions@${PROJECT_ID}.iam.gserviceaccount.com" \
--project="${PROJECT_ID}" \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/$(gcloud projects describe ${PROJECT_ID} --format='value(projectNumber)')/locations/global/workloadIdentityPools/github-pool/attribute.repository/${GITHUB_REPO}"GitHub Actions Workflow
The workflow builds on all branches but deploys differently based on the branch:
Main Branch Deployment
- Tagged with
:latest - Receives all production traffic
- Deployed to the main service URL
Feature Branch Deployment
- Tagged with commit SHA
- Deployed with
--no-trafficflag - Creates a tagged URL:
https://branch-name---service-name-hash.region.run.app - Perfect for testing before merging to main
Setting Up GitHub Secrets
Create a production environment in your GitHub repository and add these secrets:
WIF_PROVIDER- Full path to your Workload Identity ProviderWIF_SERVICE_ACCOUNT- Email of your service accountGCP_PROJECT_ID- Your Google Cloud project IDGCP_REGION- Region for Artifact Registry and Cloud Run (e.g., us-west1)
Key Workflow Features
Conditional Deployments
The workflow uses GitHub's conditional expressions to handle different branches:
if: github.ref == 'refs/heads/main' # Production deployment
if: github.ref != 'refs/heads/main' # Preview deploymentBranch Name Sanitization
Cloud Run tags must be lowercase alphanumeric with hyphens. The workflow sanitizes branch names automatically:
BRANCH_NAME=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9-]/-/g' | tr '[:upper:]' '[:lower:]')Benefits of This Approach
- Security - No long-lived credentials to manage
- Preview URLs - Test every branch before merging
- Automated - Deploys on every push
- Audit Trail - All images tagged with commit SHA
- Isolation - Branch deployments don't affect production
Common Issues and Solutions
Issue: "IAM Service Account Credentials API has not been used"
Solution: Enable the API with:
gcloud services enable iamcredentials.googleapis.com --project=${PROJECT_ID}Issue: "--no-traffic not supported when creating a new service"
Solution: The first deployment must come from the main branch. Cloud Run doesn't allow --no-traffic when creating a new service. After the initial deployment, branch previews will work correctly.
Conclusion
I've been really happy with this setup. Having preview URLs for every branch has made it so much easier to catch issues before publishing, and not having to worry about rotating service account keys is a huge relief. The initial setup took some trial and error (especially that IAM Credentials API gotcha!), but now that it's running, deployments just work.
If you're deploying containerized apps to GCP, I'd definitely recommend going this route. The security benefits alone make it worth the effort, and the preview URL workflow has genuinely improved how I work on this site.