Key Points
- Azure API Management (APIM) is worth the setup cost the moment you'd otherwise write custom gateway code for auth, rate limiting, or protocol translation — its policy engine replaces that code with declarative XML.
- The Azure CLI's
az apimcommand group covers instances, APIs, operations, products, and version sets — but has no command for setting a policy document, a real gap in the CLI module. The workaround isaz restagainst the ARM REST API directly, exactly what Azure's own CLI maintainers point to when asked. - Non-Consumption APIM tiers take up to 40 minutes to provision. Plan your CI pipeline and your patience around that, not around how fast
az apim createreturns. - Consumption tier looks free-tier attractive but has no SLA on the developer portal — treat Standard or Standard v2 as the real production floor.
Prerequisites
- CLI/SDK version tested against:
az-cli 2.6x.x(Azure CLI'sapimextension ships in-box on current 2.6x releases — runaz versionto confirm) - An Azure subscription with
Microsoft.ApiManagementresource provider registered (az provider register --namespace Microsoft.ApiManagement) - A backend HTTP service to import — the examples below use a placeholder
https://backend.example.com
Introduction
Most gateway comparisons put Azure APIM next to AWS API Gateway and Kong and stop at a feature checklist. That's the wrong first question. The right one is: are you about to write custom code to do something a policy engine already does declaratively? If the answer is yes — rate limiting per subscription key, JWT validation before the request reaches your backend, REST-to-SOAP translation for a legacy service — APIM's policy engine is doing real work for you, not just adding a hop.
I've stood up APIM for an enterprise client that needed exactly this: fifty internal APIs, one gateway, centrally enforced auth and rate limiting without touching a single backend service's code. The CLI got most of the way there cleanly. Then I hit the one thing nobody warns you about: there's no az apim policy set command. It doesn't exist. You'll go looking for it, because every other resource in APIM has an obvious CLI verb, and this one just... doesn't.
This article builds a real APIM instance end to end from the terminal — instance, API import, and a working policy document — and shows you the actual workaround for the missing piece.
What APIM Actually Is
APIM is not a thin proxy. It's three components that happen to share one Azure resource:
Diagram: the gateway, the management plane, and the developer portal are three separable concerns inside one APIM instance.
The policy engine is the part that actually replaces custom code. Policies are XML documents that run in a defined order — inbound, backend, outbound, on-error — and attach at four scopes: global, product, API, or a single operation. A five-line policy can do what would otherwise be a Lambda-equivalent function on AWS or a custom middleware layer anywhere else.
Standing Up the Instance
az group create --name rg-apim-demo --location eastus
# Non-Consumption tiers take up to 40 minutes to provision.
# Consumption tier (sku-name Consumption) provisions in under 5.
az apim create \
--name apim-demo-001 \
--resource-group rg-apim-demo \
--publisher-email platform-team@example.com \
--publisher-name "Example Platform Team" \
--sku-name Developer \
--no-wait
--no-wait matters here. If you're scripting this in CI, don't block the pipeline on a 40-minute provision — poll with az apim wait --created on a separate step, or trigger the API import as a follow-up job once provisioning completes.
az apim wait --created \
--name apim-demo-001 \
--resource-group rg-apim-demo \
--timeout 3000
Importing an API
az apim api import \
--resource-group rg-apim-demo \
--service-name apim-demo-001 \
--api-id orders-api \
--path orders \
--display-name "Orders API" \
--service-url https://backend.example.com \
--specification-format OpenApi \
--specification-url https://backend.example.com/openapi.json \
--subscription-required true
This gets you a routable API in front of your backend, with subscription-key enforcement on by default. What it doesn't get you is anything from the policy engine — no rate limiting, no JWT validation, no header rewriting. For that, keep reading.
The Policy Gap — and the Workaround
Every other APIM resource has a matching az apim verb: az apim api create, az apim product create, az apim api operation create. Look for the policy equivalent and you won't find one — az apim api's subcommand list covers create/update/delete/export/import/operation/release/revision/schema/versionset, and stops there. No policy.
Diagram: decision path once you hit the policy gap in the CLI.
The documented-nowhere-obvious answer, confirmed by Azure CLI maintainers on the azure-cli GitHub repo when the same question was raised: call the ARM REST API directly with az rest.
# Write the policy document to a file first.
cat > policy.xml << 'EOF'
<policies>
<inbound>
<base />
<rate-limit-by-key calls="100" renewal-period="60"
counter-key="@(context.Subscription.Id)" />
<set-header name="X-Forwarded-By" exists-action="override">
<value>apim-demo-001</value>
</set-header>
</inbound>
<backend><base /></backend>
<outbound><base /></outbound>
<on-error><base /></on-error>
</policies>
EOF
# Wrap the XML in the JSON body the ARM policy resource expects.
POLICY_XML=$(cat policy.xml | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')
az rest --method put \
--uri "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/rg-apim-demo/providers/Microsoft.ApiManagement/service/apim-demo-001/apis/orders-api/policies/policy?api-version=2022-08-01" \
--body "{\"properties\":{\"format\":\"xml\",\"value\":${POLICY_XML}}}"
That single az rest call does what a Set-AzApiManagementPolicy PowerShell cmdlet or a portal paste into the policy editor would otherwise do — and it's the only way to keep a policy deployment in a bash-based CI pipeline without shelling out to PowerShell Core as a second runtime dependency.
Verify it landed:
az rest --method get \
--uri "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/rg-apim-demo/providers/Microsoft.ApiManagement/service/apim-demo-001/apis/orders-api/policies/policy?api-version=2022-08-01" \
--query 'properties.value' --output tsv
Tier Comparison
| Tier | Approx. price | SLA | Notes |
|---|---|---|---|
| Consumption | Pay-per-call, 1M free/month | No SLA on developer portal | Fastest to provision (minutes), but skip it for anything customer-facing |
| Developer | ~$50/month | None — dev/test only | Use for exactly what the name says |
| Basic | ~$150/month | Yes, limited scale | Entry production tier |
| Standard | ~$700/month | Yes, full policy engine | Practical production floor for most teams |
| Standard v2 | Comparable to Standard | Yes | Adds network-isolated backend support without Premium's full VNET cost |
| Premium | ~$2,700+/month per unit | 99.95%, multi-region | Multi-region active-active, VNET injection, availability zones |
I default clients to Standard or Standard v2 for production. Consumption's lack of a developer-portal SLA and reduced policy feature set makes it a poor fit the moment external partners depend on your API catalog being reliably browsable — which is the entire point of having a developer portal in the first place.
Common Mistakes
Mistake 1: Assuming az apim is a complete CLI surface
It covers structural resources well. It does not cover policies. Budget time to discover and use az rest before you're blocked on a deploy.
Mistake 2: Blocking a CI pipeline on az apim create without --no-wait
Non-Consumption tiers take up to 40 minutes. A pipeline step with a default timeout will fail long before provisioning completes if you don't split create and wait into separate steps.
Mistake 3: Choosing Consumption tier for a customer-facing API catalog
The missing developer-portal SLA isn't a rounding-error limitation — if your onboarding flow depends on partners self-serving through the portal, an unreliable portal is a support-ticket generator.
Mistake 4: Writing policy XML by hand in the portal and never committing it to source control
The az rest path above works precisely because it turns policy deployment into something you can put in a repo and a pipeline. Portal-only policy edits are invisible to code review and impossible to diff.
Production Considerations
Performance: Policy chain complexity adds latency — a chain with JWT validation, rate limiting, and header manipulation adds measurably more than a single rate-limit-by-key policy alone. Profile with Application Insights before assuming the policy engine is "free."
Security: Pair APIM with Azure AD (Entra ID) for OAuth flows rather than rolling your own JWT validation policy from scratch — validate-jwt with an openid-config reference is less code to maintain than a custom Lambda-equivalent would be on another cloud.
Cost: The gap between Consumption and Standard is real money, but the gap between under-provisioning Standard and needing Premium's VNET injection for compliance is bigger. Confirm your network isolation requirements before committing to a tier.
Monitoring: Enable Application Insights integration at creation time, not after. Track Backend Duration separately from Gateway Duration — this is how you tell whether a slow response is APIM's policy chain or the backend service itself.
Full Example: Policy Deployment Script
#!/usr/bin/env bash
set -euo pipefail
RG="${RG:-rg-apim-demo}"
SERVICE="${SERVICE:-apim-demo-001}"
API_ID="${API_ID:-orders-api}"
SUBSCRIPTION_ID=$(az account show --query id --output tsv)
deploy_policy() {
local policy_file="$1"
local policy_xml
policy_xml=$(python3 -c "import json,sys; print(json.dumps(open('$policy_file').read()))")
az rest --method put \
--uri "https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RG}/providers/Microsoft.ApiManagement/service/${SERVICE}/apis/${API_ID}/policies/policy?api-version=2022-08-01" \
--body "{\"properties\":{\"format\":\"xml\",\"value\":${policy_xml}}}"
echo "Deployed policy from $policy_file to $API_ID"
}
deploy_policy "policy.xml"
Full source with a matching teardown script: GitHub →
cloud-apis/azure-api-management-policies/
Conclusion
APIM earns its keep the moment its policy engine replaces code you'd otherwise write and maintain yourself — auth validation, rate limiting, protocol translation. Standard or Standard v2 is the right production tier for nearly everyone; Consumption's missing portal SLA makes it a false economy for anything customer-facing. And when the CLI's az apim command group leaves you stuck on policies specifically, that's not a sign you did something wrong — it's a real, if quietly undocumented, gap that az rest closes in one call.
Further Reading
- az apim — Microsoft Learn CLI reference
- Feature-based comparison of Azure API Management tiers
- Azure API Management — V2 Tiers
- API Management pricing
- Quickstart: Create an APIM instance using the Azure CLI
If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.
Bry Writes Code — cloud and API infrastructure specialist. Standing up an enterprise API program on Azure? Get in touch.


Top comments (0)