Skip to content

Architecture

End-to-end model from Pulumi program to a running App Service instance.

flowchart TD
  subgraph consumer [Consumer projects]
    PulumiStack[Pulumi stack]
    DjangoApp[Django app repo]
  end
  subgraph azure [Azure resources]
    SA[Storage + CDN]
    PG[Postgres Flexible Server]
    ASP[App Service Plan]
    WA[Django Web App]
    BF[Principal bootstrap Function]
    PGA[pgAdmin Web App]
    KV[Key Vault]
    Redis[Redis sidecar]
  end
  subgraph deploy [Deploy on App Service]
    Boot[bootstrap.django-azu.re]
    Oryx[Oryx build]
    Start[startup.sh]
  end
  PulumiStack -->|DjangoDeployment| azure
  DjangoApp -->|Git source control| WA
  WA --> Boot --> Oryx --> Start
  Start --> WA
  BF -->|create Entra principal| PG
  WA --> PG
  WA --> SA
  WA --> KV
  WA --> Redis

Shared vs per-app

One DjangoDeployment creates the shared plane (storage, CDN, Postgres server, plan, principal bootstrap Function, pgAdmin). Each add_django_website(...) adds an application plane (database, containers, vault, Web App, optional ACS/Redis, automatic Entra DB principal).

See Multiple applications.

Runtime trust model

  • Django Web App application settings are a separate ARM resource (WebAppApplicationSettings), not nested in WebApp.siteConfig. Updating siteConfig recycles the worker.
  • Web App uses a system-assigned managed identity.
  • A separate user-assigned bootstrap identity is an Entra Postgres admin used only by the shared Function to create non-admin app roles.
  • Postgres uses Entra ID auth only (no password auth on the server). Django uses the pulumi_django_azure.db.backends.postgresql engine, which fetches a short-lived Entra access token for every new connection, in whichever process opens it (Gunicorn worker, RQ worker, management command).
  • Secrets from Pulumi config land in Key Vault; the app receives {ENV}_SECRET_NAME and reads values with AZURE_KEY_VAULT_CLIENT.
  • Static/media go to blob storage and are served through the CDN host.

Build and start

  1. App setting PRE_BUILD_COMMAND curls https://bootstrap.django-azu.re.
  2. Bootstrap fills cicd/ / utility/ and runs pre_build.sh.
  3. Oryx installs from exported requirements.txt; POST_BUILD_COMMAND runs post_build.sh.
  4. App Service starts cicd/startup.sh (startup_tasks = migrate + purge_cache, background collectstatic, supervisord, Gunicorn). Startup time matters: when the platform moves the app to another instance it kills the old container a few minutes after starting the new one, whether or not the new one is warm yet.

Details: Deploy pipeline.