Skip to content

Add multi-service-app sample: URL shortener composing seven Azure services - #111

Open
lazarkanelov wants to merge 8 commits into
mainfrom
add-multi-service-app-sample
Open

Add multi-service-app sample: URL shortener composing seven Azure services#111
lazarkanelov wants to merge 8 commits into
mainfrom
add-multi-service-app-sample

Conversation

@lazarkanelov

@lazarkanelov lazarkanelov commented Jul 30, 2026

Copy link
Copy Markdown

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

  • New sample samples/multi-service-app/python with all three deployment variants, following the sibling samples' layout and naming conventions (local-msa-* resource names on the eventhubs sample-infix precedent):
    • scripts/ — idempotent Azure CLI deploy, an 8-step validate.sh that walks the full causal chain, and a call-web-app.sh smoke test
    • terraform/ — flat configuration with variables.tf/terraform.tfvars, provider pinned to =4.81.0 like the siblings
    • bicep/main.bicep + main.bicepparam (secrets via readEnvironmentVariable), role assignments using built-in role GUIDs verified against the Azure role-definitions API
    • src/ (Flask web app) and function/ (Python v2-model worker with identity-based Service Bus trigger and queue trigger)
  • Registered in run-samples.sh (SAMPLES, TERRAFORM_SAMPLES, BICEP_SAMPLES) and in the root README outline table

Tests

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:

  • scripts: full deploy + validate.sh all 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 the call-web-app.sh smoke test (shorten → 302 redirect)
  • terraform: full deploy green, web app health OK
  • bicep: validate + deploy green, web app health OK

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

  • localstack/localstack-pro#8135 — emulator parity fixes surfaced while building this sample

…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 paolosalvatori left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread samples/url-shortener/python/scripts/deploy.sh Outdated
Comment thread samples/url-shortener/python/scripts/deploy.sh Outdated
Comment thread samples/url-shortener/python/scripts/deploy.sh
Comment thread samples/url-shortener/python/scripts/deploy.sh
Comment thread samples/url-shortener/python/scripts/deploy.sh
Comment thread samples/url-shortener/python/terraform/main.tf Outdated
Comment thread samples/url-shortener/python/terraform/main.tf Outdated
Comment thread samples/url-shortener/python/terraform/main.tf Outdated
Comment thread samples/url-shortener/python/terraform/providers.tf
Comment thread samples/url-shortener/python/README.md
- 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

@paolosalvatori paolosalvatori left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lazarkanelov one more comment from me, see below.

Comment thread README.md
@@ -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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants