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_B1ssize 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
- VM versus PaaS: when each one makes sense
- Anatomy of a virtual machine in Azure
- VM families and sizes: how to read the naming
- Disks: types, performance and cost
- Images: marketplace, custom images and galleries
- Creating the availability engine VM with Azure CLI
- Connecting over SSH and installing the service
- Automatic initial configuration: cloud-init and extensions
- VM states and what is billed in each one
- Snapshots and images: copies and clones
- Cleaning up when you finish
- Common Mistakes and Tips
- Exercises
- Conclusion
- 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.
- 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) orD:(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.
- 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 tableaz 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.
- 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
sin 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
/tmpfolder. - 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.
- 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 tableAn 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.
- 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 tableWhat 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 SSHopens 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.
- Connecting over SSH and installing the service
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.jsonFrom 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.
- 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 nginxSave 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 noneBlock 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 noneA 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.
- 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 tsvConsequences 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).
- 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 tableFirst 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.
- 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-waitaz 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 DeleteRemember 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 deallocatestops 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 Deleteoptions. - Storing data on the temporary disk.
/mntis 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 resizewith 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-valuesAzure disables password access. There is no reason to do otherwise. - Using
latestfor the image version in production. Two identical deployments can produce different machines. Pin the version. - Forgetting
--show-detailsinaz 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:
- A test VM where Diego Salas validates each version of the availability engine; it is used two hours a day.
- The availability engine in production during peak season: an estimated 4 vCPUs and 16 GB, latency matters, a single-instance SLA is required.
- An overnight process that recalculates fares: two hours of CPU at 100%, little memory, no meaningful persistence.
- A legacy reporting server that loads a 48 GB cube into memory.
Exercise 2: deploying the engine with cloud-init and proving the saving
- Write an
init-motor.yamlthat installsnginx, creates/var/www/html/salud.jsonwith{"service":"availability-engine","status":"ok"}and starts the service. - Create
vm-motor-disponibilidad-devinrg-contoso-reservas-devwithStandard_B1s, a 30 GB Standard SSD disk, an SSH key and the four mandatory tags. - Open port 80 in the NSG and check the response from your own machine.
- 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:
- Which VMs are there and what power state is each one in?
- Are there disks not attached to any VM, and how many GB do they add up to?
- Are there unassociated public IPs?
- Which VMs do not have the
propietariotag?
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/deallocatedSolution 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 tablePoints 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
- What Is Azure?
- Service Models, Regions and Availability Zones
- Creating and Setting Up Your Azure Account
- A Tour of the Azure Portal
- Azure Resource Manager: Subscriptions, Resource Groups and Tags
- Azure CLI, PowerShell and Cloud Shell
Module 2: Core Azure Services
- Azure Virtual Machines
- Compute Scaling and High Availability
- Azure App Service
- Azure Storage: Blobs, Files, Queues and Tables
- Azure Networking: Virtual Networks, Subnets and NSGs
- Hybrid Connectivity and Global Delivery
Module 3: Azure Databases
- Choosing the Right Data Service
- Azure SQL Database
- Azure Cosmos DB
- Azure Database for MySQL
- Azure Database for PostgreSQL
- Data Analytics: Data Lake, Data Factory and Synapse
Module 4: Security in Azure
- Microsoft Entra ID and Identity Management
- RBAC and Managed Identities
- Azure Key Vault
- DDoS Protection and Web Application Firewall
- Microsoft Defender for Cloud
- Governance and Compliance with Azure Policy
Module 5: Azure DevOps
- Introduction to Azure DevOps
- Azure Repos
- Azure Pipelines: Continuous Integration
- Continuous Deployment with Environments and Approvals
- Azure Artifacts
- Infrastructure as Code with Bicep
Module 6: Advanced Azure Services
- Containers in Azure: Container Registry and Container Apps
- Azure Kubernetes Service (AKS)
- Azure Functions
- Azure Logic Apps
- Messaging and Events: Service Bus, Event Grid and Event Hubs
- Azure AI Services
Module 7: Monitoring and Management
- Azure Monitor: Metrics, Alerts and Dashboards
- Log Analytics and KQL Queries
- Application Insights
- Azure Automation and Runbooks
- Backup and Disaster Recovery
Module 8: Cost Management and Optimization
- Pricing Calculator and Cost Estimation
- Azure Cost Management: Analysis, Budgets and Alerts
- Reservations, Savings Plans and Azure Hybrid Benefit
- Azure Advisor
- Optimization Strategies and FinOps Culture
