Backend Integration Patterns
Why this matters
Until now your backend has been a public Container App FQDN — convenient, but it means anyone who discovers that FQDN can bypass the gateway entirely, defeating the point of putting APIM in front. The secure end state is a private backend that only APIM can reach, over VNet integration — using the exact internal Container Apps Environment and private DNS concepts from track 06. This module also covers routing to multiple backends: named backends, load balancing, and backend pools with circuit-breaker behavior — the gateway-edge cousin of the resilience patterns you met in the service mesh (track 13).
Concepts
Named backends: backends as first-class, reusable objects
So far you've set a backend by stuffing a URL into --service-url on the API. A
named backend promotes that URL into its own reusable resource with an id,
credentials, TLS settings, and (later) health/circuit-breaker config. Instead of
repeating a URL across APIs, you define backend orders-be once and reference it
from a policy with set-backend-service backend-id="orders-be". This matters
because everything richer — load balancing, pools, per-backend auth, circuit
breakers — hangs off named backends, not off the inline serviceUrl string.
Think of it as the difference between hardcoding an IP and defining a Service in
Kubernetes: the named object is what you attach behavior to.
VNet integration: reaching a private backend
To reach an internal backend, APIM has to be on the network. Two models, mirroring what you learned about Container Apps environments in track 06:
- VNet injection (Developer/Premium tiers) — the whole APIM gateway is deployed into a subnet in your VNet, so it can route to private IPs directly. Powerful, but only on classic tiers and it's the more involved setup.
- Private/VNet connectivity via internal mode / private endpoints — depending on tier, APIM connects to backends over private networking so traffic never traverses the public internet.
The target this module fronts is an internal-only Container Apps Environment (track 06 module 04): its apps resolve to a private IP via a private DNS zone linked to the VNet. For APIM to reach it, APIM must be in (or peered to) that VNet and able to resolve that private DNS zone — the same private-DNS-linked-to-VNet requirement from track 06. When it can't resolve or route, you get 503s at the gateway, which is the diagnose-and-fix below.
Load balancing across multiple backends (backend pools)
A backend pool is a named backend that fronts several real backends and distributes requests across them — for scale, for blue/green, or for multi-region. APIM supports weighted distribution (send 90% to A, 10% to B for a canary) and priority groups (prefer group 1; fall to group 2 if all are down). This is the gateway doing L7 load balancing at the product edge — conceptually the same "spread traffic across replicas/versions" you saw with Kubernetes Services and with track 06 revision traffic-splitting, but decided by APIM per request rather than by a Service's kube-proxy.
Circuit breaker and health: failing fast (cousin of track 13)
A named backend can carry a circuit breaker: rules that trip the backend "open" (stop sending traffic to it) after a threshold of failures in a window, then probe and reset it when it recovers. While open, requests to that backend fail fast (or route to a healthy pool member) instead of piling onto a struggling service. If that sounds familiar, it's because it's the same resilience idea as the service mesh in track 13 (outlier detection / circuit breaking between services) — just applied north-south at the gateway rather than east-west in the mesh. The mental model transfers directly: don't hammer a sick backend; shed load and let it recover.
Backend authentication and header handling
A private backend still needs to trust the caller. APIM can present credentials to
the backend per named backend — a managed identity token (the same
system/user-assigned identity model from track 06 module 06), a client
certificate, or a static header/key. And because APIM terminates the client's TLS
and opens a new connection to the backend, you control which headers pass
through: forward a correlation id, inject an identity token for the backend,
strip the client's Authorization if the backend shouldn't see it. This is the
"APIM opens a fresh backend connection" consequence of the module-00 request path —
the backend leg is a separate request you shape.
Command reference
| Command | What it does | Example |
|---|---|---|
az apim backend create |
Define a named backend | az apim backend create --resource-group rg --service-name <inst> --backend-id orders-be --url https://orders.internal... --protocol http |
az apim backend show |
Inspect a named backend | az apim backend show --resource-group rg --service-name <inst> --backend-id orders-be -o jsonc |
az apim api policy create |
Attach set-backend-service / LB policy |
az apim api policy create --resource-group rg --service-name <inst> --api-id orders --policy-format xml --value "@backend.xml" |
az network vnet create |
Create the VNet APIM/backend share (track 06/2) | az network vnet create --name vnet-apim --resource-group rg --address-prefix 10.0.0.0/16 --subnet-name apim --subnet-prefix 10.0.1.0/24 |
az containerapp env create (internal) |
Internal-only backend Environment (track 06) | ... --infrastructure-subnet-resource-id <id> --internal-only true |
Flag-by-flag and XML breakdowns:
az apim backend create --resource-group rg-apim-m05 --service-name <inst> --backend-id orders-be --url https://orders.<internal-env>.azurecontainerapps.io --protocol http
--backend-id orders-be— the reusable id you reference from policies.--url— the backend's address (a private FQDN once you go internal).--protocol http— how APIM speaks to it (http/soap).
Route to a named backend (inbound policy):
<inbound>
<base />
<set-backend-service backend-id="orders-be" />
</inbound>
set-backend-service backend-id="orders-be"— overrides the API's default backend for this request, routing to the named backend (and thus its pool/ circuit-breaker config).
Weighted load balancing across a pool (concept; pool defined as a backend of
type Pool referencing members with weight/priority):
weight— relative share of traffic (90/10 for a canary).priority— failover order; higher-priority group is used until all its members are down, then the next group takes over (the "prefer A, fall back to B" pattern).
Circuit-breaker rule (on a named backend): a rule with a failure threshold, a window, and a trip duration — e.g. "trip if >50% of calls fail over 1 minute; stay open 30s before probing." While open, the backend is skipped/fails fast, mirroring track 13's outlier detection.
Hands-on exercises
Time note: this module needs VNet integration, which requires a Developer-tier (classic) instance — Consumption cannot join a VNet. That means a 30-45 minute provisioning wait and a continuously-billing instance. Kick off the
az apim createfirst, do the networking setup while it provisions, and delete the instance the same day (final cleanup step). If you want to avoid the classic-tier cost entirely, do exercises 1-4 (named backends, load balancing, circuit-breaker config) on Consumption with a public backend, and treat the VNet steps (5-8) as read-and-reason.
Kick off the (slow) Developer instance, then build the network while it provisions.
az group create --name rg-apim-m05 --location eastus $apim = "apimm05$((Get-Random -Max 99999))" az apim create --name $apim --resource-group rg-apim-m05 --location eastus ` --publisher-name "You" --publisher-email you@example.com --sku-name Developer --no-wait # ^ returns immediately; provisioning continues ~30-45 min. Build the network now: az network vnet create --name vnet-apim --resource-group rg-apim-m05 ` --address-prefix 10.0.0.0/16 --subnet-name aca-infra --subnet-prefix 10.0.0.0/23 az network vnet subnet create --resource-group rg-apim-m05 --vnet-name vnet-apim ` --name apim-subnet --address-prefix 10.0.4.0/24Create an internal-only Container Apps Environment (track 06 module 04).
$subnet = az network vnet subnet show --resource-group rg-apim-m05 --vnet-name vnet-apim --name aca-infra --query id -o tsv az containerapp env create --name env-m05i --resource-group rg-apim-m05 --location eastus ` --infrastructure-subnet-resource-id $subnet --internal-only true az containerapp create --name orders --resource-group rg-apim-m05 --environment env-m05i ` --image mcr.microsoft.com/k8se/quickstart:latest --target-port 80 --ingress external $ifqdn = az containerapp show --name orders --resource-group rg-apim-m05 --query properties.configuration.ingress.fqdn -o tsv$ifqdnresolves to a private IP — unreachable from your laptop by design (track 06 module 04). That's the point.Define a named backend (public case first, to learn the object). While the classic instance may still be provisioning, practice the concept against a throwaway Consumption instance or wait — then:
az apim backend create --resource-group rg-apim-m05 --service-name $apim ` --backend-id orders-be --url "https://$ifqdn" --protocol http az apim backend show --resource-group rg-apim-m05 --service-name $apim --backend-id orders-be -o jsoncPoint an API at the named backend. Create an
ordersAPI and attach an inbound policy<set-backend-service backend-id="orders-be" />(save asbackend.xml, apply withaz apim api policy create ...). This routes the API through the named backend rather than an inlineserviceUrl.Integrate APIM into the VNet. Once the Developer instance shows
provisioningState=Succeeded, place it in theapim-subnet(viaaz apim update/portal VNet configuration for the classic tier). Ensure the subnet and the internal env's private DNS zone are linked so APIM can resolve$ifqdn. (This mirrors track 06 module 04's private-DNS-linked-to-VNet requirement exactly.)Call through the gateway to the private backend.
$key = az apim subscription show --resource-group rg-apim-m05 --service-name $apim --sid master --query primaryKey -o tsv $gw = az apim show --name $apim --resource-group rg-apim-m05 --query gatewayUrl -o tsv curl -H "Ocp-Apim-Subscription-Key: $key" "$gw/orders/"A 200 proves APIM reached a backend your laptop cannot reach directly — the secure topology: public only at the gateway, private everywhere behind it.
Diagnose and fix: 503s from a backend VNet misconfiguration. Deliberately break resolution/routing: unlink the internal env's private DNS zone from the APIM subnet's VNet (or point the named backend at the private FQDN before linking DNS). Call the gateway:
curl -i -H "Ocp-Apim-Subscription-Key: $key" "$gw/orders/"Expect 503 Service Unavailable (or a backend-connection error) — the key passed (no 401), the API matched, but APIM can't reach/resolve the private backend. Diagnose: confirm APIM is in the VNet, the private DNS zone for the env's domain is linked to that VNet, and the subnet can route to the env's subnet. Fix: (re)link the private DNS zone to APIM's VNet so
$ifqdnresolves to the private IP, and confirm routing; re-curlfor 200. Lesson: a 503 after a passing key is a backend reachability problem — DNS/routing — not an auth or path problem (contrast module 01's 404 and module 04's 401).Load-balancing / circuit-breaker reasoning. Define a second backend (
orders-be-2) and (conceptually, or via a Pool backend) describe an 80/20 weighted split and a circuit-breaker rule that tripsorders-beafter >50% failures in 1 minute. Write down: which track-06 pattern the weighted split resembles (revision traffic-splitting) and which track-13 pattern the circuit breaker resembles (outlier detection / circuit breaking). You don't need to simulate real failures — the goal is to map the config to concepts you know.Cleanup — do this the same day (Developer tier bills continuously).
az group delete --name rg-apim-m05 --yes --no-waitThen verify nothing lingers:
az apim list -o tableshould not show the instance once deletion completes (classic-tier deletes are also slow).
Independent challenge
Build the secure topology end to end: an internal-only Container Apps
Environment hosting a backend that is unreachable from your laptop, a
Developer-tier APIM integrated into the same VNet, a named backend (not an
inline serviceUrl) pointing at the private FQDN, and a successful authenticated
call through the gateway to that private backend. Then reproduce the 503 VNet
misconfiguration (break DNS linkage or routing), capture the exact symptom, and
fix it — narrating why a 503-after-passing-key is categorically different from
module 01's 404 and module 04's 401. This draws on track 06 module 04
(internal environments, private DNS) and module 04 here (the auth layer still
applies in front of the private backend). Because this uses a classic tier,
tear the resource group down the same day and confirm the APIM instance is
gone.
Stuck? One hint
The private backend only works if APIM can resolve its FQDN to the private IP
and route to it. That resolution is the same track-06-module-04 mechanism: a
private DNS zone named after the environment's domain, linked to the VNet
APIM sits in. List zones with az network private-dns zone list and their VNet
links with az network private-dns link vnet list. A 503 with a passing key is
almost always "APIM can't resolve or reach the backend" — start at DNS linkage.
Common mistakes & troubleshooting
- Leaving the backend public. If the backend keeps external ingress, callers can bypass APIM entirely. The secure pattern is internal-only backend + APIM as the only public entry.
- Using Consumption for VNet integration. Consumption can't join a VNet — VNet integration needs Developer/Premium. Trying it on Consumption fails; that's a tier limitation, not a bug.
- Unlinked private DNS zone → 503. The classic track-06 mistake resurfaces: APIM can't resolve the internal FQDN unless the env's private DNS zone is linked to APIM's VNet. Symptom is 503/connection errors after a passing key.
- Inline
serviceUrlwhere you needed a named backend. Load balancing, pools, circuit breakers, and per-backend credentials all require a named backend; they can't hang off an inline URL. - Misreading the status code. 401 = auth (module 04); 404 = path/backend mapping (module 01); 503 = backend reachability (this module). Each points at a different layer.
- Cost pitfall — the classic-tier trap. This is the module most likely to
leave a Developer-tier instance running because it's slow to create and you
step away. It bills continuously. Set a reminder and delete the resource group
the same day; verify with
az apim list -o table.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
- Why is a public backend behind APIM a security problem, and what's the secure alternative?
- What is a named backend, and what capabilities require one rather than an
inline
serviceUrl? - Why can't you use the Consumption tier for VNet integration, and which tiers can?
- What track-06 mechanism must be in place for APIM to reach an internal Container Apps backend by its FQDN?
- You call the gateway, the key is accepted, and you get a 503. Which layer is the problem, and what do you check first?
- What is a backend pool, and give one use for weighted distribution.
- Which track-13 pattern is APIM's circuit breaker the north-south cousin of, and what does "tripping open" do?
- How can APIM authenticate to a private backend without a stored secret?
Show answers
- A public backend can be called directly, bypassing the gateway's auth/limits/ metering. The secure alternative is an internal-only backend reachable only by APIM, with APIM as the sole public entry point.
- A reusable backend resource with an id, credentials, TLS, and health/circuit config. Load balancing, backend pools, circuit breakers, and per-backend credentials all require a named backend.
- Consumption can't join a VNet — it's a serverless multi-tenant tier. VNet integration/injection requires Developer or Premium.
- A private DNS zone named after the environment's domain, linked to the VNet APIM is in, so the internal FQDN resolves to the private IP (plus subnet routing) — the same track-06-module-04 requirement.
- The backend reachability layer. Check that APIM is in the VNet, the env's private DNS zone is linked to APIM's VNet (so the FQDN resolves), and routing between subnets works.
- A named backend fronting several real backends with distribution across them. Weighted distribution enables canary/blue-green (e.g. 90/10) or spreading load.
- The service mesh's outlier detection / circuit breaking (track 13). Tripping open stops sending traffic to a failing backend so requests fail fast (or route to healthy members) until it recovers.
- With a managed identity (system/user-assigned, track 06 module 06) whose token APIM presents to the backend — no stored secret — or a client certificate.
Cumulative review
Closed-book. Don't reopen modules 03-05 while attempting these — mix across the whole track so far and the tracks it builds on.
- For one request, place these in the exact pipeline order and say which fails
with which status: subscription-key check,
validate-jwt,rate-limit-by-key,set-backend-serviceto a private backend (modules 03-05). - Match each status to its layer and a one-line fix: 401, 403, 404, 429, 503 (modules 01, 03, 04, 05).
- A per-consumer rate limit is throttling legitimate users. Give the wrong-scope
cause, the
-by-keyfix, and thecounter-keyexpression (module 03). - A token you decoded and confirmed valid is rejected 401 at the gateway. Name the single most likely policy misconfiguration and how you'd confirm it against the token (module 04, track 16).
- Explain why the same "circuit breaker" idea appears in both this track and the service mesh (track 13), and what differs about where it's applied.
- You moved the backend to an internal-only Container Apps Environment and now get 503s. List, in order, the track-06/module-05 things you'd verify to restore reachability.
- Justify, on cost grounds, using Consumption for modules 03-04 but Developer for module 05 — and state the same-day rule that follows from it (modules 01, 05).
- You need
v2of an API to route 10% of traffic to a new backend whilev1stays stable. Combine a module-02 concept and a module-05 concept to describe the setup. - Where does each of these belong — inbound, backend, outbound, or on-error:
strip a
Serverheader,validate-jwt,set-backend-service, return a friendly error body on failure (modules 03-05)? - A teammate points the API's inline
serviceUrlat the private FQDN and expects load balancing "later." Explain what they'll have to change and why (module 05).
Show answers
- Order: subscription-key check →
rate-limit-by-key→validate-jwt→set-backend-service→ forward. Key fails 401; rate limit fails 429; JWT fails 401 (or 403 if a scope is missing); an unreachable private backend fails 503. - 401 = auth/token (fix: valid key or matching audience/issuer). 403 =
valid token missing scope (fix: grant/require correct scope). 404 =
path/backend mapping (fix:
--pathvs--service-url). 429 = throttled (fix: per-consumer-by-keylimits / raise limit). 503 = backend unreachable (fix: VNet/private-DNS/routing). - Cause: a plain
rate-limitpooling all consumers at one scope. Fix: userate-limit-by-keywithcounter-key="@(context.Subscription.Id)". - An audience (
aud) mismatch invalidate-jwt(or issuer/version). Confirm by decoding the token (jwt.ms) and comparing itsaud/issto the policy's<audiences>/<issuers>. - Both prevent hammering a failing dependency by shedding load and letting it recover. The mesh applies it east-west between services (track 13); APIM applies it north-south at the product edge on a named backend.
- Verify: APIM is in the VNet; the env's private DNS zone is linked to APIM's VNet (FQDN resolves to private IP); subnet routing allows APIM→env; then re-test — a passing key + 503 means reachability, not auth.
- Modules 03-04 need no VNet, so Consumption (pay-per-call, idles free) is cheapest; module 05 needs VNet integration, which only classic tiers support, so Developer — which bills continuously, hence delete it the same day.
- Use a version set (module 02) so
v2is its own API, and a backend pool with weighted distribution (module 05) onv2's backend sending ~10% to the new member and 90% to the stable one. - Strip
Server→ outbound.validate-jwt→ inbound.set-backend-service→ inbound (or backend). Friendly error body → on-error. - Inline
serviceUrlcan't carry load balancing, pools, or circuit breakers — they require a named backend. They'll have to define a named backend (or a Pool backend) and route to it viaset-backend-service.
Next
06-developer-portal-and-api-products — package APIs into products with subscription models, enable self-service sign-up, add terms of use, and publish the auto-generated developer portal (survey level — this is the platform-ops view, not full API-product management).