Skip to content

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

pgsql_sku=azure.dbforpostgresql.SkuArgs(
    name="Standard_B1ms",
    tier=azure.dbforpostgresql.SkuTier.BURSTABLE,
)

pgsql_ip_prefix

Subnet prefix for the PostgreSQL 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

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
  • pgsql_bootstrap_function_url — shared bootstrap Function HTTP endpoint

Entra principal for the Web App (automatic)

On pulumi up, add_django_website (default auto_create_db_principal=True) invokes a shared Azure Function on the existing App Service plan. That Function:

  1. Runs as a deployment-scoped user-assigned managed identity registered as an Entra Postgres administrator.
  2. Checks whether {name}_managed_identity already exists (via pgaadauth_list_principals); if not, calls pgaadauth_create_principal_with_oid (non-admin).
  3. Grants CONNECT and sets the app role as owner of the app database.
  4. If db_schema is set: connects to the app database, runs CREATE SCHEMA IF NOT EXISTS, and grants USAGE, CREATE on that schema to the app role. If db_schema is omitted, no schema is created and public is not assumed.

The flow is idempotent — re-running pulumi up skips principal creation when the role is already present with the expected object ID.

The Function only does meaningful work when invoked (during deploy / when principal inputs change). It stays Always On on the shared plan so invokes stay reliable.

Set auto_create_db_principal=False to skip automation and manage SQL yourself.

Optional schema (db_schema)

django.add_django_website(
    name="prod",
    db_name="prod",
    db_schema="web",
    # ...
)

Pulumi creates the schema and grants permissions only. It does not set Django’s Postgres search_path. In your production settings you still need something like:

DATABASES["default"]["OPTIONS"]["options"] = "-c search_path=web"

Manual fallback

As an Entra admin on the server, on the postgres database:

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. Then grant connect/ownership on the app database.

Microsoft docs: Create a role using Microsoft Entra object identifier.

Token authentication from Django

pulumi_django_azure.settings points DATABASES["default"]["ENGINE"] at pulumi_django_azure.db.backends.postgresql. It is Django's PostgreSQL backend with one override: get_connection_params() sets the password to a fresh Managed Identity access token (get_db_password()) at the moment a connection is opened.

Why per connection and not once at import: the token lives roughly 24 h, and Gunicorn workers are separate processes. A token read into settings.DATABASES at import time, or refreshed inside one worker, is never seen by the master (which forks new workers from its own stale copy) nor by the other workers or the RQ worker. Symptom of the old approach: FATAL: The access token has expired and HTTP 500s for 1-3 minutes after every worker recycle once the app had been running for a day.

Tokens are served from the credential's in-memory cache, so the per-connection cost is negligible; with CONN_MAX_AGE = None connections are persistent anyway. CONN_HEALTH_CHECKS = True makes Django verify a persistent connection at the start of each request and reopen it (with a fresh token) when the server dropped it, e.g. after a failover.

Because tokens are resolved per connection, long-running processes no longer need periodic restarts to pick up credentials: the RQ worker and rqcron programs in supervisord.conf run without a timeout wrapper.

Administrator login

django.add_database_administrator(
    object_id="b306adf5-fc61-4a32-8156-ce032dc1571f",
    user_name="you@example.com",
)

Temporary password/token:

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.