Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Each sample is a self-contained project with its own README, Azure CLI scripts a
| Sample | Description |
|--------|-------------|
| [Function App and Storage (.NET)](./samples/function-app-storage-http/dotnet/README.md) | A gaming scoreboard built on Azure Functions (isolated worker): HTTP triggers record player scores in Table Storage, publish messages to Queue Storage and write game-session summaries to Blob Storage, all against the emulated storage account. |
| [Function App and Front Door (Python)](./samples/function-app-front-door/python/README.md) | A minimal Python Function App answering `/{name}`, published behind an Azure Front Door (Standard) profile so requests reach the function through the Front Door endpoint; deployable to real Azure or to the emulator. |
| [Function App and Front Door (Python)](./samples/function-app-front-door/python/README.md) | Two Python Function Apps serving a small *Catalog* API, published through an Azure Front Door (Standard) endpoint: the edge picks an origin by priority, matches the more specific of two routes, caches what the origin allows, and runs a rule set that stamps a response header, rewrites `/shop` to `/catalog` and redirects a retired path without calling the origin at all. |
| [Function App and Managed Identities (Python)](./samples/function-app-managed-identity/python/README.md) | A serverless text processor: an Azure Functions app reads text blobs from an `input` container, converts them to uppercase and writes the result to an `output` container, authenticating to the storage account with a managed identity instead of keys. |
| [Function App and Service Bus (.NET)](./samples/function-app-service-bus/dotnet/README.md) | An Azure Functions app on an App Service plan that exchanges messages through Service Bus queues: an HTTP trigger sends greetings and a queue trigger consumes them, connecting with either a connection string or a managed identity. |
| Web App and CosmosDB for MongoDB API ([Python](./samples/web-app-cosmosdb-mongodb-api/python/README.md), [.NET](./samples/web-app-cosmosdb-mongodb-api/dotnet/README.md)) | The *Vacation Planner* single-page web app on an Azure Web App with regional VNet integration, storing activities in the `activities` collection of an Azure Cosmos DB for MongoDB account reached through a private endpoint. |
Expand Down
7 changes: 5 additions & 2 deletions run-samples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ SAMPLES=(
"samples/servicebus/java|bash scripts/deploy.sh"
"samples/eventhubs/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/run-pipeline.sh"
"samples/eventhubs-eventgrid/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/run-pipeline.sh"
"samples/function-app-front-door/python|bash scripts/deploy_all.sh --name-prefix testafd|"
"samples/function-app-front-door/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-front-door.sh"
"samples/function-app-managed-identity/python|bash scripts/user-managed-identity.sh|bash scripts/validate.sh && bash scripts/test.sh"
"samples/function-app-service-bus/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-http-trigger.sh"
"samples/function-app-storage-http/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-http-triggers.sh"
Expand Down Expand Up @@ -201,7 +201,10 @@ if [[ "${1:-}" == "--list" ]]; then
watch=("$path" "$(dirname "$path")/src" "$(dirname "$path")/scripts")
name="${path#samples/}"
else
watch=("$path/scripts" "$path/src")
# A sample's application code lives in src/ or, for the Function App samples, function/;
# a change there has to re-run the sample as surely as a change to its scripts. Folders that
# do not exist simply never match a changed file.
watch=("$path/scripts" "$path/src" "$path/function")
name="${path#samples/}/scripts"
fi

Expand Down
394 changes: 158 additions & 236 deletions samples/function-app-front-door/python/README.md

Large diffs are not rendered by default.

This file was deleted.

98 changes: 98 additions & 0 deletions samples/function-app-front-door/python/function/function_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""The origin behind Azure Front Door: a small catalog API plus a health endpoint.

Every response says which of the two Function Apps answered and what path the origin was asked
for, so the Front Door behaviours the sample demonstrates -- origin selection, route matching, URL
rewriting -- can be read straight off the body. The caching behaviour is the origin's to decide:
the catalog sets a ``Cache-Control`` Front Door can honour, everything else says ``no-store``.
"""

import json
import os
from urllib.parse import urlparse

import azure.functions as func

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)

#: Which Function App this is, from an app setting the deployment script sets. The two apps are
#: identical apart from this value, which is what makes origin selection observable.
ORIGIN_NAME = os.environ.get("ORIGIN_NAME", "unknown")

CATALOG = {
"1": {"sku": "AFD-001", "name": "Edge cache mug", "price": 12.5},
"2": {"sku": "AFD-002", "name": "Origin group hoodie", "price": 48.0},
"3": {"sku": "AFD-003", "name": "Rules engine notebook", "price": 7.25},
}

#: The headers Front Door adds on its way to the origin. Echoed by /whoami so a reader can see
#: what arrives at an origin that sits behind an edge.
FRONT_DOOR_HEADERS = (
"host",
"via",
"x-azure-clientip",
"x-azure-socketip",
"x-azure-fdid",
"x-azure-requestchain",
"x-forwarded-for",
"x-forwarded-host",
"x-forwarded-proto",
)


def json_response(body: dict, status_code: int = 200, cache_control: str = "no-store"):
return func.HttpResponse(
json.dumps(body, indent=2),
status_code=status_code,
mimetype="application/json",
headers={"Cache-Control": cache_control},
)


def origin_path(req: func.HttpRequest) -> str:
"""The path this Function App was asked for, which is not always the one the client sent."""
return urlparse(req.url).path


@app.route(route="health", methods=["GET", "HEAD"])
def health(req: func.HttpRequest) -> func.HttpResponse:
"""Front Door's health probe target.

The probe is a HEAD request, so this route has to accept HEAD as well as GET: an origin that
answers the probe with 405 is taken out of rotation and its route starts returning 503.
"""
return json_response({"status": "healthy", "origin": ORIGIN_NAME})


@app.route(route="whoami", methods=["GET"])
def whoami(req: func.HttpRequest) -> func.HttpResponse:
"""What the origin received, including the headers Front Door added."""
headers = {
name: value for name, value in req.headers.items() if name.lower() in FRONT_DOOR_HEADERS
}
return json_response(
{
"origin": ORIGIN_NAME,
"path": origin_path(req),
"method": req.method,
"front_door_headers": headers,
}
)


@app.route(route="status", methods=["GET"])
def status(req: func.HttpRequest) -> func.HttpResponse:
"""The target of the sample's second, more specific route."""
return json_response({"origin": ORIGIN_NAME, "path": origin_path(req), "status": "ok"})


@app.route(route="catalog/{item}", methods=["GET"])
def catalog(req: func.HttpRequest) -> func.HttpResponse:
"""A cacheable response: Front Door stores it for as long as this ``Cache-Control`` allows."""
wanted = req.route_params.get("item")
item = CATALOG.get(wanted)
if item is None:
return json_response({"error": f"No catalog item {wanted}"}, status_code=404)
return json_response(
{"origin": ORIGIN_NAME, "path": origin_path(req), "item": item},
cache_control="public, max-age=300",
)

This file was deleted.

This file was deleted.

7 changes: 3 additions & 4 deletions samples/function-app-front-door/python/function/host.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
{
"version": "2.0",
"extensions": {
"http": {
"routePrefix": ""
}
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
azure-functions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

This file was deleted.

25 changes: 25 additions & 0 deletions samples/function-app-front-door/python/scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Azure CLI Deployment

This directory contains Bash scripts for deploying and validating the sample using the `lstk` CLI. For details about the sample application, see [Front Door and Function Apps](../README.md).

## Prerequisites

- [LocalStack for Azure](https://docs.localstack.cloud/azure/)
- [Docker](https://docs.docker.com/get-docker/)
- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli)
- [lstk CLI](https://docs.localstack.cloud/aws/developer-tools/running-localstack/lstk/)
- [jq](https://jqlang.org/) and `zip`

## Scripts

| Script | Purpose |
|--------|---------|
| `deploy.sh` | Idempotently provisions two Function Apps and their storage on a shared App Service plan, deploys the same zip package to both, then creates the Front Door profile, endpoint, two origin groups, three origins, the rule set with its three rules, and the two routes. Prints the endpoint URL and the commands to try it. |
| `validate.sh` | Walks the whole chain (origin health and the probe method, routing, route specificity, origin priority, the three rules, caching and purge, the headers the edge adds, an origin error, the endpoint's enabled state) and exits non-zero on any failure. |
| `call-front-door.sh` | Quick user-level smoke test: read a catalog item, read it again from the edge cache, follow the rewrite and the redirect, and print what the origin received. |
| `cleanup.sh` | Deletes the resource group and the local zip artifact. |

## Notes

- The scripts read `az account show --query environmentName` and adjust two things for the emulator: the routes forward to the origins over plain HTTP, and the endpoint is called through its `*.afd.azure.localhost.localstack.cloud:4566` alias rather than its `*.azurefd.net` host name.
- `deploy.sh` supports both spellings of `az afd rule create`: the flattened arguments of Azure CLI 2.83 and earlier, and the `--conditions`/`--actions` shorthand of the `cdn` extension used from 2.85 on.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/bin/bash

# Variables
PREFIX='local'
SUFFIX='test'
RESOURCE_GROUP_NAME="${PREFIX}-rg"
PROFILE_NAME="${PREFIX}-catalog-afd-${SUFFIX}"
ENDPOINT_NAME="${PREFIX}-catalog-${SUFFIX}"
BODY_FILE='/tmp/front_door_call.json'
HEADERS_FILE='/tmp/front_door_call_headers.txt'

# Retrieve the Front Door profile
echo "Retrieving the [$PROFILE_NAME] Front Door profile..."
PROFILE_ID=$(az afd profile show --profile-name $PROFILE_NAME --resource-group $RESOURCE_GROUP_NAME --query id --output tsv)

if [[ -n "$PROFILE_ID" ]]; then
echo "[$PROFILE_NAME] Front Door profile successfully retrieved"
else
echo "Failed to retrieve the [$PROFILE_NAME] Front Door profile"
exit 1
fi

# Where the endpoint answers: Azure's hostName, or the emulator's local alias (the emulator also
# claims the *.azurefd.net name, but it only resolves once LocalStack's DNS is in front of the machine)
ENVIRONMENT_NAME=$(az account show --query environmentName --output tsv)
if [[ "$ENVIRONMENT_NAME" == "LocalStack" ]]; then
ENDPOINT_URL="http://${ENDPOINT_NAME}.afd.azure.localhost.localstack.cloud:4566"
else
ENDPOINT_HOST_NAME=$(az afd endpoint show --endpoint-name $ENDPOINT_NAME --profile-name $PROFILE_NAME --resource-group $RESOURCE_GROUP_NAME --query hostName --output tsv)
ENDPOINT_URL="https://$ENDPOINT_HOST_NAME"
fi

# Call the endpoint and report what the edge did with the request
call_endpoint() {
local PATH_TO_CALL="$1"
STATUS=$(curl -s -m 20 -o "$BODY_FILE" -D "$HEADERS_FILE" -w "%{http_code}" "$ENDPOINT_URL$PATH_TO_CALL")
CACHE=$(grep -i "^x-cache:" "$HEADERS_FILE" | tr -d '\r' | awk '{print $2}')
SERVED_BY=$(grep -i "^x-served-by:" "$HEADERS_FILE" | tr -d '\r' | awk '{print $2}')
LOCATION=$(grep -i "^location:" "$HEADERS_FILE" | tr -d '\r' | awk '{print $2}')
}

echo "Calling [$ENDPOINT_URL/catalog/1]..."
call_endpoint /catalog/1

if [[ "$STATUS" == "200" ]]; then
jq . "$BODY_FILE"
echo "Cache status: ${CACHE:-(none)}, stamped by the rule set: ${SERVED_BY:-(none)}"
else
echo "[$ENDPOINT_URL/catalog/1] returned [$STATUS]: $(cat "$BODY_FILE")"
exit 1
fi

echo "Calling it again, to be served from the edge cache..."
call_endpoint /catalog/1
echo "Cache status: ${CACHE:-(none)}"

echo "Calling [$ENDPOINT_URL/shop/3], which the rules engine rewrites to /catalog/3..."
call_endpoint /shop/3

if [[ "$STATUS" == "200" ]]; then
echo "The origin was asked for $(jq -r '.path' "$BODY_FILE") and answered with $(jq -r '.item.name' "$BODY_FILE")"
else
echo "[$ENDPOINT_URL/shop/3] returned [$STATUS]: $(cat "$BODY_FILE")"
exit 1
fi

echo "Calling [$ENDPOINT_URL/legacy], which the rules engine redirects..."
call_endpoint /legacy
echo "Answered [$STATUS] at the edge, pointing at ${LOCATION:-(no Location header)}"

echo "Calling [$ENDPOINT_URL/whoami], which reports what the origin received..."
call_endpoint /whoami

if [[ "$STATUS" == "200" ]]; then
jq . "$BODY_FILE"
else
echo "[$ENDPOINT_URL/whoami] returned [$STATUS]: $(cat "$BODY_FILE")"
exit 1
fi
28 changes: 28 additions & 0 deletions samples/function-app-front-door/python/scripts/cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/bin/bash

# =============================================================================
# Removes everything scripts/deploy.sh created.
#
# Deleting the resource group is enough: the Front Door profile, its endpoint,
# origin groups, origins, routes and rule set, both Function Apps, their storage
# accounts and the shared App Service plan are all inside it.
# =============================================================================

PREFIX='local'
RESOURCE_GROUP_NAME="${PREFIX}-rg"
CURRENT_DIR="$(cd "$(dirname "$0")" && pwd)"

echo "Deleting resource group [$RESOURCE_GROUP_NAME] and everything in it..."
az group delete \
--name "$RESOURCE_GROUP_NAME" \
--yes \
--only-show-errors 1>/dev/null

if [[ $? -eq 0 ]]; then
echo "Resource group [$RESOURCE_GROUP_NAME] deleted."
else
echo "WARNING: could not delete resource group [$RESOURCE_GROUP_NAME] (it may not exist)."
fi

rm -f "$CURRENT_DIR"/../function/*.zip
echo "Removed local deployment artifacts."
Loading
Loading