A lightweight Azure Developer CLI (azd) sample that deploys a globally distributed,
mission-critical platform on Azure — AKS stamps, Cosmos DB, Front Door, Orleans apps, and
full observability — with a single command.
azd auth login
azd upThe current azd sample configures two stamps, one in Sweden Central and one in East US 2.
Change the topology in the regions array (see Deploy).
- 📦 What's inside → FEATURES.md — every app, tool, and platform feature, with links and sources.
- 🏭 CI/CD, Flux & production topology → ci-cd/.
- 🧱 Architecture decisions → docs/adr/.
| Layer | Technology |
|---|---|
| Compute | AKS (multi-stamp, Karpenter autoscale) |
| Framework | .NET 10, Orleans, Aspire |
| Database | Azure Cosmos DB |
| Ingress | Azure Front Door + Istio Gateway API |
| AI | Azure OpenAI |
| Observability | App Insights + OpenTelemetry + Prometheus |
| IaC | Bicep via azd |
Full catalog and sources: FEATURES.md.
- .NET SDK 10.0.402, pinned in
global.json - Aspire CLI 13.6.1:
dotnet tool install -g Aspire.Cli --version 13.6.1. No Aspire workload is required. - Azure CLI
- Azure Developer CLI (azd)
- Docker Desktop or Podman (local dev only —
azdbuilds images remotely) - kubectl
- Node.js 26 for the React SPAs and Playwright E2E. CI and frontend build images use 26.12.0.
Also required by the azd deploy hooks (the postdeploy step renders K8s manifests locally):
# Example: HelloAgents
cd src/HelloAgents
cd HelloAgents.Web && npm ci && cd ..
aspire run --apphost HelloAgents.AppHostSee each app folder (linked from FEATURES.md) for app-specific notes.
Build and .NET tests
dotnet build src/src.slnx
dotnet test --solution src/src.slnx --no-build \
--max-parallel-test-modules 1 --zero-tests-policy strictPlaywright
(
set -e
for app in DarkUxChallenge HelloOrleons GraphOrleons HelloAgents; do
(
cd "src/$app"
npm ci --prefix "$app.E2E"
(cd "$app.E2E" && npx playwright install chromium)
trap 'aspire stop --non-interactive --nologo' EXIT
CI=true OPENAI_ENDPOINT= aspire start --apphost "$app.AppHost/$app.AppHost.csproj" \
--non-interactive -- --AZURE_OPENAI_ENDPOINT= --CosmosDb:UseEphemeralEmulator=true
aspire wait api --timeout 240
if [ "$app" != HelloOrleons ]; then
aspire wait web --timeout 240
fi
aspire resource e2e start
aspire wait e2e --status down --timeout 300
aspire describe e2e --format json |
jq -e '.resources | length == 1 and .[0].exitCode == 0'
)
done
)Python
(
set -e
cd src/health-models-tui
uv sync --locked --python 3.11
uv run --locked python -m pytest azext_health_models_tui/tests/
)Docker images
(
set -e
for app in DarkUxChallenge HelloOrleons GraphOrleons HelloAgents; do
(cd "src/$app" && docker buildx bake --allow=fs.read=../.. \
--set '*.cache-from=' --set '*.cache-to=')
done
)azd auth login
azd env new my-dev --location swedencentral --subscription <subscription-id>
azd upAfter provisioning and application rollouts succeed, each app is available at its Front Door HTTPS endpoint printed by the deploy hook.
⚠️ Don't name the envalwayson— it collides with the productionbaseName. Thepreprovisionhook blocks it. Use any other name (my-dev,test,demo).
azd up deploys the entire platform from your machine — no hosted Git repo and no Flux
required. It runs:
azd up
├── azd provision Deploys all Azure infra (AKS, ACR, Front Door, Cosmos DB, AI, ...)
│ └── postprovision Enables Gateway API, gets cluster credentials, extracts deploy vars
└── azd deploy
├── predeploy Builds images remotely via `az acr build` (no Docker Desktop needed)
└── postdeploy `kustomize build | envsubst | kubectl apply`, waits for rollout,
verifies health, prints Front Door endpoints
For the existing alwaysonn environment, every rollout and recovery starts with
azd up --environment alwaysonn. Do not resume with individual phases, services or
standalone hooks. A failed phase leaves the run failed, even if some resources updated.
Review the full subscription what-if and preservation baseline before retrying.
Provisioning outputs come from that lifecycle's named environment, not the latest
successful deployment. Health-model generation is required. All seven image contexts
and builds must succeed before the new source hash/tag is saved; changed source or
missing/mismatched image attribution stops rollout. Postdeploy requires exactly seven
workload image substitutions per stamp and publishes only the HelloAgents dashboard
to the explicit aiObservability target. Publication failure fails the full run.
The topology is data in infra/main.parameters.json (the
regions array). Stamps and regions are added by editing that array — the hooks and Bicep
iterate over whatever you declare, so a single stamp "just works".
Current azd sample: 2 regions, 1 stamp per region. Sweden Central uses Standard_D2s_v5;
East US 2 uses Standard_D2s_v6. The spot NodePool admits D/E generations 4, 5, and 6.
Verify subscription-specific VM availability and quota before choosing another region.
For a lower-cost single-region deployment, reduce regions to one entry:
Two stamps in the same region — add a second entry to stamps:
"stamps": [{ "key": "001" }, { "key": "002" }]Two stamps across two regions — add a second region object:
"regions": {
"value": [
{ "key": "swedencentral", "location": "swedencentral",
"stampDefaults": { "...": "as above" }, "stamps": [{ "key": "001" }] },
{ "key": "germanywestcentral", "location": "germanywestcentral",
"stampDefaults": { "...": "as above" }, "stamps": [{ "key": "001" }] }
]
}Run azd up again after editing. For provisioned/multi-write Cosmos, Premium Front Door,
and the GitHub Actions/Flux production path, see ci-cd/.
Set these in infra/main.parameters.json:
| Parameter | Default | Effect |
|---|---|---|
enableFlux |
false |
Flux GitOps on AKS. azd deploys via the postdeploy hook when off. See ci-cd/. |
enableCustomDomain |
false |
Custom-domain routing (DNS zones, CNAMEs). Off uses Front Door default endpoints. |
enableHelloOrleonsApiDocs |
true in the azd sample |
Opts HelloOrleons into OpenAPI/Scalar outside Development. The Bicep default is false, so production profiles remain unchanged unless explicitly enabled. |
gpt41MiniCapacity |
100 in the azd sample |
Allocates existing GlobalStandard quota to gpt-41-mini for concurrent agent workflows. The Bicep default remains 10; other model allocations and the pay-per-token SKU are unchanged. |
enablePrivateEndpoints |
true |
Per-stamp VNet + private endpoints, public access disabled. |
cosmosMode |
Serverless |
Provisioned enables autoscale + multi-region write. |
frontDoorSku |
Standard_AzureFrontDoor |
Premium_AzureFrontDoor for WAF + Private Link to internal LB. |
With private networking enabled, Bicep owns the subnet NSGs and their associations.
External gateways allow AzureFrontDoor.Backend traffic on TCP 80, plus 443 in custom-domain
mode. Internal gateways retain private-network ingress. Node-NIC allow rules do not override
a subnet NSG's inbound deny.
With enableFlux = false, the postdeploy hook replaces Flux's reconciliation loop with a
local equivalent, using the same manifests in clusters/ unchanged:
kustomize buildrendersclusters/<region>/apps(what Flux would render).envsubstsubstitutes${VAR}patterns from Bicep outputs (same as FluxpostBuild.substitute).kubectl apply --server-sideapplies to AKS (same as Flux reconciliation).
The hook omits Flux-only resources when Flux is disabled and certificate-only resources when custom domains are disabled. Render, apply, missing-workload, and rollout errors fail deployment rather than being treated as successful delivery.
azd down --force --purge| Path | What |
|---|---|
FEATURES.md |
Applications, tools, and platform features — with sources |
src/ |
.NET 10 / Orleans apps + shared libraries + az health-models-tui CLI |
infra/ |
Bicep IaC (main.bicep entry point) + main.parameters.json |
clusters/ |
Kustomize/Flux manifests (also used by the azd postdeploy hook) |
hooks/ |
azd lifecycle hooks (pre/post provision & deploy) |
ci-cd/ |
GitHub Actions, Flux/GitOps, deploy keys, production profiles |
scripts/ |
Health model + Grafana dashboard generators |
docs/adr/ |
Architecture Decision Records |