10.6 VM Extensions¶
Overview¶
Azure VM extensions are small applications that provide post-deployment configuration and automation on Azure VMs. This configuration uses the Custom Script Extension to install and configure Nginx web servers on both VMs.
Custom Script Extension¶
Terraform Configuration¶
resource "azurerm_virtual_machine_extension" "example" {
count = 2
name = "Nginx"
virtual_machine_id = azurerm_linux_virtual_machine.example[count.index].id
publisher = "Microsoft.Azure.Extensions"
type = "CustomScript"
type_handler_version = "2.0"
settings = <<SETTINGS
{
"commandToExecute": "sudo apt-get update && sudo apt-get install nginx -y && echo \"Hello World from $(hostname)\" > /var/www/html/index.html && sudo systemctl restart nginx"
}
SETTINGS
}
Configuration Breakdown¶
Basic Properties¶
| Property | Value | Description |
|---|---|---|
| count | 2 | Installs on both VMs |
| name | "Nginx" | Extension identifier |
| virtual_machine_id | VM reference | Which VM to configure |
| publisher | Microsoft.Azure.Extensions | Extension provider |
| type | CustomScript | Extension type |
| type_handler_version | "2.0" | Extension version |
Extension Publishers¶
| Publisher | Use Case |
|---|---|
| Microsoft.Azure.Extensions | Linux extensions |
| Microsoft.Compute | Windows extensions |
| Microsoft.Azure.Security | Security extensions |
| Microsoft.Azure.Diagnostics | Monitoring extensions |
Command Execution¶
The Installation Script¶
sudo apt-get update && \
sudo apt-get install nginx -y && \
echo "Hello World from $(hostname)" > /var/www/html/index.html && \
sudo systemctl restart nginx
Step-by-Step Breakdown¶
1. Update Package Lists¶
- Updates the package repository cache - Ensures latest package versions are available2. Install Nginx¶
- Installs Nginx web server --y flag automatically confirms installation
- Installs dependencies automatically
3. Create Custom Homepage¶
- Creates a custom index.html file -$(hostname) expands to VM name (test-vm0 or test-vm1)
- Overwrites default Nginx welcome page
- Location: /var/www/html/index.html (Nginx default document root)
Result: - VM 0 displays: "Hello World from test-vm0" - VM 1 displays: "Hello World from test-vm1"
4. Restart Nginx¶
- Restarts Nginx service - Ensures configuration is loaded - Makes the web server immediately availableExtension Execution Flow¶
1. VM is created and running
↓
2. Terraform applies extension
↓
3. Azure Extension Agent receives command
↓
4. Agent executes script as root
↓
5. Script updates packages
↓
6. Script installs Nginx
↓
7. Script creates custom page
↓
8. Script restarts Nginx
↓
9. Extension reports success/failure
↓
10. Terraform marks resource as created
Extension Status¶
Success Indicators¶
✅ Extension succeeds when: - All commands execute without errors - Exit code is 0 - Nginx is running and accessible
❌ Extension fails when: - Any command returns non-zero exit code - Package installation fails - Syntax errors in script
Checking Extension Status¶
# Using Azure CLI
az vm extension show \
--resource-group <resource-group-name> \
--vm-name test-vm0 \
--name Nginx \
--output json
# Check provisioning state
az vm extension show \
--resource-group <resource-group-name> \
--vm-name test-vm0 \
--name Nginx \
--query provisioningState \
--output tsv
Extension Logs¶
Extensions write logs to the VM:
# SSH into VM
ssh azureadmin@<vm-private-ip>
# View extension logs
sudo cat /var/log/azure/custom-script/handler.log
# View extension output
sudo cat /var/lib/waagent/custom-script/download/0/stdout
sudo cat /var/lib/waagent/custom-script/download/0/stderr
Testing the Installation¶
From Load Balancer¶
# Get public IP
PUBLIC_IP=$(az network public-ip show \
--resource-group <resource-group-name> \
--name test-public-ip \
--query ipAddress \
--output tsv)
# Test endpoint
curl http://$PUBLIC_IP
# Expected output (alternates between VMs):
# Hello World from test-vm0
# or
# Hello World from test-vm1
Multiple Requests¶
# Send 10 requests to see load distribution
for i in {1..10}; do
echo "Request $i:"
curl http://$PUBLIC_IP
echo ""
done
Expected Output:
Request 1:
Hello World from test-vm0
Request 2:
Hello World from test-vm0
Request 3:
Hello World from test-vm1
Request 4:
Hello World from test-vm1
...
Advanced Extension Configurations¶
Using External Script File¶
Instead of inline commands, reference an external script:
resource "azurerm_virtual_machine_extension" "example" {
count = 2
name = "Nginx"
virtual_machine_id = azurerm_linux_virtual_machine.example[count.index].id
publisher = "Microsoft.Azure.Extensions"
type = "CustomScript"
type_handler_version = "2.0"
settings = <<SETTINGS
{
"fileUris": ["https://raw.githubusercontent.com/user/repo/main/install.sh"],
"commandToExecute": "bash install.sh"
}
SETTINGS
}
Protected Settings (for Secrets)¶
For scripts that need credentials:
resource "azurerm_virtual_machine_extension" "example" {
# ... other configuration ...
settings = <<SETTINGS
{
"commandToExecute": "bash install.sh"
}
SETTINGS
protected_settings = <<PROTECTED_SETTINGS
{
"fileUris": ["https://storage.blob.core.windows.net/scripts/install.sh"],
"storageAccountName": "mystorageaccount",
"storageAccountKey": "${var.storage_account_key}"
}
PROTECTED_SETTINGS
}
More Complex Installation Script¶
settings = <<SETTINGS
{
"commandToExecute": "bash -c 'apt-get update && apt-get install -y nginx && cat > /var/www/html/index.html <<EOF\n<!DOCTYPE html>\n<html>\n<head><title>$(hostname)</title></head>\n<body>\n<h1>Hello from $(hostname)</h1>\n<p>IP: $(hostname -I)</p>\n<p>Time: $(date)</p>\n</body>\n</html>\nEOF\n&& systemctl restart nginx'"
}
SETTINGS
Alternative: Cloud-Init¶
For more complex initialization, consider cloud-init:
resource "azurerm_linux_virtual_machine" "example" {
# ... other configuration ...
custom_data = base64encode(<<-EOF
#cloud-config
package_update: true
package_upgrade: true
packages:
- nginx
write_files:
- path: /var/www/html/index.html
content: |
Hello World from ${count.index}
runcmd:
- systemctl restart nginx
EOF
)
}
Cloud-Init vs Extensions¶
| Feature | Cloud-Init | Extensions |
|---|---|---|
| Timing | First boot only | Anytime after creation |
| Re-run | No | Yes (update extension) |
| Complexity | YAML-based | Script-based |
| Use Case | Initial setup | Post-deployment config |
Common Extension Types¶
1. Custom Script Extension¶
Use: Run arbitrary scripts2. Docker Extension¶
Use: Install and configure Docker3. Azure Monitor Agent¶
Use: Monitoring and diagnostics4. Desired State Configuration¶
Use: Configuration management5. Network Watcher¶
Use: Network diagnosticsTroubleshooting Extensions¶
Extension Fails to Install¶
Check Terraform Output:
Check Azure Portal: 1. Navigate to VM 2. Click "Extensions + applications" 3. View status and error messages
Check VM Logs:
# SSH to VM
ssh azureadmin@<vm-ip>
# View extension logs
sudo tail -f /var/log/azure/custom-script/handler.log
# View command output
sudo cat /var/lib/waagent/custom-script/download/0/stdout
sudo cat /var/lib/waagent/custom-script/download/0/stderr
Common Issues¶
Issue: Package Installation Fails¶
Error: "Unable to locate package nginx"
Solution: Ensure apt-get update runs first
Issue: Permission Denied¶
Error: "Permission denied writing to /var/www/html"
Solution: Use sudo for commands that need root access
Issue: Extension Timeout¶
Error: "Extension provisioning timed out"
Solution: - Reduce script complexity - Increase timeout (if supported) - Use background tasks for long-running operations
Issue: Nginx Not Starting¶
Error: "Failed to restart nginx.service"
Solution: Check Nginx configuration syntax
Updating Extensions¶
Modify Extension Script¶
# Update the command
settings = <<SETTINGS
{
"commandToExecute": "sudo apt-get update && sudo apt-get install nginx -y && echo \"Updated: Hello from $(hostname)\" > /var/www/html/index.html && sudo systemctl restart nginx"
}
SETTINGS
Force Re-run¶
# Taint the extension resource
terraform taint 'azurerm_virtual_machine_extension.example[0]'
terraform taint 'azurerm_virtual_machine_extension.example[1]'
# Apply changes
terraform apply
Best Practices¶
- Keep Scripts Simple: Complex logic should be in external files
- Error Handling: Use
set -eto exit on errors - Idempotency: Scripts should be safe to run multiple times
- Logging: Add logging for troubleshooting
- Testing: Test scripts locally before deploying
- Timeouts: Keep execution time under 90 minutes
- Security: Use protected_settings for sensitive data
Example: Production-Ready Script¶
#!/bin/bash
set -e # Exit on error
set -x # Print commands (for logging)
# Update system
apt-get update
# Install Nginx
apt-get install -y nginx
# Create custom page with more info
cat > /var/www/html/index.html <<EOF
<!DOCTYPE html>
<html>
<head>
<title>$(hostname)</title>
<style>
body { font-family: Arial; margin: 40px; }
.info { background: #f0f0f0; padding: 20px; border-radius: 5px; }
</style>
</head>
<body>
<h1>Hello from $(hostname)</h1>
<div class="info">
<p><strong>Hostname:</strong> $(hostname)</p>
<p><strong>IP Address:</strong> $(hostname -I)</p>
<p><strong>Timestamp:</strong> $(date)</p>
<p><strong>Nginx Version:</strong> $(nginx -v 2>&1)</p>
</div>
</body>
</html>
EOF
# Restart Nginx
systemctl restart nginx
# Verify Nginx is running
systemctl is-active nginx
echo "Installation completed successfully"
Next Steps¶
Proceed to 10.7-Terraform-Variables.md to learn about all configurable variables in this infrastructure.