10. Azure platform foundation

The Azure footprint every later part depends on: registry, ACA environment, Postgres, storage, Key Vault, Log Analytics, per-workload identities, and self-hosted MLflow.

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-aad tokens and connect as their own Entra principals.
  • Azure RBAC grants ACR, Blob, Key Vault, Log Analytics, and ACA execution permissions. infra/grants.sql is 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.com

The 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 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)

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.

Back to top