Add multi-service-app sample: URL shortener composing seven Azure services - #111
Add multi-service-app sample: URL shortener composing seven Azure services#111lazarkanelov wants to merge 8 commits into
Conversation
…vices Linklet, a Flask URL shortener with an event-driven Functions worker, wired through Web App, Functions, Storage (table/queue/blob), Key Vault, Service Bus, PostgreSQL flexible server, a shared user-assigned managed identity and Log Analytics. Every shortened link crosses 4-6 services in one causal chain, so a regression in any one service breaks an observable user outcome. Ships all three deployment variants (Azure CLI scripts, Terraform, Bicep), an 8-step validation script that walks the full chain, and registration in run-samples.sh and the README outline.
scripts/deploy.sh persists the generated PostgreSQL admin password to scripts/.last_deploy.env for validate.sh and re-runs; the existing .env patterns do not match that filename, so an accidental 'git add -A' after a deploy would commit a credential. The pattern also covers the function-app-front-door sample's .last_deploy_all.env for future runs (its already-tracked copy contains only resource names).
CI runs the released emulator image, which follows the short link's 302 at the gateway (fixed in localstack/localstack-pro#8135) so the client sees the followed response instead of the redirect. Accept that in the smoke test as long as the click was recorded on the link; the strict 302 path applies on emulator builds that include the fix. validate.sh already asserted the effect (click rows in PostgreSQL) rather than the status code; document why.
@localstack/lstk 0.20.0 (published 2026-07-30 11:10 UTC) fails its emulator probe against the Azure emulator image: 'lstk az start-interception' reports 'LocalStack Azure Emulator is not running' while the container is healthy and serving requests. Since the workflow installed lstk unpinned, every shard after the publish rolled the dice on the new probe, failing ~60% of jobs across all PRs (runs green yesterday on 0.19.0 fail today unchanged; the emulator image boots cleanly outside CI). Pin to 0.19.0 until lstk fixes the detection.
…-sample # Conflicts: # .github/workflows/run-samples.yml
The name described how the sample is built, not what it is; every sample uses multiple services. url-shortener names what it does and matches the repo's descriptive directory convention (eventhubs, web-app-*, etc).
paolosalvatori
left a comment
There was a problem hiding this comment.
Review summary
Composite sample reviewed file by file against the ingested Azure rules (common/python/bicep/terraform/scripts) plus sibling-sample conventions, with every Azure-behavior claim verified on Microsoft Learn or the provider docs. Local checks: bash -n on all five scripts, py_compile on the Python files, az bicep build, terraform validate — all pass.
1. Rules applied
localstack-pro-azure/CLAUDE.md.claude/rules/azure/common/: coding-style, hooks, patterns, security, testing.claude/rules/azure/python/: coding-style, hooks, patterns, security, testing.claude/rules/azure/bicep/: coding-style, hooks, patterns, security, testing.claude/rules/azure/terraform/: coding-style, hooks, patterns, security, testing.claude/rules/azure/scripts/: coding-style, hooks, patterns, security, testing
All present and read in full. Interpretation applied: these rules are path-scoped to the emulator repo; they were applied here where they translate (IaC pinning/security, az CLI conventions, fail-closed access control, "document deliberate gaps"), and sibling-sample conventions won where they conflict — notably the house deploy-script style (no set -euo pipefail, per-command $? checks, unquoted constant vars) shared by virtually all sibling samples is not flagged.
2. Existing comments
None found.
3. Findings
15 inline comments: 2 HIGH, 6 MEDIUM, 7 LOW. The two HIGH findings share one root cause — the storage account never sets allowBlobPublicAccess/--allow-blob-public-access true while the qrcodes container requests Blob public access, which per current Azure defaults (anonymous access is prohibited by default on new accounts) makes the scripts and Bicep variants fail on a fresh real-Azure subscription (Terraform is saved by the provider's allow_nested_items_to_be_public = true default). Both are one-line fixes.
4. Clean files
.gitignore, root README.md, run-samples.sh, function/function_app.py, function/host.json, function/requirements.txt, scripts/README.md, scripts/call-web-app.sh, scripts/validate.sh, src/gunicorn.conf.py (byte-identical to the validated sibling), src/requirements.txt, bicep/README.md, bicep/main.bicepparam, terraform/README.md, terraform/variables.tf, terraform/outputs.tf (postgres_password correctly sensitive), terraform/terraform.tfvars, terraform/deploy.sh (shares two findings anchored in scripts/deploy.sh and bicep/deploy.sh).
Verified-correct parity details worth recording: all six built-in role GUIDs match the published role definitions (Storage Table/Queue/Blob Data Contributor, Key Vault Secrets User, Service Bus Data Sender/Receiver); role assignments correctly use the principal id while AZURE_CLIENT_ID carries the client id; the identity-based ServiceBusConnection__fullyQualifiedNamespace/__credential/__clientId settings match the documented pattern; TextBase64EncodePolicy matches the queue trigger's documented base64 expectation; the fullyQualifiedDomainName host:port split is a clean dual-target pattern; Bicep role assignments use deterministic guid() names (idempotent redeploys).
5. Verdict
Approve — per our approve-with-comments convention for teammates' PRs, reinforced by the author's end-to-end validation on both the emulator and Azure. The two HIGH findings are real per current Azure documentation and deserve a fix (one CLI flag, one Bicep property) before this sample is exercised against a fresh real-Azure subscription — and it would be good to confirm which variant the real-Azure run used, since per the docs the scripts/Bicep public-container step cannot succeed on a fresh account with today's defaults.
- Allow anonymous blob access on the storage account (scripts + Bicep) so the public qrcodes container deploys on fresh real-Azure subscriptions - Guard az role assignment create with a list check (create is not idempotent, Azure/azure-cli#31995) - Set SCM_DO_BUILD_DURING_DEPLOYMENT/ENABLE_ORYX_BUILD in the scripts variant - Persist PostgreSQL credentials from the Terraform and Bicep deploys and run validate.sh for both variants in run-samples.sh - Override AzureWebJobsStorage with an explicit-endpoints connection in Terraform, mirroring Bicep and the eventhubs sibling - Fail closed on a missing INTERNAL_TOKEN and use hmac.compare_digest - Rename the stale msa infix to urlshort; rename Terraform example labels - Pin the random provider and src/requirements.txt versions exactly; dedupe the postgres admin login; document the best-effort diagnostics step and the plain-HTTP internal API tradeoff; add a References section
There was a problem hiding this comment.
@lazarkanelov one more comment from me, see below.
| @@ -39,6 +39,7 @@ This repository contains comprehensive sample projects demonstrating how to deve | |||
| | [Web App with Custom Docker Image](./samples/web-app-custom-image/python/README.md) | Azure Web App running a custom Docker image | | |||
There was a problem hiding this comment.
One last comment @lazarkanelov Can you ask Claude to create a diagram for the app? I don't pretend that you create a diagram manually, just ask Claude to create a mermaid diagram, thanks!
Motivation
The existing samples each exercise one or two services in isolation. This adds a composite sample — Linklet, a URL shortener with an event-driven worker — that chains seven services into a single causal chain: Web App, Functions (Service Bus trigger + Queue Storage trigger), Storage (table/queue/blob), Key Vault, Service Bus, PostgreSQL flexible server, plus a shared user-assigned managed identity and Log Analytics. Every shortened link crosses 4–6 services before it renders on the page, so a regression in any one service breaks an observable user outcome — making the sample a realistic end-to-end workout for the emulator that single-service samples structurally cannot provide. Building and validating it surfaced several emulator parity findings that are being fixed separately (localstack/localstack-pro#8135).
Changes
samples/multi-service-app/pythonwith all three deployment variants, following the sibling samples' layout and naming conventions (local-msa-*resource names on theeventhubssample-infix precedent):scripts/— idempotent Azure CLI deploy, an 8-stepvalidate.shthat walks the full causal chain, and acall-web-app.shsmoke testterraform/— flat configuration withvariables.tf/terraform.tfvars, provider pinned to=4.81.0like the siblingsbicep/—main.bicep+main.bicepparam(secrets viareadEnvironmentVariable), role assignments using built-in role GUIDs verified against the Azure role-definitions APIsrc/(Flask web app) andfunction/(Python v2-model worker with identity-based Service Bus trigger and queue trigger)run-samples.sh(SAMPLES,TERRAFORM_SAMPLES,BICEP_SAMPLES) and in the root README outline tableTests
All three variants were run against the LocalStack Azure emulator (image built from current main), each on a fresh emulator instance matching CI's one-shard-per-test isolation:
validate.shall green — web app health (Table + Key Vault + PostgreSQL via managed identity), AbuseScan verdicts for benign and suspicious links via the Service Bus trigger, QR SVG rendered via the queue trigger and fetched from the public blob container, two redirects logging two click rows in PostgreSQL, hit counter in Table Storage, Log Analytics + diagnostic settings — plus thecall-web-app.shsmoke test (shorten → 302 redirect)Note for local testing: running the variants sequentially against one emulator instance trips a known emulator issue where a deleted Key Vault's name remains held until restart (fix in localstack/localstack-pro#8135); restarting the emulator between variants (as CI's isolation effectively does) avoids it.
Note: CI runs the released emulator image, which still follows the short link's 302 at the gateway (fix pending in localstack/localstack-pro#8135). The smoke test therefore accepts the followed response when the click is verifiably recorded, and asserts the strict 302 on emulator builds that include the fix.
Related