API Management

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 on this page are current for Azure CLI 2.90.0 (checked 2026-09-27); re-verify against az version and the linked reference pages before relying on them in a pipeline. Where Azure CLI genuinely cannot provision a resource shape yet — the v2 pricing tiers, below — the page shows the Bicep or az rest alternative instead.

Bookshelf fronts both HTTP services — catalog-api and orders-api, already running on cae-bookshelf (Container Apps) with an AKS-hosted alternative on aks-bookshelf (Deploying Microservices on AKS) — with a single API Management instance, apim-bookshelf. This page covers what APIM is, how to provision and size it, how Bookshelf’s two APIs get imported, secured, cached, and routed through it, and how to operate, observe, and eventually delete it.

Concepts: gateway, management plane, developer portal

Azure API Management is made up of three components, Azure-hosted and fully managed by default:

Gateway (data plane)

Every client request reaches the gateway first. It authenticates the caller (subscription key, JWT, mutual TLS), enforces rate limits and quotas, applies whatever transformation a policy specifies, optionally serves a cached response, forwards the request to the right backend, and emits logs/metrics/traces for every call. catalog-api and orders-api never see a request that the gateway’s policies would have rejected.

Management plane (control plane)

Where an API provider configures the service — import an API’s schema (OpenAPI, WSDL, OData, a running Azure compute resource, WebSocket, GraphQL, gRPC), group operations into products, attach policies, inspect analytics, and manage developer accounts. Reached through the Azure portal, Azure CLI, Azure PowerShell, a VS Code extension, or the management REST API directly.

Developer portal

An automatically generated, customizable website where API consumers (app developers, partner integrators) discover APIs, read their documentation, try an operation from an interactive console, create an account, subscribe to a product to get a key, and pull their own usage analytics. It can be branded, extended with custom content, or self-hosted entirely.

A fourth, optional piece — the self-hosted gateway — lets the data plane run outside Azure (on aks-bookshelf, for Bookshelf) while the management plane and developer portal stay in Azure; see [Self-hosted gateway on AKS].

flowchart LR C[Client] --> GW["APIM gateway\n(data plane)"] subgraph Inbound direction TB AUTH["Verify subscription key / JWT / mTLS"] RL["Rate limit / quota"] XF["Transform request"] end GW --> AUTH --> RL --> XF --> BE subgraph BE["Backends"] direction TB CA["catalog-api / orders-api\n(Container Apps)"] AKS["self-hosted gateway\n(AKS)"] end BE --> OUT["Transform response / cache-store"] OUT --> C MP["Management plane\n(portal, CLI, REST API)"] -. "configure APIs, products,\npolicies, named values" .-> GW DEV["Developer portal"] -. "discover, subscribe,\nget subscription key" .-> C

Tiers

APIM ships two families of pricing tiers plus a serverless one, and picking the wrong family is the single most common APIM design mistake — the v2 tiers are not simply "the classic tiers, faster": several classic capabilities (multi-region deployment, the self-hosted gateway, backup/restore) are currently unavailable on any v2 tier, while others (VNet integration on Standard v2, workspaces on every v2 tier) exist only there.

Classic

Consumption, Developer, Basic, Standard, Premium. The original offering. Developer is an economical, no-SLA option for non-production use; Basic, Standard, and Premium are production-ready; Premium is the tier for enterprises needing private backends, multi-region deployments, availability zones, and the self-hosted gateway.

v2

Basic v2, Standard v2, Premium v2. A newer platform built for faster provisioning and scaling. Basic v2 targets development/test; Standard v2 is production-ready with outbound VNet integration to reach network-isolated backends; Premium v2 adds full inbound+outbound VNet injection, availability zones, and higher scale limits, running its gateway on a dedicated App Service Environment.

Consumption

A serverless gateway billed per call (no monthly unit fee), scaling automatically to zero and back; designed for spiky or low-traffic APIs fronting serverless/microservice backends. It skips most of the policy/gateway feature set the other tiers share (see the matrix below) in exchange for that scale-to-zero billing model.

Feature matrix

Feature Consumption Developer Basic Basic v2 Standard Standard v2 Premium Premium v2

VNet injection (full inbound+outbound isolation)

No

Yes

No

No

No

No

Yes

Yes

VNet integration (outbound only, to network-isolated backends)

No

Yes

No

No

No

Yes

Yes

Yes

Private endpoint (inbound)

No

Yes

Yes

No

Yes

Yes

Yes

Yes

Self-hosted gateway

No

Yes (single node)

No

No

No

No

Yes

No

Workspaces

No

No

No

Yes

No

Yes

Yes

Yes

Multi-region deployment

No

No

No

No

No

No

Yes

No

Availability zones

No

No

No

No

No

No

Yes

Yes

Backup and restore

No

Yes

Yes

No

Yes

No

Yes

No

SLA

Yes

No

Yes

Yes

Yes

Yes

Yes

Yes

Two consequences worth planning around before picking a tier: an architecture that needs the self-hosted gateway and the faster v2 provisioning/scaling model cannot have both today — self-hosted gateways are Developer- or Premium-only — and multi-region deployment is exclusively a classic-Premium capability, so a v2-tier instance is always single-region (workspace gateways can still be spread across regions independently; the core v2 service itself cannot). Bookshelf runs apim-bookshelf on classic Developer for this page’s walkthroughs (no SLA, cheapest way to exercise every policy and the self-hosted gateway); production Bookshelf environments would move to classic Premium (self-hosted gateway, multi-region) or Standard v2 (no self-hosted gateway needed, faster scaling, outbound VNet integration to reach cae-bookshelf privately) depending on which of those two features actually matters.

Creating the instance

Provisioning any APIM tier takes a long time — commonly 20 to 45 minutes for classic tiers, faster but still not instant for v2 — because the service allocates dedicated compute and configures its own gateway infrastructure rather than attaching to an existing shared pool; do not block a CI pipeline step on az apim create returning without either backgrounding it or budgeting for that wait.

In the Azure portal

  1. Search for API Management services and select Create.

  2. Basics: $RG, region, resource name apim-bookshelf, organization name, administrator email.

  3. Pricing tier: pick a tab — Classic lists Consumption/Developer/Basic/Standard/Premium; v2 lists Basic v2/Standard v2/Premium v2 — and select the tier from Tiers.

  4. Review + create, then Create. The portal shows a deployment-in-progress notification for the full provisioning window above; the instance is unusable until it completes.

With the Azure CLI

az apim create provisions every classic tier (Consumption, Developer, Basic, Standard, Premium) but, as of Azure CLI 2.90.0, its --sku-name parameter does not accept a v2-tier value — there is no BasicV2, StandardV2, or PremiumV2 the CLI will create directly. A classic-tier instance for this page’s walkthroughs:

az apim create \
  --name apim-bookshelf \
  --resource-group "$RG" \
  --location "$LOCATION" \
  --sku-name Developer \
  --sku-capacity 1 \
  --publisher-email admin@bookshelf.example \
  --publisher-name Bookshelf

To provision a v2 tier, use Bicep, an ARM template, Terraform, or a direct az rest call against the management REST API instead. A minimal Bicep resource for Standard v2:

resource apimBookshelf 'Microsoft.ApiManagement/service@2024-05-01' = {
  name: 'apim-bookshelf'
  location: location
  sku: {
    name: 'StandardV2'
    capacity: 1
  }
  properties: {
    publisherEmail: 'admin@bookshelf.example'
    publisherName: 'Bookshelf'
  }
}

deployed with az deployment group create --resource-group "$RG" --template-file apim.bicep, or, without a template file at all, directly via az rest:

az rest \
  --method put \
  --url "https://management.azure.com/subscriptions/$(az account show --query id -o tsv)/resourceGroups/$RG/providers/Microsoft.ApiManagement/service/apim-bookshelf?api-version=2024-05-01" \
  --body '{
    "location": "'"$LOCATION"'",
    "sku": {"name": "StandardV2", "capacity": 1},
    "properties": {"publisherEmail": "admin@bookshelf.example", "publisherName": "Bookshelf"}
  }'

Importing an API

Bookshelf’s catalog-api publishes an OpenAPI (Swagger) document at its own /v3/api-docs endpoint (Spring Boot’s springdoc-openapi default); APIM imports it directly from that URL rather than a hand-maintained copy.

In the Azure portal

  1. Open apim-bookshelf → APIs → + Add API → OpenAPI.

  2. OpenAPI specification: the catalog-api ingress URL’s /v3/api-docs path.

  3. Display name Catalog API, Name catalog-api, API URL suffix catalog.

  4. Create. APIM parses every operation from the spec and lists them under the new API.

With the Azure CLI

az apim api import \
  --resource-group "$RG" \
  --service-name apim-bookshelf \
  --api-id catalog-api \
  --path catalog \
  --specification-format OpenApi \
  --specification-url "https://$(az containerapp show --name catalog-api --resource-group "$RG" --query properties.configuration.ingress.fqdn -o tsv)/v3/api-docs" \
  --display-name "Catalog API"

Products and subscriptions

Bookshelf’s APIs are not called anonymously: they are grouped into a product — Bookshelf Public API, containing catalog-api and, once imported the same way, orders-api — and every product is either open (no key needed) or protected (a subscription key required). Bookshelf’s product is protected, with subscription approval left to automatic for the developer-portal self-service flow.

A caller that subscribes (from the developer portal, or provisioned directly by an administrator) gets a primary and secondary subscription key, sent on every call in the Ocp-Apim-Subscription-Key header (or as the subscription-key query parameter, discouraged since it can leak into logs and browser history). The gateway rejects any call to a protected product’s operations that omits a valid key before the request reaches any policy or backend.

In the Azure portal

  1. Products → + Add: name Bookshelf Public API, Requires subscription checked, Requires approval unchecked (automatic), add catalog-api and orders-api, Save and publish.

  2. A caller subscribes from the developer portal’s Products page, or an administrator creates a subscription directly under Subscriptions → + Add subscription, scoped to the product.

With the Azure CLI

az apim product covers the product and its API membership, but the CLI has no az apim subscription command group, so the administrator-provisioned subscription is created with az rest against the Microsoft.ApiManagement/service/subscriptions REST API instead. APIM_ID holds the instance’s resource ID and is reused by the later az rest calls on this page.

APIM_ID="$(az apim show --name apim-bookshelf --resource-group "$RG" --query id -o tsv)"

az apim product create \
  --resource-group "$RG" \
  --service-name apim-bookshelf \
  --product-id bookshelf-public-api \
  --product-name "Bookshelf Public API" \
  --subscription-required true \
  --approval-required false \
  --state published

az apim product api add \
  --resource-group "$RG" \
  --service-name apim-bookshelf \
  --product-id bookshelf-public-api \
  --api-id catalog-api

az rest \
  --method put \
  --url "https://management.azure.com${APIM_ID}/subscriptions/bookshelf-web?api-version=2024-05-01" \
  --body '{
    "properties": {
      "displayName": "Bookshelf web front end",
      "scope": "/products/bookshelf-public-api",
      "state": "active"
    }
  }'

# The keys are not returned on GET -- read them with listSecrets
az rest \
  --method post \
  --url "https://management.azure.com${APIM_ID}/subscriptions/bookshelf-web/listSecrets?api-version=2024-05-01" \
  --query primaryKey -o tsv

state is set to active explicitly because a subscription created through the REST API otherwise starts in the submitted state and cannot call the product until it is approved.

Policies

A policy is an XML statement executed at one of four pipeline stages, applicable at global, workspace, product, API, or operation scope:

flowchart TB REQ["Client request"] --> IN["inbound\n(validate, authenticate,\nrate-limit, transform,\ncache-lookup)"] IN --> BER{"Backend reachable\nand cache miss?"} BER -- yes --> BACKEND["backend\n(forward to catalog-api /\norders-api / self-hosted gateway)"] BACKEND --> OUT["outbound\n(transform response,\ncache-store)"] BER -- "cache hit" --> OUT OUT --> RESP["Response to client"] IN -. "any policy throws" .-> ONERR["on-error\n(format error response,\nlog, custom status code)"] BACKEND -. "backend error" .-> ONERR OUT -. "any policy throws" .-> ONERR ONERR --> RESP

base inside any section inherits the policy from the next broader scope (operation → API → product → global); omitting it replaces the inherited policy entirely for that scope.

Rate limit and quota

<policies>
  <inbound>
    <base />
    <rate-limit-by-key calls="100" renewal-period="60"
                        counter-key="@(context.Subscription.Id)" />
    <quota-by-key calls="10000" renewal-period="2592000"
                  counter-key="@(context.Subscription.Id)" />
  </inbound>
</policies>

rate-limit-by-key throttles short bursts (100 calls per 60-second window here); quota-by-key enforces a longer-window ceiling (10,000 calls per 30-day window) — both keyed per subscription so one caller’s traffic never throttles another’s. rate-limit and quota (without -by-key) apply the same limits product- or API-wide instead of per caller.

validate-jwt against Microsoft Entra ID

<policies>
  <inbound>
    <base />
    <validate-jwt header-name="Authorization" failed-validation-httpcode="401"
                  failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
      <openid-config url="https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration" />
      <audiences>
        <audience><audience></audience>
      </audiences>
      <issuers>
        <issuer>https://login.microsoftonline.com/<tenant-id>/v2.0</issuer>
      </issuers>
      <required-claims>
        <claim name="roles" match="any">
          <value>orders.read</value>
        </claim>
      </required-claims>
    </validate-jwt>
  </inbound>
</policies>

APIM fetches the tenant’s signing keys from the openid-config discovery document and caches them (refreshed roughly hourly), so a key rotation on the Entra ID side needs no policy change. Bookshelf uses this in front of orders-api operations that must only be called on behalf of a signed-in user, layered on top of the subscription-key check above rather than instead of it; see Identity and Access for the app-registration and token-acquisition side of this that this policy validates against.

CORS

<policies>
  <inbound>
    <base />
    <cors allow-credentials="false">
      <allowed-origins>
        <origin>https://www.bookshelf.example</origin>
      </allowed-origins>
      <allowed-methods>
        <method>GET</method>
        <method>POST</method>
      </allowed-methods>
      <allowed-headers>
        <header>Content-Type</header>
        <header>Ocp-Apim-Subscription-Key</header>
      </allowed-headers>
    </cors>
  </inbound>
</policies>

Bookshelf’s static web front end (Storage Accounts/Front Door-served) calls catalog-api and orders-api through APIM from browser JavaScript, which is what makes this policy necessary at all — a server-to-server caller never needs it.

Derived from CORS policy.

Caching

<policies>
  <inbound>
    <base />
    <cache-lookup vary-by-developer="false" vary-by-developer-groups="false"
                  downstream-caching-type="none" must-revalidate="true">
      <vary-by-query-parameter>category</vary-by-query-parameter>
    </cache-lookup>
  </inbound>
  <outbound>
    <base />
    <cache-store duration="60" />
  </outbound>
</policies>

cache-lookup in inbound short-circuits straight to outbound on a hit, skipping the backend call entirely; cache-store in outbound populates the cache for the next matching request. Bookshelf applies this to catalog-api’s read-only book-listing operation (60-second cache, varied by the `category query parameter), never to orders-api’s operations, which must always reach the backend. `Basic/higher tiers use the built-in cache sized per the Feature matrix; every tier, including Consumption, can instead point at an external Azure Managed Redis cache (Redis and Partner Databases on Azure) for a larger or shared cache.

Backend routing: rewrite-uri and set-backend-service

<policies>
  <inbound>
    <base />
    <set-backend-service backend-id="catalog-api-backend" />
    <rewrite-uri template="/api/v1/books{path}" copy-unmatched-params="true" />
  </inbound>
</policies>

set-backend-service points the request at a named backend entity instead of whatever URL the imported OpenAPI spec recorded; rewrite-uri then adjusts the path APIM forwards, letting a public-facing path (/catalog/books/{isbn}) map onto a differently shaped internal one (/api/v1/books/{isbn}) without catalog-api or its clients needing to agree on a single path shape.

set-header

<policies>
  <outbound>
    <base />
    <set-header name="X-Powered-By" exists-action="delete" />
    <set-header name="X-Correlation-Id" exists-action="override">
      <value>@(context.RequestId.ToString())</value>
    </set-header>
  </outbound>
</policies>

Bookshelf strips backend-identifying response headers (exists-action="delete") and stamps every response with APIM’s own request ID for correlation across catalog-api/orders-api logs and client bug reports.

Derived from Set HTTP header policy.

[[Named values]] == Named values with Key Vault-backed secrets

A named value is a reusable, policy-referenceable \{{name}\} placeholder — a plain string, or a secret backed directly by an Azure Key Vault secret rather than stored in APIM at all. Bookshelf keeps `orders-api’s partner-webhook signing key as a Key Vault-backed named value, so no policy XML or exported configuration ever contains the raw secret value, matching the pattern on Key Vault and Secrets.

APIM’s system-assigned (or a chosen user-assigned) managed identity needs Key Vault Secrets User on kv-bookshelf before a Key-Vault-backed named value will resolve; the identity is enabled once, then reused for every named value.

In the Azure portal

  1. APIs → Named values → + Add.

  2. Name orders-webhook-signing-key; toggle Secret; Value → Key Vault; select kv-bookshelf and the secret; confirm the managed identity offered has (or is granted) Key Vault Secrets User on the vault.

  3. Reference it from a policy as \{{orders-webhook-signing-key}\}.

With the Azure CLI

az apim nv create only takes a literal --value (optionally marked --secret); it has no parameter for a Key Vault reference. The identity and role assignment below use the CLI, and the Key Vault-backed named value itself is created with az rest against the Microsoft.ApiManagement/service/namedValues REST API, setting properties.keyVault.secretIdentifier. identityClientId is left out so APIM uses its system-assigned identity (set it to a user-assigned identity’s client ID to use that one instead). The secret URI is unversioned, so APIM picks up a rotated secret automatically.

az apim update \
  --name apim-bookshelf \
  --resource-group "$RG" \
  --set identity.type=SystemAssigned

az role assignment create \
  --assignee-object-id "$(az apim show --name apim-bookshelf --resource-group "$RG" --query identity.principalId -o tsv)" \
  --assignee-principal-type ServicePrincipal \
  --role "Key Vault Secrets User" \
  --scope "$(az keyvault show --name kv-bookshelf --query id -o tsv)"

az rest \
  --method put \
  --url "https://management.azure.com${APIM_ID}/namedValues/orders-webhook-signing-key?api-version=2024-05-01" \
  --body '{
    "properties": {
      "displayName": "orders-webhook-signing-key",
      "secret": true,
      "keyVault": {
        "secretIdentifier": "https://kv-bookshelf.vault.azure.net/secrets/orders-webhook-signing-key"
      }
    }
  }'

Backends

A backend is an entity that records a target URL plus its own TLS/credential settings and, optionally, a circuit breaker — referenced from a policy with set-backend-service instead of an inline URL, so a backend move (a new catalog-api revision’s URL, a cutover to the AKS-hosted deployment) is a single entity update rather than an edit to every policy that used the old URL.

In the Azure portal

  1. APIs → Backends → + Add.

  2. Name catalog-api-backend, Runtime URL the catalog-api Container Apps ingress FQDN (Container Apps); enable a circuit breaker (trip after N consecutive 5xx, cool down before retrying) so a catalog-api outage fails fast instead of piling up slow calls at the gateway.

  3. A second backend, catalog-api-aks-backend, instead points at the Kubernetes Service/ingress FQDN exposing the same API on aks-bookshelf (Deploying Microservices on AKS) — useful for a staged cutover between the two hosting options, switching which backend a policy’s set-backend-service targets rather than re-importing the API.

With the Azure CLI

az apim backend create \
  --resource-group "$RG" \
  --service-name apim-bookshelf \
  --backend-id catalog-api-backend \
  --url "https://$(az containerapp show --name catalog-api --resource-group "$RG" --query properties.configuration.ingress.fqdn -o tsv)" \
  --protocol http

Versions and revisions

Revisions are non-breaking, work-in-progress copies of an existing API: create one, edit it (a policy change, a new operation) without affecting the currently published traffic, test it against a ;rev=2-suffixed URL, and either make it current (traffic cuts over) or discard it. Versions are for breaking changes the API’s own consumers must opt into deliberately — v1 and v2 coexist as separately callable API entities, distinguished by a versioning scheme (path segment, query parameter, or request header), each with its own revisions.

Bookshelf uses a revision for a routine catalog-api policy tweak (say, tightening the cache TTL) and reserves a new version for the day `orders-api’s public contract actually changes shape (a renamed field, a removed operation).

In the Azure portal

  1. Open the API → the Revisions dropdown next to its name → + Add revision; edit the new revision; Test it at its own URL suffix; Make current when ready.

  2. To add a version instead: + Add version on the API’s list entry; choose a versioning scheme (Path is the common default: /v2/catalog/…​); the new version starts as a copy and evolves independently.

With the Azure CLI

az apim api revision create \
  --resource-group "$RG" \
  --service-name apim-bookshelf \
  --api-id catalog-api \
  --api-revision 2 \
  --api-revision-description "Tighten cache TTL to 30s"

# ... test at .../catalog;rev=2/books, then promote it:
az apim api release create \
  --resource-group "$RG" \
  --service-name apim-bookshelf \
  --api-id catalog-api \
  --api-revision 2 \
  --notes "Promote revision 2 to current"

[[Self-hosted gateway on AKS]] == Self-hosted gateway on AKS

The managed gateway above always runs inside Azure. A self-hosted gateway is the same gateway runtime, packaged as a Linux container, deployed wherever the API traffic actually needs to be handled — on aks-bookshelf for Bookshelf, so calls between services already running in that cluster never leave it to reach an Azure-hosted gateway and back. It still reports to, and is configured entirely from, `apim-bookshelf’s management plane: policies, APIs, and products are authored once and pushed to every gateway, managed or self-hosted, that serves them.

The self-hosted gateway is Developer or Premium tier only (single node on Developer, unlimited on Premium; see Feature matrix) and is deployed to a Kubernetes cluster via its official Helm chart, authenticating back to apim-bookshelf with a gateway token generated from the instance.

In the Azure portal

  1. apim-bookshelf → Deployment and infrastructure → Self-hosted gateways → + Add: name aks-bookshelf-gateway, a location label, and the APIs it should serve (catalog-api, orders-api).

  2. Open the new gateway → Deployment: adjust the token Expiry (30 days at most) and Secret key, select Generate, and note the Token and Configuration URL for the Helm install below.

With az rest and Helm

The Azure CLI has no az apim gateway command group, so the gateway resource and its access token come from the Microsoft.ApiManagement/service/gateways REST API via az rest (reusing APIM_ID from Products and subscriptions). A gateway token lasts at most 30 days. The configuration URL the pod connects to is the Configuration API v2 endpoint, https://<apim-name>.configuration.azure-api.net.

# Create the self-hosted gateway resource on apim-bookshelf
az rest \
  --method put \
  --url "https://management.azure.com${APIM_ID}/gateways/aks-bookshelf-gateway?api-version=2024-05-01" \
  --body '{
    "properties": {
      "description": "Self-hosted gateway on aks-bookshelf",
      "locationData": {"name": "aks-bookshelf"}
    }
  }'

# Serve catalog-api from it
az rest \
  --method put \
  --url "https://management.azure.com${APIM_ID}/gateways/aks-bookshelf-gateway/apis/catalog-api?api-version=2024-05-01" \
  --body '{"properties": {"provisioningState": "created"}}'

# Generate a gateway token (30 days is the maximum expiry)
GATEWAY_TOKEN="$(az rest \
  --method post \
  --url "https://management.azure.com${APIM_ID}/gateways/aks-bookshelf-gateway/generateToken?api-version=2024-05-01" \
  --body '{"keyType": "primary", "expiry": "'"$(date -u -d '+30 days' +%Y-%m-%dT%H:%M:%SZ)"'"}' \
  --query value -o tsv)"

helm repo add azure-apim-gateway https://azure.github.io/api-management-self-hosted-gateway/helm-charts/
helm repo update

helm upgrade --install aks-bookshelf-gateway azure-apim-gateway/azure-api-management-gateway \
  --namespace bookshelf \
  --set gateway.configuration.uri="https://apim-bookshelf.configuration.azure-api.net" \
  --set gateway.auth.key="GatewayKey ${GATEWAY_TOKEN}"

A self-hosted gateway on AKS is one concrete way to implement the general API-gateway/BFF pattern described on API Gateway and Backends for Frontends with a managed product rather than a hand-rolled Spring Cloud Gateway/YARP deployment; the broader question of which gateway/secret-manager/API-manager combination Bookshelf should standardize on across its Container Apps and AKS footprints is tracked separately in issue #184, not settled by this page.

Observability via Application Insights

apim-bookshelf sends request telemetry — dependency calls, response times, failed requests, and Ocp-Apim-Trace-Location-driven detailed traces when explicitly enabled per call — to an Application Insights resource, the same one catalog-api and orders-api already report to if they use Spring Cloud Azure’s auto-instrumentation, giving a single distributed trace across the gateway hop and both backends.

In the Azure portal

  1. APIs → Application Insights (under Diagnostic Logs, or per-API under Settings) → connect an existing Application Insights resource; a sampling percentage controls telemetry volume/cost.

  2. Per API, Settings → Diagnostics → enable the Application Insights logger for that API specifically, or rely on the instance-wide default.

The portal connection above stores an Application Insights instrumentation key in the logger. Support for instrumentation-key ingestion ended on 31 March 2025, so Bookshelf connects with a connection string instead, which the portal can’t configure yet.

With az rest

The Azure CLI has no az apim logger or az apim api diagnostic commands. The logger (Microsoft.ApiManagement/service/loggers) and the per-API diagnostic (Microsoft.ApiManagement/service/apis/diagnostics) are created with az rest (reusing APIM_ID from Products and subscriptions). The logger authenticates with the Application Insights connection string plus APIM’s system-assigned identity, which needs the Monitoring Metrics Publisher role on appi-bookshelf.

APPI_ID="$(az monitor app-insights component show --app appi-bookshelf --resource-group "$RG" --query id -o tsv)"
APPI_CONNECTION_STRING="$(az monitor app-insights component show --app appi-bookshelf --resource-group "$RG" --query connectionString -o tsv)"

az role assignment create \
  --assignee-object-id "$(az apim show --name apim-bookshelf --resource-group "$RG" --query identity.principalId -o tsv)" \
  --assignee-principal-type ServicePrincipal \
  --role "Monitoring Metrics Publisher" \
  --scope "$APPI_ID"

az rest \
  --method put \
  --url "https://management.azure.com${APIM_ID}/loggers/appinsights-bookshelf?api-version=2024-05-01" \
  --body '{
    "properties": {
      "loggerType": "applicationInsights",
      "resourceId": "'"$APPI_ID"'",
      "credentials": {
        "connectionString": "'"$APPI_CONNECTION_STRING"'",
        "identityClientId": "SystemAssigned"
      }
    }
  }'

az rest \
  --method put \
  --url "https://management.azure.com${APIM_ID}/apis/catalog-api/diagnostics/applicationinsights?api-version=2024-05-01" \
  --body '{
    "properties": {
      "loggerId": "'"$APIM_ID"'/loggers/appinsights-bookshelf",
      "alwaysLog": "allErrors",
      "sampling": {"samplingType": "fixed", "percentage": 100}
    }
  }'

Networking

By default every managed-gateway tier is reachable over the public internet; Developer/Basic/Standard/Standard v2 add private-endpoint support for inbound traffic, and Premium/Premium v2 additionally support full VNet injection so the gateway itself gets a private IP inside vnet-bookshelf with no public inbound path at all (Feature matrix). Standard v2 and Premium v2 separately support outbound VNet integration purely to reach network-isolated backends (cae-bookshelf on an internal-only Container Apps environment, `aks-bookshelf’s internal load balancer) while keeping the gateway’s own inbound side public — the two are independent settings, not the same feature at two price points.

Scaling

Classic Developer/Basic/Standard/Premium scale in whole units, each unit a fixed capacity increment, added or removed manually or via az apim scale / an Azure Monitor autoscale rule watching the Capacity metric; v2 tiers instead scale faster, in the same unit model, up to 10 units (Basic v2/Standard v2) or 30 units (Premium v2). Consumption scales automatically with no unit concept at all, billed per million calls instead. Multi-region deployment (classic Premium only) adds whole additional regions, each independently scaled and each able to serve gateway traffic, backed by the same configuration; Bookshelf’s single-region apim-bookshelf does not use this.

Pricing model

Consumption bills per call (roughly the first million calls per month free, then per-million after); classic and v2 tiers bill per unit per hour regardless of call volume, with v2 units additionally bundling a monthly included call allowance before per-million overage charges apply. Storage, bandwidth, and the external-cache option are priced separately from the base unit/call charge. Current rates, which change independently of anything on this page, are on the API Management pricing page; that page — not this one — is the source of truth for sizing a real budget.

Soft delete and purge

Deleting apim-bookshelf (from the portal, the CLI, or the REST API on API version 2020-06-01-preview or later) does not remove it immediately: like Key Vault (Key Vault and Secrets), it moves to a soft-deleted state, and its name stays reserved. Left soft-deleted for more than 48 hours without being recovered or purged, it is purged automatically; purging explicitly frees the name immediately instead of waiting out that window.

With the Azure CLI

# List soft-deleted instances in a region
az apim deletedservice list --output table

# Purge one immediately instead of waiting the 48-hour window
az apim deletedservice purge --service-name apim-bookshelf --location "$LOCATION"

Troubleshooting

  • A caller gets 401 Access denied due to missing subscription key — the request omitted Ocp-Apim-Subscription-Key (or subscription-key), or used a key for a subscription that does not include the called product; check the product/subscription mapping under Products and subscriptions, not the validate-jwt policy, which is a separate, additional check.

  • validate-jwt rejects a token that looks valid — the token’s aud or iss claim does not match the policy’s <audiences>/<issuers> exactly (a trailing slash or v1.0 vs. v2.0 endpoint mismatch is the most common cause), or the signing-key cache is stale after an unusual Entra ID key rollover; APIM’s Trace tool (developer portal Test console, or Ocp-Apim-Trace-Location header) shows exactly which claim failed.

  • A v2-tier deployment fails or a feature silently does not apply — confirm it is not one of the currently-unavailable-in-v2 features (multi-region, self-hosted gateway, backup/restore, Git-based configuration) rather than a misconfiguration; the v2 tiers overview page keeps the current list, since it changes as v2 catches up to classic feature-for-feature.

  • A named value stays empty or a policy referencing it fails — almost always the managed identity’s Key Vault role assignment is missing or scoped to the wrong vault; re-check [Named values with Key Vault-backed secrets], and confirm the identity type (system- vs. user-assigned) the named value was created with still exists and is enabled on the APIM instance.

  • A self-hosted gateway shows unhealthy or stops receiving configuration updates — the gateway token expired (they are time-limited, per [Self-hosted gateway on AKS]) or the pod cannot reach the configuration endpoint from inside aks-bookshelf’s network; `kubectl logs on the gateway pod surfaces the specific connection or auth error, and a new token from the generateToken az rest call (or the portal’s Deployment blade) resolves an expiry.

  • catalog-api calls succeed directly but fail through APIM — check the backend entity’s URL and circuit breaker state first (Backends): a tripped circuit breaker fails fast with a 5xx that looks like an APIM problem but is really the backend’s own recent failure rate.

Clean up

az group delete --name "$RG" --yes --no-wait

# apim-bookshelf survives resource-group deletion in its soft-deleted state for up to 48 hours --
# purge it explicitly to free the name immediately
az apim deletedservice purge --service-name apim-bookshelf --location "$LOCATION"
  • Key Vault and Secrets covers Key Vault itself, including the soft-delete/purge model APIM instances follow the same way, and the RBAC roles a named value’s managed identity needs.

  • Identity and Access covers Microsoft Entra ID, app registrations, and token acquisition in depth; this page only covers the validate-jwt policy that checks the resulting token.

  • AKS Clusters and Deploying Microservices on AKS cover aks-bookshelf cluster setup and workload identity in more depth than the self-hosted-gateway-focused summary above.

  • Container Apps covers cae-bookshelf and the catalog-api/orders-api deployments that APIM’s backends point at.

  • API Gateway and Backends for Frontends covers the general API gateway/BFF pattern APIM is one managed implementation of.

References