You closed module 1 with the foundations in place: the account, the billing hierarchy, the rg-contoso-reservas-dev and rg-contoso-reservas-pro resource groups, the mandatory tagging scheme and an Azure CLI script that deploys all of it idempotently. Now the real construction of the platform begins, and we start with the least glamorous and most unavoidable piece: a virtual machine.

Contoso Airlines has a problem that shows up in almost every migration. Their legacy availability engine — the process that works out how many seats are still free on each flight and at what price — is a monolithic Java application written eleven years ago, with operating system dependencies, absolute paths and a service that starts through a home-made script. Nobody on the team is willing to rewrite it before peak season. Diego Salas sums it up in one sentence: "it works, nobody fully understands it, and if we touch it now we don't sell tickets in July".

That application cannot go to a platform service yet. It needs a server with operating system access. In Azure, that means a virtual machine. In this lesson you will learn when a VM is the right answer and when it is expensive laziness, which resources it drags along with it, how to choose size and disk without ruining yourself, how to create and configure it from Azure CLI and — above all — how to shut it down so that it really stops billing, a subtlety that catches almost everyone out the first time.

Cost warning: this lesson creates resources billed by the hour. A Standard_B1s size with a Standard SSD disk costs very little per day, but it does not cost zero. At the end of the lesson you have the full cleanup; run it if you are not going to keep using the VM.

Contents

  1. VM versus PaaS: when each one makes sense
  2. Anatomy of a virtual machine in Azure
  3. VM families and sizes: how to read the naming
  4. Disks: types, performance and cost
  5. Images: marketplace, custom images and galleries
  6. Creating the availability engine VM with Azure CLI
  7. Connecting over SSH and installing the service
  8. Automatic initial configuration: cloud-init and extensions
  9. VM states and what is billed in each one
  10. Snapshots and images: copies and clones
  11. Cleaning up when you finish
  12. Common Mistakes and Tips
  13. Exercises
  14. Conclusion

  1. VM versus PaaS: when each one makes sense

In lesson 01-02 you saw the shared responsibility model: with IaaS you manage the operating system upwards; with PaaS, only your application and its data. The practical question is not which one is "better", but what forces you to stay down at the bottom.

Situation Reasonable choice Why
Legacy software with OS dependencies, home-made services, fixed paths Virtual machine You need full control of the operating system
Third-party license that requires installation on a server Virtual machine The vendor does not support any other model
Modern web application (Java, .NET, Node, Python) that can be packaged App Service (02-03) Less surface to administer and patch
Event-driven process, short and intermittent execution Azure Functions (06-03) You pay per execution, not for a server left running
Application that is already containerized Container Apps / AKS (06-01, 06-02) Portability and density
You need a GPU, a custom kernel or low-level networking software Virtual machine Only IaaS gives you that level

The real cost of a VM is not the monthly bill: it is the recurring work that comes with it. Operating system patches, backups, configuration hardening, agent monitoring, key rotation. With App Service, a good part of that disappears from your list.

That is why Contoso's decision is recorded like this, and it is worth internalizing because it shapes the whole module:

  • The legacy availability engine goes to a VM, as an intermediate and explicitly temporary step.
  • Contoso Bookings (the public website) and the new Availability API go to App Service (lesson 02-03).
  • The VM is modernized later on, when the team can rewrite the engine; in module 6 you will see where it ends up.

This is exactly what the Cloud Adoption Framework calls rehost ("lift and shift"): move first, optimize afterwards. It is a legitimate strategy as long as the "afterwards" has a date. If it does not, the VM stays for ten years.

  1. Anatomy of a virtual machine in Azure

When you click "Create virtual machine" in the portal, Azure does not create one resource: it creates several, each with its own lifecycle and its own line on the bill. Understanding this avoids the classic surprise of deleting the VM and carrying on paying.

graph TD
    RG["Resource group<br/>rg-contoso-reservas-dev"] --> VM["Virtual machine<br/>vm-motor-disponibilidad-dev"]
    VM --> OSD["Managed OS disk<br/>(persistent)"]
    VM --> TMP["Temporary disk /mnt<br/>(volatile, not billed separately)"]
    VM --> DAT["Data disks<br/>(optional, persistent)"]
    VM --> NIC["Network interface (NIC)"]
    NIC --> SUBNET["Subnet of a virtual network"]
    NIC --> PIP["Public IP<br/>(optional)"]
    NIC --> NSG["Network security group<br/>(filters the traffic)"]

The resources a VM drags along:

  • Operating system disk: a managed, persistent disk. It survives the VM unless you mark it for deletion.
  • Temporary disk: local space on the physical host, usually mounted at /mnt (Linux) or D: (Windows). Its contents are lost if the VM is deallocated or moved to another host. It is for swap files and disposable working files. Never for data.
  • Data disks: optional, persistent, attached and detached while the VM is running.
  • NIC: the virtual network card. It lives inside a subnet of a virtual network.
  • Public IP: optional. If it is static, it is billed even while the VM is powered off.
  • NSG: the network-level firewall. It can be associated with the NIC or with the subnet (lesson 02-05).

The useful mental rule: the VM is the compute; everything else outlives the VM. That is why module 1 ended with that query for orphaned disks and unassociated public IPs.

  1. VM families and sizes: how to read the naming

Azure offers hundreds of sizes grouped into families, each optimized for a workload profile.

Family Profile When to use it Typical example at Contoso
B (burstable) Accumulates CPU credits while idle and spends them on spikes Development environments, lightly loaded servers with short spikes The availability engine test VM
D (general purpose) Balanced CPU/memory (≈4 GB per vCPU) Application servers, web, normal workloads The availability engine in production
E (memory optimized) ≈8 GB per vCPU Databases, caches, in-memory analytics A legacy reporting server
F (compute optimized) ≈2 GB per vCPU, faster CPU Intensive calculation, batch processing Overnight fare calculation
L (storage optimized) Very fast local NVMe disks NoSQL databases, large data stores Not applicable today
N (GPU) Graphics or AI acceleration Model training, rendering Not applicable today
M (massive memory) Hundreds of GB or TB of RAM SAP HANA, huge databases Not applicable today

Reading a size name

A name such as Standard_D4ds_v5 breaks down like this:

Standard_D 4 d s _v5
          │ │ │ │  │
          │ │ │ │  └── generation version (v5, v6…)
          │ │ │ └───── s = supports premium storage (Premium SSD)
          │ │ └─────── d = includes a local temporary disk
          │ └───────── number of vCPUs (4)
          └─────────── family (D = general purpose)

Other suffixes that show up often:

Suffix Meaning
a AMD processor
p Arm processor (Ampere); cheaper, but requires Arm binaries
s Premium storage capable
d With a local temporary disk
i Isolated instance (dedicated physical host)
m Higher-memory variant within the family

Concrete examples to orient yourself:

  • Standard_B1s: 1 vCPU, 1 GB of RAM. Ideal for testing and for labs like the one in this lesson.
  • Standard_B2ms: 2 vCPUs, 8 GB. A decent development environment.
  • Standard_D4ds_v5: 4 vCPUs, 16 GB. A modest production application server.
  • Standard_E8ds_v5: 8 vCPUs, 64 GB. A database on a VM.

To see what is available in your region and at what indicative price, the CLI helps:

# Sizes available in West Europe, filtering the B series and showing
# only what matters for the decision: name, vCPUs and memory.
az vm list-sizes \
  --location westeurope \
  --query "[?starts_with(name, 'Standard_B')].{Size:name, vCPU:numberOfCores, MemoryMB:memoryInMb}" \
  --output table

az vm list-sizes returns the region's catalog; the --query with JMESPath that you learned in 01-06 filters by prefix and renames the columns. Careful: not every size is available in every region or in every subscription (there are quotas, as you saw in 01-05).

Sizing tip: start small. Resizing a VM in Azure is a matter of minutes (az vm resize), not a project. Oversizing "just in case" is the number one cost mistake in migrations, and Azure Advisor will remind you of it in module 8.

  1. Disks: types, performance and cost

Azure disks are managed disks: you create a disk resource and Azure takes care of the storage accounts, the replication and the availability underneath. Unmanaged disks in your own accounts used to exist; today there is no reason to use them.

Type Technology Performance Use cases Relative cost
Standard HDD Magnetic disk Low, variable latency (ms) Backups, archives, workloads with sporadic access €
Standard SSD SSD Moderate and more consistent Development, testing, lightweight web servers €€
Premium SSD SSD, guaranteed IOPS High, single-digit millisecond latency Production, databases, latency-sensitive applications €€€
Premium SSD v2 SSD, IOPS and throughput configurable independently of size High and precisely adjustable Production with fine-grained needs €€€
Ultra Disk Sub-millisecond latency SSD Very high, IOPS and MB/s adjustable on the fly SAP HANA, extreme OLTP databases €€€€

Details worth being clear about from the start:

  • Performance depends on size in classic Standard and Premium SSD: a P10 disk (128 GB) gives fewer IOPS than a P30 (1 TB). If you need more IOPS, sometimes the answer is a bigger disk, not a more expensive one.
  • Only VM sizes with an s in the name support Premium SSD.
  • The 99.9% single-instance SLA requires Premium SSD (or better) on every disk of the VM. With Standard disks there is no single-instance SLA; for that you need zones or availability sets, and that is what lesson 02-02 is about.
  • The temporary disk is not billed separately, but it is volatile: it is lost when the VM is deallocated or if Azure moves it to another physical host. Treat it as a large /tmp folder.
  • Disks are billed by provisioned capacity, not by space used: a 1 TB disk with 10 GB written costs the same as 1 TB. And it keeps costing even while the VM is powered off.

Contoso Airlines' decision for the availability engine:

  • Development: Standard_B1s + a 30 GB Standard SSD OS disk. Enough to validate the deployment.
  • Production (when the time comes): Standard_D4ds_v5 + Premium SSD, to get the SLA and predictable latency.

  1. Images: marketplace, custom images and galleries

An image is the operating system disk template the VM boots from.

  • Marketplace images: published by Microsoft or by third parties. They range from clean operating systems (Ubuntu, Debian, RHEL, Windows Server) to appliances with preinstalled software. Some carry an extra hourly license cost on top of the compute: always check the price before deploying.
  • Custom images: created from a VM you have already configured. They serve the golden image pattern: you install and harden once, you deploy a hundred identical machines.
  • Azure Compute Gallery: the service for organizing custom images with versions, replicating them to several regions and sharing them across subscriptions. It is what you will use when your images stop being one and become a catalog.

Searching for images from the CLI:

# Handy aliases that Azure maintains (UbuntuLTS, Debian11, Win2022Datacenter...).
az vm image list --output table

# A real marketplace search: every Ubuntu Server 22.04 image
# published by Canonical and available in West Europe.
az vm image list \
  --publisher Canonical \
  --location westeurope \
  --all \
  --query "[?contains(sku, '22_04')].{Publisher:publisher, Offer:offer, SKU:sku, Version:version}" \
  --output table

An image's full identifier has the format Publisher:Offer:SKU:Version, for example Canonical:ubuntu-24_04-lts:server:latest. Using latest is convenient for testing; in production pin a specific version so that two runs of the same script produce the same thing.

  1. Creating the availability engine VM with Azure CLI

Straight to the point. We create the development VM for the availability engine, with SSH key authentication only (never a password), an economical size and Contoso's mandatory tags.

Step 1: generate the SSH key pair

# Generates an ed25519 key pair (shorter and more modern than RSA).
# -C adds a comment that helps you identify the key later.
ssh-keygen -t ed25519 -f ~/.ssh/contoso_motor -C "[email protected]"

This creates two files: ~/.ssh/contoso_motor (the private key, which never leaves your machine) and ~/.ssh/contoso_motor.pub (the public key, the one we upload to Azure). If the command asks for a passphrase, set one: it protects the key if your laptop is stolen.

Step 2: create the virtual machine

#!/usr/bin/env bash
set -euo pipefail

# ---- Parameters (same naming convention as module 1) ----
GROUP="rg-contoso-reservas-dev"
REGION="westeurope"
VM="vm-motor-disponibilidad-dev"
IMAGE="Canonical:ubuntu-24_04-lts:server:latest"
SIZE="Standard_B1s"

# ---- Creating the VM ----
az vm create \
  --resource-group "${GROUP}" \
  --name "${VM}" \
  --image "${IMAGE}" \
  --size "${SIZE}" \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/contoso_motor.pub \
  --public-ip-sku Standard \
  --public-ip-address-allocation static \
  --os-disk-name "disco-so-${VM}" \
  --os-disk-size-gb 30 \
  --storage-sku StandardSSD_LRS \
  --nsg-rule SSH \
  --tags entorno=desarrollo \
         proyecto=contoso-reservas \
         centro-coste=CC-1042 \
         [email protected] \
         criticidad=baja \
  --output table

What each option does, one by one:

Option Effect
--image The starting image, in Publisher:Offer:SKU:Version format
--size VM size (B family, 1 vCPU, 1 GB)
--admin-username The administrator user created on the system
--ssh-key-values The public key installed in ~/.ssh/authorized_keys. By supplying it, Azure disables password access
--public-ip-sku Standard Standard SKU: closed by default, zone-compatible. The Basic SKU has been retired
--public-ip-address-allocation static The IP does not change on restart. Remember: it is billed even while the VM is powered off
--os-disk-name An explicit disk name, so you do not end up with random ones
--storage-sku StandardSSD_LRS The operating system disk type
--nsg-rule SSH Creates an NSG with an inbound rule for port 22
--tags Contoso's mandatory tags, in lowercase and without accents

If you do not specify a virtual network, az vm create creates one with a derived name (vm-motor-disponibilidad-devVNET) and a default subnet. That is fine for the lab; not in production. In lesson 02-05 we will design vnet-contoso-pro with its subnets and connect the VMs there with --vnet-name and --subnet.

Security warning: --nsg-rule SSH opens port 22 to the whole internet (0.0.0.0/0). That is acceptable for ten minutes in a lab; it is not in production. In 02-05 you will see how to restrict it by source IP and, better still, how to use Azure Bastion so that SSH is not exposed at all.

The output includes the assigned public IP. You can also retrieve it at any time:

IP=$(az vm show \
  --resource-group rg-contoso-reservas-dev \
  --name vm-motor-disponibilidad-dev \
  --show-details \
  --query publicIps \
  --output tsv)
echo "Public IP: ${IP}"

--show-details (or -d) is what makes az vm show include networking data; without it, the query comes back empty. It is one of those CLI quirks worth memorizing.

  1. Connecting over SSH and installing the service

# Connect with the matching private key.
ssh -i ~/.ssh/contoso_motor azureuser@"${IP}"

Once inside the VM, we simulate deploying the availability engine. The real engine is Java, so we install the runtime and a minimal HTTP service that responds the way the engine would:

# --- Inside the VM ---
sudo apt-get update
sudo apt-get install -y openjdk-21-jre-headless nginx

# A health page that mimics the legacy engine's response.
echo '{"service":"availability-engine","status":"ok","version":"legacy-3.4"}' \
  | sudo tee /var/www/html/salud.json

sudo systemctl enable --now nginx
curl -s http://localhost/salud.json

From your own machine, check that it responds over the internet. First you have to open port 80 in the NSG, because we only opened 22:

# Inbound rule for HTTP in the NSG that az vm create built.
az network nsg rule create \
  --resource-group rg-contoso-reservas-dev \
  --nsg-name "vm-motor-disponibilidad-devNSG" \
  --name permitir-http \
  --priority 320 \
  --protocol Tcp \
  --destination-port-ranges 80 \
  --access Allow \
  --direction Inbound \
  --output none

curl -s "http://${IP}/salud.json"

The priority (320) determines the evaluation order: the lower the number, the earlier it is evaluated. In 02-05 you will see the default rules and service tags in detail.

  1. Automatic initial configuration: cloud-init and extensions

Installing by hand works once. To do it a hundred times identically there are two mechanisms.

cloud-init (Linux)

cloud-init is the industry standard for configuring a Linux machine on its first boot. You pass a YAML file and Azure injects it.

#cloud-config
package_update: true
packages:
  - openjdk-21-jre-headless
  - nginx
write_files:
  - path: /var/www/html/salud.json
    content: '{"service":"availability-engine","status":"ok","version":"legacy-3.4"}'
    permissions: '0644'
runcmd:
  - systemctl enable --now nginx

Save it as init-motor.yaml and use it when creating the VM:

az vm create \
  --resource-group rg-contoso-reservas-dev \
  --name vm-motor-disponibilidad-dev \
  --image Canonical:ubuntu-24_04-lts:server:latest \
  --size Standard_B1s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/contoso_motor.pub \
  --custom-data init-motor.yaml \
  --output none

Block by block: package_update refreshes the package index; packages installs; write_files creates files with specific permissions; runcmd runs commands at the end. The first line, #cloud-config, is mandatory and must be exactly that: without it, cloud-init ignores the file. You can verify the result inside the VM with cloud-init status --wait and review /var/log/cloud-init-output.log when something goes wrong.

VM extensions

Extensions are small agents that Azure installs and runs inside the VM after boot, through the Azure agent. Unlike cloud-init, they can be applied to an existing VM and are managed from the control plane.

Extension What it is for
customScript Runs an arbitrary script (the wildcard)
AzureMonitorLinuxAgent Sends metrics and logs to Log Analytics (module 7)
AADSSHLoginForLinux SSH sign-in with a Microsoft Entra ID identity (module 4)
NetworkWatcherAgentLinux Network diagnostics (lesson 02-05)
# Run a configuration script on an already created VM.
az vm extension set \
  --resource-group rg-contoso-reservas-dev \
  --vm-name vm-motor-disponibilidad-dev \
  --name customScript \
  --publisher Microsoft.Azure.Extensions \
  --version 2.1 \
  --settings '{"commandToExecute":"echo availability-engine-deployed > /var/www/html/estado.txt"}' \
  --output none

A practical rule: cloud-init for the immutable base configuration on first boot; extensions for platform agents and for acting on already deployed machines. If you end up writing long scripts in either of them, the real answer is a custom image or a container.

  1. VM states and what is billed in each one

This section is the one that saves the most money in the whole lesson.

State Command Is compute billed? Is the disk billed? Notes
Running az vm start Yes Yes The normal state
Stopped (shut down from inside the OS) sudo shutdown -h now Yes Yes The hardware is still reserved
Stopped (deallocated) az vm deallocate No Yes Releases the host; the temporary disk is lost
Deleted az vm delete No It depends The disks and the IP may survive

Read that again: shutting the VM down from inside the operating system does NOT stop compute billing. Azure keeps the host resources reserved for you. Only deallocation releases the hardware and stops the compute charge.

# Stop and deallocate: this does stop the compute clock.
az vm deallocate \
  --resource-group rg-contoso-reservas-dev \
  --name vm-motor-disponibilidad-dev

# Check the VM's real state.
az vm get-instance-view \
  --resource-group rg-contoso-reservas-dev \
  --name vm-motor-disponibilidad-dev \
  --query "instanceView.statuses[?starts_with(code,'PowerState')].displayStatus" \
  --output tsv

Consequences of deallocating that you need to know:

  • The contents of the temporary disk (/mnt) are lost.
  • If the public IP is dynamic, it is released and you will get a different one on start-up. With a static one you keep it, but you pay for it as long as it exists.
  • The dynamic private IP can also change on reallocation.

Contoso applies this systematically: the VMs in rg-contoso-reservas-dev are deallocated every night and at weekends. In module 7 you will automate that shutdown with Azure Automation, and in module 8 you will see how much it represents on the bill (typically, more than half the spend of non-production environments).

  1. Snapshots and images: copies and clones

Two mechanisms that are often confused:

Mechanism What it is Typical use
Snapshot A point-in-time copy of one specific disk A rollback point before a risky change
Image A template of a complete VM (OS + data disks), normally generalized Creating many identical VMs
# 1. Snapshot of the operating system disk before upgrading the engine.
DISK_ID=$(az vm show \
  --resource-group rg-contoso-reservas-dev \
  --name vm-motor-disponibilidad-dev \
  --query "storageProfile.osDisk.managedDisk.id" \
  --output tsv)

az snapshot create \
  --resource-group rg-contoso-reservas-dev \
  --name "snap-motor-$(date +%Y%m%d)" \
  --source "${DISK_ID}" \
  --tags entorno=desarrollo proyecto=contoso-reservas centro-coste=CC-1042 \
  --output table

First we get the disk's resource ID (that /subscriptions/.../disks/... string you saw in 01-05) and then we create the snapshot from it. A snapshot takes up space and is billed; delete it when you no longer need it.

For a generalized Linux image, the full process is: sudo waagent -deprovision+user inside the VM, then az vm deallocate, az vm generalize and az image create. A generalized VM can no longer be used: it only serves as an image source. Do not do it with the machine you need tomorrow.

For real backups (with policy, retention and granular recovery) you do not use manual snapshots but Azure Backup, which you will see in lesson 07-05.

  1. Cleaning up when you finish

# Option A: delete only the VM and its associated resources (--yes skips the confirmation).
az vm delete \
  --resource-group rg-contoso-reservas-dev \
  --name vm-motor-disponibilidad-dev \
  --yes

# The essential check: were any disks or IPs left orphaned?
az disk list --resource-group rg-contoso-reservas-dev --output table
az network public-ip list --resource-group rg-contoso-reservas-dev --output table
az network nic list --resource-group rg-contoso-reservas-dev --output table

# Option B (lab): delete the whole group. Irreversible.
# az group delete --name rg-contoso-reservas-dev --yes --no-wait

az vm delete does not delete by default the OS disk, the NIC or the public IP. When creating the VM you can ask for them to be deleted with it:

az vm create ... \
  --os-disk-delete-option Delete \
  --nic-delete-option Delete \
  --data-disk-delete-option Delete

Remember that the rg-contoso-reservas-pro group has the no-borrar-produccion (CanNotDelete) lock you applied in 01-05: any attempt to delete something there will fail until you remove it. That is exactly what we want.

Common Mistakes and Tips

  • Believing that powering off the VM stops the billing. Only az vm deallocate stops the compute charge. Shutting down from the operating system does not.
  • Deleting the VM and leaving orphaned disks and IPs. They are billed for existing. Always check after deleting, or use the --*-delete-option Delete options.
  • Storing data on the temporary disk. /mnt is emptied on deallocation or on host migration. Only disposable things go there.
  • Oversizing the VM "just in case". Resizing is a two-minute az vm resize with a restart. Start small.
  • Opening SSH or RDP to the whole internet. An open port 22 receives automated access attempts within minutes. Restrict it by source IP or use Azure Bastion (02-05).
  • Using passwords instead of SSH keys. With --ssh-key-values Azure disables password access. There is no reason to do otherwise.
  • Using latest for the image version in production. Two identical deployments can produce different machines. Pin the version.
  • Forgetting --show-details in az vm show. Without it you will not see the IPs and you will think the VM has no networking.
  • Naming tip: name the disk, NIC and IP explicitly (disco-so-…, nic-…, ip-…). Automatic names with random suffixes turn cleanup into archaeology.
  • Licensing tip: if you are migrating Windows Server or SQL Server with Software Assurance, look at Azure Hybrid Benefit (lesson 08-03) before deploying; the saving can exceed 40%.

Exercises

Exercise 1: choosing size and disk with judgment

For each Contoso Airlines case, propose a family, an approximate size and an OS disk type, and justify the choice:

  1. A test VM where Diego Salas validates each version of the availability engine; it is used two hours a day.
  2. The availability engine in production during peak season: an estimated 4 vCPUs and 16 GB, latency matters, a single-instance SLA is required.
  3. An overnight process that recalculates fares: two hours of CPU at 100%, little memory, no meaningful persistence.
  4. A legacy reporting server that loads a 48 GB cube into memory.

Exercise 2: deploying the engine with cloud-init and proving the saving

  1. Write an init-motor.yaml that installs nginx, creates /var/www/html/salud.json with {"service":"availability-engine","status":"ok"} and starts the service.
  2. Create vm-motor-disponibilidad-dev in rg-contoso-reservas-dev with Standard_B1s, a 30 GB Standard SSD disk, an SSH key and the four mandatory tags.
  3. Open port 80 in the NSG and check the response from your own machine.
  4. Deallocate the VM and prove with a command that its state is deallocated.

Exercise 3: hidden-spend audit

Write the Azure CLI commands that answer these questions in the development subscription:

  1. Which VMs are there and what power state is each one in?
  2. Are there disks not attached to any VM, and how many GB do they add up to?
  3. Are there unassociated public IPs?
  4. Which VMs do not have the propietario tag?

Solutions

Solution 1:

Case Proposal Justification
1. Testing two hours a day Standard_B2s + Standard SSD The B series accumulates credits while idle; with nightly deallocation the cost is minimal. It needs no SLA
2. The engine in production Standard_D4ds_v5 + Premium SSD Balanced general purpose at 4 vCPUs/16 GB; the 99.9% single-instance SLA requires Premium disks on every disk
3. Overnight recalculation Standard_F4s_v2 + Standard SSD A compute family: more CPU per euro and little memory needed. Deallocate when it finishes
4. Reporting with a 48 GB cube Standard_E8ds_v5 (8 vCPUs / 64 GB) + Premium SSD A memory family: ≈8 GB per vCPU, with headroom over the cube's 48 GB

Solution 2:

#cloud-config
package_update: true
packages:
  - nginx
write_files:
  - path: /var/www/html/salud.json
    content: '{"service":"availability-engine","status":"ok"}'
    permissions: '0644'
runcmd:
  - systemctl enable --now nginx
#!/usr/bin/env bash
set -euo pipefail

GROUP="rg-contoso-reservas-dev"
VM="vm-motor-disponibilidad-dev"

# 2. Creating the VM with cloud-init and the mandatory tags.
az vm create \
  --resource-group "${GROUP}" \
  --name "${VM}" \
  --image Canonical:ubuntu-24_04-lts:server:latest \
  --size Standard_B1s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/contoso_motor.pub \
  --custom-data init-motor.yaml \
  --os-disk-name "disco-so-${VM}" \
  --os-disk-size-gb 30 \
  --storage-sku StandardSSD_LRS \
  --nsg-rule SSH \
  --tags entorno=desarrollo proyecto=contoso-reservas \
         centro-coste=CC-1042 [email protected] \
  --output none

# 3. Open HTTP and test.
az network nsg rule create \
  --resource-group "${GROUP}" \
  --nsg-name "${VM}NSG" \
  --name permitir-http --priority 320 \
  --protocol Tcp --destination-port-ranges 80 \
  --access Allow --direction Inbound --output none

IP=$(az vm show -g "${GROUP}" -n "${VM}" --show-details --query publicIps -o tsv)
curl -s "http://${IP}/salud.json"

# 4. Deallocate and check the state.
az vm deallocate --resource-group "${GROUP}" --name "${VM}"
az vm get-instance-view -g "${GROUP}" -n "${VM}" \
  --query "instanceView.statuses[?starts_with(code,'PowerState')].code" -o tsv
# Expected output: PowerState/deallocated

Solution 3:

# 1. VMs and their power state (--show-details includes powerState).
az vm list --show-details \
  --query "[].{VM:name, Group:resourceGroup, State:powerState, Size:hardwareProfile.vmSize}" \
  --output table

# 2. Unattached disks and their size.
az disk list \
  --query "[?diskState=='Unattached'].{Disk:name, GB:diskSizeGb, Group:resourceGroup}" \
  --output table

# 3. Public IPs not associated with any network configuration.
az network public-ip list \
  --query "[?ipConfiguration==null].{IP:name, Address:ipAddress, Group:resourceGroup}" \
  --output table

# 4. VMs without the propietario tag.
az vm list \
  --query "[?tags.propietario == \`null\`].{VM:name, Group:resourceGroup}" \
  --output table

Points 2 and 3 are the most common source of invisible spend: they are billed for existing, not for being used.

Conclusion

You now know how to deploy IaaS compute in Azure with judgment. You have seen when a VM is the right answer — Contoso's legacy availability engine, which cannot be rewritten yet — and when it is simply the comfortable, expensive path. You know the full anatomy of a VM and the resources it drags along: OS disk, volatile temporary disk, data disks, NIC, public IP and NSG, each with its own lifecycle. You can read the size naming (Standard_D4ds_v5) and choose a family according to the workload profile, and compare disk types understanding that the single-instance SLA requires Premium SSD. You have created a Linux VM with SSH keys, configured it with cloud-init and extensions, and — the most profitable part of the lesson — internalized the difference between stopped and stopped (deallocated), along with the cleanup that prevents orphaned disks and IPs.

But this VM has an underlying problem that no size fixes: it is a single machine. If the host fails, if there is platform maintenance, or if in April the summer season goes on sale and ten times the usual number of requests arrive, there is no safety net. A single instance has no SLA except with Premium disks, and even then it remains a single point of failure.

In the next lesson, Compute Scaling and High Availability, we solve exactly that: vertical versus horizontal scaling and why the cloud bets on the latter, virtual machine scale sets with automatic metric-based and scheduled rules for Contoso's sales peak, availability zones and availability sets with their fault and update domains, and Azure's four load balancing options compared. By the end you will have the Availability API serving behind a load balancer with two instances in different zones.

Azure Course

Module 1: Introduction to Azure

Module 2: Core Azure Services

Module 3: Azure Databases

Module 4: Security in Azure

Module 5: Azure DevOps

Module 6: Advanced Azure Services

Module 7: Monitoring and Management

Module 8: Cost Management and Optimization

Module 9: Case Studies and Best Practices

© Copyright 2026. All rights reserved