GitHub Actions for GCP: Deploy to Google Cloud Without Keys
GitHub Actions can build, test, and deploy to Google Cloud directly from your repository. The recommended authentication method is Workload Identity Federation: a keyless approach where GCP accepts a short-lived token issued by GitHub instead of a stored credential. This page explains how the full setup works, shows real workflow examples for Cloud Run and Terraform, and covers when GitHub Actions is the right choice versus GCP-native tools like Cloud Build.
Simple explanation
If you are new to any of these pieces, here is the plain-language version.
GitHub Actions is GitHub’s built-in automation system. You write a YAML file that describes a sequence of steps: install dependencies, run tests, build a Docker image, deploy a service. GitHub runs those steps automatically when something happens in your repository, like a push or a pull request.
Google Cloud Platform (GCP) is where your application actually runs. You might be deploying a container to Cloud Run, pushing an image to Artifact Registry, or applying Terraform changes to your infrastructure.
Authentication is the bridge between the two. When a GitHub Actions workflow needs to call GCP APIs — to deploy a service, push an image, or update infrastructure — it needs to prove to GCP that it is authorised to do so.
The old approach was a service account key: download a JSON file from GCP, store it as a GitHub secret, use it in your workflow. It works, but it creates a credential that never expires, must be rotated manually, and can leak through logs, PR descriptions, or a compromised fork. Service account keys are risky even when handled carefully. Do not start new workflows this way.
The modern answer is Workload Identity Federation. GitHub issues each workflow run a short-lived token proving which repository and branch triggered it. GCP is configured to trust those tokens. The workflow exchanges the GitHub token for a GCP access token valid for one hour, scoped to a specific service account. No credential file exists anywhere. When the workflow ends, the token expires automatically.
When GitHub Actions is the right choice for GCP
- Your team is already centred on GitHub for code review and pull requests
- You want CI (tests, linting, security scans) and CD (deployment) in the same place
- You are deploying to Cloud Run and want a straightforward path from push to production
- You are running Terraform and want plan output visible on pull requests
- Your team is small and does not want to adopt additional delivery tooling immediately
- You need builds to run inside a private VPC (Cloud Build supports this natively)
- You want structured promotion pipelines across environments with GCP-native approval gates (Cloud Deploy is designed for this)
- Your organisation manages many services with complex release coordination across GCP projects
- Your team is not primarily on GitHub (GitLab, Bitbucket, or other platforms work better with Cloud Build triggers)
A common and sensible hybrid: GitHub Actions runs tests and PR checks because it integrates tightly with GitHub’s review flow, and Cloud Build or Cloud Deploy handles the actual deployment and environment promotion. This page focuses on the GitHub Actions side. See Cloud Build Overview and Cloud Deploy Overview for the GCP-native side.
How it works end to end
Here is the full sequence from a code push to a running deployment:
- Developer pushes code to GitHub. This triggers a GitHub Actions workflow defined in
.github/workflows/deploy.yml. - GitHub starts a runner. A fresh virtual machine starts. The job’s
permissionsblock includesid-token: write. - GitHub issues an OIDC token. Because the job requested it, GitHub’s infrastructure generates a short-lived JSON Web Token (JWT) that encodes facts about the workflow: which repository triggered it, which branch, which environment. This token is cryptographically signed by GitHub.
- The
google-github-actions/authaction sends the token to GCP. GCP’s Security Token Service (STS) receives the GitHub token and validates it against the Workload Identity Provider you configured, which points to GitHub’s OIDC issuer. - GCP checks the IAM binding. The binding specifies which GitHub repository (and optionally which branch) is allowed to impersonate which service account. If the token matches, GCP allows the exchange.
- GCP issues a short-lived access token. The runner now has credentials that behave exactly like the specified service account, valid for one hour.
- The remaining workflow steps use those credentials. Pushing an image to Artifact Registry, deploying to Cloud Run, running
gcloudcommands. All of it uses the exchanged token. The service account only needs the roles required for those specific tasks. - The token expires. When the workflow finishes (or after one hour), the token is automatically invalid. Nothing to revoke.
Think of WIF like a hotel key card system. GitHub is your employer: they give you a badge (the OIDC token) proving who you are and which team you work on. The hotel (GCP) has a prior agreement to accept those employer badges. The front desk verifies the badge and hands you a room key (the access token) valid only for that stay. You never carry a permanent key. When you check out, the key stops working automatically.
Setting up Workload Identity Federation
You create three resources in GCP, once per project. After that, any workflow in the configured repository can authenticate using the same setup.
What you are creating
- Workload Identity Pool: a container that groups external identity providers. Think of it as a namespace for trust relationships.
- OIDC Provider: tells GCP to trust tokens issued by GitHub’s OIDC endpoint, and defines how to map GitHub token claims (repository, branch, etc.) to GCP attributes.
- IAM Binding: grants a specific GitHub repository (and optionally branch) the
roles/iam.workloadIdentityUserrole on a service account. This is the permission that allows the token exchange.
GCP uses two different identifiers. The project ID is the human-readable string you chose, like my-app-prod. The project number is a numeric identifier GCP assigned, like 123456789. The workload_identity_provider path in your workflow YAML requires the numeric project number. Using the project ID is the single most common WIF misconfiguration and the error is not always obvious. Get the number with:
gcloud projects describe my-app-prod —format=‘value(projectNumber)‘
Setup commands
PROJECT_ID="my-app-prod"
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
POOL_NAME="github-pool"
PROVIDER_NAME="github-provider"
GITHUB_ORG="your-org"
GITHUB_REPO="your-repo"
SERVICE_ACCOUNT="ci-deployer"
# Step 1: Create the Workload Identity Pool
gcloud iam workload-identity-pools create $POOL_NAME \
--project=$PROJECT_ID \
--location=global \
--display-name="GitHub Actions Pool"
# Step 2: Create the OIDC provider pointing at GitHub's issuer
gcloud iam workload-identity-pools providers create-oidc $PROVIDER_NAME \
--project=$PROJECT_ID \
--location=global \
--workload-identity-pool=$POOL_NAME \
--display-name="GitHub Provider" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner,attribute.ref=assertion.ref"
# Step 3: Bind the specific GitHub repository to the service account
# This allows workflows in that repository to impersonate the service account
gcloud iam service-accounts add-iam-policy-binding \
${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com \
--project=$PROJECT_ID \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_NAME}/attribute.repository/${GITHUB_ORG}/${GITHUB_REPO}"The binding above allows any workflow in the repository to authenticate. For production deployments, restrict to a specific branch by changing the —member value to use attribute.ref instead:
“principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_NAME}/attribute.ref/refs/heads/main”
This prevents a compromised feature branch from authenticating with the production service account.
To read more about how this token exchange works, see Workload Identity Federation. For an explanation of the risks you are avoiding, see Why Service Account Keys Are Dangerous.
Complete GitHub Actions workflow: Cloud Run deployment
This workflow runs on every push to main. It checks out the code, authenticates to GCP using WIF, builds and pushes a Docker image to Artifact Registry, then deploys the new image to Cloud Run.
The permissions block is required. Without id-token: write, GitHub will not issue an OIDC token and the auth step will fail. This is the most common first-time mistake.
name: Deploy to Cloud Run
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
# Required: id-token:write lets GitHub issue an OIDC token for WIF
permissions:
contents: read
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v4
# Authenticate to GCP using Workload Identity Federation
# workload_identity_provider uses the numeric project NUMBER, not project ID
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
service_account: 'ci-deployer@my-app-prod.iam.gserviceaccount.com'
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
# Configure Docker to push to Artifact Registry
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker europe-west2-docker.pkg.dev
# Build and push using the git commit SHA as the image tag
# This ensures every deployment is traceable to a specific commit
- name: Build and push Docker image
run: |
IMAGE="europe-west2-docker.pkg.dev/my-app-prod/api/api:${{ github.sha }}"
docker build -t $IMAGE .
docker push $IMAGE
# Deploy the new image to Cloud Run
- name: Deploy to Cloud Run
uses: google-github-actions/deploy-cloudrun@v2
with:
service: api-service
region: europe-west2
image: europe-west2-docker.pkg.dev/my-app-prod/api/api:${{ github.sha }}Tagging images with github.sha ties every deployed container back to the exact commit that built it. If something breaks in production, you know exactly which change caused it and can roll back to the previous SHA tag. Avoid the :latest tag in production pipelines — it makes it impossible to tell what is actually running. For more on image lifecycle management, see Artifact Registry Best Practices.
For a deeper look at the full Cloud Run deployment pipeline, see CI/CD Pipelines for Cloud Run.
Production deployments with approval gates
If you want a human to approve before a workflow deploys to production, use GitHub Environments with required reviewers. Define a production environment in your repository settings, add required reviewers, then reference it in the job:
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment: production # Job pauses here until a required reviewer approves
permissions:
contents: read
id-token: write
steps:
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
service_account: 'ci-deployer@my-app-prod.iam.gserviceaccount.com'This is a lightweight approval gate that requires no extra GCP tooling. For more structured multi-stage promotion pipelines with GCP managing the promotion state, Cloud Deploy is built for that. See also Managing Environments in CI/CD and Dev vs Staging vs Production.
Running Terraform from GitHub Actions
Terraform and WIF work naturally together. After the google-github-actions/auth step, Terraform picks up the exchanged credentials via Application Default Credentials (ADC) automatically, with no extra environment variables or configuration needed.
The standard pattern: run terraform plan on pull requests so reviewers can see what infrastructure changes are coming, then run terraform apply on merge to main.
name: Terraform
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
pull-requests: write # Needed to post plan output as a PR comment
steps:
- uses: actions/checkout@v4
- id: auth
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
service_account: 'ci-deployer@my-app-prod.iam.gserviceaccount.com'
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.0"
- name: Terraform Init
working-directory: environments/prod
run: terraform init
# On pull requests: show the plan but don't apply
- name: Terraform Plan
if: github.event_name == 'pull_request'
working-directory: environments/prod
run: terraform plan -no-color
# On merge to main: apply the changes
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
working-directory: environments/prod
run: terraform apply -auto-approveOnce the auth step runs, Terraform finds the exchanged credentials through Application Default Credentials without any extra setup. You do not need to set GOOGLE_CREDENTIALS or point Terraform at a key file. The auth step does all the work.
For more on Terraform and GCP, including how to structure your code across environments, see Terraform for Google Cloud and Terraform State Management.
Google-provided GitHub Actions
Google maintains a set of official GitHub Actions that wrap common GCP operations. Using them is preferable to writing raw gcloud commands in run steps because they handle common edge cases, are actively maintained, and often provide cleaner output:
google-github-actions/auth@v2: authenticate to GCP via WIF or (not recommended) a service account keygoogle-github-actions/setup-gcloud@v2: install and configure thegcloudCLI on the runnergoogle-github-actions/deploy-cloudrun@v2: deploy a container image to a Cloud Run servicegoogle-github-actions/get-gke-credentials@v2: configurekubectlto connect to a GKE cluster
GitHub Actions vs Cloud Build vs Cloud Deploy
All three can ship code to GCP. They are designed for different parts of the pipeline and for different team contexts.
| GitHub Actions | Cloud Build | Cloud Deploy | |
|---|---|---|---|
| Where it runs | GitHub-hosted (or self-hosted) runners | GCP-managed build environment | GCP-managed delivery service |
| Primary strength | CI + CD tightly integrated with GitHub | GCP-native builds, VPC support, Binary Authorization | Structured promotion pipelines across environments |
| Trigger source | GitHub events (push, PR, schedule) | Source code push, manual, GitHub/GitLab triggers | Delivery pipeline progression |
| Approval gates | GitHub Environments + required reviewers | Build approval step | Cloud Deploy release promotion approvals |
| Best for | Teams centred on GitHub, Cloud Run deployments, Terraform workflows | GCP-native teams, private network builds, regulated workloads | Multi-environment promotion, GKE/Cloud Run at scale |
| Not ideal for | Complex multi-stage GCP promotions, private VPC builds | Teams who want everything in GitHub | Simple one-step deployments |
GitHub Actions is like your workshop bench: where you do the detailed work, run tests, check quality. Cloud Build is like a dedicated factory floor: purpose-built for building and packaging reliably at scale. Cloud Deploy is like the logistics system that moves finished goods from factory to warehouse to store: it tracks what is where, manages approvals between stops, and handles rollbacks. Most teams benefit from mixing all three rather than trying to do everything in one.
Common hybrid pattern
Many teams use GitHub Actions for tests, linting, and PR checks (where GitHub integration is valuable), then trigger a Cloud Build build or a Cloud Deploy release for the actual deployment. This gives you GitHub-native developer experience for review and testing, with GCP-native tooling for release management.
See Deploying with Cloud Build for the Cloud Build side of this pattern.
Security best practices
GitHub Actions workflows run with real credentials. A misconfigured workflow can be exploited to access or modify production infrastructure. These practices reduce that risk significantly.
Service account keys are permanent credentials that can be leaked via logs, PR descriptions, or compromised forks. WIF tokens are scoped to a single workflow run and expire automatically. If you have existing workflows using stored keys, migrating to WIF is worth doing now rather than later.
Apply least privilege to the deployment service account
The ci-deployer service account should only have the roles it actually needs. For a Cloud Run deployment, that means roles/run.developer and roles/artifactregistry.writer, not roles/owner or roles/editor. For Terraform, grant specific roles per resource type. See Principle of Least Privilege and IAM Roles Explained.
Use separate service accounts for different environments
A ci-deployer-staging service account and a separate ci-deployer-prod service account ensure that a workflow running in a staging context cannot accidentally or maliciously affect production. Pair each with its own WIF binding that restricts the repository, branch, or GitHub Environment.
Restrict WIF bindings by branch for production
Do not let the production deployment service account be impersonated by any branch. Use attribute.ref=refs/heads/main in the IAM binding to restrict production deployments to the main branch only. Feature branches should not have production access. See Service Account Impersonation for more on how binding restrictions work.
Protect production with GitHub Environments
Define a production environment in your repository settings with required reviewers. The deployment job pauses for human approval before continuing. This is especially important when using auto-approve in Terraform apply steps.
Store non-GCP secrets in GitHub Secrets
For third-party credentials (database passwords, API keys for external services) that cannot use WIF, use GitHub Secrets. Never hardcode them in workflow YAML. See Secrets in CI/CD Pipelines for patterns that keep secrets out of build logs.
Audit IAM bindings and workflow permissions regularly
Review service account roles periodically to check for privilege creep. Check that WIF bindings are still scoped correctly, especially after repository renames or team changes. Cloud Audit Logs can help identify unexpected API calls from CI service accounts.
Common mistakes
Forgetting
id-token: writein the job’s permissions block. Without this permission, GitHub does not issue an OIDC token. The auth action fails, usually with a vague error. Thepermissionsblock must be at the job level, not the workflow level, if you want it to apply only to specific jobs.Using the project ID instead of the project number in the provider path. The
workload_identity_providervalue must use the numeric project number (e.g.,123456789), not the string project ID (e.g.,my-app-prod). The error this causes is not always obvious.Binding WIF to
attribute.repository_ownerinstead ofattribute.repository. Using the repository owner alone allows any repository in your organisation to authenticate against the service account. Always bind to the specific repository unless you have a deliberate reason to share access.Giving the deployment service account overly broad roles.
roles/editororroles/ownergives far more access than a deployment needs. Audit the roles on your CI service accounts and remove anything not required.Not restricting production deployments by branch or environment. If any branch can trigger a production deployment, a compromised feature branch or a confused developer can push to production accidentally. Use
attribute.refin the WIF binding, GitHub Environments with required reviewers, or both.Mixing up GitHub Environments with GCP environment concepts. A GitHub Environment named
productioncontrols approval gates in GitHub. It is separate from GCP projects or Cloud Deploy targets. You still need to ensure the right GCP project and service account are used in the corresponding workflow job.Running
terraform applywithout a plan review step on PRs. Applying infrastructure changes without visible plan output in the pull request means reviewers cannot see what is actually changing. Always runterraform planon PRs and post the output as a comment before merging.
Real-world use cases
Deploying a Cloud Run API on every merge to main
The most common pattern. Tests run on the pull request. On merge to main, the workflow builds the Docker image, pushes it to Artifact Registry, and deploys it to Cloud Run. The deployment service account needs roles/run.developer and roles/artifactregistry.writer. See the full workflow example above.
Terraform plan on PRs, apply on merge
The Terraform workflow posts plan output to the pull request as a comment, giving reviewers visibility into infrastructure changes. On merge to main, apply runs automatically. The Terraform service account needs roles matching the resources it manages: for example, roles/compute.networkAdmin for networking, roles/run.admin for Cloud Run. See Terraform for Google Cloud.
Building and pushing images to Artifact Registry
Even if deployment is handled by Cloud Deploy or another tool, GitHub Actions can own the build step: checkout, run tests, build the Docker image, push to Artifact Registry with the git commit SHA as the tag, then notify downstream systems. The service account only needs roles/artifactregistry.writer. See Artifact Registry Best Practices for image tagging and cleanup strategies.
Multi-environment deployment with approval before production
Use two jobs in sequence: one deploys to staging automatically on merge to main, and a second job references the production GitHub Environment. The production job pauses for a required reviewer to approve before continuing. Each job uses a separate service account with its own WIF binding scoped to the appropriate branch. This pattern works well for teams that want manual production gates without adopting Cloud Deploy. See Managing Environments in CI/CD.
Summary
- GitHub Actions deploys to GCP without storing credentials using Workload Identity Federation
- WIF tokens are short-lived, scoped to a specific repository, and expire automatically with nothing to rotate
- Jobs need
permissions: id-token: writeandcontents: read - Use the numeric project number (not the project ID string) in the
workload_identity_providerpath - Restrict the IAM binding to a specific branch for production deployments
- After the auth step, Terraform picks up GCP credentials via ADC automatically
- GitHub Actions works best for teams centred on GitHub. Use Cloud Build or Cloud Deploy for GCP-native delivery pipelines
- Apply least privilege to the deployment service account and audit roles regularly
Frequently asked questions
Do I need a service account key to use GitHub Actions with GCP?
No. Workload Identity Federation lets GitHub Actions authenticate to GCP without any key file. GitHub issues a short-lived OIDC token per workflow run; GCP exchanges it for an access token valid for one hour. Nothing to create, store, or rotate. If you are still using a service account key stored as a GitHub secret, migrating to WIF is worth the 15 minutes it takes.
What permissions does a GitHub Actions job need for Workload Identity Federation?
The job needs id-token: write to request an OIDC token from GitHub, and contents: read to check out the repository. Both must be declared in the job-level permissions block. Without id-token: write, GitHub will not issue a token and the auth step will fail, often with a confusing error message.
Can GitHub Actions deploy to Cloud Run?
Yes. The google-github-actions/deploy-cloudrun@v2 action handles the deployment step. You build and push a Docker image to Artifact Registry, then point the deploy action at that image. The complete workflow on this page shows the full sequence.
Should I use GitHub Actions or Cloud Build for GCP deployments?
It depends where your team lives. If your code review, PR checks, and deployment logic are already in GitHub, GitHub Actions keeps everything in one place and is a reasonable choice for most teams. Cloud Build is a better fit when you need tight integration with other GCP services, want builds to run inside your VPC, or your team is not centred on GitHub. A common hybrid pattern uses GitHub Actions for tests and PR checks, and Cloud Build or Cloud Deploy for the actual release and promotion pipeline.
Can I run Terraform against GCP from GitHub Actions?
Yes. After the WIF authentication step, Terraform picks up the exchanged credentials automatically via Application Default Credentials. No extra configuration is needed. A common pattern runs terraform plan on pull requests and terraform apply on merge to main. This page includes an example.