API Gateway — walidacja i routing¶
API Gateway to jedyna brama do backendu. Każde żądanie przechodzi przez nią — bez ważnego JWT, Cloud Run nie dostanie requestu. To tzw. "zero trust at the perimeter".
Czym jest Google Cloud API Gateway¶
API Gateway to managed proxy opisany specyfikacją OpenAPI (Swagger 2.0). Każde żądanie:
- Sprawdza JWT (podpis, issuer, audience, expiry)
- Jeśli OK — przekazuje request do Cloud Run z OIDC tokenem SA + dodaje nagłówek
X-Apigateway-Api-Userinfoz claims użytkownika - Jeśli nie — zwraca 401 bez dotykania backendu
[Klient]
│ HTTPS + Authorization: Bearer <JWT użytkownika>
▼
[API Gateway — europe-west1]
├─ walidacja JWT (Identity Platform issuer) ✓ / ✗
│ ↓ jeśli OK
├─ ZASTĘPUJE Authorization: Bearer <OIDC token api-gateway-sa> (1)
├─ DODAJE X-Apigateway-Api-Userinfo: <base64 JSON claims> (2)
▼
[Cloud Run — europe-central2]
├─ sprawdza IAM: czy api-gateway-sa ma roles/run.invoker? ✓
▼
[FastAPI: get_user_claims() z X-Apigateway-Api-Userinfo, logika biznesowa]
Authorizationjest zastępowany tokenem SA — Cloud Run widzi tylko SA, nie użytkownikaX-Apigateway-Api-Userinfozawiera base64url-encoded JSON z claims zwalidowanego JWT — to jedyny sposób na pobranie tożsamości użytkownika w backendzie
OpenAPI spec — serce konfiguracji¶
# tf/api-gateway/openapi.yaml.tpl
# Plik jest szablonem Terraform — ${variable} zastępowane przy terraform apply
swagger: "2.0"
info:
title: "gpc-api"
version: "1.0.0"
x-google-backend:
address: "${cloud_run_url}" # (1)
jwt_audience: "${cloud_run_url}" # (2)
path_translation: APPEND_PATH_TO_ADDRESS
securityDefinitions:
firebase:
type: "oauth2"
flow: "implicit"
authorizationUrl: ""
x-google-issuer: "https://securetoken.google.com/${project_id}"
x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com"
x-google-audiences: "${project_id}" # (3)
paths:
/health:
get:
operationId: "health_check"
security:
- firebase: [] # (4) JWT wymagany
responses:
"200":
description: "OK"
/:
get:
operationId: "root"
security:
- firebase: [] # (5) JWT wymagany — zwraca {"status":"ok"}
responses:
"200":
description: "OK"
/api:
options:
operationId: "api_cors_preflight"
security: [] # (6) preflight BEZ JWT
responses:
"204":
description: "CORS preflight"
get:
operationId: "api_list"
security:
- firebase: []
responses:
"200":
description: "OK"
post:
operationId: "api_create"
security:
- firebase: []
responses:
"200":
description: "OK"
address— URL Cloud Run (pobierany przezdata.google_cloud_run_v2_service.backend.uri)jwt_audience— audience dla OIDC tokenu do Cloud Run. Ten sam URL co adres backendux-google-audiences— audience dla JWT użytkownika (Firebase ID Token maaud = PROJECT_ID)security: - firebase: []— ta ścieżka wymaga JWT/— root endpoint, zwraca{"status": "ok", "service": "gcp-prototype"}. Przydatny do health check z JWT (sprawdza czy gateway + backend działają)security: []— CORS preflight (OPTIONS) nie ma nagłówkaAuthorization. Musi być bez security, inaczej API Gateway zwróci 401 na preflight i przeglądarka zablokuje cały request
Terraform — warstwa tf/api-gateway/¶
Trzy zasoby na każdy gateway (prod i staging)¶
# API — kontener logiczny
resource "google_api_gateway_api" "api" {
provider = google-beta # (1)
api_id = "gpc-api"
display_name = "GPC API"
}
# Config — konkretna wersja OpenAPI spec
resource "google_api_gateway_api_config" "config" {
provider = google-beta
api = google_api_gateway_api.api.api_id
api_config_id = "cfg-${substr(md5(local.openapi_spec), 0, 8)}" # (2)
openapi_documents {
document {
path = "spec.yaml"
contents = base64encode(local.openapi_spec) # (3)
}
}
gateway_config {
backend_config {
google_service_account = google_service_account.api_gw_sa.email
}
}
lifecycle {
create_before_destroy = true # (4)
}
}
# Gateway — publiczny endpoint
resource "google_api_gateway_gateway" "gateway" {
provider = google-beta
api_config = google_api_gateway_api_config.config.id
gateway_id = "gpc-gateway"
region = "europe-west1" # (5)
}
google-beta—google_api_gateway_*zasoby są w beta providerze. Wymaga osobnej konfiguracjiprovider "google-beta"wprovider.tfmd5(local.openapi_spec)— config ID zawiera hash specyfikacji. Gdy spec się zmienia → nowy config ID → nowy zasób → automatyczne rolowanie. Bez tego Terraform chciałby zmodyfikować immutable field i wywalał errorbase64encode— API Gateway wymaga specyfikacji jako base64create_before_destroy— stary config jest usuwany dopiero po stworzeniu nowego. Bez tego byłby downtime podczas aktualizacji speceurope-west1— API Gateway nie obsługujeeurope-central2. Stały region dla wszystkich gatewayów
Templatefile — dynamiczny spec¶
locals {
openapi_spec = templatefile("${path.module}/openapi.yaml.tpl", {
cloud_run_url = data.google_cloud_run_v2_service.backend.uri
project_id = var.project_id
})
}
data.google_cloud_run_v2_service.backend.uri pobiera URL Cloud Run z istniejącego stanu — dlatego warstwa backend musi być wdrożona przed api-gateway.
IAM: API Gateway → Cloud Run¶
resource "google_service_account" "api_gw_sa" {
account_id = "api-gateway-sa"
}
# SA może wywoływać Cloud Run
resource "google_cloud_run_v2_service_iam_member" "api_gw_invoker" {
name = data.google_cloud_run_v2_service.backend.name
role = "roles/run.invoker"
member = "serviceAccount:${google_service_account.api_gw_sa.email}"
}
Pułapki¶
CORS preflight musi być bez security
Browser przed każdym cross-origin requestem wysyła OPTIONS preflight bez nagłówków autoryzacji. Jeśli OPTIONS /api ma security: - firebase: [], API Gateway zwróci 401 → przeglądarka zablokuje właściwy request. Zawsze: security: [] dla OPTIONS.
api_config_id jest immutable
Nie możesz zmodyfikować istniejącego API Config — musisz stworzyć nowy. Dlatego ID zawiera hash spec: cfg-${substr(md5(spec), 0, 8)}. Terraform automatycznie tworzy nowy config gdy spec się zmienia.
Zależność: backend musi być wdrożony przed api-gateway
data.google_cloud_run_v2_service.backend odpytuje istniejący Cloud Run. Jeśli backend nie istnieje → Terraform plan zwróci błąd. Kolejność deploy: backend → api-gateway.
Authorization header jest zastępowany przez API Gateway
API Gateway używa Service Account (api-gateway-sa) do wywołania Cloud Run przez IAM (run.invoker). Nagłówek Authorization z JWT użytkownika jest zastępowany OIDC tokenem SA. Jeśli backend spróbuje zdekodować Authorization, dostanie claims SA (email: api-gateway-sa@...), nie użytkownika. Właściwe źródło tożsamości: nagłówek X-Apigateway-Api-Userinfo.