# Pulumi Django Azure > Opinionated Pulumi + Django helpers for deploying Django apps on Azure App Service. Dual-purpose Python package: (1) Pulumi Azure IaC via DjangoDeployment, (2) Django runtime helpers (settings, middleware, management commands), (3) App Service deploy scripts bootstrapped from https://bootstrap.django-azu.re. Pulumi stacks and Django apps may live in separate repositories; import Pulumi types from pulumi_django_azure.django_deployment only. # Core # pulumi-django-azure Opinionated helpers to deploy Django applications on Azure with Pulumi. The package has three surfaces that often live in **separate consumer repositories**: 1. **Pulumi IaC** — `DjangoDeployment` / `HostDefinition` provision shared and per-app Azure resources. 1. **Django runtime** — settings, middleware, context processors, and management commands for Azure App Service. 1. **Deploy scripts** — Oryx pre/post build and App Service startup scripts, bootstrapped from `https://bootstrap.django-azu.re`. ## What you get - Storage account for media and static files, with Azure Front Door CDN in front - PostgreSQL Flexible Server with Entra ID authentication only - Azure Communication Services for email (optional per app) - Linux App Service Web Apps with custom hostnames and managed certificates - Key Vault per application - Optional Redis sidecar and django-tasks RQ workers - pgAdmin on the shared App Service plan ## Documentation for humans and LLMs | Resource | URL | | ---------------------------------- | ------------------------------------- | | This site | | | LLM index (`llms.txt`) | | | Full LLM context (`llms-full.txt`) | | Start with [Scope](https://django-azu.re/scope/index.md) (what this package owns) and [For consuming projects](https://django-azu.re/for-consumers/index.md) if you are wiring this into another repo’s agent docs. ## Install ```bash poetry add pulumi-django-azure ``` Or pin a Git branch: ```toml pulumi-django-azure = { git = "https://gitlab.com/MaartenUreel/pulumi-django-azure.git", branch = "main" } ``` ## Correct imports Pulumi stacks (do **not** import Django settings here): ```python from pulumi_django_azure.django_deployment import DjangoDeployment, HostDefinition ``` Django apps: ```python from pulumi_django_azure.settings import * # noqa: F403 from pulumi_django_azure.settings import patch_django_settings_for_azure ``` See [Getting started](https://django-azu.re/getting-started/overview/index.md) for a full walkthrough. # Scope: what this package owns Use this page when deciding what an agent or developer should attribute to `pulumi-django-azure` versus the consumer application. ## In scope ### Pulumi (`django_deployment.py`) - Shared stack resources created by `DjangoDeployment(...)`: storage account, CDN (Azure Front Door), Postgres Flexible Server + private DNS, App Service plan subnet, shared Linux App Service plan, pgAdmin Web App. - Per-app resources from `add_django_website(...)`: database, media/static blob containers, Key Vault, Web App (system-assigned managed identity, Git source control, health check path), optional Redis sidecar, optional Communication Services, hostname bindings + managed certs, RBAC role assignments. - Entra database administrators via `add_database_administrator(object_id, user_name)`. - Stack exports such as `cdn_cname`, `pgsql_host`, `{name}_site_db_user`, `{name}_deploy_url`, domain verification IDs. ### Django runtime - Azure-oriented settings when `IS_AZURE_ENVIRONMENT=true`: secure cookies/HSTS, Entra token as Postgres password, Azure Storage + Collectfasta, CDN URLs, Communication email backend, optional Redis cache and django-tasks-rq. - `HealthCheckMiddleware` (token rotation + DB probe + Gunicorn self-heal) and optional `WagtailHostAliasMiddleware`. - `patch_django_settings_for_azure(...)` for apps/middleware/context processors. - `add_build_info` template context from `build-info.json`. - Management commands: `purge_cdn`, `purge_cache`, `fix_cache_control`, `test_redis`. ### Deploy pipeline (`deploy_scripts/`) - Bootstrap URL `https://bootstrap.django-azu.re` downloads scripts into the app repo’s `cicd/` and `utility/` (skips files that already exist). - Pre-build: optional npm build, HTML/SVG minify, Tailwind, Poetry export to `requirements.txt`, `build-info.json`. - Post-build: `npm prune --production` when applicable. - Startup: migrations, background collectstatic + CDN purge, cache purge, supervisord RQ worker, Gunicorn. ## Out of scope This package does **not**: - Own your Django application code, models, URLs, or migration *content*. - Create your resource group or VNet (you pass those into `DjangoDeployment`). - Manage public DNS records for you (you create CNAME / A / TXT / DKIM / SPF based on Pulumi outputs). - Create the Entra Postgres principal for the app’s managed identity (you run `pgaadauth_create_principal_with_oid` once and grant DB rights). - Automatically enable HTTPS on a custom CDN hostname (manual step due to Azure API limits). - Provide a local Docker Compose or full local Azure emulator; local Django runs with package settings mostly inert unless Azure env vars are set. - Re-export Pulumi types from the package root — always import from `pulumi_django_azure.django_deployment`. ## Two-project pattern | Consumer project | Depends on | Typical imports | | ---------------- | ------------ | ------------------------------------------------------------------------------------ | | Pulumi / infra | This package | `from pulumi_django_azure.django_deployment import DjangoDeployment, HostDefinition` | | Django app | This package | `pulumi_django_azure.settings`, middleware, management commands | They may be the same Git repo or separate ones. Pulumi must not need to import Django settings modules from this package’s settings surface. # 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] 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 WA --> PG WA --> SA WA --> KV WA --> Redis ``` ## Shared vs per-app One `DjangoDeployment` creates the **shared** plane (storage, CDN, Postgres server, plan, pgAdmin). Each `add_django_website(...)` adds an **application** plane (database, containers, vault, Web App, optional ACS/Redis). See [Multiple applications](https://django-azu.re/guides/multiple-applications/index.md). ## Runtime trust model - Web App uses a **system-assigned managed identity**. - Postgres uses **Entra ID auth only** (no password auth on the server). The app password in Django is a short-lived Entra access token refreshed via health checks. - 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`. 1. Bootstrap fills `cicd/` / `utility/` and runs `pre_build.sh`. 1. Oryx installs from exported `requirements.txt`; `POST_BUILD_COMMAND` runs `post_build.sh`. 1. App Service starts `cicd/startup.sh` (migrate, collectstatic, supervisord, Gunicorn). Details: [Deploy pipeline](https://django-azu.re/guides/deploy-pipeline/index.md). # For consuming projects Paste one of these into the Django or Pulumi project that depends on `pulumi-django-azure` so agents know what this package does without inventing behavior. ## Recommended: full context ```markdown ## Azure deployment (pulumi-django-azure) This project is deployed with [`pulumi-django-azure`](https://django-azu.re). Treat the following as the deployment and runtime contract (do not invent Azure or CI behavior outside it): - Full docs for LLMs: https://django-azu.re/llms-full.txt - Index: https://django-azu.re/llms.txt - Scope (what the package owns): https://django-azu.re/scope/ Pulumi imports (infra project): `from pulumi_django_azure.django_deployment import DjangoDeployment, HostDefinition` Django imports (app project): `from pulumi_django_azure.settings import *` and optional `patch_django_settings_for_azure`. ``` ## Minimal: index only ```markdown Deployment contract for this app: https://django-azu.re/llms.txt ``` ## Cursor rule snippet (consumer repo) ```markdown --- description: Azure deploy contract via pulumi-django-azure alwaysApply: true --- # pulumi-django-azure This app’s Azure infra, App Service build/startup, and Azure Django settings are provided by `pulumi-django-azure`. Prefer https://django-azu.re/llms-full.txt over guessing. - Pulumi: import from `pulumi_django_azure.django_deployment` only. - Django: use `pulumi_django_azure.settings` / middleware / management commands as documented. - Do not invent DNS, Entra DB principal creation, or CDN HTTPS enablement as automatic package steps. ``` ## Human docs Site: \ Source: [GitLab](https://gitlab.com/MaartenUreel/pulumi-django-azure/)\ PyPI: `pulumi-django-azure` # Getting started # Getting started overview ## Prerequisites - Python 3.12–3.14 (package requires `>=3.12,<3.15`) - A Pulumi project with Azure credentials and an existing **resource group** + **VNet** you create yourself - A Django project that will be deployed from Git to App Service - Entra tenant ID for Postgres AAD authentication ## Two projects, one package You can use one Git repository or two. Functionally there are two integration surfaces: 1. **Infra (Pulumi)** — creates Azure resources. Import only: ```python from pulumi_django_azure.django_deployment import DjangoDeployment, HostDefinition ``` 1. **App (Django)** — consumes App Settings and runtime helpers. Import settings/middleware from `pulumi_django_azure.settings` and related modules. Do not import `django_deployment` unless you also run Pulumi from that codebase. Next: - [Pulumi stack](https://django-azu.re/getting-started/pulumi/index.md) - [Django app](https://django-azu.re/getting-started/django/index.md) Then follow [Domains and HTTPS](https://django-azu.re/guides/domains-https/index.md) and [Database](https://django-azu.re/guides/database/index.md) for the multi-pass deploy and Entra role setup. # Pulumi stack Install the package in your Pulumi project, then define a stack similar to the sample below. Important Always import from the submodule: ```python from pulumi_django_azure.django_deployment import DjangoDeployment, HostDefinition ``` Do not use `from pulumi_django_azure import DjangoDeployment` — the package root does not re-export these types (so Pulumi-only projects do not pull Django settings by accident). ## Sample stack ```python import pulumi import pulumi_azure_native as azure from pulumi_django_azure.django_deployment import DjangoDeployment, HostDefinition stack = pulumi.get_stack() rg = azure.resources.ResourceGroup(f"rg-{stack}") vnet = azure.network.VirtualNetwork( f"vnet-{stack}", resource_group_name=rg.name, address_space=azure.network.AddressSpaceArgs( address_prefixes=["10.0.0.0/16"], ), ) django = DjangoDeployment( stack, tenant_id="00000000-0000-0000-0000-000000000000", # Entra tenant resource_group_name=rg.name, vnet=vnet, pgsql_sku=azure.dbforpostgresql.SkuArgs( name="Standard_B1ms", tier=azure.dbforpostgresql.SkuTier.BURSTABLE, ), pgsql_ip_prefix="10.0.10.0/24", app_service_ip_prefix="10.0.20.0/24", app_service_sku=azure.web.SkuDescriptionArgs( name="B2", tier="Basic", ), storage_account_name="mystorageaccount", # globally unique cdn_host="cdn.example.com", # optional str; omit or None for default AFD hostname ) django.add_django_website( name="web", db_name="mywebsite", repository_url="git@gitlab.com:project/website.git", repository_branch="main", website_hosts=[ HostDefinition("example.com", aliases=["www.example.com"]), ], django_settings_module="mywebsite.settings.production", environment_variables={}, # pass {} if you have no extras (do not pass None) secrets={}, comms_data_location="europe", comms_domains=["example.com"], ) django.add_database_administrator( object_id="00000000-0000-0000-0000-000000000000", user_name="you@example.com", ) ``` ## Parameter notes | Parameter | Notes | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `app_service_ip_prefix` | Subnet for App Service (not `appservice_ip_prefix`). | | `cdn_host` | Optional `str`, not a `HostDefinition`. | | `website_hosts` | `list[HostDefinition]`. Constructor is `HostDefinition(host, aliases=None)`. `identifier` is a derived property (`example.com` → `example-com`). | | `environment_variables` / `secrets` | Prefer empty dicts over `None` — the implementation mutates these mappings. | | `add_database_administrator` | Takes `object_id` and `user_name` only; tenant comes from the deployment. | Full API: [DjangoDeployment reference](https://django-azu.re/reference/django-deployment/index.md). ## After first `pulumi up` 1. Create DNS from stack outputs ([Domains and HTTPS](https://django-azu.re/guides/domains-https/index.md)). 1. Create the Entra DB principal for `{name}_managed_identity` ([Database](https://django-azu.re/guides/database/index.md)). 1. Wire Git deploy SSH key / webhook from `{name}_deploy_ssh_key_url` and `{name}_deploy_url`. # Django app integration Add `pulumi-django-azure` to the Django project that runs on App Service. ## Minimal settings wiring ```python from pulumi_django_azure.settings import * # noqa: F403 from pulumi_django_azure.settings import patch_django_settings_for_azure INSTALLED_APPS = [ # ... your apps ... "django.contrib.staticfiles", ] MIDDLEWARE = [ # ... your middleware ... ] TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "OPTIONS": { "context_processors": [ # ... ], }, }, ] patch_django_settings_for_azure(INSTALLED_APPS, MIDDLEWARE, TEMPLATES) ``` `patch_django_settings_for_azure` will: - Insert `collectfasta` before `django.contrib.staticfiles` - Append `pulumi_django_azure`, `django_rq`, and `django_tasks_rq` if missing - Append `HealthCheckMiddleware` - Insert `WagtailHostAliasMiddleware` before Wagtail’s redirect middleware when present - Append the `add_build_info` context processor ## Manual alternative Without the patch helper: ```python from pulumi_django_azure.settings import * # noqa: F403 INSTALLED_APPS += ["collectfasta", "pulumi_django_azure", "django_rq", "django_tasks_rq"] MIDDLEWARE += ["pulumi_django_azure.middleware.HealthCheckMiddleware"] ``` Place `collectfasta` **before** `django.contrib.staticfiles`. ## What settings do on Azure When App Service sets `IS_AZURE_ENVIRONMENT=true` (done by Pulumi), the imported settings configure Postgres with Entra tokens, Azure Storage/CDN, secure cookies, optional Redis and tasks, and logging. See [Settings and env](https://django-azu.re/reference/settings-and-env/index.md) and [Local vs production](https://django-azu.re/guides/local-vs-production/index.md). ## Health check Pulumi configures App Service health checks at `/health-check`. The middleware serves that path, refreshes the DB token, probes the database, and can recycle Gunicorn workers on failure. ## Override freely Import Azure defaults first, then override any setting in your module afterward. # Guides # Deploy pipeline App Service is configured with Oryx builds (`SCM_DO_BUILD_DURING_DEPLOYMENT=true`). Collectstatic is disabled during Oryx (`DISABLE_COLLECTSTATIC=true`) and runs at startup instead. ## Bootstrap `PRE_BUILD_COMMAND` is: ```bash curl -sSL https://bootstrap.django-azu.re | bash ``` That downloads scripts from this repository’s `deploy_scripts/` into the app checkout: | Destination | Files | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `cicd/` | `pre_build.sh`, `post_build.sh`, `startup.sh`, `collectstatic.sh`, `gunicorn.conf.py`, `supervisord.conf`, `wsgi.py`, `html-minifier.json` | | `utility/` | `pgdump.sh` | Existing files are **skipped**, so you can vendor/customize by committing your own copies under `cicd/`. Branch override: set App Setting `CICD_SCRIPTS_BRANCH` (default `main`). After download, bootstrap runs `cicd/pre_build.sh`. ## Pre-build (`cicd/pre_build.sh`) - If `package.json` exists: `npm ci` and `npm run build:js` (if present) - HTML minify via `html-minifier-next` (`ENABLE_HTML_MINIFY`, `EXCLUDE_HTML_MINIFY`) - SVG minify via `svgo` (`ENABLE_SVG_MINIFY`; optional `cicd/svgo.config.mjs`) - Optional Tailwind if `TAILWIND_INPUT_PATH` is set (`TAILWIND_OUTPUT_PATH` optional) - Install Poetry, export `requirements.txt` for Oryx - Write `build-info.json` from `SCM_COMMIT_ID` ## Post-build (`cicd/post_build.sh`) Runs as `POST_BUILD_COMMAND`. Prunes npm production deps when a lockfile is present. ## Startup (`cicd/startup.sh`) Configured as the site startup command by the package’s deploy layout: 1. Optional `EXTRA_APT_PACKAGES` 1. `compilemessages` (gettext) 1. Background `collectstatic.sh` (collectstatic + `purge_cdn`) 1. `migrate` 1. `purge_cache` 1. Supervisord RQ worker (`rqworker` with `django_tasks_rq.Job`) when tasks/Redis are enabled 1. Optional consumer hook `cicd/pre_startup.sh` 1. Gunicorn via `cicd/gunicorn.conf.py` loading `cicd.wsgi` ## What the consumer app must provide - A Poetry project that exports cleanly to `requirements.txt` - `DJANGO_SETTINGS_MODULE` pointing at a module that imports this package’s Azure settings - Git repository wired via Pulumi `repository_url` / `repository_branch` - Optional: committed overrides under `cicd/` # Domains and HTTPS Custom domains require a **multi-pass** Pulumi deploy because of Azure binding/certificate ordering and CDN validation. ## Recommended order 1. Deploy without relying on custom hosts being fully validated yet (first `pulumi up` may fail on custom domain resources until DNS exists — that is expected). 1. Configure Postgres Entra principal for the app ([Database](https://django-azu.re/guides/database/index.md)). 1. Retrieve deploy SSH key from `{name}_deploy_ssh_key_url` and configure the Git remote; use `{name}_deploy_url` for webhooks. 1. Point CDN hostname at `cdn_cname` (and TXT validation if exported). 1. Point website hostnames (CNAME/A) and create `asuid` TXT records from `{name}_site_domain_verification_id`. 1. Re-deploy with custom hosts in place. 1. Re-deploy again so managed certificates can attach to existing hostname bindings. 1. Manually enable HTTPS on the custom CDN domain in the Azure portal (Azure API limitation: [azure-rest-api-specs#17498](https://github.com/Azure/azure-rest-api-specs/issues/17498)). 1. Configure DKIM/SPF/etc. for Communication Services custom domains in the Azure portal. ## CDN custom domain Stack exports: - `cdn_cname` — CNAME target for your CDN hostname - When `cdn_host` is set: `cdn_validation_record_txt_name` / `cdn_validation_record_txt_value` Create the DNS records, then redeploy so the custom domain resource can succeed. ## App Service custom domains For each hostname (including aliases on `HostDefinition`): ```text asuid.example.com. TXT "<{name}_site_domain_verification_id>" asuid.www.example.com. TXT "" ``` Also create CNAME (or A) records to `{name}_site_domain_cname` / `{name}_site_virtual_ip` as appropriate for your DNS layout. Hostname bindings are created sequentially. Certificates need an existing binding, so a second deploy after bindings exist is required for HTTPS on the web app. # Database One `DjangoDeployment` creates a single **Azure Database for PostgreSQL Flexible Server** (Entra ID authentication only; password auth disabled). Multiple Django apps on that deployment each get their own database via `db_name`. Defaults include 32 GB storage with auto-grow and 7-day backup retention. ## Deployment parameters ### `pgsql_sku` ```python pgsql_sku=azure.dbforpostgresql.SkuArgs( name="Standard_B1ms", tier=azure.dbforpostgresql.SkuTier.BURSTABLE, ) ``` ### `pgsql_ip_prefix` Subnet prefix for the Postgres delegated subnet (typically `/24`). ### `pgsql_version` Defaults to `"17"`. ### `pgsql_parameters` Optional server configuration key/value map. ### `pgadmin_access_ip` IP allowlist for pgAdmin. Empty means open (still password-protected). ## Per-website database ```python django.add_django_website( name="prod", db_name="prod", # ... ) ``` Exports: - `{name}_site_db_user` — e.g. `prod_managed_identity` - `{name}_site_principal_id` — managed identity object ID - `pgsql_host` — server FQDN ## Entra principal for the Web App (manual) Postgres does not auto-create the app’s AAD principal. As an Entra admin on the server, run on the `postgres` database: ```sql SELECT * FROM pgaadauth_create_principal_with_oid( 'prod_managed_identity', 'c8b25b85-d060-4cfc-bad4-b8581cfdf946', 'service', false, false ); ``` Use the role name from `{name}_site_db_user` and the GUID from `{name}_site_principal_id`. Grant connect/privileges on the app database. Microsoft docs: [Create a role using Microsoft Entra object identifier](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/how-to-manage-azure-ad-users#create-a-role-using-microsoft-entra-object-identifier). ## Administrator login ```python django.add_database_administrator( object_id="b306adf5-fc61-4a32-8156-ce032dc1571f", user_name="you@example.com", ) ``` Temporary password/token: ```bash az account get-access-token --resource-type oss-rdbms ``` Use your email as the username and the token as the password (pgAdmin or `psql`). ## pgAdmin Created on the shared App Service plan. Export: `pgadmin_url`. Default credentials (change immediately): - Login: `dbadmin@dbadmin.net` - Password: `dbadmin` Create your own user and remove the default. # Secrets Each website gets its own Key Vault. The Web App’s managed identity is granted **Key Vault Secrets User**. Optional `vault_administrators` receive admin role assignments. ## Pulumi config → Key Vault Store structured secrets in Pulumi config: ```bash pulumi config set --secret --path 'mywebsite_social_auth_azure.key' '...' pulumi config set --secret --path 'mywebsite_social_auth_azure.secret' '...' pulumi config set --secret --path 'mywebsite_social_auth_azure.tenant_id' '...' pulumi config set --secret --path 'mywebsite_social_auth_azure.client_id' '...' ``` Pass a mapping into `add_django_website`: ```python django.add_django_website( # ... secrets={ "mywebsite_social_auth_azure": "AZURE_OAUTH", }, environment_variables={}, ) ``` - Key: Pulumi config object name - Value: logical name used for the Key Vault secret and the App Setting prefix The app receives `AZURE_OAUTH_SECRET_NAME` (the vault secret’s name). Secret names are normalized (underscores → hyphens, lowercased) in the vault. ## Reading secrets in Django With Azure settings imported, `AZURE_KEY_VAULT_CLIENT` is available when `AZURE_KEY_VAULT` is set: ```python import json import environ from pulumi_django_azure.settings import AZURE_KEY_VAULT_CLIENT env = environ.Env() oauth_secret = AZURE_KEY_VAULT_CLIENT.get_secret(env("AZURE_OAUTH_SECRET_NAME")) oauth_secret = json.loads(oauth_secret.value) SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = oauth_secret["client_id"] SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = oauth_secret["secret"] SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID = oauth_secret["tenant_id"] ``` ## What is not in Key Vault `DJANGO_SECRET_KEY` is a Pulumi `RandomString` injected directly as an App Setting, not stored in Key Vault. # Redis and tasks ## Redis sidecar `add_django_website(..., redis_sidecar=True)` (default) attaches a Redis container (`mcr.microsoft.com/mirror/docker/library/redis:7.2`) to the Web App and sets `REDIS_SIDECAR=true`. Django settings then configure `django_redis` against `redis://localhost:6379/0`, with exceptions ignored so the site can start if Redis is briefly unavailable. ## Django tasks `django_tasks=True` (default) requires `redis_sidecar=True` and sets `DJANGO_TASKS=true`. Settings wire: - `RQ_QUEUES` using the Redis cache - `TASKS` backend `django_tasks_rq.RQBackend` `patch_django_settings_for_azure` adds `django_rq` and `django_tasks_rq` to `INSTALLED_APPS`. At startup, supervisord runs: ```text python manage.py rqworker --job-class django_tasks_rq.Job ``` (with a long timeout so Entra credentials can refresh). ## Disabling ```python django.add_django_website( # ... redis_sidecar=False, django_tasks=False, ) ``` Setting `django_tasks=True` with `redis_sidecar=False` raises `ValueError`. # Multiple applications One `DjangoDeployment` shares: - Storage account and CDN - PostgreSQL Flexible Server - App Service subnet and (by default) App Service plan - pgAdmin Call `add_django_website` once per Django app: ```python django.add_django_website( name="site_a", db_name="site_a", repository_url="git@gitlab.com:org/site-a.git", repository_branch="main", website_hosts=[HostDefinition("a.example.com")], django_settings_module="site_a.settings.production", environment_variables={}, secrets={}, ) django.add_django_website( name="site_b", db_name="site_b", repository_url="git@gitlab.com:org/site-b.git", repository_branch="main", website_hosts=[HostDefinition("b.example.com")], django_settings_module="site_b.settings.production", environment_variables={}, secrets={}, dedicated_app_service_sku=azure.web.SkuDescriptionArgs( name="B2", tier="Basic", ), # optional dedicated plan for this app only ) ``` Each site gets its own database, blob containers (`{name}-media`, `{name}-static`), Key Vault, Web App, exports (`{name}_site_*`), and optional ACS/Redis. Use distinct `name` values — they prefix Azure resource names and exports. # Local vs production | Concern | Production (Azure App Service) | Local / non-Azure | | ------------------------------------ | --------------------------------------- | --------------------------------------------------------------- | | `IS_AZURE_ENVIRONMENT` | `true` (set by Pulumi) | unset/false | | HSTS / secure cookies / SSL redirect | Enabled | Not applied by this package | | Database | Entra token password, `sslmode=require` | Not configured by this package unless you set the same env vars | | Storage / CDN / email | From App Settings | Only if you set matching env vars | | Redis / tasks | Sidecar + flags when enabled | Off unless you set `REDIS_SIDECAR` / `DJANGO_TASKS` | | `build_info` | From `BASE_DIR/build-info.json` | In DEBUG, synthetic values | | Deploy scripts | Used by Oryx / startup | Not used unless you run them | | Health path | `/health-check` via App Setting | Settings default is `/health` if env unset | Importing `pulumi_django_azure.settings` locally is mostly inert without Azure environment variables. Provide your own local `DATABASES`, storages, and email backends as usual. # Reference # DjangoDeployment API Import: ```python from pulumi_django_azure.django_deployment import DjangoDeployment, HostDefinition ``` ## `HostDefinition` ```python HostDefinition(host: str, aliases: list[str] | None = None) ``` | Member | Meaning | | ------------ | ------------------------------------ | | `host` | Primary hostname | | `aliases` | Optional alias hostnames (sorted) | | `identifier` | Property: dots replaced with hyphens | | `all_hosts` | Property: `[host] + aliases` | ## `DjangoDeployment(...)` Shared infrastructure constructor parameters: | Parameter | Required | Description | | ------------------------- | -------- | ----------------------------------- | | `name` | yes | Resource name prefix | | `tenant_id` | yes | Entra tenant for Postgres AAD | | `resource_group_name` | yes | Target resource group | | `vnet` | yes | Existing `VirtualNetwork` | | `pgsql_sku` | yes | Flexible Server SKU | | `pgsql_ip_prefix` | yes | Postgres subnet CIDR | | `app_service_ip_prefix` | yes | App Service subnet CIDR | | `app_service_sku` | yes | Shared Linux plan SKU | | `storage_account_name` | yes | Globally unique storage name | | `storage_allowed_origins` | no | Blob CORS origins | | `pgsql_version` | no | Default `"17"` | | `pgsql_parameters` | no | Server parameters | | `pgadmin_access_ip` | no | pgAdmin IP allowlist | | `cdn_host` | no | Custom CDN hostname (`str \| None`) | Class constant: `HEALTH_CHECK_PATH = "/health-check"`. ### Shared exports - `cdn_cname`, optional CDN validation TXT exports - `pgsql_host` - `pgadmin_url` ## `add_database_administrator(object_id, user_name)` Registers an Entra user as Postgres administrator. Tenant is taken from the deployment. ## `add_django_website(...)` | Parameter | Default | Description | | --------------------------- | -------- | --------------------------------------------- | | `name` | required | Per-app prefix | | `db_name` | required | Database name | | `repository_url` | required | Git repo URL | | `repository_branch` | required | Branch to deploy | | `website_hosts` | required | `list[HostDefinition]` | | `django_settings_module` | required | Settings module path | | `python_version` | `"3.14"` | App Service Python version | | `environment_variables` | `None` | Extra app settings — prefer `{}` | | `secrets` | `None` | Pulumi config name → env prefix — prefer `{}` | | `comms_data_location` | `None` | ACS data location | | `comms_domains` | `None` | ACS email custom domains | | `dedicated_app_service_sku` | `None` | Dedicated plan for this app | | `vault_administrators` | `None` | Entra object IDs | | `redis_sidecar` | `True` | Redis sidecar container | | `django_tasks` | `True` | Requires Redis | | `startup_timeout` | `600` | Container start time limit (seconds) | | `log_retention_days` | `7` | HTTP log retention; `0` skips setting | ### Per-app exports - `{name}_site_principal_id` - `{name}_site_db_user` - `{name}_site_domain_verification_id` - `{name}_site_domain_cname` - `{name}_site_virtual_ip` - `{name}_deploy_url` - `{name}_deploy_ssh_key_url` Returns the `azure.web.WebApp` resource. # Settings and environment variables ## Set by Pulumi on the Web App | Variable | Purpose | | ------------------------------------------------ | -------------------------------------------- | | `IS_AZURE_ENVIRONMENT` | Enables Azure-specific settings | | `SCM_DO_BUILD_DURING_DEPLOYMENT` | Oryx build on deploy | | `PRE_BUILD_COMMAND` | Bootstrap curl | | `POST_BUILD_COMMAND` | `cicd/post_build.sh` | | `DISABLE_*_BUILD` | Disable unrelated Oryx detectors | | `DISABLE_COLLECTSTATIC` | Defer collectstatic to startup | | `HEALTH_CHECK_PATH` | `/health-check` | | `DJANGO_SETTINGS_MODULE` | Your settings module | | `DJANGO_SECRET_KEY` | Random 50-char string | | `DJANGO_ALLOWED_HOSTS` | Comma-separated hosts from `HostDefinition`s | | `DJANGO_HOSTS_MAP` | JSON host→aliases when aliases exist | | `AZURE_KEY_VAULT` | Vault name | | `AZURE_STORAGE_ACCOUNT_NAME` | Storage account | | `AZURE_STORAGE_CONTAINER_MEDIA` / `_STATICFILES` | Blob containers | | `CDN_HOST` / `CDN_PROFILE` / `CDN_ENDPOINT` | CDN wiring | | `DB_HOST` / `DB_NAME` / `DB_USER` | Postgres connection | | `REDIS_SIDECAR` | When Redis enabled | | `DJANGO_TASKS` | When tasks enabled | | `AZURE_COMMUNICATION_SERVICE_ENDPOINT` | When ACS enabled | | `WEBSITE_HTTPLOGGING_RETENTION_DAYS` | When retention > 0 | | `{ENV}_SECRET_NAME` | Key Vault secret names from `secrets=` | | plus | Consumer `environment_variables` | Azure also injects `WEBSITE_HOSTNAME` (and others). ## Read by `pulumi_django_azure.settings` Also consumed when present: - `DJANGO_DEFAULT_FROM_EMAIL` - `AZURE_CACHE_CONTROL` (default long-lived immutable) - `REDIRECT_ALIASES` (bool, default true) - `CICD_SCRIPTS_BRANCH` (bootstrap; not settings.py) ## Helper `patch_django_settings_for_azure(INSTALLED_APPS, MIDDLEWARE, TEMPLATES)` — see [Django app](https://django-azu.re/getting-started/django/index.md). ## Runtime clients When configured: - `AZURE_KEY_VAULT_CLIENT` — Key Vault `SecretClient` - `AZURE_CREDENTIAL` / storage token credential via managed identity helpers in `azure_helper.py` # Management commands Available when `pulumi_django_azure` is in `INSTALLED_APPS`. | Command | Purpose | | ------------------- | -------------------------------------------------------------------- | | `purge_cdn` | Purge the Azure Front Door / CDN endpoint (used after collectstatic) | | `purge_cache` | Clear the Django cache backend | | `fix_cache_control` | Adjust cache-control on stored blobs | | `test_redis` | Connectivity check for the Redis sidecar | Startup runs `purge_cache` and background `collectstatic` + `purge_cdn` automatically via `cicd/startup.sh` / `cicd/collectstatic.sh`.