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.

Avoid this pattern

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

Good fit
  • 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
Where it starts to show limits
  • 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:

  1. Developer pushes code to GitHub. This triggers a GitHub Actions workflow defined in .github/workflows/deploy.yml.
  2. GitHub starts a runner. A fresh virtual machine starts. The job’s permissions block includes id-token: write.
  3. 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.
  4. The google-github-actions/auth action 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.
  5. 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.
  6. GCP issues a short-lived access token. The runner now has credentials that behave exactly like the specified service account, valid for one hour.
  7. The remaining workflow steps use those credentials. Pushing an image to Artifact Registry, deploying to Cloud Run, running gcloud commands. All of it uses the exchanged token. The service account only needs the roles required for those specific tasks.
  8. The token expires. When the workflow finishes (or after one hour), the token is automatically invalid. Nothing to revoke.
Analogy

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.workloadIdentityUser role on a service account. This is the permission that allows the token exchange.
Project ID vs project number

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}"
Restricting to a specific branch

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.

Do not skip this

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 }}
Why use the commit SHA as the image tag

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-approve
ADC picks up credentials automatically

Once 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 key
  • google-github-actions/setup-gcloud@v2: install and configure the gcloud CLI on the runner
  • google-github-actions/deploy-cloudrun@v2: deploy a container image to a Cloud Run service
  • google-github-actions/get-gke-credentials@v2: configure kubectl to 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 ActionsCloud BuildCloud Deploy
Where it runsGitHub-hosted (or self-hosted) runnersGCP-managed build environmentGCP-managed delivery service
Primary strengthCI + CD tightly integrated with GitHubGCP-native builds, VPC support, Binary AuthorizationStructured promotion pipelines across environments
Trigger sourceGitHub events (push, PR, schedule)Source code push, manual, GitHub/GitLab triggersDelivery pipeline progression
Approval gatesGitHub Environments + required reviewersBuild approval stepCloud Deploy release promotion approvals
Best forTeams centred on GitHub, Cloud Run deployments, Terraform workflowsGCP-native teams, private network builds, regulated workloadsMulti-environment promotion, GKE/Cloud Run at scale
Not ideal forComplex multi-stage GCP promotions, private VPC buildsTeams who want everything in GitHubSimple one-step deployments
How to think about this

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.

Use Workload Identity Federation, not service account keys

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

  1. Forgetting id-token: write in the job’s permissions block. Without this permission, GitHub does not issue an OIDC token. The auth action fails, usually with a vague error. The permissions block must be at the job level, not the workflow level, if you want it to apply only to specific jobs.

  2. Using the project ID instead of the project number in the provider path. The workload_identity_provider value 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.

  3. Binding WIF to attribute.repository_owner instead of attribute.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.

  4. Giving the deployment service account overly broad roles. roles/editor or roles/owner gives far more access than a deployment needs. Audit the roles on your CI service accounts and remove anything not required.

  5. 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.ref in the WIF binding, GitHub Environments with required reviewers, or both.

  6. Mixing up GitHub Environments with GCP environment concepts. A GitHub Environment named production controls 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.

  7. Running terraform apply without 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 run terraform plan on 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.

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.

Last verified: 25 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.