02. Platform foundation

Phase 0 — the Azure footprint every later phase depends on: registry, ACA environment, Postgres, storage, Key Vault, Grafana, per-workload identities, and self-hosted MLflow.

Outcome

Every plane from the overview has a home Azure resource, each workload has its own least-privilege managed identity, and the whole footprint is described as code so it can be recreated and reviewed. The self-hosted MLflow app is running. This is Phase 0 of the delivery journey (docs/07); it implements docs/01.

The foundation is intentionally small — a container registry, a container-apps environment, two Postgres databases, one storage account, a Key Vault, a Log Analytics workspace, and Grafana. There is no Service Bus, Redis, Durable Functions storage, or Azure ML workspace here; those appear only if a documented upgrade (broker, Ch 09) or exception (multi-GPU, Ch 08) is triggered.

## Design — resource inventory

The entire baseline footprint (docs/01):

Resource Purpose
Azure Container Registry Immutable images for Jobs, serving App, MLflow, dashboard — referenced by digest, never latest
Container Apps Environment Hosts all Jobs and Apps; Log Analytics attached
Postgres (flexible server) mlflow DB (registry/tracking) + results DB (run state) — one small server, two logical databases, separate logins
Azure Storage (Blob) MLflow artifacts, batch outputs, immutable manifests — separate containers per concern
Azure Key Vault Connection strings / unavoidable secrets — read via managed identity, never baked into images
Log Analytics workspace Infra telemetry and alert rules; feeds App Insights and Grafana
Azure Managed Grafana Deep operational dashboards; reads Log Analytics + Postgres
Microsoft Entra ID Per-workload managed identities + dashboard sign-in (Easy Auth)

## 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/

Phase 0 authors the infrastructure and the self-hosted MLflow image. The source lives in the project and is what the rest of the course builds on — this chapter just wires it together:

projects/ml-platform/
├── infra/
│   ├── main.tf                 # composes foundation + mlflow_app (two-pass)
│   ├── variables.tf  outputs.tf
│   ├── grants.sql              # Postgres principals + least-priv grants
│   ├── environments/
│   │   └── dev.tfvars
│   └── modules/
│       ├── foundation/         # RG, ACR, ACA env, Log Analytics, storage,
│       │   ├── main.tf         #   Key Vault, Postgres (Entra-only), Grafana
│       │   ├── identities.tf   # 6 user-assigned identities + scoped RBAC
│       │   ├── variables.tf  outputs.tf
│       └── mlflow_app/         # MLflow ACA App (image-pinned, 2nd pass)
│           └── main.tf  variables.tf  outputs.tf
├── src/mlflow_app/             # self-hosted MLflow container image
│   ├── Dockerfile              # pinned MLflow + psycopg + azure-storage-blob
│   ├── requirements.txt
│   └── entrypoint.sh           # fetches Entra token → mlflow server
└── deploy/
    └── deploy.ps1              # two-pass deploy orchestration

The registered-model identity used everywhere downstream comes from this MLflow app, so it is part of the foundation rather than a later add-on.

## How the pieces connect

Auth is managed-identity end to end — no passwords in Terraform, no secrets in Git. Three details make that work:

  • Postgres is Entra-only. foundation/main.tf sets authentication { password_auth_enabled = false, active_directory_auth_enabled = true } and names the deployer as AAD admin. Workloads connect with an access token as the libpq password (see src/mlflow_app/entrypoint.sh, which calls DefaultAzureCredential for the ossrdbms-aad scope).
  • Least privilege is declarative except per-database grants. Azure RBAC (AcrPull, Storage Blob roles, Key Vault Secrets User, the custom job_starter and aca_deployer roles) lives in foundation/identities.tf. Postgres role membership can’t be expressed in azurerm, so infra/grants.sql maps each managed identity to a Postgres principal with only the databases it needs — the one deliberate exception to pure IaC (docs/01).
  • The MLflow App is image-pinned, so it deploys in a second pass. main.tf guards the module with count = var.mlflow_image == "" ? 0 : 1. deploy.ps1 applies the foundation, builds+pushes the image to ACR, runs grants.sql, then re-applies with the pinned digest.
# from projects/ml-platform/ (pass your own Entra admin UPN)
./deploy/deploy.ps1 -TfVars infra/environments/dev.tfvars -PgAdminUpn you@example.com

Prerequisite (docs/01, open decision): writing role assignments needs User Access Administrator on the RG. The deployer has only Contributor today, so this grant is requested before the first apply.

## 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 plan is 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 mlflow Postgres DB and Blob artifact store — a registered test model appears in both.
  • Tear-down (terraform destroy for dev) leaves no residual billable resources.

## Extensions (deferred from the MVP)

What the production contract (docs/01, docs/06) asks for that this Phase-0 MVP intentionally defers:

Deferred Contract MVP substitute
Private endpoints / VNet integration docs/01 Public access with IP firewall rules
Governance / Azure Policy docs/01 Manual review of IaC
Identity bootstrap ordering (deployer → workload identities) docs/01 Single deployment script, shared RG
Multiple environments (dev/stage/prod) docs/06 One dev env, parameterized for copy
Postgres topology split (results onto its own server) docs/00 open decision Two databases on one small server

Next: 03 — Reproducible training & registry runs the first real workload on this foundation.

Back to top