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.
Authenticating without a key file
Section titled “Authenticating without a key file”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.comWhat 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:
-
GitHub issues a signed token describing the workflow — repository, ref, workflow name, environment. This is what
id-token: writepermits, and it grants nothing on its own. -
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.
Pushing to Artifact Registry
Section titled “Pushing to Artifact Registry”- 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.
Cloud Run
Section titled “Cloud Run”- 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: candidateno_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=10Rolling 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-west1Everything 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.
Scoping the service account
Section titled “Scoping the service account”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/editoron a project is close to administrative access. - Use
roles/run.developerandroles/artifactregistry.writerrather 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.serviceAccountUseron 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 namesiam.serviceaccounts.actAs.
Rollback
Section titled “Rollback”| Deployment | Rollback | Reliable? |
|---|---|---|
| Cloud Run | update-traffic --to-revisions PREVIOUS=100 | Yes, seconds, no rebuild |
| GKE Deployment | kubectl rollout undo | Yes, if the previous ReplicaSet is retained |
| Cloud Functions | Redeploy the previous source | Slower; no immutable revision by default |
| App Engine | Migrate traffic to the previous version | Yes — versions are retained |
| Database migration | — | No. |
| Terraform-managed infrastructure | Re-apply the previous configuration | Sometimes — see deploying Terraform |
Exercise
Section titled “Exercise”-
Follow Google Cloud OIDC to create a workload identity pool and provider with an attribute condition naming your repository.
-
Create a deploy service account with
roles/run.developeron one Cloud Run service only. -
Write a workflow that authenticates and runs
gcloud auth list. Confirm it shows the impersonated service account and that no key file exists anywhere. -
Deploy a revision with
no_traffic: trueand a tag. Confirm the candidate URL serves the new code while the main URL still serves the old. -
Shift 10% of traffic, then shift it back to the previous revision. Time how long the rollback takes — it should be seconds.