Skip to main content

Cloud Run

Cloud Run is the simplest GCP deployment: push a container, get a URL. Each revision runs with a bound Google Cloud service account — no key files needed.

When to choose Cloud Run

  • Serverless containers that scale to zero — $0 at idle
  • Simple HTTP APIs without complex networking requirements
  • You want zero infrastructure management
  • Cold starts are acceptable (mitigable with --min-instances)

Build and push to Artifact Registry

# Enable APIs
gcloud services enable run.googleapis.com artifactregistry.googleapis.com

# Create registry
gcloud artifacts repositories create fastapi-repo \
--repository-format=docker \
--location=us-central1

# Build and push
gcloud auth configure-docker us-central1-docker.pkg.dev

docker build -t fastapi-books .
docker tag fastapi-books:latest \
us-central1-docker.pkg.dev/my-project/fastapi-repo/fastapi-books:latest
docker push us-central1-docker.pkg.dev/my-project/fastapi-repo/fastapi-books:latest

Create a dedicated service account

# Create service account for the Cloud Run service
gcloud iam service-accounts create fastapi-books-sa \
--display-name "FastAPI Books Service"

# Grant it access to Secret Manager
gcloud projects add-iam-policy-binding my-project \
--member "serviceAccount:fastapi-books-sa@my-project.iam.gserviceaccount.com" \
--role "roles/secretmanager.secretAccessor"

# Grant access to Cloud SQL (example)
gcloud projects add-iam-policy-binding my-project \
--member "serviceAccount:fastapi-books-sa@my-project.iam.gserviceaccount.com" \
--role "roles/cloudsql.client"

Deploy

gcloud run deploy fastapi-books \
--image us-central1-docker.pkg.dev/my-project/fastapi-repo/fastapi-books:latest \
--region us-central1 \
--service-account fastapi-books-sa@my-project.iam.gserviceaccount.com \
--set-env-vars GCP_PROJECT=my-project \
--allow-unauthenticated \
--min-instances 0 \
--max-instances 10

The --service-account flag binds the IAM service account to the Cloud Run revision. No keyfile, no environment variable credentials.

Observability

Install the GCP observability packages:

uv add google-cloud-logging opentelemetry-sdk opentelemetry-exporter-gcp-trace

Structured logging

Send JSON-structured logs directly to Cloud Logging:

import google.cloud.logging

# Call once at startup — routes stdlib logging to Cloud Logging
google.cloud.logging.Client().setup_logging()

After this, standard logging.getLogger() calls emit structured JSON visible in the Logs Explorer and queryable with Log Analytics.

Distributed tracing (X-Ray equivalent on GCP)

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(CloudTraceSpanExporter()))
trace.set_tracer_provider(provider)

tracer = trace.get_tracer(__name__)

Add roles/cloudtrace.agent to the service account to allow writing traces.

Secrets with caching

Fetch secrets once on cold start via the lifespan and store on app.state. Call access_secret_version only when needed — Cloud Run instances persist between requests:

import os
from contextlib import asynccontextmanager
from typing import AsyncIterator
from google.cloud import secretmanager
from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
client = secretmanager.SecretManagerServiceClient() # ADC picks up bound SA
project = os.environ["GCP_PROJECT"]

def _get(name: str) -> str:
path = f"projects/{project}/secrets/{name}/versions/latest"
return client.access_secret_version(request={"name": path}).payload.data.decode()

app.state.jwt_secret = _get("jwt-signing-key")
app.state.db_password = _get("db-password")
yield


app = FastAPI(lifespan=lifespan)

No keyfiles, no GOOGLE_APPLICATION_CREDENTIALS — ADC resolves the bound service account automatically.

Next steps