Minimal template to run FastAPI on Cloud Run as a private service, fronted by API Gateway enforcing Google API Key auth (x-api-key).
- Request hits API Gateway at
https://<gateway-host>. - For protected routes, Gateway validates the Google API key from the
x-api-keyheader (open routes skip this). - Gateway calls Cloud Run using its invoker service account (
roles/run.invoker). - Cloud Run remains private; direct calls to the Cloud Run URL return 403/401, while calls through Gateway succeed.
- Cloud Run service is private (no
allUsersinvoke) - API Gateway uses an invoker service account to call Cloud Run
- Selected routes enforce Google API key via Gateway security definition
- gcloud CLI installed and logged in
- jq installed (for key scripts)
- Docker (for local dev with Docker Compose)
- Authenticate, select project, scaffold env files
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
make env-examples
# edit .env.infra, .env.app, .env.deploy- Initialize everything (enable APIs, build & deploy Cloud Run, create/update Gateway)
make init- Create an API key (print it once for testing)
make api-key- Test through the Gateway
HOST="$(gcloud api-gateway gateways describe "$GATEWAY_ID" \
--location "$REGION" --project "$PROJECT_ID" --format='value(defaultHostname)')"
curl -i "https://${HOST}/v1/healthz" # open route → 200
curl -i -H "x-api-key: ${KEY}" "https://${HOST}/v1/hello" # protected → 200- Verify setup
make doctor- Manual deployment (all gcloud steps): see MANUAL_DEPLOYMENT.md.
- Scripts and Make targets reference: see scripts/README.md.
- OpenAPI v2 spec (source of truth for API Gateway):
deploy/gateway-openapi.yaml.
scripts/init_project.sh: Enable required APIs, ensure Artifact Registry and Gateway SA, build & deploy Cloud Run, grantrun.invoker, render OpenAPI, create/update Gateway.scripts/build_and_deploy.sh: Build container with Cloud Build and deploy Cloud Run as private (optionalSECRETS,ENV_VARS).scripts/update_gateway.sh: Inject Cloud Run URL into OpenAPI, create API config, and create/update Gateway.scripts/create_api_key.sh: Create a Google API key and restrict it to the Gateway managed service. Config via env:PRINT_KEY=1,KEY_PREFIX=<str>.scripts/list_api_keys.sh: List API keys with restrictions.scripts/delete_api_key.sh: Delete an API key (setYES=trueto skip confirmation).scripts/dev_uvicorn.sh: Run local dev server with reload (optional; Docker Compose dev is the default).scripts/doctor.sh: Validate environment, required APIs, IAM, and probe health endpoints.
These convenience targets wrap the scripts in scripts/ and set sane defaults.
env-examples: Create missing.env.infra,.env.app,.env.deployfrom their*.exampletemplates.
make env-examplesIf you ever delete the .env.infra, .env.app, or .env.deploy files, rerun make env-examples (or make env-example) to regenerate them from their templates. The command will error out when a corresponding .env.*.example file is missing so you notice and restore the template before continuing.
init: End-to-end project bootstrap (enable APIs, build & deploy Cloud Run, grant IAM, render OpenAPI, create/update Gateway).
make initbuild-deploy: Build the container with Cloud Build and deploy to Cloud Run.
make build-deploygw-update: Regenerate API config fromdeploy/gateway-openapi.yamland update/create the API Gateway.
make gw-updateapi-key: Create a Google API key restricted to this Gateway. Optionally prefix the key name.
# basic
make api-key
# prefix key name
make api-key PREFIX=myapp
# print key once
make api-key PRINT_KEY=1keys: List API keys and their restrictions.
make keysdel-key: Delete an API key by name. RequiresKEY_NAME. AddYES=trueto skip confirmation.
make del-key KEY_NAME=<KEY_NAME>
make del-key KEY_NAME=<KEY_NAME> YES=truedev: Start local development via Docker Compose (gunicorn; prod parity).
make devdev-logs: Tail logs from Docker Compose dev.
make dev-logsdev-down: Stop and remove dev containers.
make dev-downdev-rebuild: Rebuild the image without cache and start.
make dev-rebuilddoctor: Run environment and deployment diagnostics.
make doctor- Non-secret app config for local/dev →
.env.app(e.g.,ENV,LOG_LEVEL,PORT). - Infra/deploy identifiers →
.env.infra(e.g.,PROJECT_ID,REGION,SERVICE). - Deployment-time bindings →
.env.deploywith:SECRETS(Secret Manager names, not values), e.g.,API_TOKEN=api-token:latestENV_VARS(regular env vars), e.g.,ENV=prod,LOG_LEVEL=info
At runtime the service reads logging preferences from environment variables:
LOG_LEVEL(default:INFO) — one ofCRITICAL,ERROR,WARNING,INFO,DEBUG, orNOTSET.LOG_JSON(default:true) — set tofalseto emit plain-text logs with timestamps instead of JSON payloads.
make dev
# app served at http://localhost:${PORT:-8080}- App change →
make build-deploy - Gateway spec change →
make gw-update