Skip to content

Deploy to Google Cloud with GitHub Actions

Lesson 3 of 7Intermediate → Advanced4 min readGitHub Actions & CI/CD · Continuous DeliveryVerified: google-github-actions/auth v3, google-github-actions/deploy-cloudrun v3, google-github-actions/setup-gcloud v3, August 2026

A Google Cloud service account key is a JSON file containing a private key. It does not expire. It works from any machine. It is, in practice, the single most commonly leaked cloud credential, because it is a file — and files get copied into Dockerfiles, committed by accident, and pasted into chat.

Workload Identity Federation removes it entirely.

permissions:
contents: read
id-token: write
steps:
- uses: google-github-actions/auth@v3
with:
workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github/providers/repo
service_account: deployer@PROJECT_ID.iam.gserviceaccount.com

What it doesExchanges the workflow's OIDC token for a short-lived Google Cloud access token through a workload identity pool.

Why we run itNo key file exists, so there is nothing to leak, commit, or rotate. Google validates the token against a provider configuration that names your repository.

Expected resultCredentials exported for the gcloud CLI and client libraries in subsequent steps.

The mental model is a two-step exchange, and holding it clearly makes every error message readable:

  1. GitHub issues a signed token describing the workflow — repository, ref, workflow name, environment. This is what id-token: write permits, and it grants nothing on its own.

  2. Google’s workload identity pool validates that token against a provider configuration, checks its attribute conditions, and — if satisfied — allows it to impersonate a service account.

The service account is where permissions live. Federation decides who may impersonate it; IAM decides what it can do. Failures at step 2 look like permission errors but are usually attribute condition mismatches, which is why the setup page walks through the condition syntax.

- uses: google-github-actions/auth@v3
with:
workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github/providers/repo
service_account: deployer@PROJECT_ID.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@v3
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker europe-west1-docker.pkg.dev --quiet
- name: Push
run: docker push europe-west1-docker.pkg.dev/PROJECT_ID/apps/example:${{ github.sha }}

gcloud auth configure-docker writes a credential helper entry so docker push uses the federated credentials already in the environment. Without it, the push fails with an authentication error even though gcloud itself is authenticated — the Docker CLI has its own credential store and does not inherit gcloud’s session.

Note the registry hostname carries the region. A mismatch between the region in the hostname and the repository’s actual location produces a “repository not found” error rather than a region error, which sends people looking for the wrong problem.

- id: deploy
uses: google-github-actions/deploy-cloudrun@v3
with:
service: example-app
region: europe-west1
image: europe-west1-docker.pkg.dev/PROJECT_ID/apps/example@${{ needs.build.outputs.digest }}
- name: Smoke test
run: curl -fsS "${{ steps.deploy.outputs.url }}/healthz"

Deploy by digest — @sha256:… — rather than by tag. Cloud Run resolves a tag to a digest at deployment time, so a latest tag that moves between the resolve and a later autoscaling event can produce two revisions running different code under one name.

Cloud Run’s revision model is the best rollback story of the three clouds in this cluster. Every deployment creates an immutable revision, traffic is routed to revisions by percentage, and nothing is deleted:

- name: Deploy without taking traffic
uses: google-github-actions/deploy-cloudrun@v3
with:
service: example-app
region: europe-west1
image: europe-west1-docker.pkg.dev/PROJECT_ID/apps/example@${{ needs.build.outputs.digest }}
no_traffic: true
tag: candidate

no_traffic: true with a tag deploys the revision and gives it its own URL without sending it any production traffic. You can then test the candidate URL against real infrastructure — real database, real service accounts — before any user reaches it, and shift traffic gradually:

- name: Send 10% of traffic to the candidate
run: |
gcloud run services update-traffic example-app \
--region europe-west1 \
--to-tags candidate=10

Rolling back is --to-revisions PREVIOUS=100, which takes effect in seconds and requires no rebuild.

For Kubernetes on Google Cloud, authenticate with the same federation, then fetch cluster credentials:

- uses: google-github-actions/get-gke-credentials@v3
with:
cluster_name: production
location: europe-west1

Everything after that is standard Kubernetes deployment, covered in deploying to Kubernetes. The federation step replaces the usual practice of storing a kubeconfig with an embedded service account token in repository secrets — which is a long-lived credential to a cluster’s API server, and one of the worst things to leak.

Federation controls who may impersonate; IAM controls what happens next. Both need to be narrow:

  • Grant roles on the specific resource — this Cloud Run service, this Artifact Registry repository — rather than at the project level. roles/editor on a project is close to administrative access.
  • Use roles/run.developer and roles/artifactregistry.writer rather than broad predefined roles.
  • Give staging and production separate service accounts, each impersonable only from the matching environment.
  • The deploy service account usually also needs roles/iam.serviceAccountUser on the runtime service account, because deploying a service that runs as an identity requires permission to act as that identity. This is the step people miss; the error names iam.serviceaccounts.actAs.
DeploymentRollbackReliable?
Cloud Runupdate-traffic --to-revisions PREVIOUS=100Yes, seconds, no rebuild
GKE Deploymentkubectl rollout undoYes, if the previous ReplicaSet is retained
Cloud FunctionsRedeploy the previous sourceSlower; no immutable revision by default
App EngineMigrate traffic to the previous versionYes — versions are retained
Database migrationNo.
Terraform-managed infrastructureRe-apply the previous configurationSometimes — see deploying Terraform
  1. Follow Google Cloud OIDC to create a workload identity pool and provider with an attribute condition naming your repository.

  2. Create a deploy service account with roles/run.developer on one Cloud Run service only.

  3. Write a workflow that authenticates and runs gcloud auth list. Confirm it shows the impersonated service account and that no key file exists anywhere.

  4. Deploy a revision with no_traffic: true and a tag. Confirm the candidate URL serves the new code while the main URL still serves the old.

  5. Shift 10% of traffic, then shift it back to the previous revision. Time how long the rollback takes — it should be seconds.

GitHub Actions Security ChecklistAudit your workflows against the failure modes that actually cause incidents. Free and complete.

Want production-ready workflow templates? The Professional Toolkit has five, with permissions set correctly.