La domanda che separa un progetto di data science da uno MLOps è brutale: se domani il resource group sparisse, quanto ci vorrebbe a rimetterlo in piedi identico? Se la risposta è «bisogna chiedere a chi lo ha creato», l’ambiente non è infrastruttura ma una serie di clic fortunata. Qui mettiamo insieme i tre pezzi che rendono la risposta ripetibile: la descrizione dichiarativa con Bicep e Azure CLI, l’automazione del provisioning con GitHub Actions e il controllo di versione di ciò che davvero va in Git.
Bicep e Azure CLI: due strati, non uno
Bicep è un linguaggio dichiarativo che transpila in JSON ARM. Tre proprietà contano per un MLOps engineer: è idempotente, quindi rieseguire lo stesso deployment non duplica nulla; non ha uno state file da custodire, perché lo stato è Azure stesso; e orchestra da solo ordine e parallelismo delle dipendenze. Il workspace si descrive con il tipo Microsoft.MachineLearningServices/workspaces, e le proprietà interessanti sono quelle già viste: identity, keyVault, storageAccount, publicNetworkAccess, managedNetwork.
Il deployment si lancia da Azure CLI, con lo scope scelto in base a cosa crei — resource group, subscription, management group o tenant:
az deployment group what-if \
--resource-group rg-mlops-prod \
--template-file main.bicep --parameters prod.bicepparam
az deployment group create \
--resource-group rg-mlops-prod --name ws-2026-08 \
--template-file main.bicep --parameters prod.bicepparam
Due opzioni sono materiale d’esame. What-if mostra in anteprima le modifiche prima di applicarle, classificandole in Create, Delete, Modify, Deploy, NoChange e Ignore; in pipeline si usa come gate prima di promuovere in produzione, o con --confirm-with-what-if in interattivo. E il mode: Incremental è il default e lascia stare ciò che non è nel template, mentre Complete elimina dal resource group tutto ciò che il template non dichiara — su un resource group condiviso cancella risorse che nessuno voleva toccare. Per i segreti, niente valori nel repository: si dichiara un parametro @secure() alimentato da Azure Key Vault, referenziando un key vault existing con getSecret.
Il punto che molti mancano è che gli strati sono due. Bicep crea le risorse Azure: workspace, storage, key vault, Application Insights, container registry. Tutto ciò che vive dentro il workspace — compute, datastore, environment, component, data asset, job, endpoint — si crea invece con l’estensione CLI v2 e i suoi YAML. Si installa con az extension add -n ml, rimuovendo la vecchia azure-cli-ml, e conviene fissare i default con az configure --defaults group=... workspace=....
GitHub Actions senza segreti: OIDC e workload identity federation
L’obiettivo «configurare l’integrazione GitHub per abilitare un accesso sicuro» ha una risposta precisa: workload identity federation. Si stabilisce una relazione di trust fra un issuer esterno (GitHub) e un’applicazione di Microsoft Entra ID oppure una user-assigned managed identity; il workflow ottiene un token da GitHub, lo scambia con un token Entra e agisce su Azure. Nessun client secret nel repository, nessuna rotazione da calendarizzare. La documentazione di Azure Machine Learning è esplicita: OpenID Connect è l’opzione raccomandata, mentre service principal con client secret è descritto come meno sicuro e non consigliato.
Il workflow vive in .github/workflows/ e servono due cose: il permesso id-token: write, senza cui il runner non può chiedere il token, e i tre valori client-id, tenant-id, subscription-id — identificatori, non credenziali, ma da tenere comunque nei secret.
name: train
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
train:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- run: |
az extension add -n ml -y
az ml job create --file jobs/train.yml \
-g rg-mlops-dev -w ws-dev --stream
La federated identity credential è vincolata a un subject che identifica repository e contesto — branch, environment o pull request — quindi un workflow su un branch di feature non ottiene un token valido per la produzione: è ciò che rende reale la separazione fra ambienti. Nei repository pubblici Microsoft consiglia i secret di environment invece di quelli di repository, perché possono richiedere l’approvazione di un revisore. Il comando az ml job è la spina dorsale della pipeline: create con un file YAML, poi stream, show, download, cancel. Su Azure DevOps l’architettura è identica, con service connection al posto del login OIDC e un task Azure CLI che invoca la stessa estensione ml.
Git in un progetto di machine learning: cosa si versiona
In Git va il codice sorgente dell’automazione: script di training e scoring, i YAML di job, componenti, environment ed endpoint, le specifiche conda o i Dockerfile, i file Bicep con i loro .bicepparam, i workflow. Non ci vanno i dataset né i binari dei modelli, che vivono nei datastore e nei registry e si referenziano per nome e versione; non ci vanno segreti né output dei notebook.
C’è poi una tracciabilità che Azure Machine Learning offre gratis. Se sottometti un job da una directory che è un repository Git locale e il comando git è disponibile, il servizio registra come proprietà di sistema del job azureml.git.repository_uri, azureml.git.branch, azureml.git.commit e azureml.git.dirty, più le equivalenti mlflow.source.git.*. Si leggono da studio, dall’SDK o con az ml job show --query. Il campo dirty è il più istruttivo: vale True quando ci sono modifiche non committate, cioè quando quel run non è riproducibile. Un runner di CI fa un checkout pulito e produce sempre False; è dal portatile che arrivano i run sporchi. Se git non è disponibile o i file non stanno nel repository, non viene tracciato nulla — e l’assenza delle proprietà è essa stessa un segnale.
Nota operativa: sulle compute instance puoi clonare il repository nel file system locale, più veloce ma perso se ricrei l’istanza, oppure nel file system condiviso montato in ~/cloudfiles/code/, che sopravvive. Clona nella tua directory utente, per non collidere con i branch dei colleghi.
Da ricordare per l esame
- Bicep è dichiarativo, idempotente e senza state file; il deployment si lancia con
az deployment group createe si verifica prima con what-if. Incrementalè il mode di default;Completeelimina dal resource group le risorse non dichiarate nel template.- Bicep crea le risorse Azure,
az mlcon file YAML crea ciò che vive dentro il workspace: sono due strati distinti. - Per GitHub Actions usa OIDC con federated identity credential: serve
permissions: id-token: writee i tre valori client-id, tenant-id, subscription-id — nessun client secret. - Git traccia automaticamente
azureml.git.branch,azureml.git.commiteazureml.git.dirtysul job;dirtyaTruesignifica run non riproducibile.