This sample demonstrates Azure Front Door Standard in front of two Azure Function Apps. The Function Apps serve a small Catalog API and are otherwise identical: each one reports its own name in every response, so which origin answered, which route matched and what path the origin was asked for can all be read straight off the body.
Clients only ever call the Front Door endpoint. Between the client and the function, the edge picks an origin by priority, decides which of two routes applies, caches what the origin allows it to cache, and runs a rule set that stamps a response header, rewrites one path prefix into another and answers a retired path with a redirect without calling an origin at all.
The sample exercises both halves of Front Door on the LocalStack Azure emulator: the control plane (profile, endpoint, origin groups, origins, routes, rule set, rules, purge) and the data plane (routing, origin selection, health probes, caching, the rules engine and the headers the edge adds).
The solution is composed of the following Azure resources:
- Azure Resource Group: A logical container scoping all resources in this sample.
- Azure Storage Accounts (two): The Function Apps' runtime storage (
AzureWebJobsStorage), one each. - Azure App Service Plan (Linux, B1): Shared by both Function Apps.
- Azure Function Apps (Python v2 model), primary and secondary: the Catalog origin, with four HTTP routes —
GET /api/catalog/{item}(cacheable,Cache-Control: public, max-age=300),GET /api/whoami(what the origin received,no-store),GET /api/statusandGET|HEAD /api/health(the health probe target). AnORIGIN_NAMEapp setting is the only difference between the two apps. - Azure Front Door Standard profile:
- One endpoint, the address clients call.
- The catalog origin group, holding the primary origin at priority 1 and the secondary as a priority-2 standby, with a health probe that sends
HEAD /api/healthevery 30 seconds. - The status origin group, holding the secondary origin alone.
- The catalog route (
/*), which sends traffic to the catalog origin group with caching switched on and the rule set attached. - The status route (
/status), a more specific pattern pointing at the other origin group, with no caching and no rules. - The catalogrules rule set:
stampHeader(addsX-Served-Byto every GET response, which is what itsRequestMethod Equal GETcondition matches),rewriteShop(/shop/*→/catalog/*on the way to the origin) andredirectLegacy(/legacy→302to/status, answered at the edge).
%%{init: {"flowchart": {"nodeSpacing": 50, "rankSpacing": 70}}}%%
flowchart LR
client((Client))
subgraph afd["Front Door Standard profile"]
direction TB
routes["Routes<br/>/* · /status"]
rules["Rule set catalogrules<br/>stampHeader · rewriteShop · redirectLegacy"]
cache["Edge cache<br/>on the /* route"]
routes --> rules --> cache
end
subgraph origins["Origin groups"]
direction TB
primary["catalog-origin-group<br/>primary (priority 1)<br/>standby (priority 2)"]
secondary["status-origin-group<br/>secondary"]
end
subgraph apps["Function Apps (Python)"]
direction TB
app1["primary<br/>/api/catalog/{item} · /api/whoami<br/>/api/status · /api/health"]
app2["secondary<br/>same code, ORIGIN_NAME=secondary"]
end
client -->|"1: GET /catalog/1"| routes
cache -->|"2: on a miss, GET /api/catalog/1<br/>+ X-Forwarded-Host · X-Azure-ClientIP · X-Azure-FDID"| primary
primary --> app1
secondary --> app2
cache -->|"3: 200 + X-Served-By + X-Cache"| client
client -. "GET /status: the more specific route" .-> secondary
style afd fill:#ffffff,stroke:#999999,color:#333333
style origins fill:#ffffff,stroke:#999999,color:#333333
style apps fill:#ffffff,stroke:#999999,color:#333333
The life of a request: the client calls GET /catalog/1 on the endpoint → the /* route matches, since no more specific pattern does → the rule set runs → the edge looks in its cache, and on a miss picks the healthy origin with the lowest priority number → the route's origin path puts /api back on the front of the path and the request goes to the primary Function App as GET /api/catalog/1 → the response comes back, is stored because its Cache-Control allows it, gets X-Served-By from the rule set and X-Cache/X-Azure-Ref from Front Door, and reaches the client. The second identical request never leaves the edge.
- Docker
- Azure CLI
- lstk CLI
- jq and
zip - A LocalStack account with a valid
LOCALSTACK_AUTH_TOKEN(see the Auth Token guide)
Start LocalStack for Azure and point the Azure CLI at it:
export LOCALSTACK_AUTH_TOKEN=<your-auth-token>
IMAGE_NAME=localstack/localstack-azure localstack start -d
lstk az start-interception
az login --service-principal -u any-app -p any-pass --tenant any-tenant
az account set --subscription 00000000-0000-0000-0000-000000000000Every command below is the same against real Azure; sign in with az login instead.
bash scripts/deploy.shThe script provisions the resource group, the App Service plan, the two storage accounts and the two Function Apps, deploys the same zip package to both, then creates the Front Door profile, endpoint, origin groups, origins, rule set, rules and routes. It finishes by printing the endpoint URL and a handful of curl commands to try.
It is safe to re-run: the Azure resources it creates are either checked for first or created with an idempotent PUT.
bash scripts/validate.sh
bash scripts/call-front-door.shvalidate.sh walks the whole chain and exits non-zero on any failure:
| # | Check | What it proves |
|---|---|---|
| 1 | Both Function Apps answer /api/health, including on HEAD |
The origins are up and answer the method the health probe uses |
| 2 | GET /catalog/1 through the endpoint |
The catch-all route, the priority-1 origin, and the route's origin path |
| 3 | X-Served-By and X-Azure-Ref on the response |
The rule set ran; the edge stamped its reference id |
| 4 | GET /status |
A more specific route wins, and sends the request to a different origin group |
| 5 | GET /shop/2 |
The UrlRewrite rule: the origin is asked for /api/catalog/2 |
| 6 | GET /legacy |
The UrlRedirect rule answers 302 at the edge, without calling an origin |
| 7 | /catalog/3 twice, then a purge, then a no-store path |
Caching, X-Cache, Age, purge, and the origin's power to refuse caching |
| 8 | GET /whoami |
X-Forwarded-Host, X-Azure-ClientIP and X-Azure-FDID reach the origin |
| 9 | Ten requests to an uncached path | Priority is a strict tier: all ten are answered by the priority-1 origin |
| 10 | GET /catalog/99 |
The origin's own 404 passes through the edge untouched |
| 11 | The endpoint disabled, then enabled again | enabledState takes the endpoint out of service and back |
call-front-door.sh is the short version: read a catalog item, read it again from the cache, follow the rewrite and the redirect, and print what the origin received.
ENDPOINT_URL=http://local-catalog-test.afd.azure.localhost.localstack.cloud:4566
# A cacheable response: the first request is a miss, the second a hit
curl -si $ENDPOINT_URL/catalog/1 | grep -iE "^(HTTP|x-cache|age|x-served-by)"HTTP/1.1 200 OK
x-served-by: front-door
x-cache: MISS
# The rules engine rewrites the path before the origin sees it
curl -s $ENDPOINT_URL/shop/2 | jq '{origin, path, sku: .item.sku}'{
"origin": "primary",
"path": "/api/catalog/2",
"sku": "AFD-002"
}# What Front Door tells the origin about the caller and about itself
curl -s $ENDPOINT_URL/whoami | jq .front_door_headers{
"host": "local-catalog-primary-test.azurewebsites.azure.localhost.localstack.cloud:4566",
"via": "1.1 Azure",
"x-azure-clientip": "127.0.0.1",
"x-azure-fdid": "8c7dc56e48154939834f0469a7e5c1bf",
"x-azure-requestchain": "hops=1",
"x-azure-socketip": "127.0.0.1",
"x-forwarded-for": "127.0.0.1",
"x-forwarded-host": "local-catalog-test.afd.azure.localhost.localstack.cloud",
"x-forwarded-proto": "http"
}# Empty the cache for a set of paths
az afd endpoint purge \
--endpoint-name local-catalog-test \
--profile-name local-catalog-afd-test \
--resource-group local-rg \
--content-paths '/catalog/*'bash scripts/cleanup.sh- The endpoint's local address. Front Door assigns the endpoint a
*.azurefd.nethost name, and the emulator reports one too, but that name only resolves once LocalStack's DNS server is in front of the machine. The scripts use the emulator's own alias instead,http://<endpoint-name>.afd.azure.localhost.localstack.cloud:4566, which resolves to127.0.0.1without any DNS setup. - Plain HTTP to the origins. The emulator serves Function Apps over HTTP on port 4566, so the routes forward with
HttpOnlyand do not redirect HTTP to HTTPS. Against real Azure the same script usesHttpsOnlyand--https-redirect Enabled, because*.azurewebsites.netis HTTPS-only. This is the only difference in what the script deploys. - The origin's host name and port. An origin's
--host-nameis a bare host name, so the script splits thehost:4566the emulator reports and passes the port as--http-port. The--origin-host-headerkeeps the port, because that is the name the emulator routes the Function App by. - Cache status values. The emulator reports
X-Cache: HIT,MISSandUNCACHEABLE; Azure reportsTCP_HIT,TCP_MISSand friends.validate.shlooks for the word, not the whole value.
UrlPathconditions see the path without its leading slash. A rule that should fire on/shop/2matches onshop;UrlRewrite's--source-pattern, on the other hand, keeps it (/shop). Real Azure ignores a leading slash in the match value, so/shopmatches there too, while the emulator compares the configured value as written. Writing the match value without the slash works on both.az afd rule createhas two spellings. Up to Azure CLI 2.83 theafdcommands are part of the CLI and take one flattened condition and action per rule (--match-variable,--action-name, …). From 2.85 they live in thecdnextension, which takes--conditionsand--actionsin its own shorthand syntax, and spells the route's rule sets and caching differently too.deploy.shdetects which one is installed and uses it.
Custom domains and their certificates, WAF policies and security policies, private link origins, and the older classic Front Door and classic CDN profiles. The emulator implements all of them; see the Front Door coverage page for what each one supports.