Skip to content

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

sudo apt-get update
- Updates the package repository cache - Ensures latest package versions are available

2. Install Nginx

sudo apt-get install nginx -y
- Installs Nginx web server - -y flag automatically confirms installation - Installs dependencies automatically

3. Create Custom Homepage

echo "Hello World from $(hostname)" > /var/www/html/index.html
- 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

sudo systemctl restart nginx
- Restarts Nginx service - Ensures configuration is loaded - Makes the web server immediately available

Extension 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

publisher = "Microsoft.Azure.Extensions"
type      = "CustomScript"
Use: Run arbitrary scripts

2. Docker Extension

publisher = "Microsoft.Azure.Extensions"
type      = "DockerExtension"
Use: Install and configure Docker

3. Azure Monitor Agent

publisher = "Microsoft.Azure.Monitor"
type      = "AzureMonitorLinuxAgent"
Use: Monitoring and diagnostics

4. Desired State Configuration

publisher = "Microsoft.Azure.Extensions"
type      = "DSC"
Use: Configuration management

5. Network Watcher

publisher = "Microsoft.Azure.NetworkWatcher"
type      = "NetworkWatcherAgentLinux"
Use: Network diagnostics

Troubleshooting Extensions

Extension Fails to Install

Check Terraform Output:

terraform apply
# Look for extension errors

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

sudo nginx -t
sudo systemctl status nginx

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

  1. Keep Scripts Simple: Complex logic should be in external files
  2. Error Handling: Use set -e to exit on errors
  3. Idempotency: Scripts should be safe to run multiple times
  4. Logging: Add logging for troubleshooting
  5. Testing: Test scripts locally before deploying
  6. Timeouts: Keep execution time under 90 minutes
  7. 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.