API Gateway, Apigee and Cloud Endpoints

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Commands and behaviour below are current for the gcloud CLI 586.x line and the API Gateway, Apigee and Cloud Endpoints documentation as checked on 2026-09-30; re-verify against gcloud version, gcloud api-gateway --help, gcloud apigee --help and the linked reference pages before relying on them in a pipeline. Dated facts quoted here (Cloud Endpoints accepting OpenAPI 3.0 on 2025-11-12, the Endpoints developer portal discontinued on 2023-03-21, an MCP Preview on API Gateway since 2026-09) come from the release notes of each product.

Once the Bookshelf catalog-api and orders-api run on Cloud Run (see Cloud Run) or GKE (see Deploying Microservices on GKE), clients should not call them directly. A gateway in front gives one public entry point, authentication, quotas, a stable contract and a place to evolve versions. Google Cloud offers three managed products for this job, plus the Kubernetes Gateway API when the backends live on GKE. This page explains how to choose, builds the Bookshelf gateway with each product, and shows how tokens are validated at the edge. The generic pattern (backend for frontend, gateway aggregation, when a gateway becomes a bottleneck) is covered in API Gateway and Backends for Frontends, and a follow-up page on API management patterns is tracked as issue #184 of this repository; neither is repeated here.

Choosing a product

Aspect API Gateway Apigee Cloud Endpoints

What it is

A serverless, regional gateway configured from an OpenAPI document. Fully managed; nothing to size or patch.

A full API management platform: proxies with policies, API products, developer portal, analytics, monetisation, hybrid runtime.

A distributed gateway: the Extensible Service Proxy v2 (ESPv2), an Envoy-based proxy, that you run yourself (Cloud Run service, sidecar, GKE or VM) and configure from an OpenAPI or gRPC service config.

Typical backends

Cloud Run, Cloud Run functions, App Engine

Anything reachable by HTTP: Cloud Run, GKE through Private Service Connect, on-premises, other clouds

Anything ESPv2 can reach, including GKE and Compute Engine

Specification

OpenAPI 2.0, 3.0 and 3.1 (with Google extensions); gRPC supported

Proxy bundles of XML policies and flows; OpenAPI used to import or document

OpenAPI 2.0 and, since 2025-11-12, 3.0; gRPC service config

Authentication

API keys, JWT (Google ID tokens, Firebase, any OIDC issuer with a JWKS URI)

API keys, OAuth 2.0 token endpoint, JWT verification, SAML, mutual TLS, custom code

API keys, JWT (same model as API Gateway)

Traffic management

Per-method quotas

Spike arrest, quotas, caching, retries, fault rules, per-product limits

Per-method quotas

Developer experience

None built in

Integrated and drop-in developer portals, API products and apps, key management

The old Endpoints developer portal was discontinued on 2023-03-21; none built in

Operational burden

Lowest

Medium (environments, environment groups, networking), more with hybrid

Highest of the three: you own the proxy runtime and its upgrades

Cost shape

Per million calls with a monthly free allotment

Pay-as-you-go environments and API calls, or a committed subscription

Per million calls with a free allotment, plus the compute that runs ESPv2

Rules of thumb for Bookshelf:

  • One team, a handful of Cloud Run services, JWT or API keys, simple quotas: API Gateway.

  • Many consumer teams or external partners, products and plans, per-consumer analytics, request transformation, or monetisation: Apigee.

  • Need gRPC transcoding, or must run the gateway as a sidecar next to the backend, or already use ESPv2: Cloud Endpoints.

  • GKE-only traffic with path routing and no API management needs: just the Kubernetes Gateway API (below).

  • A global external Application Load Balancer can sit in front of any of them for a custom domain, Cloud Armor, Cloud CDN and multi-region failover; see Load Balancing and Cloud Armor.

flowchart LR C[Clients] --> LB[Global external ALB
Cloud Armor, custom domain] LB --> AG[API Gateway
bookshelf-gw] LB --> AP[Apigee
environment group] AG -->|x-google-backend
ID token| CR1[Cloud Run catalog-api] AG --> CR2[Cloud Run orders-api] AP -->|target server
via PSC| GK[GKE services] AP --> CR2

API Gateway

API Gateway has three resources:

  1. An API is a named container (bookshelf-api). Creating it also creates a Service Management managed service, which is what API keys and quotas attach to.

  2. An API config is an immutable snapshot of one OpenAPI document plus the service account the gateway uses to call backends. A change means a new config; you never edit one.

  3. A gateway is the regional deployment that serves one API config on a *.gateway.dev hostname. Updating a gateway to a new API config is a rolling, zero-downtime operation.

Operational facts: gateways are regional and only available in a subset of regions, a project may have up to 50 gateways per region, the data plane accepts TLS 1.2 or later only, and the control plane uses the services apigateway.googleapis.com, servicemanagement.googleapis.com and servicecontrol.googleapis.com.

Service accounts and roles

Two identities are involved, and mixing them up is the commonest first-day failure.

Identity Purpose Roles

Gateway service account (you create it, for example bookshelf-gw-sa)

Passed as --backend-auth-service-account; the gateway mints Google ID tokens as this account to call Cloud Run

roles/run.invoker on each Cloud Run backend service; the API Gateway Service Agent needs to be able to impersonate it (roles/iam.serviceAccountTokenCreator is granted automatically in the normal flow; check if you use organisation policies)

The person or pipeline creating APIs

Runs the gcloud api-gateway commands

roles/apigateway.admin (or roles/apigateway.editor), roles/iam.serviceAccountUser on the gateway service account, roles/servicemanagement.admin for key and quota set-up

In the Google Cloud console

  1. Open the project picker and select the Bookshelf project (or use Cloud Shell, which is already authenticated).

  2. Open API Gateway from the navigation menu (search "API Gateway"). On first visit, click Enable API to enable API Gateway and its dependent services.

  3. Click Create gateway.

  4. Under API, choose Create new API and name it bookshelf-api (display name "Bookshelf API").

  5. Under API config, upload the OpenAPI file (next section), name the config bookshelf-config-v1 and select the Service account the gateway will use to call backends (bookshelf-gw-sa).

  6. Under Gateway details, name the gateway bookshelf-gw and pick a region such as us-central1.

  7. Click Create gateway. After a few minutes the Default hostname ending in gateway.dev appears; open it with a path from the specification to test.

  8. To add a new version later, open the API, choose Configs then Create config, and finally open the gateway and use Edit to switch it to the new config.

The OpenAPI document with x-google-backend

The gateway reads standard OpenAPI plus a few x-google-* extensions. The most important is x-google-backend, which says where requests are forwarded:

  • address — the backend URL (a Cloud Run service URL or Cloud Run functions URL).

  • path_translation — APPEND_PATH_TO_ADDRESS (default for backend addresses without a path: the request path is appended) or CONSTANT_ADDRESS (the path in address is used and request path parameters become query parameters).

  • jwt_audience — the audience of the ID token the gateway mints for the backend; set it to the backend service URL.

  • deadline, protocol (http/1.1 or h2), disable_auth and min_deadline for timeouts and protocol choice.

swagger: "2.0"
info:
  title: Bookshelf API
  description: Public API of the Bookshelf application
  version: "1.0.0"
host: bookshelf-gw-REPLACE_ME.uc.gateway.dev   # informational; the gateway hostname is assigned on creation
schemes:
  - https
produces:
  - application/json

# Default backend for every operation (can be overridden per operation)
x-google-backend:
  address: https://catalog-api-REPLACE_ME-uc.a.run.app
  jwt_audience: https://catalog-api-REPLACE_ME-uc.a.run.app
  deadline: 15.0

securityDefinitions:
  api_key:
    type: apiKey
    name: x-api-key          # a header; use "key" with "in: query" for the legacy query form
    in: header
  bookshelf_idp:
    authorizationUrl: ""
    flow: implicit
    type: oauth2
    x-google-issuer: https://idp.example.com/
    x-google-jwks_uri: https://idp.example.com/.well-known/jwks.json
    x-google-audiences: bookshelf-api

paths:
  /v1/books:
    get:
      operationId: listBooks
      summary: List books (public, needs an API key)
      security:
        - api_key: []
      responses:
        "200":
          description: A page of books
  /v1/books/{id}:
    get:
      operationId: getBook
      parameters:
        - name: id
          in: path
          required: true
          type: string
      security:
        - api_key: []
      responses:
        "200":
          description: One book
  /v1/orders:
    post:
      operationId: createOrder
      summary: Create an order (needs a user JWT)
      x-google-backend:
        address: https://orders-api-REPLACE_ME-uc.a.run.app
        jwt_audience: https://orders-api-REPLACE_ME-uc.a.run.app
        path_translation: APPEND_PATH_TO_ADDRESS
      security:
        - bookshelf_idp: []
      responses:
        "201":
          description: Order created

Derived from the API Gateway — OpenAPI overview and Authenticating with a service account pages. OpenAPI 3.x documents use the same extension names and the components.securitySchemes section instead of securityDefinitions; check the OpenAPI 3 page of the API Gateway documentation for the exact support level.

With the gcloud CLI

gcloud services enable apigateway.googleapis.com servicemanagement.googleapis.com \
    servicecontrol.googleapis.com run.googleapis.com

PROJECT_ID=$(gcloud config get-value project)
REGION=us-central1
API=bookshelf-api
GATEWAY_SA=bookshelf-gw-sa

# 1. The service account the gateway uses to call Cloud Run backends
gcloud iam service-accounts create "$GATEWAY_SA" --display-name="Bookshelf API Gateway"
GATEWAY_SA_EMAIL="$GATEWAY_SA@$PROJECT_ID.iam.gserviceaccount.com"

for SVC in catalog-api orders-api; do
  gcloud run services add-iam-policy-binding "$SVC" --region="$REGION" \
      --member="serviceAccount:$GATEWAY_SA_EMAIL" --role="roles/run.invoker"
done

# 2. The API, an immutable API config, and the gateway
gcloud api-gateway apis create "$API" --project="$PROJECT_ID"

gcloud api-gateway api-configs create bookshelf-config-v1 \
    --api="$API" --openapi-spec=openapi.yaml \
    --backend-auth-service-account="$GATEWAY_SA_EMAIL" --project="$PROJECT_ID"

gcloud api-gateway gateways create bookshelf-gw \
    --api="$API" --api-config=bookshelf-config-v1 \
    --location="$REGION" --project="$PROJECT_ID"

# 3. Find the hostname and test
HOST=$(gcloud api-gateway gateways describe bookshelf-gw --location="$REGION" \
    --format='value(defaultHostname)')
curl -s "https://$HOST/v1/books" -H "x-api-key: $API_KEY"

Shipping a new version:

gcloud api-gateway api-configs create bookshelf-config-v2 \
    --api="$API" --openapi-spec=openapi.yaml \
    --backend-auth-service-account="$GATEWAY_SA_EMAIL"

gcloud api-gateway gateways update bookshelf-gw \
    --api="$API" --api-config=bookshelf-config-v2 --location="$REGION"

Derived from API Gateway — Creating an API config and Deploying an API to a gateway. To roll back, point the gateway at the previous config; keep at least the last two configs until the new one has proven itself.

API keys

An API key identifies the calling project or application, not a user. It is suitable for quota attribution and low-risk public data, never for authorising access to private data.

MANAGED_SERVICE=$(gcloud api-gateway apis describe "$API" --format='value(managedService)')

# Allow the API to be called with keys (the managed service must be enabled in the consumer project)
gcloud services enable "$MANAGED_SERVICE"

# Create a restricted key; the key string is printed once and goes to the consumer, not to source control
gcloud services api-keys create --display-name="bookshelf-web-key" \
    --api-target=service="$MANAGED_SERVICE"

Derived from API Gateway — Authentication methods and gcloud services api-keys create. Keep keys out of repositories and mobile bundles you cannot rotate; store server-side keys in Secret Manager (see Secret Manager and Cloud KMS).

JWT validation at the gateway

When a path requires an OAuth 2.0 / OIDC access token or ID token, the gateway validates the token before forwarding: signature (from the JWKS URI), iss, aud and expiry. The mapping is the securityDefinitions block above:

Extension Meaning

x-google-issuer

Exact value of the token’s iss claim. For Google ID tokens it is https://accounts.google.com; for Firebase Authentication it is https://securetoken.google.com/<firebase-project-id>;; for your own IdP it is that IdP’s issuer URL.

x-google-jwks_uri

Where the public keys are published (for Google service accounts, the service account’s own x509 JWKS URL).

x-google-audiences

Comma-separated accepted aud values. If omitted, the gateway accepts the API’s own hostname as audience.

After validation, the gateway forwards the original token in Authorization unchanged only if the backend needs it; otherwise it calls the backend with its own ID token (the jwt_audience above), and passes the verified end-user claims base64url-encoded in the X-Apigateway-Api-Userinfo header. The Cloud Run service itself is private (--no-allow-unauthenticated), so nobody can bypass the gateway by calling the run.app URL.

On the Spring Boot side nothing changes if the service should also validate the end-user JWT (defence in depth): add spring-boot-starter-oauth2-resource-server and set the issuer. The token concepts are covered in JWT and the JOSE Family, OpenID Connect and ID Tokens vs. Access Tokens; resource-server wiring in Authorization Server & Social Login and, for Quarkus, JWT, OIDC and Keycloak.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com/
          audiences: bookshelf-api

Derived from API Gateway — Authenticating users with JWT and the Spring Security resource server reference.

Quotas and rate limiting

Per-method quotas are declared in the OpenAPI document with the x-google-management extension, then attributed to API keys (quota counts per consuming project).

x-google-management:
  metrics:
    - name: read-requests
      displayName: Read requests
      valueType: INT64
      metricKind: DELTA
  quota:
    limits:
      - name: read-limit
        metric: read-requests
        unit: 1/min/{project}
        values:
          STANDARD: 600

paths:
  /v1/books:
    get:
      operationId: listBooks
      x-google-quota:
        metricCosts:
          read-requests: 1
      security:
        - api_key: []
      responses:
        "200":
          description: A page of books

Derived from API Gateway — Configuring quotas. Exceeding a quota returns HTTP 429. API Gateway has no spike-arrest or per-IP limiter; for burst protection or IP-based limits put Cloud Armor rate limiting in front (see Load Balancing and Cloud Armor) or use Apigee.

Custom domains

A gateway.dev hostname is fine for tests. For api.bookshelf.example.com put a global external Application Load Balancer in front: a serverless network endpoint group of type API Gateway, as the backend of a backend service, plus a Google-managed certificate from Certificate Manager. The load balancer also brings Cloud Armor and Cloud CDN.

gcloud compute network-endpoint-groups create bookshelf-gw-neg \
    --region="$REGION" --network-endpoint-type=serverless \
    --serverless-deployment-platform=apigateway.googleapis.com \
    --serverless-deployment-resource=bookshelf-gw

gcloud compute backend-services create bookshelf-gw-backend \
    --global --load-balancing-scheme=EXTERNAL_MANAGED

gcloud compute backend-services add-backend bookshelf-gw-backend --global \
    --network-endpoint-group=bookshelf-gw-neg \
    --network-endpoint-group-region="$REGION"

Derived from API Gateway — Getting started with serverless NEGs. The remaining pieces (URL map, target proxy, forwarding rule, certificate, DNS) are the same as for any global external ALB and are built in Load Balancing and Cloud Armor and Cloud DNS and Certificate Manager.

Monitoring, limits and the MCP Preview

  • Logs and metrics land in Cloud Logging and Cloud Monitoring (request count, latency, response codes per gateway and per API); see Monitoring and Observability.

  • The request size, header size and timeout limits are documented on the quotas page; a deadline larger than the gateway maximum is capped.

  • API Gateway has had a Preview for exposing APIs as Model Context Protocol (MCP) servers since 2026-09. Preview features carry no SLA and may change; confirm the current stage on the release notes before relying on it.

  • There is no built-in developer portal, request transformation or response cache. If you need those, step up to Apigee.

Cloud Endpoints and ESPv2

Cloud Endpoints predates API Gateway and uses the same service-management back end (API keys, JWT definitions and quotas use the same extensions). The difference is where the proxy runs: you deploy the ESPv2 container yourself. That buys gRPC transcoding, a sidecar placement (lowest latency) and control over networking, at the cost of operating it. Two points often wrong in older material:

  • ESPv2 on Cloud Run is generally available (it was Beta for years), and ESP version 1 (the NGINX-based proxy) is superseded by ESPv2; new designs should not start on v1.

  • Cloud Endpoints accepts OpenAPI 3.0 documents since 2025-11-12; older guides say OpenAPI 2.0 only.

The flow on Cloud Run has three moves: deploy a placeholder ESPv2 service to learn its hostname, publish the API config to Service Management, then build an ESPv2 image bound to that config and redeploy.

gcloud services enable servicemanagement.googleapis.com servicecontrol.googleapis.com \
    endpoints.googleapis.com run.googleapis.com cloudbuild.googleapis.com

ESP_SERVICE=bookshelf-esp
# 1. Deploy a placeholder ESPv2 (the sample image) to obtain the service hostname
gcloud run deploy "$ESP_SERVICE" --region="$REGION" \
    --image="gcr.io/endpoints-release/endpoints-runtime-serverless:2" \
    --allow-unauthenticated --platform=managed
ESP_HOST=$(gcloud run services describe "$ESP_SERVICE" --region="$REGION" \
    --format='value(status.url)' | sed 's#https://##')

# 2. Put the hostname in the OpenAPI document ("host: $ESP_HOST", plus x-google-backend for the real service),
#    then publish the config to Service Management and note the printed config ID
gcloud endpoints services deploy openapi-run.yaml --project="$PROJECT_ID"
CONFIG_ID=$(gcloud endpoints configs list --service="$ESP_HOST" --limit=1 --format='value(id)')

# 3. Build an ESPv2 image bound to that config (script from the esp-v2 repository), then redeploy
./gcloud_build_image -s "$ESP_HOST" -c "$CONFIG_ID" -p "$PROJECT_ID"
gcloud run deploy "$ESP_SERVICE" --region="$REGION" \
    --image="gcr.io/$PROJECT_ID/endpoints-runtime-serverless:$ESP_HOST-$CONFIG_ID" \
    --allow-unauthenticated --platform=managed \
    --service-account="esp-sa@$PROJECT_ID.iam.gserviceaccount.com"

Derived from Cloud Endpoints — Getting started with Cloud Endpoints for Cloud Run and gcloud reference — endpoints. The gcloud_build_image script lives in the esp-v2 repository linked from that page; it builds the image in Cloud Build, so the image repository is Artifact Registry or Container Registry’s replacement (see Container Registry and Builds) and the exact image path in your project may differ; use the path the script prints.

Security notes specific to ESPv2:

  • The ESPv2 service account needs roles/servicemanagement.serviceController so it can report usage and fetch the config, and roles/run.invoker on the backend if the backend is private.

  • ESPv2 itself is public (it is the front door); the backend Cloud Run service should be authenticated and invoked with an ID token (x-google-backend with jwt_audience), exactly as with API Gateway.

  • Pin the config ID: --rollout style deployments that follow "latest" make rollbacks harder.

When to still choose Cloud Endpoints: gRPC services needing HTTP/JSON transcoding, a sidecar on GKE, or an existing estate that already runs ESPv2. Otherwise prefer API Gateway (less to operate) or Apigee (more features).

Apigee

Apigee is Google Cloud’s API management platform. The Google Cloud variant (Apigee, formerly "Apigee X") runs the runtime in a Google-managed tenant project peered with your VPC; Apigee hybrid puts the runtime on your own Kubernetes cluster while the management plane stays in Google Cloud.

The resource model

Concept Meaning

Organization

The top-level Apigee resource; one per Google Cloud project. Holds environments, proxies, products, developers and apps.

Instance

A runtime deployment in a region, attached to an environment (the managed tenant network is reached over VPC peering or Private Service Connect).

Environment

A deployment target for proxies (dev, test, prod), with its own target servers, key-value maps and flow hooks.

Environment group

A set of hostnames (api.bookshelf.example.com) mapped to one or more environments; a global external ALB or a managed domain routes the hostname to Apigee.

API proxy

A bundle (apiproxy/) of XML: ProxyEndpoint, TargetEndpoint and policies. Deployed as numbered, immutable revisions.

API product

A bundle of proxies/paths plus quotas and scopes that consumers can subscribe to.

Developer and app

The consumer and the credentials (key and secret) they get for one or more API products.

Provisioning options:

  • Evaluation: free for 60 days, for learning; created from the console wizard or with a single provision command.

  • Pay-as-you-go: billed by environment type (Base, Intermediate, Comprehensive) per hour plus per API call volume, with no commitment; the sensible default for a first production use.

  • Subscription: a committed annual entitlement (Standard, Enterprise, Enterprise Plus) with included API call volume.

Pricing figures change; read the pay-as-you-go and subscription pages in References rather than quoting numbers.

In the Google Cloud console

  1. Select the project, then open Apigee from the navigation menu; click Enable APIs if prompted.

  2. Choose the Evaluation or Pay-as-you-go set-up wizard. Select the network (VPC) to peer, the runtime location (such as us-central1) and the analytics region, then confirm. Provisioning takes up to an hour.

  3. Open Proxy development > API proxies > Create. Choose Reverse proxy, set the name catalog-api, base path /catalog and the target URL of the Cloud Run service.

  4. Accept the default policies, create the proxy and use Develop to edit XML or the visual flow editor.

  5. Click Deploy and select the environment (eval in the evaluation organisation).

  6. Open Publish > API products > Create and add the catalog-api proxy with a quota and an OAuth scope; then Publish > Apps > Create to create a developer and an app that receives a key.

  7. Use Analyze > API metrics to see traffic, latency and errors per product, app, and developer.

With gcloud, apigeecli and Maven

gcloud services enable apigee.googleapis.com compute.googleapis.com servicenetworking.googleapis.com \
    cloudkms.googleapis.com

# Evaluation / pay-as-you-go organisation, runtime in us-central1 on the default VPC (create the peering first)
gcloud apigee organizations provision \
    --runtime-location="$REGION" --analytics-region="$REGION" \
    --authorized-network=default

# Verify the organisation, environments and environment groups
ORG=$PROJECT_ID
gcloud apigee organizations describe "$ORG"
gcloud apigee environments list --organization="$ORG"

Derived from Apigee — Provisioning overview and gcloud reference — apigee. The gcloud apigee group mostly covers organisations, environments, instances and deployments; proxy authoring and API products are usually scripted with the open-source apigeecli or the Maven plugins, which fit a CI pipeline better.

TOKEN=$(gcloud auth print-access-token)

# Import a proxy bundle directory and deploy revision 1 to the eval environment
apigeecli apis create bundle -f apiproxy --name catalog-api --org "$ORG" --token "$TOKEN"
apigeecli apis deploy --name catalog-api --env eval --ovr --wait --org "$ORG" --token "$TOKEN"

# API product, developer and app (key is returned once)
apigeecli products create --name bookshelf-public --displayname "Bookshelf public" \
    --proxies catalog-api --envs eval --approval auto \
    --quota 1000 --interval 1 --unit minute --org "$ORG" --token "$TOKEN"
apigeecli developers create --email dev@example.com --first Dev --last User \
    --user dev@example.com --org "$ORG" --token "$TOKEN"
apigeecli apps create --name bookshelf-web --email dev@example.com \
    --prods bookshelf-public --org "$ORG" --token "$TOKEN"

Derived from the apigeecli documentation; flag names change between releases, so check apigeecli --help. The Maven route uses apigee-deploy-maven-plugin (bundle build and deploy) and apigee-config-maven-plugin (products, developers, apps, key-value maps) driven from the Bookshelf build; see Apigee — Deploying proxies with Maven.

A proxy for catalog-api

A proxy bundle has this shape:

apiproxy/
  catalog-api.xml                  # proxy descriptor
  proxies/default.xml              # ProxyEndpoint (what clients see)
  targets/default.xml              # TargetEndpoint (the Cloud Run service)
  policies/
    Verify-API-Key.xml
    Spike-Arrest.xml
    Quota.xml
    Verify-JWT.xml
    Add-Headers.xml
    Cache-Books.xml
    CORS.xml

The ProxyEndpoint wires the policies into the request flow:

<ProxyEndpoint name="default">
  <PreFlow name="PreFlow">
    <Request>
      <Step><Name>CORS</Name></Step>
      <Step><Name>Spike-Arrest</Name></Step>
      <Step><Name>Verify-API-Key</Name></Step>
      <Step><Name>Quota</Name></Step>
    </Request>
    <Response/>
  </PreFlow>
  <Flows>
    <Flow name="orders">
      <Condition>(proxy.pathsuffix MatchesPath "/orders/**") and (request.verb = "POST")</Condition>
      <Request>
        <Step><Name>Verify-JWT</Name></Step>
      </Request>
    </Flow>
    <Flow name="books-cache">
      <Condition>(proxy.pathsuffix MatchesPath "/books") and (request.verb = "GET")</Condition>
      <Request><Step><Name>Cache-Books</Name></Step></Request>
      <Response><Step><Name>Cache-Books</Name></Step></Response>
    </Flow>
  </Flows>
  <PostFlow name="PostFlow">
    <Request/>
    <Response/>
  </PostFlow>
  <HTTPProxyConnection>
    <BasePath>/catalog</BasePath>
  </HTTPProxyConnection>
  <RouteRule name="default">
    <TargetEndpoint>default</TargetEndpoint>
  </RouteRule>
</ProxyEndpoint>

The TargetEndpoint calls the Cloud Run service with a Google ID token, so the backend stays private:

<TargetEndpoint name="default">
  <PreFlow name="PreFlow">
    <Request><Step><Name>Add-Headers</Name></Step></Request>
    <Response/>
  </PreFlow>
  <HTTPTargetConnection>
    <URL>https://catalog-api-REPLACE_ME-uc.a.run.app</URL>
    <Authentication>
      <GoogleIDToken>
        <Audience>https://catalog-api-REPLACE_ME-uc.a.run.app</Audience>
      </GoogleIDToken>
    </Authentication>
  </HTTPTargetConnection>
</TargetEndpoint>

Derived from Apigee — Understanding APIs and API proxies and Authentication with Google tokens. The Apigee service account (shown on the organisation page) needs roles/run.invoker on catalog-api. For a GKE backend, publish the Service through Private Service Connect (a service attachment) and define an Apigee target server or an Apigee endpoint attachment pointing at it; the VPC-side details are in VPC Networking on Google Cloud.

flowchart TD REQ[Client request] --> PRE[ProxyEndpoint PreFlow
CORS, SpikeArrest, VerifyAPIKey, Quota] PRE --> COND{Conditional flows
path and verb} COND -->|orders POST| JWT[VerifyJWT] COND -->|books GET| CACHE[ResponseCache lookup] COND -->|other| ROUTE[RouteRule] JWT --> ROUTE CACHE --> ROUTE ROUTE --> TPRE[TargetEndpoint PreFlow
AssignMessage headers] TPRE --> TGT[Backend Cloud Run
GoogleIDToken auth] TGT --> TPOST[Target response flow] TPOST --> POST[ProxyEndpoint PostFlow
ResponseCache populate] POST --> RESP[Response to client] PRE -.->|error| FAULT[FaultRules and DefaultFaultRule] JWT -.->|error| FAULT TGT -.->|error| FAULT FAULT --> RESP

Policy examples

Rate protection in the first step of every request, then consumer identity and quota:

<!-- Spike-Arrest.xml: smooths bursts; 30pm means one request every 2 seconds, not 30 at once -->
<SpikeArrest name="Spike-Arrest">
  <Rate>30pm</Rate>
  <UseEffectiveCount>true</UseEffectiveCount>
</SpikeArrest>
<!-- Verify-API-Key.xml: reads the key from a header, resolves the app and its API product -->
<VerifyAPIKey name="Verify-API-Key">
  <APIKey ref="request.header.x-api-key"/>
</VerifyAPIKey>
<!-- Quota.xml: limits come from the API product's quota settings -->
<Quota name="Quota">
  <Identifier ref="verifyapikey.Verify-API-Key.client_id"/>
  <Allow count="1000" countRef="verifyapikey.Verify-API-Key.apiproduct.developer.quota.limit"/>
  <Interval ref="verifyapikey.Verify-API-Key.apiproduct.developer.quota.interval">1</Interval>
  <TimeUnit ref="verifyapikey.Verify-API-Key.apiproduct.developer.quota.timeunit">minute</TimeUnit>
  <Distributed>true</Distributed>
  <Synchronous>false</Synchronous>
</Quota>

Validating a user token issued by the identity provider (OIDC discovery keys, not a shared secret):

<!-- Verify-JWT.xml -->
<VerifyJWT name="Verify-JWT">
  <Algorithm>RS256</Algorithm>
  <Source>request.header.authorization</Source>
  <PublicKey>
    <JWKS uri="https://idp.example.com/.well-known/jwks.json"/>
  </PublicKey>
  <Issuer>https://idp.example.com/</Issuer>
  <Audience>bookshelf-api</Audience>
  <IgnoreIssuedAt>false</IgnoreIssuedAt>
</VerifyJWT>

When the IdP issues opaque tokens instead, use the OAuthV2 policy against Apigee’s own token endpoint (operation GenerateAccessToken / VerifyAccessToken) or an introspection callout; see Opaque Tokens, Introspection and Revocation. Shaping the request, caching and CORS:

<!-- Add-Headers.xml: pass verified identity to the backend, remove the client-supplied header first -->
<AssignMessage name="Add-Headers">
  <Remove><Headers><Header name="X-Bookshelf-Client"/></Headers></Remove>
  <Set>
    <Headers>
      <Header name="X-Bookshelf-Client">{verifyapikey.Verify-API-Key.client_id}</Header>
    </Headers>
  </Set>
  <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</AssignMessage>
<!-- Cache-Books.xml: used in both request and response flows -->
<ResponseCache name="Cache-Books">
  <CacheKey>
    <KeyFragment ref="request.uri" type="string"/>
  </CacheKey>
  <ExpirySettings>
    <TimeoutInSec>60</TimeoutInSec>
  </ExpirySettings>
  <SkipCacheLookup>request.header.cache-control = "no-cache"</SkipCacheLookup>
</ResponseCache>
<!-- CORS.xml -->
<CORS name="CORS">
  <AllowOrigins>https://bookshelf.example.com</AllowOrigins>
  <AllowMethods>GET, POST, OPTIONS</AllowMethods>
  <AllowHeaders>authorization, content-type, x-api-key</AllowHeaders>
  <MaxAge>3628800</MaxAge>
</CORS>

All derived from the Apigee — Policy reference overview and the individual policy pages of the same section (SpikeArrest, Quota, VerifyAPIKey, VerifyJWT, AssignMessage, ResponseCache, CORS). Note that SpikeArrest and Quota solve different problems: spike arrest protects the backend from bursts; quota enforces per-consumer entitlements.

Other Apigee features

  • Analytics record each call by proxy, product, app and developer, and can be exported to BigQuery for custom reports.

  • Developer portals (integrated, or Drupal-based) publish documentation and let developers self-register and obtain keys against your API products.

  • Monetisation turns API products into priced rate plans, for partner APIs.

  • API hub is a catalogue for APIs of any gateway (including API Gateway and Cloud Endpoints) with lint and versioning metadata; useful once teams multiply, and out of scope for a first build.

  • Security: Advanced API Security and Cloud Armor integration add abuse detection and WAF in front of the environment group; mTLS to targets is configured on the target server.

The Gateway API on GKE

For Bookshelf services that run on GKE, the Kubernetes Gateway API is the native way to expose routes: a Gateway resource creates a Google Cloud load balancer, and HTTPRoute objects attach hosts and paths to Services. It is the successor to Ingress on GKE, and the full walkthrough, with GatewayClasses such as gke-l7-global-external-managed, policies and Certificate Manager integration, lives in Deploying Microservices on GKE; the vendor-neutral model is in Kubernetes. The part that matters for API management:

  • The Gateway API provides routing, TLS termination, header matching and traffic splitting, plus Cloud Armor via a GCPBackendPolicy. It does not issue API keys, apply per-consumer quotas or publish a developer portal.

  • JWT validation is not a built-in feature of the standard HTTPRoute. Options: Identity-Aware Proxy (for Google identities and workforce access), Cloud Service Mesh RequestAuthentication and AuthorizationPolicy when the mesh is installed, validation in the Spring Boot resource server, or place Apigee or API Gateway in front.

  • A common layout: external Application Load Balancer (from the Gateway) for the public hostname, with the Gateway routing /catalog to the catalog-api Service and /api to Apigee through Private Service Connect.

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: catalog-api
spec:
  parentRefs:
    - name: bookshelf-gateway
  hostnames:
    - api.bookshelf.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /catalog
      backendRefs:
        - name: catalog-api
          port: 8080

Derived from GKE — Deploying Gateways and the Kubernetes Gateway API specification.

Security checklist

  • Make every backend private (--no-allow-unauthenticated on Cloud Run, internal load balancing or a firewall on GKE) so the only way in is through the gateway. For Cloud Run, also set ingress to internal-and-cloud-load-balancing when the gateway sits behind a load balancer; API Gateway and Apigee call from Google-managed networks, so rely on IAM (roles/run.invoker for the gateway identity only).

  • Give the gateway identity the narrowest role: roles/run.invoker on specific services, not at project level.

  • Do not use API keys to authorise users. Use JWT (OIDC) for users; API keys only to identify an application and apply a quota. Restrict each key to the API’s managed service and, for browser keys, to referrers.

  • Validate iss, aud and expiry; reject tokens with the wrong audience even if the signature is valid. See Security Best Practices.

  • Put Cloud Armor in front for IP controls and WAF (see Load Balancing and Cloud Armor).

  • Keep secrets (backend credentials, signing keys, API key strings for server use) in Secret Manager; see Secret Manager and Cloud KMS and, for service accounts, Identity and Access.

  • Never download service-account keys for the gateway or ESPv2; they use attached identities.

  • Enable audit logs for API Gateway, Service Management and Apigee admin activity.

Troubleshooting

Symptom Likely cause and fix

403 with "The caller does not have permission" from the backend

The gateway service account lacks roles/run.invoker on that Cloud Run service, or jwt_audience does not match the service URL.

401 Jwt is missing or Jwt issuer is not configured

The path requires a JWT but the client sent none, or the iss in the token does not exactly equal x-google-issuer (a trailing slash matters).

403 PERMISSION_DENIED with "API key not valid" or "API has not been used"

The API’s managed service is not enabled in the consumer project, or the key is restricted to a different service.

404 on a valid-looking path

The path is not declared in the OpenAPI document, or path_translation is CONSTANT_ADDRESS where APPEND_PATH_TO_ADDRESS was meant.

Gateway stays in UPDATING or API config in FAILED

The OpenAPI document is invalid; read gcloud api-gateway api-configs describe for state and the service-config error message.

429

A quota or Apigee SpikeArrest/Quota policy triggered; check the metric and the per-consumer counts.

Apigee deployment ERROR with no …​ environment group

The environment is not attached to an environment group, or the hostname does not resolve to the load balancer.

ESPv2 returns 500 with "Failed fetching service config"

The ESPv2 service account lacks roles/servicemanagement.serviceController, or the image is bound to a wrong config ID.

Pricing model and cost drivers

Prices are deliberately not quoted; they change and differ by region. Read the dated pages in References.

  • API Gateway: billed per million API calls after a monthly free allotment, plus data transferred out. The gateway itself has no hourly charge.

  • Cloud Endpoints: billed per million API calls after a free allotment, plus the Cloud Run, GKE or VM capacity that runs ESPv2.

  • Apigee: pay-as-you-go charges per environment-hour by type (Base, Intermediate, Comprehensive), per API call volume and for add-ons; the subscription model bills a committed annual tier. An idle evaluation or pay-as-you-go environment still costs money, which makes Apigee the product most worth deleting after a lab.

  • All three: egress, Cloud Logging volume, the optional load balancer (forwarding rules and data processed) and Cloud Armor if used. See Cost Management and Billing for budgets and alerts.

Clean up

Delete in dependency order so nothing is left billing:

# Load balancer pieces first, if you created them (see the load balancing page)
gcloud compute backend-services delete bookshelf-gw-backend --global --quiet
gcloud compute network-endpoint-groups delete bookshelf-gw-neg --region="$REGION" --quiet

# API Gateway: gateways, then configs, then the API
gcloud api-gateway gateways delete bookshelf-gw --location="$REGION" --quiet
gcloud api-gateway api-configs delete bookshelf-config-v1 --api="$API" --quiet
gcloud api-gateway api-configs delete bookshelf-config-v2 --api="$API" --quiet
gcloud api-gateway apis delete "$API" --quiet

# API keys and the gateway service account
KEY_NAME=$(gcloud services api-keys list --filter='displayName=bookshelf-web-key' --format='value(name)')
gcloud services api-keys delete "$KEY_NAME"
gcloud iam service-accounts delete "$GATEWAY_SA_EMAIL" --quiet

# Cloud Endpoints / ESPv2
gcloud run services delete "$ESP_SERVICE" --region="$REGION" --quiet
gcloud endpoints services delete "$ESP_HOST" --quiet

# Apigee: undeploy and delete proxies, then the evaluation organisation (stops the billing for the runtime)
apigeecli apis undeploy --name catalog-api --env eval --rev 1 --org "$ORG" --token "$TOKEN"
apigeecli apis delete --name catalog-api --org "$ORG" --token "$TOKEN"
gcloud apigee organizations delete "$ORG"

Derived from API Gateway — Deleting an API, gcloud endpoints services delete and Apigee — Deleting an organization. Service Management keeps a deleted service for 30 days (you can gcloud endpoints services undelete), so the name cannot be reused immediately. Deleting an Apigee organization is permanent and removes every proxy, product and app in it. For a throwaway lab project, deleting the whole project (see Resource Hierarchy and Projects) is the surest way to stop all charges.