Created
August 26, 2025 17:26
-
-
Save hkboujrida/2f1c07a18057e3fba9e3f3794aff199a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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": [ | |
"/" | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.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