Skip to content

Instantly share code, notes, and snippets.

@hkboujrida
Created August 26, 2025 17:26
Show Gist options
  • Save hkboujrida/2f1c07a18057e3fba9e3f3794aff199a to your computer and use it in GitHub Desktop.
Save hkboujrida/2f1c07a18057e3fba9e3f3794aff199a to your computer and use it in GitHub Desktop.
{
"Name": "Automation Jumpbox Manager",
"IsCustom": true,
"Description": "Allows an Automation Account to start, stop, and assess patches for VMs across a tenant.",
"Actions": [
"Microsoft.Resources/subscriptions/read",
"Microsoft.Compute/virtualMachines/read",
"Microsoft.Compute/virtualMachines/start/action",
"Microsoft.Compute/virtualMachines/deallocate/action",
"Microsoft.Compute/virtualMachines/assessPatches/action"
],
"NotActions": [],
"DataActions": [],
"NotDataActions": [],
"AssignableScopes": [
"/"
]
}
#!/bin/bash
# ======================================================================================
# Azure CLI Script to Create Automation Account and Custom Role
# This script performs two key tasks:
# 1. Creates a custom RBAC role with permissions to manage VMs across a tenant.
# 2. Creates an Azure Automation Account with a system-assigned managed identity.
# 3. Assigns the custom role to the Automation Account's managed identity.
# ======================================================================================
# --- SCRIPT VARIABLES ---
# Customize these variables for your environment.
# Ensure the LOCATION is the same for the resource group and the automation account.
RESOURCE_GROUP_NAME="your-automation-rg"
LOCATION="eastus" # Example: westeurope, centralus, etc.
AUTOMATION_ACCOUNT_NAME="your-automation-account-name"
ROLE_NAME="Automation Jumpbox Manager"
SUBSCRIPTION_ID="your-subscription-id"
RUNBOOK_NAME="Automate-Jumpbox-Updates"
SCRIPT_FILE_NAME="vm-management-script.ps1"
# --- SCRIPT BODY ---
echo "--- Step 1: Create or verify the resource group ---"
if [ $(az group exists --name "$RESOURCE_GROUP_NAME") = false ]; then
echo "Resource group '$RESOURCE_GROUP_NAME' not found. Creating it now..."
az group create --name "$RESOURCE_GROUP_NAME" --location "$LOCATION"
echo "Resource group '$RESOURCE_GROUP_NAME' created."
else
echo "Resource group '$RESOURCE_GROUP_NAME' already exists. Skipping creation."
fi
echo "--- Step 2: Create the custom RBAC role definition ---"
# Define the role's permissions in a JSON format.
# This approach is self-contained and doesn't require a separate file.
cat << EOF > custom-role-definition.json
{
"Name": "$ROLE_NAME",
"IsCustom": true,
"Description": "Allows an Automation Account to start, stop, and assess patches for VMs across a tenant.",
"Actions": [
"Microsoft.Resources/subscriptions/read",
"Microsoft.Compute/virtualMachines/read",
"Microsoft.Compute/virtualMachines/start/action",
"Microsoft.Compute/virtualMachines/deallocate/action",
"Microsoft.Compute/virtualMachines/assessPatches/action"
],
"NotActions": [],
"DataActions": [],
"NotDataActions": [],
"AssignableScopes": [
"/subscriptions/${SUBSCRIPTION_ID}"
]
}
EOF
# Note: You MUST replace `<YourTenantId>` in the AssignableScopes with your actual tenant ID.
# This ensures the role can be assigned at the tenant level. You can find this ID in the Azure AD overview blade.
# Create the role. We'll check if it already exists to make the script re-runnable.
if az role definition list --name "$ROLE_NAME" --query "[].name" -o tsv | grep -q "$ROLE_NAME"; then
echo "Custom role '$ROLE_NAME' already exists. Skipping creation."
else
echo "Creating custom role '$ROLE_NAME'..."
az role definition create --role-definition @custom-role-definition.json
echo "Custom role '$ROLE_NAME' created successfully."
fi
echo "--- Step 3: Create the Azure Automation Account ---"
echo "Creating automation account '$AUTOMATION_ACCOUNT_NAME'..."
# Create the account without enabling the managed identity yet.
az automation account create \
--resource-group "$RESOURCE_GROUP_NAME" \
--name "$AUTOMATION_ACCOUNT_NAME" \
--location "$LOCATION"
echo "Automation account '$AUTOMATION_ACCOUNT_NAME' created successfully."
echo "--- Step 4: Enable the system-assigned managed identity using az rest ---"
# Use 'az rest' to call the REST API and enable the managed identity.
echo "Enabling a system-assigned managed identity for the automation account..."
az rest \
--method PATCH \
--uri "https://management.azure.com/subscriptions/$(az account show --query id -o tsv)/resourceGroups/$RESOURCE_GROUP_NAME/providers/Microsoft.Automation/automationAccounts/$AUTOMATION_ACCOUNT_NAME?api-version=2022-08-08" \
--body '{"identity": {"type": "SystemAssigned"}}'
echo "Managed identity enabled. Retrieving its Principal ID..."
principal_id=$(az automation account show \
--resource-group "$RESOURCE_GROUP_NAME" \
--name "$AUTOMATION_ACCOUNT_NAME" \
--query "identity.principalId" -o tsv)
if [ -z "$principal_id" ]; then
echo "ERROR: Could not retrieve Principal ID from the automation account. Role assignment failed."
exit 1
fi
echo "Principal ID found: $principal_id"
echo "--- Step 5: Assign the custom role to the managed identity ---"
echo "Assigning role '$ROLE_NAME' to the managed identity at the tenant scope..."
tenant_id=$(az account show --query "tenantId" -o tsv)
az role assignment create --assignee "$principal_id" \
--role "$ROLE_NAME" \
--scope "/subscriptions/${SUBSCRIPTION_ID}"
# --scope "/providers/Microsoft.Management/managementGroups/${tenant_id}"
echo "Role assignment completed. The managed identity can now manage VMs in the tenant."
echo "--- Step 5: Create and import the PowerShell runbook ---"
echo "Creating a new PowerShell runbook named '$RUNBOOK_NAME'..."
az automation runbook create \
--automation-account-name "$AUTOMATION_ACCOUNT_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--name "$RUNBOOK_NAME" \
--type PowerShell
echo "Importing content into runbook '$RUNBOOK_NAME'..."
# Save the PowerShell script content to a temporary file
echo "$VM_MANAGEMENT_SCRIPT_CONTENT" > temp_runbook.ps1
az automation runbook replace-content \
--automation-account-name "$AUTOMATION_ACCOUNT_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--name "$RUNBOOK_NAME" \
--path temp_runbook.ps1
# Clean up the temporary file
rm temp_runbook.ps1
echo "Content imported. Now publishing the runbook..."
az automation runbook publish \
--automation-account-name "$AUTOMATION_ACCOUNT_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--name "$RUNBOOK_NAME"
echo "Runbook '$RUNBOOK_NAME' created, populated, and published."
echo "--- Script execution complete! ---"
echo "Your automation environment is set up. You can now schedule your runbook in the Azure portal."
echo "--- Script execution complete! ---"
echo "You can now create your Bash runbook and use 'az login --identity' to authenticate."
#!/bin/bash
# ======================================================================================
# Azure CLI Script to Create and Populate an Automation Runbook
# This script creates a new Bash runbook and imports the VM management script into it.
# ======================================================================================
# --- SCRIPT VARIABLES ---
# Customize these variables to match your environment.
RESOURCE_GROUP_NAME="your-automation-rg"
AUTOMATION_ACCOUNT_NAME="your-automation-account-name"
RUNBOOK_NAME="Automate-Jumpbox-Updates"
SCRIPT_FILE_NAME="vm-management-script.sh"
# ======================================================================================
# THE CONTENT OF YOUR VM MANAGEMENT SCRIPT
# This is the script you created in a previous step.
# You can update the variables inside this text block if needed.
# ======================================================================================
VM_MANAGEMENT_SCRIPT_CONTENT=$(cat <<'EOF'
#!/bin/bash
# ======================================================================================
# Azure Automation Runbook: VM Lifecycle and Update Management
# This script starts all VMs in a tenant, waits for scheduled updates to apply,
# and then deallocates them.
# ======================================================================================
# Exit immediately if a command exits with a non-zero status.
set -e
# --- SCRIPT VARIABLES ---
# Customize these variables to fit your environment.
# You can use tags to filter which VMs this script should manage.
# For example, to only manage VMs with a 'role' tag set to 'jumpbox':
JUMPBOX_TAG="role"
JUMPBOX_TAG_VALUE="jumpbox"
# Time to wait after starting a VM before proceeding to updates (in seconds).
# This gives the VM time to boot and connect to the network.
WAIT_TIME_AFTER_START_SECONDS=300 # 5 minutes
# Time to wait for updates to run and complete (in seconds).
# This should be long enough for your update schedule.
WAIT_TIME_FOR_UPDATES_SECONDS=3600 # 1 hour
# --- SCRIPT BODY ---
echo "Starting VM lifecycle management script..."
# Authenticate with Azure using the system-assigned managed identity of the Automation Account.
# The Automation Account's identity must have the custom role you created assigned at the tenant scope.
echo "Authenticating with Managed Identity..."
az login --identity
# Get a list of all subscription IDs in the tenant and iterate through them.
echo "Retrieving all subscriptions in the tenant..."
subscription_ids=$(az account list --query "[].id" -o tsv)
# Check if any subscriptions were found
if [ -z "$subscription_ids" ]; then
echo "No subscriptions found. Exiting."
exit 1
fi
# Loop through each subscription
for sub_id in $subscription_ids
do
echo "========================================================"
echo "Processing subscription: $sub_id"
az account set --subscription "$sub_id"
# Get a list of all VMs in the current subscription with a specific tag.
vms=$(az vm list --query "[?tags.$JUMPBOX_TAG=='$JUMPBOX_TAG_VALUE'].{Name:name, ResourceGroup:resourceGroup, PowerState:powerState}" -o tsv)
if [ -z "$vms" ]; then
echo "No VMs with tag '$JUMPBOX_TAG'='$JUMPBOX_TAG_VALUE' found in subscription $sub_id. Skipping."
continue
fi
echo "Found VMs with tag. Starting any that are not running..."
# Loop through the list of VMs to start them
while read -r vm_name vm_rg vm_state; do
if [[ "$vm_state" != "VM running" ]]; then
echo "Starting VM '$vm_name' in Resource Group '$vm_rg'..."
az vm start --resource-group "$vm_rg" --name "$vm_name"
else
echo "VM '$vm_name' is already running. Skipping start."
fi
done <<< "$vms"
echo "All VMs are now starting. Waiting for $WAIT_TIME_AFTER_START_SECONDS seconds for them to be ready."
sleep "$WAIT_TIME_AFTER_START_SECONDS"
echo "Initiating patch assessment for all VMs in subscription $sub_id..."
while read -r vm_name vm_rg vm_state; do
echo "Assessing patches for VM '$vm_name'..."
az vm assess-patches --resource-group "$vm_rg" --name "$vm_name"
done <<< "$vms"
echo "Patch assessment initiated. Waiting for a generous $WAIT_TIME_FOR_UPDATES_SECONDS seconds for updates to complete."
sleep "$WAIT_TIME_FOR_UPDATES_SECONDS"
echo "Updates should be complete. Deallocating all VMs in subscription $sub_id to save costs."
while read -r vm_name vm_rg vm_state; do
echo "Deallocating VM '$vm_name' in Resource Group '$vm_rg'..."
az vm deallocate --resource-group "$vm_rg" --name "$vm_name" --output none
done <<< "$vms"
echo "Finished processing subscription: $sub_id"
done
echo "========================================================"
echo "Script completed successfully. All VMs have been processed."
EOF
)
# --- SCRIPT BODY ---
echo "--- Step 1: Create an empty runbook ---"
echo "Creating a new Bash runbook named '$RUNBOOK_NAME'..."
az automation runbook create \
--automation-account-name "$AUTOMATION_ACCOUNT_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--name "$RUNBOOK_NAME" \
--type "Bash" \
--location "$LOCATION"
echo "Runbook '$RUNBOOK_NAME' created."
echo "--- Step 2: Save the script content to a temporary file ---"
echo "$VM_MANAGEMENT_SCRIPT_CONTENT" > "$SCRIPT_FILE_NAME"
echo "Script content saved to '$SCRIPT_FILE_NAME'."
echo "--- Step 3: Import the script file and publish the runbook ---"
echo "Importing content from '$SCRIPT_FILE_NAME' into runbook '$RUNBOOK_NAME'..."
az automation runbook import \
--automation-account-name "$AUTOMATION_ACCOUNT_NAME" \
--resource-group "$RESOURCE_GROUP_NAME" \
--name "$RUNBOOK_NAME" \
--path "$SCRIPT_FILE_NAME" \
--type "Bash" \
--publish
echo "Content imported and runbook '$RUNBOOK_NAME' published."
echo "You can now schedule this runbook in your Azure Automation account."
echo "Remember to delete the temporary script file '$SCRIPT_FILE_NAME' when you're done."
# Note: The `az automation runbook import` command is experimental.
# If you encounter issues, you can also use the portal to create the runbook
# and paste the contents of the VM_MANAGEMENT_SCRIPT_CONTENT variable.
az automation schedule create \
--automation-account-name "your-automation-account-name" \
--resource-group "your-automation-rg" \
--name "Weekly-Sunday-Schedule" \
--description "Runs every Sunday at 08:00 UTC." \
--frequency "Week" \
--interval 1 \
--week-days "Sunday" \
--time-zone "UTC" \
--start-time "$(date -u +"%Y-%m-%dT08:00:00Z")"
az automation job-schedule create \
--automation-account-name "your-automation-account-name" \
--resource-group "your-automation-rg" \
--account-name "your-automation-account-name" \
--runbook-name "Automate-Jumpbox-Updates" \
--schedule-name "Weekly-Sunday-Schedule"
<#
.SYNOPSIS
PowerShell Script to Start and Patch Azure VMs using Azure CLI.
.DESCRIPTION
This script automates the full lifecycle of VM management and patching.
It authenticates using a managed identity, starts VMs, and then triggers
a patch installation via the Azure CLI.
.NOTES
This runbook is intended for use in an Azure Automation account with a
system-assigned managed identity.
#>
# --- SCRIPT VARIABLES ---
# Customize these variables to fit your environment.
# Time to wait after starting a VM before proceeding (in seconds).
# This gives the VM time to boot and connect.
$WAIT_TIME_AFTER_START_SECONDS = 300 # 5 minutes
# Time to wait for updates to run and complete (in seconds).
# This should be generous enough for a typical patch window.
$WAIT_TIME_FOR_UPDATES_SECONDS = 3600 # 1 hour
# --- SCRIPT BODY ---
Write-Host "Starting VM management and patching script..."
# Authenticate with Azure using the system-assigned managed identity.
Write-Host "Authenticating with Managed Identity..."
az login --identity
# Get a list of all subscription IDs in the tenant.
Write-Host "Retrieving all subscriptions in the tenant..."
$subscriptions = az account list --query "[].id" -o tsv
# Check if any subscriptions were found.
if ($null -eq $subscriptions) {
Write-Warning "No subscriptions found. Exiting."
exit
}
# Loop through each subscription.
foreach ($subId in $subscriptions) {
Write-Host "========================================================"
Write-Host "Processing subscription: $subId"
# Set the current subscription context for Azure CLI commands.
az account set --subscription "$subId"
# Get a list of all VMs in the current subscription.
$vms = az vm list --query "[].{Name:name, ResourceGroup:resourceGroup, PowerState:powerState}" -o tsv
if ($null -eq $vms) {
Write-Host "No VMs found in this subscription. Skipping."
continue
}
Write-Host "Found VMs. Starting any that are not running..."
# Loop through the list of VMs and start them if they are not running.
foreach ($line in $vms) {
$vmDetails = $line.Split("`t")
$vmName = $vmDetails[0]
$vmRg = $vmDetails[1]
$vmState = $vmDetails[2]
if ($vmState -ne "VM running") {
Write-Host "Starting VM '$vmName' in Resource Group '$vmRg'..."
az vm start --resource-group "$vmRg" --name "$vmName"
} else {
Write-Host "VM '$vmName' is already running. Skipping start."
}
}
Write-Host "All VMs are now starting. Waiting for $WAIT_TIME_AFTER_START_SECONDS seconds for them to be ready."
Start-Sleep -Seconds $WAIT_TIME_AFTER_START_SECONDS
Write-Host "Initiating patch installation on all VMs..."
foreach ($line in $vms) {
$vmDetails = $line.Split("`t")
$vmName = $vmDetails[0]
$vmRg = $vmDetails[1]
Write-Host "Running patch installation command on VM '$vmName'..."
# This command is for Windows VMs. For Linux, use a different command.
az vm run-command invoke --resource-group "$vmRg" --name "$vmName" --command-id RunPowerShellScript --scripts "Install-WindowsUpdate -MicrosoftUpdate -AcceptAll -Force"
}
Write-Host "Patching initiated. Waiting for a generous $WAIT_TIME_FOR_UPDATES_SECONDS seconds for updates to complete."
Start-Sleep -Seconds $WAIT_TIME_FOR_UPDATES_SECONDS
Write-Host "Updates should be complete. Deallocating all VMs in subscription $subId to save costs."
foreach ($line in $vms) {
$vmDetails = $line.Split("`t")
$vmName = $vmDetails[0]
$vmRg = $vmDetails[1]
Write-Host "Deallocating VM '$vmName' in Resource Group '$vmRg'..."
az vm deallocate --resource-group "$vmRg" --name "$vmName"
}
Write-Host "Finished processing subscription: $subId"
Write-Host "========================================================"
}
Write-Host "Script completed successfully. All VMs have been processed."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment