Deployment¶
The framework ships three deployable artifacts:
- CLI / library —
pip install -e ".[dev,dashboard,api]"for local use. - Docker Compose — single-host stack (PostgreSQL + API + Dashboard).
- Helm chart — Kubernetes deployment under
deploy/helm/.
This document covers Compose and Helm. For development use, see the Quickstart in the project README.
Configuration model¶
Every deployment surface reads the same set of environment variables. Copy
.env.example to .env and edit before bringing services up.
| Variable | Purpose | Required | Default |
|---|---|---|---|
AML_ENV / API_ENV |
Set to production for non-demo API startup gates |
yes (prod) | unset (dev/demo mode) |
COSMOS_ENDPOINT |
Cosmos DB account endpoint for run / case persistence (Sponsorship-sub-friendly alternative to Postgres). When set, takes precedence over DATABASE_URL. Auth uses DefaultAzureCredential — pair with managed-identity / workload-identity + Cosmos Built-in Data Contributor role. |
yes for Cosmos deployments | unset |
COSMOS_DATABASE |
Cosmos database name. | optional | aml |
DATABASE_URL |
Postgres URL for run / case persistence (used when COSMOS_ENDPOINT is unset). |
yes for Postgres prod deployments | SQLite if both COSMOS_ENDPOINT and DATABASE_URL are unset |
JWT_SECRET |
HMAC secret for API tokens (32+ bytes) | yes (prod) | random per-process secret in dev/demo mode only |
ALLOW_DEMO_AUTH |
Explicitly re-enable built-in demo users in production | no | unset / disabled in production |
OIDC_ISSUER_URL |
OIDC discovery endpoint for SSO | recommended (prod) | unset (built-in users in dev only) |
OIDC_AUDIENCE |
Expected aud claim |
required when OIDC_ISSUER_URL is set |
unset |
OIDC_ALLOW_MISSING_AUDIENCE |
Disable OIDC audience verification for local-only testing | no (dev only) | unset |
OIDC_ROLE_CLAIM |
Claim path used for API role mapping | optional | roles |
OIDC_TENANT_CLAIM |
Claim path used for tenant isolation | optional | tid |
OIDC_ALLOWED_TENANTS |
Comma-separated tenant allowlist | optional | unset |
API_RATE_LIMIT |
Per-IP requests per minute | no | 600 |
API_DATA_ROOTS |
Local file roots allowed for API CSV/Parquet/ISO20022/DuckDB sources | no | data |
API_UPLOAD_ROOT |
Tenant-scoped upload storage root | no | data/uploads |
API_ARTIFACT_ROOT |
API run artifact storage root | no | data/api-artifacts |
API_MAX_UPLOAD_BYTES |
Max size for each uploaded CSV file | no | 26214400 |
API_ALLOW_REMOTE_DATA_SOURCES |
Enable API access to S3/GCS/Snowflake/BigQuery sources | no | unset |
SPEC_PATH |
Spec file the dashboard loads | no | examples/canadian_schedule_i_bank/aml.yaml |
JIRA_URL, JIRA_TOKEN, JIRA_PROJECT |
Jira case sync | optional | unset (no-op) |
SLACK_WEBHOOK_URL |
Slack alert push | optional | unset (no-op) |
TEAMS_WEBHOOK_URL |
Teams alert push | optional | unset (no-op) |
Persistence-backend selection (highest priority first):
COSMOS_ENDPOINTset → Azure Cosmos DB. Required combination for Azure Sponsorship subscriptions where Postgres Flexible Server is region-locked in every available region. UsesDefaultAzureCredential; needs a managed-identity (Container Apps UAMI / AKS workload-identity) granted the "Cosmos DB Built-in Data Contributor" role on the account.DATABASE_URLset (andCOSMOS_ENDPOINTunset) → PostgreSQL via psycopg2. The default for PAYG / EA / MCA Azure subscriptions and self-hosted deployments.- Otherwise → local SQLite at
~/.aml_framework/runs.db. Fine for demos, never for production.
Production mode is enabled with
AML_ENV=production or API_ENV=production; in that mode the API refuses to
start without JWT_SECRET and disables the built-in demo users unless
ALLOW_DEMO_AUTH=true is set deliberately. Outside production mode, an unset
JWT_SECRET logs a warning and uses a random per-process secret, so locally
issued JWTs do not survive a restart.
Docker Compose¶
docker-compose.yml defines three services on a single host:
cp .env.example .env
# edit JWT_SECRET, DATABASE_URL, and any integrations
docker-compose up --build
| Service | Image | Port | Depends on |
|---|---|---|---|
postgres |
postgres:16-alpine |
5432 |
– |
api |
built from Dockerfile |
8000 |
postgres healthy |
dashboard |
built from Dockerfile |
8501 |
– |
Volumes:
pgdata→/var/lib/postgresql/data(Postgres data files; survivesdocker-compose down).
The api container runs uvicorn aml_framework.api.main:app; the dashboard
container runs streamlit run src/aml_framework/dashboard/app.py. Both are
built from the same image — Dockerfile installs the project with the
[dev,dashboard,api] extras.
Health checks¶
The dashboard renders at http://localhost:8501. In local/demo mode the API
ships default demo users: admin / admin, analyst / analyst,
auditor / auditor, manager / manager. Do not use those credentials for
production: set AML_ENV=production, configure JWT_SECRET, and prefer OIDC.
Tear down¶
Helm¶
The chart under deploy/helm/ deploys API, Dashboard, and an optional
in-cluster Postgres. See deploy/helm/README.md for the rendered parameter
table; the full set of values lives in deploy/helm/values.yaml.
Build and push the image¶
Install¶
helm install aml ./deploy/helm/ \
--set image.repository=your-registry/aml-framework \
--set image.tag=0.1.0 \
--set jwt.secret="$(openssl rand -hex 32)" \
--set postgres.password="$(openssl rand -hex 16)"
Common overrides¶
# values-prod.yaml
image:
repository: registry.example.com/aml-framework
tag: "0.1.0"
api:
replicas: 3
resources:
requests: { cpu: "500m", memory: "1Gi" }
limits: { cpu: "2", memory: "4Gi" }
dashboard:
replicas: 2
spec: /etc/aml/spec/aml.yaml # mounted from a ConfigMap or PVC
postgres:
enabled: false # use a managed Postgres (RDS, Cloud SQL)
ingress:
enabled: true
host: aml.example.com
External Postgres¶
Set postgres.enabled=false and provide database.url. The chart stores it in
the release secret as database-url and the API deployment reads from that key:
Uninstall¶
The PVC backing in-cluster Postgres survives helm uninstall. Delete it
explicitly when wiping state:
Production checklist¶
Before exposing any deployment to non-demo traffic:
- Replace
JWT_SECRETwith a long random value (32+ bytes). - Replace
postgres.password(or move to a managed database). - Replace the demo user backend (
api/auth.py::DEMO_USERS) with OIDC or your IdP. Demo users are auto-disabled in production mode (api/auth.py::is_demo_auth_enabled()) unlessALLOW_DEMO_AUTHis explicitly set — verify the env override is unset. - Configure
OIDC_ROLE_CLAIM,OIDC_TENANT_CLAIM, andOIDC_ALLOWED_TENANTS. - Set
API_DATA_ROOTSand leave remote API sources disabled unless explicitly needed. - Set
API_RATE_LIMITappropriate to expected load. - Mount your real
aml.yamlfrom a ConfigMap, secret, or PVC. - Configure ingress TLS (cert-manager / managed cert).
- Confirm retention policy values in
aml.yamlmatch institutional policy. - Validate the spec in CI:
aml validate path/to/aml.yaml. - Wire
aml verifyinto a scheduled job to detect audit-ledger tampering.
Deploying on Azure / AKS¶
For banks deploying on Microsoft Azure, the framework supports a
zero-static-secrets shape via workload identity + Key Vault + Entra ID
OIDC. All Azure-specific behaviour is opt-in via the existing Helm
chart — leave the azure: block empty for an Azure-agnostic install
identical to the on-prem deployment.
A reference values file ships at deploy/helm/values-azure.example.yaml.
Prerequisites¶
# 1. Create the AKS cluster with OIDC issuer + workload identity.
az aks create --resource-group <rg> --name <cluster> \
--enable-oidc-issuer --enable-workload-identity \
--node-count 3
# 2. Get the OIDC issuer URL (needed for federated identity creds).
export OIDC_URL=$(az aks show -g <rg> -n <cluster> \
--query oidcIssuerProfile.issuerUrl -o tsv)
# 3. Create a user-assigned managed identity for the workload.
az identity create --resource-group <rg> --name aml-workload
# 4. Create Key Vault + grant the managed identity Secrets User.
az keyvault create --name kv-bank-aml-prod --resource-group <rg>
az role assignment create --role "Key Vault Secrets User" \
--assignee $(az identity show -g <rg> -n aml-workload --query principalId -o tsv) \
--scope $(az keyvault show -g <rg> -n kv-bank-aml-prod --query id -o tsv)
# 5. Federate the managed identity to the chart's ServiceAccounts.
az identity federated-credential create --name aml-api-fic \
--identity-name aml-workload --resource-group <rg> \
--issuer "$OIDC_URL" \
--subject "system:serviceaccount:default:<release>-api" \
--audiences api://AzureADTokenExchange
az identity federated-credential create --name aml-dashboard-fic \
--identity-name aml-workload --resource-group <rg> \
--issuer "$OIDC_URL" \
--subject "system:serviceaccount:default:<release>-dashboard" \
--audiences api://AzureADTokenExchange
Install¶
# Copy the example file and fill in the placeholders.
cp deploy/helm/values-azure.example.yaml my-values.yaml
# Edit: image.repository, oidc.issuerUrl + audience + allowedTenants,
# azure.workloadIdentityClientId, azure.keyVaultName,
# azure.storageAccountName, optionally synapseConnString /
# azureSqlConnString, ingress.host.
helm install aml ./deploy/helm -f my-values.yaml
What workload identity buys you¶
When azure.workloadIdentityClientId is set, the chart:
- Renders a
ServiceAccountwith theazure.workload.identity/client-idannotation for both the API and dashboard pods. - Adds the
azure.workload.identity/use: "true"pod label so the AKS webhook injects the OIDC token. - Threads
AZURE_KEY_VAULT_NAME+AZURE_STORAGE_ACCOUNT_NAME(and Synapse / Azure SQL conn strings) as env vars.
Inside the pod, every Azure SDK call (SecretClient, BlobServiceClient, pyodbc with Authentication=ActiveDirectoryMsi) automatically picks up the federated identity. No client secrets, no managed-identity ARM ID lookups, no cert rotation.
Data sources¶
After install, the dashboard + API pick up Azure data sources via the new types added in PR-AZ-1:
| Source type | URI / connection |
|---|---|
azure_blob / adls |
--data-dir abfss://<container>@<account>.dfs.core.windows.net/<path> |
synapse |
ODBC connection string in --data-dir (or env var AZURE_SYNAPSE_CONN) |
azuresql |
ODBC connection string in --data-dir (or env var AZURE_SQL_CONN) |
The Round-12 lineage chain (walk_lineage(case_id)) picks up Azure-sourced runs unchanged — source_path shows azure_blob:abfss://…/txn, synapse:DRIVER=…#trades, etc.
Production checklist (Azure additions)¶
- Federated identity credentials created BEFORE
helm install. - Key Vault populated:
JWT-SECRET,OPENAI-API-KEY(note the dashes — Key Vault disallows underscores; the SecretsProvider translates_→-automatically). - Entra ID app registration created with
rolesclaim +tidclaim mapped (the chart's defaults). - App roles defined in Entra ID match
analyst/auditor/manager/admin. -
oidc.allowedTenantsset to the tenant ID — single-tenant by default. - AKS cluster RBAC scoped per-namespace; the Key Vault role binding is on the managed identity, not the SA token.
- Storage account configured with Hierarchical Namespace (ADLS Gen2) for
azure_blob/adlssources.
Deploying on Azure via the cloud landing zone (Container Apps)¶
For banks consuming the prebuilt landing zone at
tomqwu/cloud_landing_zone_for_ai_coding,
the framework ships a Terraform module under deploy/terraform/
that deploys to Container Apps with one of two Entra-ID-authenticated
persistence backends:
- Postgres Flexible Server (B1ms) — default for PAYG / EA / MCA subscriptions.
- Cosmos DB serverless — alternative for Azure Sponsorship
subscriptions where Postgres Flexible Server returns
LocationIsOfferRestrictedin every available region. Setenable_cosmos = trueandenable_postgres = falseinterraform.tfvars. The module provisions a Cosmos account, theamldatabase, four containers (runs,run_alerts,run_metrics,spec_versions, partition key/tenant_id), grants the per-app UAMI the Cosmos Built-in Data Contributor role, and wiresCOSMOS_ENDPOINT/COSMOS_DATABASEinto both Container Apps. The Python persistence layer (src/aml_framework/api/db.py) selects Cosmos automatically whenCOSMOS_ENDPOINTis set.
This is the alternative path to the AKS Helm chart above.
The landing zone's CLAUDE.md forbids AKS, so this is the required path when consuming that landing zone. For self-managed Azure (no landing zone) the AKS Helm chart above remains the recommended shape.
Detailed cookbook: deploy/terraform/README.md.
Quick version:
cd deploy/terraform
terraform init \
-backend-config="resource_group_name=<from landing zone bootstrap>" \
-backend-config="storage_account_name=<from landing zone bootstrap>" \
-backend-config="container_name=tfstate" \
-backend-config="key=aml-compliance.tfstate"
cat > terraform.tfvars <<EOF
env = "dev"
owner_email = "you@example.com"
github_repo = "tomqwu/aml_open_framework"
platform_tfstate_resource_group = "<from landing zone>"
platform_tfstate_storage_account = "<from landing zone>"
platform_tfstate_container = "tfstate"
# Persistence backend — pick exactly one:
# Postgres (default, PAYG/EA/MCA subs):
# enable_postgres = true
# Cosmos serverless (Sponsorship subs where Postgres is region-locked):
# enable_postgres = false
# enable_cosmos = true
# # cosmos_database_name = "aml" # optional
EOF
terraform apply
Cost expectations on top of the landing zone's $5/mo baseline:
| Component | Approx. monthly |
|---|---|
| Container App API (min 1 replica) | ~$10 |
| Container App dashboard | ~$10 |
Postgres Flexible Server B1ms (enable_postgres=true) |
~$13 (or $0 with the Sponsorship-sub free tier in canadacentral) |
Cosmos DB serverless (enable_cosmos=true) |
~$0 idle (no provisioned RU/s) |
| Total (Postgres) | ~$33/mo |
| Total (Cosmos) | ~$20/mo |
After install, populate the per-app Key Vault with JWT-SECRET (and
optionally OPENAI-API-KEY for the GenAI co-pilot). Subsequent
deploys run locally from an operator workstation (az login +
terraform apply + az containerapp update --image …). CI does not
have Azure credentials and does not deploy. See
deploy/terraform/README.md → "Subsequent deploys (local)" for the
exact commands.
See audit-evidence.md for the evidence bundle contract
and architecture.md for the layered runtime view.