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:

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:

  1. Build on every branch - Docker images are built and tagged with the commit SHA
  2. Push to Artifact Registry - All images are stored for auditability
  3. 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:

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

Feature Branch Deployment

Setting Up GitHub Secrets

Create a production environment in your GitHub repository and add these secrets:

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 deployment

Branch 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

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.