Background
Sections
IntroductionFoundations1. Resource Hierarchy2. Resource Manager3. Identity and RBAC4. Regions and Availability5. Naming and TaggingVirtual Machines1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetVirtual Network1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetBlob Storage1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure SQL Database1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Kubernetes Service1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Container Registry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetMicrosoft Entra ID1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure RBAC1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Functions1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAPI Management1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure App Configuration1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Machine Learning1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Monitor1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure AI Foundry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and Cheatsheet

4. Getting Started

8 min read

One task, three ways. Create a workspace, add a compute cluster that scales to zero, submit a tracked training job, and register the model it produces. Then delete everything.

This page is deliberately throwaway. Nothing here is production-shaped — no private endpoints, no user-assigned identity, no remote state, no pinned versions. That is Deployment. The point is to see the objects move once.

[Image Prompt: 2D minimalistic diagram comparing three provisioning paths, Azure Portal, Azure CLI, and Terraform, converging on the same Azure Machine Learning workspace containing one compute cluster and one registered model, flat design, clean vector art style, white background]

Before you start

  • An Azure subscription where you can create resources and assign roles.

  • Azure CLI, with the v2 ML extension: az extension add -n ml (or az extension update -n ml).

  • Check quota first. This is the step people skip and regret:

    az ml compute list-usage --location eastus -o table

    If the Standard_DS family shows zero available vCPUs, nothing below will run and no amount of YAML will fix it. Request quota, or pick a region where you have some.

  • A throwaway resource group, created first, so teardown is one command:

    export RG=rg-aml-demo
    export LOC=eastus
    export WS=aml-demo-$RANDOM
    az group create -n $RG -l $LOC

Path 1 — Portal

Blade names, not a click chain, because portal navigation rots fast.

  • Azure Machine LearningCreate → fill in resource group, workspace name, and region. Leave the storage account, Key Vault, Application Insights, and container registry as auto-created — this is the throwaway path; in real work you name these yourself.
  • Once deployed, Launch studio. You are now at ml.azure.com, on the workspace's data plane.
  • In the studio: ComputeCompute clustersNew. Pick a small CPU size, set minimum nodes = 0, maximum nodes = 2, idle seconds before scale down = 120.
  • JobsCreate (or Notebooks if you'd rather run interactively). Point it at a command, an environment, and the cluster you just made.
  • Models shows anything you register; Endpoints is where a deployment would go.

Two things worth noticing while you're here. The Compute instances tab is not the clusters tab — an instance is a personal VM billed while it runs, and if you create one, set idle shutdown now. And the workspace's Overview blade lists the four dependent resources; open the resource group and confirm you now own five things, not one.

Path 2 — Azure CLI (az ml)

Everything below is copy-pasteable and assumes the environment variables from above.

Create the workspace

az ml workspace create \
  --name $WS \
  --resource-group $RG \
  --location $LOC

This auto-creates the storage account, Key Vault, and Application Insights with generated names. Fine for a demo; see Deployment for how to bring your own.

Make it the default so later commands are shorter:

az configure --defaults group=$RG workspace=$WS

Create a compute cluster that scales to zero

az ml compute create \
  --name cpu-cluster \
  --type AmlCompute \
  --size Standard_DS3_v2 \
  --min-instances 0 \
  --max-instances 2 \
  --idle-time-before-scale-down 120

--min-instances 0 is the whole point. With it, an idle cluster costs nothing.

A training script

src/train.py — note that it logs with MLflow, not an Azure SDK:

import argparse, mlflow
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

p = argparse.ArgumentParser()
p.add_argument("--n-estimators", type=int, default=100)
args = p.parse_args()

X, y = load_iris(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)

mlflow.sklearn.autolog()            # logs params, metrics, and the model
model = RandomForestClassifier(n_estimators=args.n_estimators).fit(X_tr, y_tr)
mlflow.log_metric("test_accuracy", model.score(X_te, y_te))

mlflow.sklearn.autolog() registers the model artifact in MLflow format, which is what makes the no-code deployment path possible later.

Submit it as a job

job.yml:

$schema: https://azuremlschemas.azureedge.net/latest/commandJob.schema.json
code: ./src
command: python train.py --n-estimators ${{inputs.n_estimators}}
inputs:
  n_estimators: 200
environment: azureml://registries/azureml/environments/sklearn-1.5/labels/latest
compute: azureml:cpu-cluster
display_name: iris-rf
experiment_name: iris-demo
az ml job create -f job.yml --web

--web opens the run in the studio. The first submission is slow — code snapshot upload, then image pull, then node allocation — and that has nothing to do with your training loop. See Architecture.

The environment line uses a curated environment from the shared azureml registry. Note it's pinned to labels/latest, which is fine here and wrong in production.

Register the model

Grab the job name from the previous command's output (or az ml job list -o table), then:

export JOB=<job-name-from-output>

az ml model create \
  --name iris-rf \
  --version 1 \
  --type mlflow_model \
  --path azureml://jobs/$JOB/outputs/artifacts/model
az ml model list -o table

You now have a versioned artifact whose lineage points back at the job that made it, which points at the code snapshot, the environment, and the metrics. That chain is the product.

Optional — deploy it and score once

Only if you want to see the serving path. This costs money per hour from the moment it succeeds.

az ml online-endpoint create --name iris-ep-$RANDOM --auth-mode key
# use the name you just created:
export EP=<endpoint-name>

az ml online-deployment create \
  --endpoint-name $EP \
  --name blue \
  --model azureml:iris-rf:1 \
  --instance-type Standard_DS3_v2 \
  --instance-count 1 \
  --all-traffic

No scoring script and no environment: an mlflow_model carries enough metadata for Azure ML to generate both. Then:

echo '{"input_data": {"columns":[0,1,2,3],"data":[[5.1,3.5,1.4,0.2]]}}' > sample.json
az ml online-endpoint invoke --name $EP --request-file sample.json

Delete it as soon as you've seen it work:

az ml online-endpoint delete --name $EP --yes

Path 3 — Terraform (minimal)

Enough to create the workspace, its dependencies, and the cluster. Not a module, not parameterised, no remote state — Deployment does all of that properly.

terraform {
  required_providers {
    azurerm = { source = "hashicorp/azurerm", version = "~> 4.0" }
  }
}

provider "azurerm" {
  features {
    # Azure ML workspaces soft-delete. Without this, `terraform destroy`
    # followed by re-apply fails on a name that is "gone" but not purged.
    machine_learning {
      purge_soft_deleted_workspace_on_destroy = true
    }
    key_vault {
      purge_soft_delete_on_destroy = true
    }
  }
}

resource "azurerm_resource_group" "demo" {
  name     = "rg-aml-demo-tf"
  location = "eastus"
}

resource "azurerm_storage_account" "demo" {
  name                     = "stamldemo${random_string.suffix.result}"
  resource_group_name      = azurerm_resource_group.demo.name
  location                 = azurerm_resource_group.demo.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_key_vault" "demo" {
  name                = "kv-amldemo-${random_string.suffix.result}"
  resource_group_name = azurerm_resource_group.demo.name
  location            = azurerm_resource_group.demo.location
  tenant_id           = data.azurerm_client_config.current.tenant_id
  sku_name            = "standard"
  purge_protection_enabled = false   # demo only — see Deployment
}

resource "azurerm_application_insights" "demo" {
  name                = "appi-amldemo"
  resource_group_name = azurerm_resource_group.demo.name
  location            = azurerm_resource_group.demo.location
  application_type    = "web"
}

resource "azurerm_machine_learning_workspace" "demo" {
  name                    = "aml-demo-tf"
  resource_group_name     = azurerm_resource_group.demo.name
  location                = azurerm_resource_group.demo.location
  storage_account_id      = azurerm_storage_account.demo.id
  key_vault_id            = azurerm_key_vault.demo.id
  application_insights_id = azurerm_application_insights.demo.id

  identity { type = "SystemAssigned" }
}

resource "azurerm_machine_learning_compute_cluster" "cpu" {
  name                          = "cpu-cluster"
  location                      = azurerm_resource_group.demo.location
  machine_learning_workspace_id = azurerm_machine_learning_workspace.demo.id
  vm_priority                   = "Dedicated"
  vm_size                       = "Standard_DS3_v2"

  scale_settings {
    min_node_count                       = 0
    max_node_count                       = 2
    scale_down_nodes_after_idle_duration = "PT2M"
  }
}

data "azurerm_client_config" "current" {}

resource "random_string" "suffix" {
  length  = 6
  special = false
  upper   = false
}
terraform init && terraform apply

Two things to notice, because they generalise. First, the features {} block is doing real work: Azure ML workspaces and Key Vaults both soft-delete, and without those flags a destroy-then-apply cycle fails on names that appear free and aren't. Second, there is no azurerm resource for jobs, models, environments, or online deployments. Terraform builds the container; the assets inside it are data-plane objects created by az ml or azapi. That division is the central design decision in Deployment.

Teardown

Do this now, not later.

az group delete --name $RG --yes --no-wait

For the Terraform path:

terraform destroy

What deletion does not do, and this is the trap that generates duplicate resources next week:

  • The workspace soft-deletes. Its name is reserved for a retention period. Recreating with the same name fails until you recover it or purge it. ⚠️ Retention period and whether soft delete is on by default vary — verify against current Azure docs. Check with:

    az ml workspace list-deleted -o table
    az ml workspace purge --name $WS --resource-group $RG --location $LOC
  • Key Vault soft-deletes too, and if purge protection is on, it cannot be purged early — the name is gone for the retention period no matter what you do. That is why the demo above sets purge_protection_enabled = false and production sets it to true.

  • Deleting the workspace alone leaves storage, ACR, App Insights, and Key Vault running and billing. Deleting the resource group is what actually stops the meter, which is why we made one.

You have seen the objects. Deployment turns this into something you would put a change ticket against — a parameterised module, remote state, user-assigned identity, blue/green endpoints, and a pipeline that authenticates without a secret.


Next: Deployment →

← Back to the Azure Machine Learning overview · ← Previous: Architecture