This chapter covers two critical mechanisms for automating the initial configuration of Linux virtual machines in Azure: cloud-init and Azure VM Extensions. Both technologies are heavily tested on the AZ-104 exam (approximately 15-20% of Compute questions). Cloud-init is the preferred method for customizing Linux VMs at provisioning time, while VM Extensions perform post-deployment configuration and management. You must understand when to use each, how they interact, and the exact syntax for common tasks. We will dive deep into the internal workings, defaults, timers, and verification commands that the exam expects you to know.
Jump to a section
Imagine you order a brand-new, empty house from a prefab manufacturer. The house arrives as just a shell: four walls, a roof, and a floor. There is no furniture, no paint, no wiring for lights. You need to turn it into a livable home. Cloud-init is like a detailed, step-by-step recipe card that you hand to the general contractor the moment the shell is delivered. The recipe says: 'First, install the electrical wiring (install packages). Then, paint the walls (configure firewall rules). Next, place the kitchen cabinets (create user accounts). Finally, set up the thermostat (register with monitoring).' The contractor reads the recipe and executes each step in order, without asking you any questions. If the recipe has a typo (e.g., 'install pakage apache2'), the contractor stops and the house remains unfinished. You cannot change the recipe after the work starts—you must tear down and rebuild. The recipe is written in a standard format (YAML) that every contractor (cloud-init) understands, so you can use the same recipe for different house models (Ubuntu, CentOS, etc.). The key is that the recipe is executed exactly once, at the very first power-on. After that, the contractor leaves and you manage the house manually. If you want to add a new light fixture later, you must use a different tool (e.g., an extension script or configuration management tool).
What is cloud-init and Why Does It Exist?
cloud-init is the industry-standard, cross-platform tool for early initialization of cloud instances. It originated in Ubuntu (Canonical) and is now supported by almost all Linux distributions on Azure. Its primary purpose is to execute a set of configuration directives the *first time* a VM boots after being created from a generalized image. This is known as "first boot" or "provisioning time." The configuration is passed to the VM as a text file (typically YAML) via Azure's custom data feature. cloud-init runs before any configuration management tools (like Chef, Puppet, or Ansible) and before the VM becomes fully operational.
How cloud-init Works Internally
The mechanism is a multi-stage process that happens during the VM's initial boot cycle:
Source: The cloud-init configuration (user data) is provided by the Azure platform when the VM is created. The configuration is stored as a base64-encoded string in the VM's metadata service (IMDS). Azure injects this data into the VM's serial console or via a temporary ISO image (for Generation 1 VMs) or via the UEFI variable (for Generation 2 VMs).
Boot: When the VM boots for the first time, the cloud-init service (a systemd unit) starts early in the boot process. It reads the user data from the platform-specific source.
Parsing: cloud-init parses the user data. It can be in multiple formats: cloud-config YAML (most common), shell script (starting with #!), or include file. The format is auto-detected by the first line.
Execution: cloud-init executes the directives in a specific order defined by its modules. For example, the write_files module runs before the runcmd module, which runs before the package_update module. This ordering is important: you can write a config file and then use it in a command.
Network: cloud-init also configures the network interfaces using the Azure DHCP server. It reads the network metadata from IMDS to set IP addresses, DNS servers, and routes. This happens even if you don't provide custom data.
Idempotency: cloud-init is designed to run only once. It creates a semaphore file (e.g., /var/lib/cloud/instance/boot-finished) to indicate that initialization is complete. On subsequent boots, cloud-init skips most modules (unless you force it with cloud-init clean).
Key Components, Values, Defaults, and Timers
User data size limit: Azure limits custom data to 64 KB (base64-encoded). For cloud-init, this is usually sufficient, but for large scripts, consider using an include file or a script URL.
cloud-init versions: Azure supports cloud-init 19.x and above. The exam expects you to know that cloud-init is pre-installed on Azure Marketplace Linux images (Ubuntu, CentOS, RHEL, SUSE, Debian). For custom images, you must install cloud-init yourself.
Default timeout: cloud-init has a default timeout of 120 seconds for network boot. If the network is not ready within that time, cloud-init may fail. You can configure this in /etc/cloud/cloud.cfg.
Logs: All cloud-init activity is logged to /var/log/cloud-init.log and /var/log/cloud-init-output.log. The output log contains the stdout/stderr of all commands executed via runcmd.
Modules: Common modules include write_files, runcmd, package_update, package_upgrade, packages, users, groups, ssh_authorized_keys, timezone, locale, ntp, growpart (auto-resize root partition), and resizefs (auto-resize filesystem).
Configuration and Verification Commands
To use cloud-init, you create a YAML file (e.g., cloud-init.txt) and pass it to Azure CLI or ARM template using the --custom-data parameter. Example:
az vm create \
--resource-group myRG \
--name myVM \
--image UbuntuLTS \
--admin-username azureuser \
--generate-ssh-keys \
--custom-data cloud-init.txtExample cloud-init.txt:
#cloud-config
package_upgrade: true
packages:
- nginx
- git
write_files:
- path: /etc/nginx/sites-available/default
content: |
server {
listen 80 default_server;
root /var/www/html;
}
runcmd:
- [ systemctl, enable, nginx ]
- [ systemctl, start, nginx ]
- echo "Hello, Azure!" > /var/www/html/index.htmlTo verify that cloud-init executed successfully, SSH to the VM and check:
sudo cat /var/log/cloud-init-output.log
sudo cloud-init status # returns 'status: done' or 'status: error'How cloud-init Interacts with Azure VM Extensions
Azure VM Extensions are small applications that provide post-deployment configuration and automation. They run *after* cloud-init completes. Common extensions include:
Custom Script Extension: Downloads and runs a script on the VM.
Docker Extension: Installs and configures Docker.
Azure Monitor Agent: Installs the monitoring agent.
Extensions are applied via ARM templates, CLI, or Portal. They can be installed at VM creation or later. The exam tests that cloud-init is for *first-boot* customization, while extensions are for *ongoing* management. However, extensions can also run on first boot if deployed at creation time.
VM Extensions Deep Dive
VM Extensions are managed by the Azure Guest Agent (for Windows) or the Linux Agent (waagent). The agent runs as a service and communicates with the Azure Fabric Controller over a secure channel. Extensions are downloaded from Azure storage, installed, and executed. The extension handler reports status back to Azure.
Extension naming: Extensions have a publisher, type, and version. For example, Microsoft.Compute.CustomScriptExtension.
Protected settings: Sensitive data (like passwords) can be passed in protected settings, which are encrypted.
Extension status: You can check extension status via CLI: az vm extension list --vm-name myVM --resource-group myRG.
Troubleshooting: Logs are in /var/lib/waagent/ for Linux. Each extension has its own directory.
Interaction Between cloud-init and Extensions
If you deploy both cloud-init and an extension at VM creation, cloud-init runs first (during boot), then the extension runs after the VM is fully booted. This ordering is important: if your extension depends on files created by cloud-init, it will work. Conversely, if cloud-init depends on an extension, it will fail because the extension hasn't run yet.
Common Exam Scenarios
Automated VM creation with custom software: Use cloud-init to install packages and configure services.
Joining a VM to a domain: Cloud-init can run a script that joins the VM to Azure AD DS or on-prem AD.
Resizing root partition: Cloud-init's growpart and resizefs modules automatically expand the root filesystem to use all available disk space. This is crucial for VMs created from custom images with small root partitions.
Passing secrets: Do NOT hardcode secrets in cloud-init. Use Azure Key Vault and retrieve secrets via a script (e.g., using curl to the IMDS endpoint with managed identity). The exam will test that cloud-init is not secure for secrets because the custom data is visible in the Azure Portal and ARM template history.
Specific Numbers and Defaults
cloud-init module frequency: Most modules run only on first boot. The bootcmd module runs on every boot.
User data size: Maximum 64 KB base64-encoded.
Extension timeout: Each extension has a timeout (default 90 minutes). If the extension script doesn't complete within that time, it is marked as failed.
Extension retries: The Azure agent retries extension installation up to 3 times before failing.
Verification Commands for Extensions
# List extensions on a VM
az vm extension list --vm-name myVM --resource-group myRG --query "[].{Name:name, ProvisioningState:provisioningState}"
# Get extension settings
az vm extension get --vm-name myVM --resource-group myRG --name myExtensionName
# View extension logs on the VM
sudo cat /var/lib/waagent/custom-script/download/0/stdout
sudo cat /var/lib/waagent/custom-script/download/0/stderrEdge Cases
Custom images: If you create a VM from a custom image that already has cloud-init configured, the user data you provide may be ignored if the image was sysprepped incorrectly. You must run sudo cloud-init clean before generalizing the image.
Generation 2 VMs: cloud-init user data is passed via UEFI variable, which has a smaller size limit (64 KB still applies).
Multiple extensions: Extensions run in sequence based on the order they are specified in the ARM template. The exam may ask about dependency ordering.
Summary of Key Differences
| Feature | cloud-init | VM Extensions | |---------|------------|---------------| | Timing | First boot only | Anytime (creation, update, or post-deployment) | | Scope | OS-level configuration | Application-level or agent installation | | Idempotency | Yes (runs once) | Depends on extension (some are idempotent) | | Language | YAML or script | Script (PowerShell, Bash) or binary | | Security | Custom data visible to admins | Protected settings encrypted | | Logging | /var/log/cloud-init*.log | /var/lib/waagent/ |
What the Exam Tests
The AZ-104 exam expects you to:
Know that cloud-init is the preferred method for Linux VM customization at provisioning time.
Understand that cloud-init runs only on first boot.
Be able to read a cloud-init YAML and predict what it does.
Know that VM Extensions can be added after VM creation.
Recognize that Custom Script Extension is for running scripts post-deployment.
Understand the difference between custom-data (cloud-init) and protected-settings (extension).
Know that cloud-init is not suitable for sensitive data.
Be aware that cloud-init can auto-resize disks (growpart).
Common Wrong Answers on the Exam
"cloud-init runs on every boot" – Wrong. It runs only on first boot unless you manually clean the state.
"VM Extensions run before cloud-init" – Wrong. cloud-init runs during boot; extensions run after boot.
"Custom data can be used for Windows VMs" – Wrong. cloud-init is Linux-only. Windows uses Custom Script Extension or PowerShell DSC.
"You can update cloud-init configuration after VM creation" – Wrong. cloud-init config is only applied at provisioning. To change configuration, use extensions or configuration management.
"Extension scripts run with root privileges by default" – Correct, but the exam may test that you need to use sudo in the script if not running as root (though the extension agent runs as root).
Conclusion
Mastering cloud-init and VM Extensions is essential for automating Linux VM deployments in Azure. The exam will test your ability to choose the right tool for the job and interpret configuration files. Practice creating VMs with custom data and extensions using CLI and ARM templates. Understand the ordering and limitations to avoid common pitfalls.
Prepare cloud-init YAML file
Create a YAML file with the desired configuration. The file must start with `#cloud-config` to be recognized as cloud-init configuration. You can include directives to update packages, install software, write files, create users, set timezone, or run arbitrary commands. The file size must be under 64 KB when base64-encoded. Use the `write_files` module to create configuration files, and `runcmd` to execute commands in order. For example, you can write an Nginx configuration file and then enable and start Nginx. Ensure that the YAML is valid; invalid YAML will cause cloud-init to fail silently. Test the file locally with `cloud-init devel schema --config-file cloud-init.txt`.
Create VM with custom data
Use Azure CLI to create a Linux VM and pass the cloud-init file using the `--custom-data` parameter. The CLI will read the file, base64-encode it, and send it to the Azure Resource Manager. The Azure platform stores this encoded data in the VM's metadata and makes it available to cloud-init during first boot. For example: `az vm create --resource-group myRG --name myVM --image UbuntuLTS --admin-username azureuser --generate-ssh-keys --custom-data cloud-init.txt`. The VM will be provisioned with the custom data. You can also use ARM templates or Terraform to pass custom data.
First boot and cloud-init execution
When the VM boots for the first time, the cloud-init service (cloud-init-local, cloud-init, cloud-config, cloud-final) runs in sequence. It reads the custom data from the platform source (e.g., serial console, ISO, UEFI variable). It then parses the YAML and executes modules in a predefined order. For example, `write_files` runs before `runcmd`. Network configuration is also applied from the metadata. cloud-init logs all actions to `/var/log/cloud-init-output.log`. If any module fails, cloud-init marks the instance as 'error' and the VM may not be fully configured. The entire process is non-interactive and happens within the first few minutes of boot.
Verify cloud-init completion
After the VM is running, SSH into it and check the cloud-init status using `sudo cloud-init status`. It should return 'status: done'. You can also view the output log at `/var/log/cloud-init-output.log` to see the stdout/stderr of all commands. Check that the expected packages are installed (e.g., `nginx`) and that services are running (e.g., `systemctl status nginx`). If cloud-init failed, the status will show 'error' and you can inspect the log for errors. Common failures include network timeouts, invalid YAML, or missing repositories.
Deploy VM Extension post-creation
If you need to perform additional configuration after the VM is running, use Azure VM Extensions. For example, to run a script that installs a web application, use the Custom Script Extension. Deploy it via CLI: `az vm extension set --resource-group myRG --vm-name myVM --name customScript --publisher Microsoft.Azure.Extensions --settings '{"commandToExecute": "sudo bash myscript.sh"}' --protected-settings '{"storageAccountName": "mystorage", "storageAccountKey": "mykey"}'`. The extension will download the script from Azure Storage (or inline) and execute it. The extension agent reports progress and final status back to Azure. You can check the status with `az vm extension list`.
In a large enterprise, cloud-init is used to standardize the deployment of thousands of Linux VMs for a microservices platform. The DevOps team creates a golden cloud-init configuration that includes setting the correct timezone (UTC), installing the company’s monitoring agent (Datadog), configuring syslog forwarding to a central SIEM, and creating a local user with SSH keys for break-glass access. This configuration is stored in a Git repository and version-controlled. When developers spin up VMs for testing, they use the same cloud-init file to ensure consistency. The team also uses cloud-init to automatically resize the root partition (growpart) because their custom images are built with a small root partition (30 GB) to save storage costs. Without cloud-init, the VM would boot with only 30 GB of usable space, even if the managed disk is 128 GB. Cloud-init’s growpart module detects the disk size and expands the partition and filesystem automatically.
Another scenario involves a managed service provider (MSP) that manages VMs for multiple clients. They use Azure VM Extensions to deploy security agents (like Trend Micro Deep Security) and to join VMs to a domain. The extension is deployed after the VM is provisioned, using Azure Policy to automatically assign the extension to all VMs in a subscription. For example, they have a policy that deploys the Custom Script Extension to run a PowerShell script that installs the Trend Micro agent and registers it with the management console. The script is stored in a secured storage account with a SAS token. The extension’s protected settings contain the storage account key, which is encrypted at rest. This approach ensures that every new VM automatically gets the security agent without manual intervention.
A common failure scenario occurs when cloud-init is misconfigured with a typo in the YAML. For example, a missing colon or an incorrect indentation will cause cloud-init to ignore the entire configuration. The VM boots without any customizations, and the administrator only discovers the issue when they SSH in and find no packages installed. The fix requires deleting the VM and recreating it with the corrected cloud-init file. Another failure is when the cloud-init script tries to access an external resource (like a package repository) that is not reachable due to network security group rules. In that case, cloud-init times out and marks the instance as error. The administrator must check the cloud-init logs and adjust NSG rules accordingly. For extensions, a common issue is that the script fails because it runs as root but the script expects environment variables that are not set. The extension logs in /var/lib/waagent/ show the error, and the administrator must fix the script and re-run the extension.
The AZ-104 exam objective 3.1 (Configure virtual machines) specifically tests your ability to configure VM extensions and use cloud-init for Linux VMs. You must know the exact difference between custom data (cloud-init) and extensions (Custom Script Extension). The exam will present scenarios where you need to choose the correct approach: if the task is to install software at provisioning time, use cloud-init; if the task is to run a script after the VM is already running, use an extension. A common trap question asks: 'You need to deploy 100 Linux VMs with a specific application installed. What should you use?' The obvious wrong answer is 'Custom Script Extension' because candidates think of scripts. However, the correct answer is 'cloud-init' because it runs at first boot and is more efficient for initial setup. Another trap: 'You need to change the hostname of a running Linux VM.' Candidates may choose cloud-init, but cloud-init cannot change hostname after first boot. The correct answer is to use a VM extension (Custom Script Extension) or manually modify /etc/hostname and reboot.
Specific numbers to memorize: custom data size limit is 64 KB (base64-encoded). cloud-init runs only on first boot. Extensions can be added at any time. The Custom Script Extension publisher is Microsoft.Azure.Extensions (for Linux) or Microsoft.Compute (for Windows). The extension type is customScript for Linux. Protected settings are encrypted; public settings are not. The exam loves to test the order of operations: cloud-init runs before extensions. They may ask: 'You deploy a VM with both cloud-init and an extension. The extension fails because it cannot find a file. What is the likely cause?' The answer: 'The extension ran before cloud-init completed.' But in reality, extensions run after cloud-init. So the correct answer might be that the extension script has a bug or the file path is wrong.
Edge cases: If you create a VM from a custom image that already has cloud-init installed, you must run sudo cloud-init clean before generalizing the image; otherwise, the new VM will not run cloud-init because it thinks it has already run. The exam may test that you need to use waagent -deprovision+user for Linux before capturing an image. Also, cloud-init is not supported on Windows; for Windows, you use Custom Script Extension or PowerShell DSC. Another edge case: If you pass a script (starting with #!) as custom data instead of YAML, cloud-init will execute it as a shell script. This is a valid alternative to YAML. The exam may ask: 'You need to run a simple script at first boot. What format should you use?' The answer could be either YAML with runcmd or a script file. Both work.
To eliminate wrong answers, focus on the timing: if the requirement says 'during provisioning' or 'at first boot', choose cloud-init. If it says 'after deployment' or 'on an existing VM', choose extension. Also, if the requirement involves secrets (like a storage account key), use extensions with protected settings, not cloud-init custom data (which is visible in plaintext in the portal and ARM template history).
cloud-init is the standard for Linux VM customization at provisioning time; it runs only on first boot.
Custom data size limit is 64 KB (base64-encoded); use include files or URLs for larger scripts.
VM Extensions (e.g., Custom Script Extension) run after cloud-init and can be added to existing VMs.
Protected settings in extensions are encrypted; custom data is not secure for secrets.
cloud-init automatically resizes the root partition via the growpart module.
Use `az vm create --custom-data` to pass cloud-init config; use `az vm extension set` to add extensions.
Verify cloud-init with `sudo cloud-init status` and check /var/log/cloud-init-output.log.
For Windows VMs, use Custom Script Extension or PowerShell DSC instead of cloud-init.
cloud-init is pre-installed on Azure Marketplace Linux images; custom images require manual installation.
The Custom Script Extension publisher for Linux is Microsoft.Azure.Extensions; type is customScript.
These come up on the exam all the time. Here's how to tell them apart.
cloud-init
Runs only on first boot after provisioning.
Configured via custom data (YAML or script).
Ideal for OS-level tasks: package install, user creation, file writes.
Limited to 64 KB of user data (base64-encoded).
Logs to /var/log/cloud-init*.log.
VM Extensions
Can run at any time: during creation, after deployment, or on schedule.
Deployed via ARM template, CLI, or Portal.
Ideal for application-level tasks: install agents, run scripts, configure software.
No size limit for script content (can reference storage URL).
Logs to /var/lib/waagent/ per extension.
Mistake
cloud-init runs on every boot of the VM.
Correct
cloud-init runs only on the first boot after provisioning. It creates a semaphore file that prevents re-execution on subsequent boots. To force re-run, you must run `sudo cloud-init clean` and then reboot.
Mistake
Custom data can be used for both Linux and Windows VMs.
Correct
Custom data is primarily for Linux VMs using cloud-init. Windows VMs do not process custom data natively; instead, you must use a VM extension (like Custom Script Extension) or PowerShell DSC.
Mistake
You can modify the cloud-init configuration after the VM is created.
Correct
Cloud-init configuration is applied only at provisioning time. You cannot change it later. To make configuration changes on a running VM, use VM Extensions or configuration management tools.
Mistake
VM Extensions run before cloud-init when deployed at VM creation.
Correct
Cloud-init runs during the boot process, before the VM is fully operational. VM Extensions are deployed after the VM is running, so cloud-init always executes first.
Mistake
Custom data is secure for passing passwords or secrets.
Correct
Custom data is stored in the ARM template history and can be viewed in the Azure Portal. It is not encrypted. For secrets, use protected settings in VM Extensions or retrieve secrets from Azure Key Vault at runtime.
Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.
Cloud-init runs during the first boot of a Linux VM to perform initial configuration like installing packages and writing files. It is configured via custom data and runs only once. Custom Script Extension can be deployed at any time (including at creation) to run a script on the VM. It is not limited to first boot and can be used on both Linux and Windows. For initial setup, cloud-init is preferred; for post-deployment tasks, use Custom Script Extension.
No, cloud-init is not supported on Windows. Windows VMs use the Windows Azure Guest Agent and can be configured with Custom Script Extension (PowerShell scripts), PowerShell DSC, or Desired State Configuration. For initial setup, you can use the Custom Script Extension at deployment time or include commands in the ARM template.
You can pass a shell script as custom data. The script must start with `#!` (shebang) and will be executed by cloud-init as a script. For example, create a file `script.sh` with `#!/bin/bash` followed by commands, then use `--custom-data script.sh` when creating the VM. cloud-init automatically detects the format and runs it.
SSH into the VM and check the status with `sudo cloud-init status`. If it shows 'error', view the logs: `/var/log/cloud-init-output.log` for command output and `/var/log/cloud-init.log` for detailed module execution. Common issues include network timeouts, invalid YAML, or missing dependencies. Ensure the custom data is correctly formatted and base64-encoded (Azure CLI does this automatically).
The maximum size for custom data is 64 KB when base64-encoded. This is a platform limit. If you need to pass larger configuration, consider using an include file in cloud-init (pointing to a URL) or use a VM Extension that downloads a script from Azure Storage.
No, cloud-init configuration is only applied at provisioning time. To change configuration on a running VM, use VM Extensions (e.g., Custom Script Extension) or configuration management tools like Ansible, Chef, or Puppet.
Do not use cloud-init custom data for secrets because it is visible in the portal and ARM template history. Instead, use Azure Key Vault with a managed identity. At runtime, the VM can retrieve secrets from Key Vault using the IMDS endpoint. Alternatively, use VM Extension protected settings, which are encrypted at rest.
You've just covered Linux VMs on Azure: cloud-init and Extensions — now see how well it sticks with free AZ-104 practice questions. Full explanations included, no account needed.
Done with this chapter?