The Azure quickstart gets a VM running in five minutes. It also skips every decision that matters once that VM is carrying real traffic: which SKU family, how it authenticates to other services, and how its network exposure is scoped. This is the baseline I use on every Azure project, past the quickstart.

Region and availability design

Azure's region list is large, but the decision that actually affects uptime is Availability Zones vs. Availability Sets:

  • Availability Zones spread VMs across physically separate datacenters within a region, each with independent power and networking. Use these whenever the region supports them — they're the stronger guarantee.
  • Availability Sets spread VMs across fault domains within a single datacenter. Use this only in regions without zone support.
az vm create \
  --resource-group prod-rg \
  --name app-vm-1 \
  --zone 1 \
  --image Ubuntu2204 \
  --size Standard_D4s_v5 \
  --vnet-name prod-vnet \
  --subnet app-subnet \
  --nsg app-nsg \
  --public-ip-address "" \
  --generate-ssh-keys

--public-ip-address "" is deliberate — application VMs shouldn't have public IPs at all; they sit behind a Load Balancer or Application Gateway, and administrative access goes through Azure Bastion, not a public SSH endpoint.

Choosing a VM SKU family

Azure's naming tells you the shape of the machine once you know the pattern:

Series Use case Comparable to
Dsv5 General purpose, balanced CPU/memory AWS m6i, Alibaba ecs.g7
Fsv2 Compute-optimized AWS c6i
Esv5 Memory-optimized AWS r6i
Lsv3 Storage-optimized, local NVMe AWS i4i

For a typical containerized service, Dsv5 covers it. Move to Fsv2 for CPU-bound work (build agents, encoding pipelines) and Esv5 for in-memory workloads that outgrow a managed cache tier.

Identity first: Managed Identity, not stored credentials

The single highest-leverage security decision on any Azure VM is enabling a system-assigned Managed Identity and granting it exactly the roles it needs via Azure RBAC — never embedding a service principal's client secret on the VM itself.

az vm identity assign --resource-group prod-rg --name app-vm-1

az role assignment create \
  --assignee-object-id $(az vm show -g prod-rg -n app-vm-1 --query identity.principalId -o tsv) \
  --role "Storage Blob Data Reader" \
  --scope /subscriptions/<sub-id>/resourceGroups/prod-rg/providers/Microsoft.Storage/storageAccounts/appstorage

From inside the VM, any SDK that supports DefaultAzureCredential picks this identity up automatically — no key vault lookups, no rotation burden, no secret that can leak in a log line.

Network Security Groups: default-deny, then open narrowly

az network nsg rule create \
  --resource-group prod-rg \
  --nsg-name app-nsg \
  --name allow-lb-only \
  --priority 100 \
  --source-address-prefixes AzureLoadBalancer \
  --destination-port-ranges 8080 \
  --access Allow \
  --protocol Tcp

Two habits that prevent the incidents I've actually seen happen:

  1. Never create a rule with --source-address-prefixes '*' for anything other than a public load balancer's health probe. It's the fastest way to accidentally expose an internal service.
  2. Tag every resource on creationEnvironment, Owner, CostCenter. Retrofitting tags across a resource group after the fact is a genuinely miserable Friday.

Wiring it into the rest of the stack

An Azure VM in isolation isn't useful on its own. What I connect immediately:

  • Azure Monitor Agent, installed via VM extension, so CPU/memory/disk alerts exist before the first incident rather than after.
  • Azure Bastion for administrative access — no public SSH/RDP surface, ever.
  • A VNet with private subnets for anything that isn't a load balancer or Bastion host itself.

What I'd tell someone starting today

Provision this through Terraform or Bicep from the very first VM, even for a single-instance proof of concept. The moment a second environment exists — staging next to production — doing this by hand in the portal is exactly the thing that drifts quietly and becomes the incident nobody can explain six months later. I cover the Terraform version of this same VM in a follow-up article.