10. Azure platform foundation
Outcome
Every plane from the overview has an Azure resource, each workload has its own least-privilege managed identity, and the footprint is described as code. The self-hosted MLflow app is running.
The foundation is intentionally small: a container registry, a Container Apps environment, two Postgres databases, one storage account, a Key Vault, and a Log Analytics workspace. There is no Service Bus, Redis, Durable Functions storage, Managed Grafana, Application Insights, or Azure ML workspace in the baseline. Azure ML appears only if the chapter 14 multi-GPU exception is admitted.
Resource inventory
| Resource | Purpose |
|---|---|
| Azure Container Registry | Immutable workload images referenced by digest |
| Container Apps Environment | Hosts Jobs and Apps; sends console logs to Log Analytics |
| Postgres flexible server | Separate mlflow and results databases |
| Azure Blob Storage | MLflow artifacts and large batch outputs |
| Azure Key Vault | Unavoidable runtime secrets read through managed identity |
| Log Analytics workspace | ACA container logs and the two log-backed batch alert rules |
| Microsoft Entra ID | Per-workload managed identities plus dashboard sign-in and operator group |
## Design — identities and RBAC
Every workload gets its own user-assigned managed identity with the minimum roles it needs. No workload shares an identity; none gets broad Contributor.
| Identity | Assigned to | Roles (least privilege) |
|---|---|---|
id-jobs-train |
Training/eval Jobs | ACR pull; Blob read/write; Postgres mlflow + results; Key Vault get |
id-jobs-batch |
Batch inference Jobs | ACR pull; Blob read/write; Postgres results; Blob read of MLflow artifacts |
id-serving |
Serving App | ACR pull; Blob read of MLflow artifacts; Key Vault get |
id-mlflow |
MLflow App | Postgres mlflow; Blob read/write (artifacts) |
id-dashboard |
Dashboard App | ACA execution start (scoped Jobs); Postgres read of results; Log Analytics read |
id-ci (OIDC) |
GitHub Actions | ACR push; ACA Job/App definition update; no runtime data access |
Human access is separate from these machine identities: people sign in through the dashboard’s Entra Easy Auth, and who may do what is controlled by Entra security groups (ml-platform-operators, ml-platform-viewers), not by managed identities.
Build in projects/ml-platform/
projects/ml-platform/
├── infra/
│ ├── main.tf # foundation, MLflow, and workload composition
│ ├── variables.tf outputs.tf
│ ├── grants.sql # Postgres principals + least-privilege grants
│ ├── secret.auto.tfvars.example
│ ├── environments/dev.tfvars
│ └── modules/
│ ├── foundation/ # RG, ACR, ACA env, Log Analytics, storage,
│ │ # Key Vault, Postgres, identities, and RBAC
│ ├── mlflow_app/ # self-hosted MLflow ACA App
│ ├── train_job/ # reusable train/eval ACA Job adapter
│ ├── batch_job/
│ ├── serving_app/
│ ├── dashboard/ # ACA App + Easy Auth child resource
│ └── observability/ # two console-log alert rules
├── src/mlflow_app/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── entrypoint.sh
└── deploy/
└── deploy.ps1
The registered-model identity used downstream comes from this MLflow app, so it belongs in the foundation rather than a later add-on.
Identity, secrets, and the staged deployment
Machine authentication is managed-identity based:
- PostgreSQL disables password authentication. Workloads obtain short-lived
ossrdbms-aadtokens and connect as their own Entra principals. - Azure RBAC grants ACR, Blob, Key Vault, Log Analytics, and ACA execution permissions.
infra/grants.sqlis the deliberate exception to pure IaC: it maps identity object IDs to database roles and table privileges. - MLflow deploys after the foundation because its image must first be pushed to the newly created registry. The remaining workloads follow in the final image-pinned apply.
Human dashboard sign-in needs an Entra app registration. Its client ID, client secret, and operator-group object ID live in ignored secret.auto.tfvars; the secret variable is sensitive and becomes a Container App secret consumed by Easy Auth. Terraform state therefore contains sensitive material and must be protected before shared or CI-driven deployments. No secret is committed or baked into an image.
The dashboard hostname is assigned during deployment. Create the single-tenant app registration and secret first, apply the dashboard, then add terraform output -raw dashboard_auth_callback_url as a Web redirect URI in the registration. Configure the registration’s token settings to emit security-group claims; the app compares those claims with dashboard_operator_group_id. These explicit steps resolve the hostname bootstrap cycle and make the viewer/operator boundary enforceable.
# from projects/ml-platform/
./deploy/deploy.ps1 -TfVars infra/environments/dev.tfvars -PgAdminUpn you@example.comThe dashboard module refuses deployment when its image is supplied without a complete Easy Auth configuration. Creating role assignments also requires User Access Administrator at the resource-group scope.
## Golden-path position & acceptance evidence
Foundation sits before the golden path: it is the ground every step stands on. Nothing in the path can run until Phase 0 exists.
Acceptance evidence (not “terraform apply succeeded”):
terraform planis clean and every resource is created by IaC, reviewable in Git.- Each identity exists with only its listed roles (no broad
Contributor). - The MLflow app answers over HTTP and its registry is backed by the
mlflowPostgres DB and Blob artifact store — a registered test model appears in both. - Tear-down (
terraform destroyfordev) leaves no residual billable resources.
Extensions (deferred from the MVP)
The Phase-0 baseline intentionally defers several production-hardening choices:
| Production hardening | Phase-0 baseline |
|---|---|
| Private endpoints / VNet integration | Public access with IP firewall rules |
| Governance / Azure Policy | Manual review of IaC |
| Identity bootstrap ordering (deployer → workload identities) | Single deployment script, shared RG |
| Multiple environments (dev/stage/prod) | One dev env, parameterized for copy |
| Postgres topology split (results onto its own server) | Two databases on one small server |
Next: 11 — Porting jobs & apps to ACA moves the shared images onto the workload definitions.