LW IT Solutions logo LW IT Solutions logo LW IT Solutions
« Blog Overview /Cloud & AI/Tutorials / Serverless Microservices: Scaling on Google Cloud Run...
Read this article in other languages:

Serverless Microservices: Scaling on Google Cloud Run with GitHub Actions

Serverless Microservices: Scaling on Google Cloud Run with GitHub Actions

Management Summary

Google Cloud Run operates applications in the cloud and adjusts the number of running copies to demand. This reduces the work of maintaining servers and can suit services with changing traffic. GitHub Actions automates preparation and delivery of new versions, helping make releases repeatable. The benefit depends on sensible capacity and cost limits: automatic scaling does not remove the need for monitoring, and storage or other connected services can still cost money while the application is idle.

Contents
  1. 1. Prerequisites and separate identities
  2. 2. WIF restrictions for the owner, repository and main branch
  3. 3. Image build, authentication and revision staging
  4. 4. Scaling and cost settings for the application
  5. Questions and answers
  6. Sources

Google Cloud Run hosts containerized web services without operating a server or Kubernetes cluster. With suitable settings, a service with no requests can scale to zero instances. Traffic increases the instance count within available quotas and scaling limits. An instance is a running copy of the application. Starting a new copy takes time; this delay is called a cold start. Application maintenance and charges for other services still matter.

An image packages the application and the files needed to run it. A revision is a deployed version with a specific image and configuration. GitHub Actions can build images and deploy through Workload Identity Federation (WIF). WIF replaces a permanently stored Google service-account key with short-lived credentials. Compromised workflows, excessive permissions and accidentally bundled credentials remain risks. This example therefore builds before cloud authentication and initially deploys a revision without regular traffic.

1. Prerequisites and separate identities

The project, billing, required APIs, Artifact Registry repository services, deployer account and separate runtime account must already exist. The example updates an existing api-service in us-central1. The service has IAM invocation checks enabled and no public invocation grants. Initial provisioning and public-access changes require suitable administrative permissions outside this pipeline.

  • Deployer: ci-cd-deployer needs Artifact Registry Writer on the image repository, Cloud Run Developer on the existing service and Service Account User on the runtime account. Writer also includes the image-reading permissions needed for deployment.
  • Runtime account: api-runtime receives only application permissions, such as access to a specific datastore. Deployment permissions do not belong to this identity.
  • Application: The container must listen on 0.0.0.0:$PORT and keep sessions or persistent data outside the ephemeral container. A Dockerfile and an executable, project-specific ci/test.sh are prerequisites.
Image build before cloud authentication, deployment by image digest, followed by testing and release
The build runs before the cloud credential file is created. WIF restricts cloud access; application tests and an explicit traffic switch complete the release.

2. WIF restrictions for the owner, repository and main branch

These commands establish a new trust relationship. All example identifiers need replacement beforehand. PROJECT_NUMBER is the numeric Google project number, not the project ID. OWNER_ID and REPOSITORY_ID are numeric GitHub identifiers. These avoid trusting a deleted organization or repository name that another account later reclaims.

Shell
#!/usr/bin/env bash
set -euo pipefail
# Replace all example identifiers before administrative setup.
PROJECT_ID="enterprise-ai-project"
PROJECT_NUMBER="1234567890"
OWNER_ID="11111111"
REPOSITORY_ID="22222222"
DEPLOYER="ci-cd-deployer@${PROJECT_ID}.iam.gserviceaccount.com"

gcloud iam workload-identity-pools create github-actions-pool \
  --project="$PROJECT_ID" --location=global \
  --display-name="GitHub Actions Pool"

gcloud iam workload-identity-pools providers create-oidc github-provider \
  --project="$PROJECT_ID" --location=global \
  --workload-identity-pool=github-actions-pool \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository_id=assertion.repository_id,attribute.repository_owner_id=assertion.repository_owner_id,attribute.ref=assertion.ref,attribute.event_name=assertion.event_name" \
  --attribute-condition="assertion.repository_owner_id == '${OWNER_ID}' && assertion.repository_id == '${REPOSITORY_ID}' && assertion.ref == 'refs/heads/main' && assertion.event_name == 'push'"

gcloud iam service-accounts add-iam-policy-binding "$DEPLOYER" \
  --project="$PROJECT_ID" --role=roles/iam.workloadIdentityUser \
  --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-actions-pool/attribute.repository_id/${REPOSITORY_ID}"

The provider condition permits only push events from the specified repository on main. A workflow trigger alone would not enforce this restriction in Google Cloud. WIF and IAM changes can take time to propagate. The roles/iam.workloadIdentityUser binding permits impersonation of the deployer account; resource permissions are still required separately.

Branch protection and reviewed workflow changes remain essential: another eligible workflow on main can satisfy the same conditions. The GitHub production environment needs appropriate protection rules. Writing the environment name in YAML does not create an approval requirement.

3. Image build, authentication and revision staging

Before the first run, gha-creds-*.json belongs in .gitignore, with the following exclusions in .dockerignore. Additional secret files and Dockerfile-specific ignore files require project-specific handling; later exception rules must not re-include credentials.

Text
gha-creds-*.json
.git
.env
.env.*

The auth action writes a short-lived credential file into the workspace by default. A later docker build with a broad COPY could include this file in the image. Tests and image construction therefore run before cloud authentication. Action versions are pinned to verified commits; updates still require review.

YAML
name: Build and stage a Cloud Run revision

on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write

concurrency:
  group: cloud-run-api-service
  cancel-in-progress: false

env:
  PROJECT_ID: enterprise-ai-project
  REGION: us-central1
  IMAGE: us-central1-docker.pkg.dev/enterprise-ai-project/services/api-service
  PROVIDER: projects/1234567890/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider
  DEPLOYER: ci-cd-deployer@enterprise-ai-project.iam.gserviceaccount.com
  RUNTIME: api-runtime@enterprise-ai-project.iam.gserviceaccount.com

jobs:
  stage:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    environment: production
    defaults:
      run:
        shell: bash
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
        with:
          persist-credentials: false

      - name: Application tests and build before cloud authentication
        run: |
          set -euo pipefail
          ./ci/test.sh
          docker build --tag "$IMAGE:$GITHUB_SHA" .

      - name: Authenticate for image push
        uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
        with:
          workload_identity_provider: ${{ env.PROVIDER }}
          service_account: ${{ env.DEPLOYER }}
          project_id: ${{ env.PROJECT_ID }}

      - name: Set up gcloud
        uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3

      - name: Push image
        run: |
          set -euo pipefail
          gcloud auth configure-docker "$REGION-docker.pkg.dev" --quiet
          docker push "$IMAGE:$GITHUB_SHA"

      - name: Refresh authentication before deployment
        uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3
        with:
          workload_identity_provider: ${{ env.PROVIDER }}
          service_account: ${{ env.DEPLOYER }}
          project_id: ${{ env.PROJECT_ID }}

      - name: Configure refreshed gcloud credentials
        uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3

      - name: Stage revision without changing traffic
        run: |
          set -euo pipefail
          gcloud run services describe api-service \
            --project="$PROJECT_ID" --region="$REGION" >/dev/null
          DIGEST="$(gcloud artifacts docker images describe "$IMAGE:$GITHUB_SHA" \
            --project="$PROJECT_ID" --format='value(image_summary.digest)')"
          [[ "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]
          gcloud run deploy api-service \
            --project="$PROJECT_ID" --region="$REGION" \
            --image="$IMAGE@$DIGEST" --service-account="$RUNTIME" \
            --no-traffic --tag=candidate \
            --min-instances=0 --max-instances=50 \
            --concurrency=80 --memory=512Mi --cpu=1 --cpu-throttling \
            --quiet

The pipeline pushes a commit-tagged image and deploys the resolved digest. A digest identifies image content; a tag alone can change. Authentication is renewed before deployment so build and push time do not unnecessarily consume the short authentication window. Very long uploads or deployments can still require fresh authentication and a controlled retry.

--no-traffic keeps regular traffic on previous revisions. The candidate tag creates a separate test URL and moves to the new revision on the next run. This URL still uses the service’s access rules. Smoke tests check essential application functions, such as a sample API request. These tests need an authorized caller, typically with Cloud Run Invoker and a suitable ID token. A passing startup check alone does not establish application correctness.

After testing and checking the exact revision identifier, traffic can be moved explicitly. The placeholder below must become the actual tested revision. Rollback similarly selects an earlier verified revision. This does not undo database schema changes or external side effects.

Shell
# After authenticated smoke tests and approval of this exact revision:
gcloud run services update-traffic api-service \
  --project=enterprise-ai-project --region=us-central1 \
  --to-revisions=REVIEWED_REVISION_NAME=100

4. Scaling and cost settings for the application

  • Concurrency 80: This is the example’s maximum simultaneous requests per instance, not a threshold that must first be filled. Autoscaling considers metrics including CPU and concurrency utilization. CPU-intensive or memory-heavy requests often need lower values; greater concurrency does not automatically reduce memory usage.
  • Zero minimum instances: --min-instances=0 applies to the new revision. Service-level minimum instances also need checking. With request-based billing, ordinary idle instances without minimum-instance retention incur no idle compute charges; startup, shutdown and requests can still be billed. Registry storage, networking, logs and GitHub Actions add costs.
  • Maximum 50: --max-instances=50 is a revision-level limit here. Multiple revisions and temporary overshoots can increase the total. A suitable service-level limit also helps protect dependencies; neither setting is a hard billing cap.
  • Background work: --cpu-throttling selects request-based CPU allocation. Instance-based billing with continuously allocated CPU and minimum instances enables background work, but does not guarantee an uninterrupted process. Instances can terminate. Retryable work can use a durable queue with Cloud Tasks or appropriate Cloud Run jobs.

The values 80, 50 and 512 MiB are starting settings for this example. Load tests need to measure latency, error rate, memory use and database connections. Cloud Run can roll out revisions without planned downtime, but faulty code, incompatible migrations or exhausted quotas can still cause outages.

Three folding tables each hold the same maximum number of plates, while a fourth leans folded against the wall. Likewise, concurrency sets how many requests one instance handles at once; unused ones cost nothing.
Three folding tables each hold the same maximum number of plates, while a fourth leans folded against the wall. Likewise, concurrency sets how many requests one instance handles at once; unused ones cost nothing.

Questions and answers

Is keyless deployment maintenance-free?

No. WIF removes the permanently stored Google key from this workflow. Short-lived tokens, GitHub access and runtime permissions still need protection. Dependencies, base images, action commits and IAM rules require maintenance.

Why is –allow-unauthenticated absent?

The flag grants public invocation and requires additional IAM permissions. This example assumes an already private service and preserves invocation policy. Omitting the flag does not make an existing public service private.

What still costs money after every instance stops?

Stored images and other cloud services, among other resources. Registry cleanup should explicitly retain required release and rollback images. Scale-to-zero means neither free deployments nor free infrastructure.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in touch

A question or an idea for a web project? Tell me a little about it. This form uses reCAPTCHA and formconfirmed to protect against spam.

Write a comment

Experience with other models or providers and questions about the implementation are welcome here.

The email address is not published. Required fields are marked with an asterisk.

LUKASWOJCIK.COM · NEWSLETTER

Fresh ideas in your inbox

New articles, practical tools and updates from the projects.

Choose topics (optional)
No selection: all topics.

Subscription starts after confirmation by email. · Privacy

Privacy · Newsletter

Email address, language, selected topics and subscription/confirmation dates are stored to deliver the newsletter. Double opt-in is required. Every issue includes an immediate unsubscribe link. Formconfirmed checks the signup for spam. No email open or click tracking.

Read the full privacy policy ↗