DevOpsil
GCP
90%
Needs Review

Google Cloud Run: Serverless Containers That Scale to Zero

Aareez AsifAareez Asif6 min read

What Cloud Run Actually Does

Cloud Run runs your container and handles everything else: load balancing, auto-scaling (including scale-to-zero), TLS termination, and health checks. You push an image and get an HTTPS endpoint. Zero infrastructure to manage.

The constraint: your container must serve HTTP (or gRPC) on a port and handle requests within a configurable timeout (up to 60 minutes). It's not a replacement for persistent workloads or stateful services — it excels at stateless, request-driven services.

Two compute platforms:

  • Cloud Run (fully managed): Google manages everything, scale-to-zero, pay per 100ms
  • Cloud Run for Anthos: Runs on GKE, you manage the cluster, same API

Deploy Your First Service

# Authenticate
gcloud auth configure-docker

# Build and push to Artifact Registry
gcloud artifacts repositories create myapp \
  --repository-format=docker \
  --location=us-central1

docker build -t us-central1-docker.pkg.dev/my-project/myapp/api:v1 .
docker push us-central1-docker.pkg.dev/my-project/myapp/api:v1

# Deploy to Cloud Run
gcloud run deploy myapp-api \
  --image us-central1-docker.pkg.dev/my-project/myapp/api:v1 \
  --region us-central1 \
  --platform managed \
  --allow-unauthenticated \
  --port 8080 \
  --cpu 1 \
  --memory 512Mi \
  --min-instances 0 \
  --max-instances 100 \
  --concurrency 80 \
  --timeout 300

Cloud Run prints the service URL after deploy. The first request may have a cold start (~100-500ms for most images).


Service Configuration (YAML)

For reproducible deployments, define services in YAML:

# service.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: myapp-api
  annotations:
    run.googleapis.com/ingress: all
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "1"
        autoscaling.knative.dev/maxScale: "100"
        run.googleapis.com/cpu-throttling: "false"  # Always-on CPU (no cold starts)
        run.googleapis.com/startup-cpu-boost: "true"  # Extra CPU during startup
    spec:
      containerConcurrency: 80
      timeoutSeconds: 300
      serviceAccountName: myapp-service-account@my-project.iam.gserviceaccount.com
      containers:
        - image: us-central1-docker.pkg.dev/my-project/myapp/api:v1
          ports:
            - containerPort: 8080
          resources:
            limits:
              cpu: "2"
              memory: 1Gi
          env:
            - name: APP_ENV
              value: production
            - name: DB_HOST
              value: "10.0.0.5"
          volumeMounts:
            - name: secrets
              mountPath: /secrets
      volumes:
        - name: secrets
          secret:
            secretName: db-password
gcloud run services replace service.yaml --region us-central1

Secrets from Secret Manager

Never put secrets in environment variables. Use Secret Manager:

# Create a secret
echo -n "my-db-password" | gcloud secrets create db-password \
  --data-file=-

# Grant Cloud Run SA access
gcloud secrets add-iam-policy-binding db-password \
  --member="serviceAccount:[email protected]" \
  --role="roles/secretmanager.secretAccessor"

# Mount as file (recommended)
gcloud run deploy myapp-api \
  --image us-central1-docker.pkg.dev/my-project/myapp/api:v1 \
  --set-secrets /secrets/db-password=db-password:latest

# Or inject as environment variable
gcloud run deploy myapp-api \
  --image us-central1-docker.pkg.dev/my-project/myapp/api:v1 \
  --set-secrets DB_PASSWORD=db-password:latest

Using a specific version (instead of latest) is more secure — it prevents automatic secret rotation from affecting running instances without a redeploy:

--set-secrets DB_PASSWORD=db-password:3

Traffic Splitting and Canary Releases

Cloud Run supports splitting traffic between revisions — use this for canary deployments and A/B testing:

# Deploy a new revision without sending traffic to it
gcloud run deploy myapp-api \
  --image us-central1-docker.pkg.dev/my-project/myapp/api:v2 \
  --no-traffic \
  --tag v2-candidate

# Test the new revision directly via its tagged URL
curl https://v2-candidate---myapp-api-<hash>.run.app/health

# Send 10% of traffic to the new revision
gcloud run services update-traffic myapp-api \
  --to-revisions LATEST=10,myapp-api-00001-abc=90 \
  --region us-central1

# Gradually increase
gcloud run services update-traffic myapp-api \
  --to-revisions LATEST=50,myapp-api-00001-abc=50

# Full rollout
gcloud run services update-traffic myapp-api \
  --to-latest \
  --region us-central1

# Rollback: send 100% to previous revision
gcloud run services update-traffic myapp-api \
  --to-revisions myapp-api-00001-abc=100

VPC Connectivity

By default, Cloud Run can't reach private resources (Cloud SQL private IP, Redis, Memorystore). Enable VPC access:

# Create a VPC connector
gcloud compute networks vpc-access connectors create myapp-connector \
  --region us-central1 \
  --network default \
  --range 10.8.0.0/28

# Deploy with VPC connector
gcloud run deploy myapp-api \
  --vpc-connector myapp-connector \
  --vpc-egress private-ranges-only  # Only route private IPs through VPC; public via internet

For Cloud SQL, use the Cloud SQL Auth Proxy pattern or Cloud SQL connectors:

# Using Cloud SQL connector (Python example)
gcloud run deploy myapp-api \
  --set-env-vars INSTANCE_CONNECTION_NAME=my-project:us-central1:my-db \
  --vpc-connector myapp-connector

Cloud Run Jobs (Batch Workloads)

Cloud Run Jobs run containers to completion — no HTTP server needed. Perfect for data pipelines, migrations, and scheduled tasks:

# Create a job
gcloud run jobs create my-migration-job \
  --image us-central1-docker.pkg.dev/my-project/myapp/migrations:latest \
  --region us-central1 \
  --task-count 1 \
  --max-retries 3 \
  --set-env-vars DB_HOST=10.0.0.5 \
  --set-secrets DB_PASSWORD=db-password:latest \
  --vpc-connector myapp-connector

# Run it
gcloud run jobs execute my-migration-job --region us-central1

# Run with a Cloud Scheduler trigger (every day at 2am)
gcloud scheduler jobs create http daily-cleanup \
  --schedule "0 2 * * *" \
  --uri "https://us-central1-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/my-project/jobs/my-cleanup-job:run" \
  --message-body "{}" \
  --oauth-service-account-email [email protected]

# Parallel job execution (10 tasks running concurrently)
gcloud run jobs create parallel-processor \
  --task-count 100 \
  --parallelism 10   # Run 10 at a time

Each task gets a CLOUD_RUN_TASK_INDEX env var (0-99 in this case) so tasks can process different data shards.


Authentication: Service-to-Service

Cloud Run services can require authentication — only requests with a valid Google-signed JWT are accepted:

# Deploy without --allow-unauthenticated
gcloud run deploy internal-api \
  --image ... \
  --region us-central1
  # No --allow-unauthenticated flag

# Grant a service account permission to call it
gcloud run services add-iam-policy-binding internal-api \
  --member="serviceAccount:[email protected]" \
  --role="roles/run.invoker" \
  --region us-central1

Calling an authenticated Cloud Run service:

# Get identity token for the calling service
TOKEN=$(gcloud auth print-identity-token \
  --audiences=https://internal-api-<hash>.run.app)

curl -H "Authorization: Bearer $TOKEN" \
  https://internal-api-<hash>.run.app/internal/process

In code (Python):

import google.auth.transport.requests
import google.oauth2.id_token

request = google.auth.transport.requests.Request()
target_audience = "https://internal-api-<hash>.run.app"
token = google.oauth2.id_token.fetch_id_token(request, target_audience)
headers = {"Authorization": f"Bearer {token}"}

Cost Optimization

Cloud Run bills for CPU and memory during request processing only (by default):

  • $0.00002400/vCPU-second
  • $0.00000250/GB-second
  • First 180,000 vCPU-seconds and 360,000 GB-seconds free per month

Scale to zero: Set --min-instances 0. Cold starts add 100-500ms on first request after idle. Acceptable for most APIs.

CPU always allocated: --cpu-throttling=false keeps CPU allocated between requests (better for connection pooling, background tasks). Billed per 100ms even when idle — worth it for high-traffic services.

CPU Boost on startup: --startup-cpu-boost gives extra CPU during container startup to reduce cold start time. Free.

Minimum instances: Set --min-instances 1 to eliminate cold starts for SLA-sensitive services. One instance runs continuously (~$5-15/month depending on size) but eliminates latency variability.

Share:

Was this article helpful?

Aareez Asif
Aareez Asif

Senior Kubernetes Architect

10+ years orchestrating containers in production. Battle-tested opinions on everything from pod scheduling to service mesh. I've seen clusters burn and helped rebuild them better.

Related Articles

More in GCP

View all →

Discussion