Files
Cloud-20Engineering/Powershell/Lists/Azure/ManagementGroups.ps1
Jurjen Ladenius a226ca97ac added documetation
2025-11-03 08:12:01 +01:00

392 lines
22 KiB
PowerShell
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
Generates a hierarchical inventory of Azure Management Groups and Subscriptions.
.DESCRIPTION
This script creates a comprehensive mapping of Azure organizational structure by traversing
the management group hierarchy starting from a specified root management group. It discovers
and documents all subscriptions within a 3-level management group structure (Level 0-2).
The script provides detailed organizational visibility including:
- Hierarchical management group structure mapping
- Subscription placement and organizational context
- Subscription state tracking (Active, Disabled, etc.)
- Multi-level governance structure documentation
- CSV export for organizational analysis and compliance reporting
Note: The script is optimized for a maximum 3-level management group depth and starts
from a configurable root management group ID.
.PARAMETER RootManagementGroupId
The GUID of the root management group to start the hierarchy discovery from.
Defaults to the Effectory organization root management group.
Example: "12345678-1234-1234-1234-123456789012"
.OUTPUTS
CSV File: "<date> azure_managementgroups.csv"
Contains columns for:
- Subscription identification (ID, name, state)
- Level 0 Management Group (root level)
- Level 1 Management Group (department/division level)
- Level 2 Management Group (team/project level)
.EXAMPLE
.\ManagementGroups.ps1
Uses the default Effectory root management group and generates:
"2024-10-30 1435 azure_managementgroups.csv"
.EXAMPLE
.\ManagementGroups.ps1 -RootManagementGroupId "87654321-4321-4321-4321-210987654321"
Discovers management group hierarchy starting from a custom root management group.
.EXAMPLE
.\ManagementGroups.ps1 -RootManagementGroupId "tenant-root-mg" -Verbose
Runs with verbose output for detailed discovery logging.
.NOTES
File Name : ManagementGroups.ps1
Author : Cloud Engineering Team
Prerequisite : Azure PowerShell module (Az.Resources, Az.Accounts)
Copyright : (c) 2024 Effectory. All rights reserved.
Version History:
1.0 - Initial release with 3-level management group hierarchy discovery
1.1 - Added parameterized root management group for flexibility
.LINK
https://docs.microsoft.com/en-us/azure/governance/management-groups/
https://docs.microsoft.com/en-us/powershell/module/az.resources/
.COMPONENT
Requires Azure PowerShell modules:
- Az.Resources (for management group and subscription enumeration)
- Az.Accounts (for authentication and context management)
.ROLE
Required Azure permissions:
- Management Group Reader on the root management group and all child groups
- Reader access to view subscription details within management groups
.FUNCTIONALITY
- Hierarchical management group discovery (3-level maximum)
- Subscription placement mapping and state tracking
- Organizational structure documentation and CSV export
- Cross-tenant compatible with configurable root management group
#>
#Requires -Modules Az.Resources, Az.Accounts
#Requires -Version 5.1
# Uncomment the following line if authentication is required
#Connect-AzAccount
[CmdletBinding()]
param(
[Parameter(
Mandatory = $false,
HelpMessage = "The GUID of the root management group to start hierarchy discovery from"
)]
[ValidateNotNullOrEmpty()]
[string]$RootManagementGroupId = 'e9792fd7-4044-47e7-a40d-3fba46f1cd09'
)
class ResourceCheck {
[string] $SubscriptionId = ""
[string] $SubscriptionName = ""
[string] $SubscriptionState = ""
[string] $Level0_ManagementGroupId = ""
[string] $Level1_ManagementGroupId = ""
[string] $Level2_ManagementGroupId = ""
[string] $Level0_ManagementGroupName = ""
[string] $Level1_ManagementGroupName = ""
[string] $Level2_ManagementGroupName = ""
}
# Initialize script execution
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$startTime = Get-Date
Write-Host "======================================================================================================================================================================"
Write-Host "🏗️ AZURE MANAGEMENT GROUP STRUCTURE DISCOVERY" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Green
Write-Host ""
try {
# Validate Azure authentication
$context = Get-AzContext
if (-not $context) {
throw "No Azure context found. Please run Connect-AzAccount first."
}
Write-Host "🔐 Authenticated as: $($context.Account.Id)" -ForegroundColor Green
Write-Host "🏢 Tenant: $($context.Tenant.Id)" -ForegroundColor Green
Write-Host "🎯 Root Management Group: $RootManagementGroupId" -ForegroundColor Yellow
Write-Host ""
# Initialize output file and tracking variables
[string] $date = Get-Date -Format "yyyy-MM-dd HHmm"
$fileName = ".\$date azure_managementgroups.csv"
Write-Host "📄 Output file: $fileName" -ForegroundColor Yellow
Write-Host ""
[ResourceCheck[]]$Result = @()
$totalSubscriptions = 0
$managementGroupCount = 0
# Get root management group with error handling
Write-Host "🔍 Discovering root management group structure..." -ForegroundColor Cyan
$rootManagementGroup = Get-AzManagementGroup -GroupId $RootManagementGroupId -Expand -ErrorAction Stop
if (-not $rootManagementGroup) {
throw "Root management group '$RootManagementGroupId' not found or not accessible."
}
Write-Host "✅ Root Management Group: $($rootManagementGroup.DisplayName)" -ForegroundColor Green
Write-Host " ID: $($rootManagementGroup.Id)" -ForegroundColor DarkGray
Write-Host ""
# Process Level 0 (Root) subscriptions
$managementGroupCount++
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
Write-Host "📋 LEVEL 0 (Root): $($rootManagementGroup.DisplayName)" -ForegroundColor Cyan
Write-Host "----------------------------------------------------------------------------------------------------------------------------------------------------------------------"
$subscriptions = $rootManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
Write-Host " 📊 Direct subscriptions: $($subscriptions.Count)" -ForegroundColor Green
foreach ($subscription in $subscriptions) {
try {
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
# Color code subscription state
$stateColor = switch ($subscription.State) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " 📋 Subscription: $($subscription.DisplayName) [$subscriptionId] - $($subscription.State)" -ForegroundColor $stateColor
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
# Process Level 1 management groups
$level1Groups = $rootManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'
Write-Host ""
Write-Host "🔍 Found $($level1Groups.Count) Level 1 management groups" -ForegroundColor Green
Write-Host ""
foreach ($level1ManagementGroupLister in $level1Groups) {
try {
$managementGroupCount++
$level1ManagementGroup = Get-AzManagementGroup -Group $level1ManagementGroupLister.Name -Expand -ErrorAction Stop
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
Write-Host " 📂 LEVEL 1: $($level1ManagementGroup.DisplayName)" -ForegroundColor Yellow
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
$subscriptions = $level1ManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
Write-Host " 📊 Direct subscriptions: $($subscriptions.Count)" -ForegroundColor Green
foreach ($subscription in $subscriptions) {
try {
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
# Color code subscription state
$stateColor = switch ($subscription.State) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " 📋 Subscription: $($subscription.DisplayName) [$subscriptionId] - $($subscription.State)" -ForegroundColor $stateColor
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.Level1_ManagementGroupId = $level1ManagementGroup.Id
$resourceCheck.Level1_ManagementGroupName = $level1ManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error accessing Level 1 management group '$($level1ManagementGroupLister.Name)': $($_.Exception.Message)" -ForegroundColor Red
}
# Process Level 2 management groups (nested within Level 1)
$level2Groups = $level1ManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups'
if ($level2Groups.Count -gt 0) {
Write-Host " 🔍 Found $($level2Groups.Count) Level 2 management groups" -ForegroundColor Green
foreach ($level2ManagementGroupLister in $level2Groups) {
try {
$managementGroupCount++
$level2ManagementGroup = Get-AzManagementGroup -Group $level2ManagementGroupLister.Name -Expand -ErrorAction Stop
Write-Host ""
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
Write-Host " 📁 LEVEL 2: $($level2ManagementGroup.DisplayName)" -ForegroundColor Magenta
Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────"
$subscriptions = $level2ManagementGroup.Children | Where-Object Type -EQ '/subscriptions'
Write-Host " 📊 Direct subscriptions: $($subscriptions.Count)" -ForegroundColor Green
foreach ($subscription in $subscriptions) {
try {
$scope = $subscription.Id.Substring($subscription.Parent.Length, $subscription.Id.Length - $subscription.Parent.Length)
$subscriptionId = $scope.Replace("/subscriptions/", "")
# Color code subscription state
$stateColor = switch ($subscription.State) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " 📋 Subscription: $($subscription.DisplayName) [$subscriptionId] - $($subscription.State)" -ForegroundColor $stateColor
[ResourceCheck] $resourceCheck = [ResourceCheck]::new()
$resourceCheck.Level0_ManagementGroupId = $rootManagementGroup.Id
$resourceCheck.Level0_ManagementGroupName = $rootManagementGroup.DisplayName
$resourceCheck.Level1_ManagementGroupId = $level1ManagementGroup.Id
$resourceCheck.Level1_ManagementGroupName = $level1ManagementGroup.DisplayName
$resourceCheck.Level2_ManagementGroupId = $level2ManagementGroup.Id
$resourceCheck.Level2_ManagementGroupName = $level2ManagementGroup.DisplayName
$resourceCheck.SubscriptionId = $subscriptionId
$resourceCheck.SubscriptionName = $subscription.DisplayName
$resourceCheck.SubscriptionState = $subscription.State
$Result += $resourceCheck
$totalSubscriptions++
} catch {
Write-Host " ❌ Error processing subscription: $($_.Exception.Message)" -ForegroundColor Red
}
}
} catch {
Write-Host " ❌ Error accessing Level 2 management group '$($level2ManagementGroupLister.Name)': $($_.Exception.Message)" -ForegroundColor Red
}
}
} else {
Write-Host " No Level 2 management groups found" -ForegroundColor DarkGray
}
}
# Export results to CSV
Write-Host ""
Write-Host "💾 Exporting results to CSV..." -ForegroundColor Cyan
$Result | Export-Csv -Path $fileName -NoTypeInformation
# Calculate execution time and generate summary report
$endTime = Get-Date
$executionTime = $endTime - $startTime
Write-Host ""
Write-Host "======================================================================================================================================================================"
Write-Host "📊 AZURE MANAGEMENT GROUP DISCOVERY SUMMARY" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
Write-Host "⏰ Execution Time: $($executionTime.ToString('hh\:mm\:ss'))" -ForegroundColor Green
Write-Host "🏗️ Management Groups Discovered: $managementGroupCount" -ForegroundColor Green
Write-Host "📋 Total Subscriptions Found: $totalSubscriptions" -ForegroundColor Green
Write-Host "📄 Results Exported To: $fileName" -ForegroundColor Yellow
if (Test-Path $fileName) {
$fileSize = (Get-Item $fileName).Length
Write-Host "💾 File Size: $([math]::Round($fileSize/1KB, 2)) KB" -ForegroundColor Green
}
# Analyze subscription states
$subscriptionStates = $Result | Group-Object SubscriptionState
if ($subscriptionStates.Count -gt 0) {
Write-Host ""
Write-Host "📈 SUBSCRIPTION STATE ANALYSIS:" -ForegroundColor Cyan
foreach ($state in $subscriptionStates) {
$stateColor = switch ($state.Name) {
"Enabled" { "Green" }
"Disabled" { "Red" }
"Warned" { "Yellow" }
default { "White" }
}
Write-Host " $($state.Name): $($state.Count) subscriptions" -ForegroundColor $stateColor
}
}
# Provide organizational insights
$level0Subs = ($Result | Where-Object { [string]::IsNullOrEmpty($_.Level1_ManagementGroupId) }).Count
$level1Subs = ($Result | Where-Object { -not [string]::IsNullOrEmpty($_.Level1_ManagementGroupId) -and [string]::IsNullOrEmpty($_.Level2_ManagementGroupId) }).Count
$level2Subs = ($Result | Where-Object { -not [string]::IsNullOrEmpty($_.Level2_ManagementGroupId) }).Count
Write-Host ""
Write-Host "🏗️ ORGANIZATIONAL STRUCTURE:" -ForegroundColor Cyan
Write-Host " Root Level (Level 0): $level0Subs subscriptions" -ForegroundColor Green
Write-Host " Department/Division Level (Level 1): $level1Subs subscriptions" -ForegroundColor Yellow
Write-Host " Team/Project Level (Level 2): $level2Subs subscriptions" -ForegroundColor Magenta
Write-Host ""
Write-Host "📈 NEXT STEPS:" -ForegroundColor Cyan
Write-Host " 1. Review the generated CSV file for detailed organizational mapping" -ForegroundColor White
Write-Host " 2. Analyze subscription placement for governance compliance" -ForegroundColor White
Write-Host " 3. Consider moving orphaned subscriptions to appropriate management groups" -ForegroundColor White
Write-Host " 4. Use this data for policy assignment and resource organization planning" -ForegroundColor White
Write-Host ""
Write-Host "✅ Azure Management Group discovery completed successfully!" -ForegroundColor Green
Write-Host "======================================================================================================================================================================"
} catch {
Write-Host ""
Write-Host "❌ CRITICAL ERROR OCCURRED" -ForegroundColor Red
Write-Host "======================================================================================================================================================================"
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Line: $($_.InvocationInfo.ScriptLineNumber)" -ForegroundColor Red
Write-Host "Position: $($_.InvocationInfo.OffsetInLine)" -ForegroundColor Red
Write-Host ""
Write-Host "🔧 TROUBLESHOOTING STEPS:" -ForegroundColor Yellow
Write-Host " 1. Verify you are authenticated to Azure (Connect-AzAccount)" -ForegroundColor White
Write-Host " 2. Ensure you have Management Group Reader permissions on the root management group" -ForegroundColor White
Write-Host " 3. Verify the root management group ID '$RootManagementGroupId' exists and is accessible" -ForegroundColor White
Write-Host " 4. Check that Azure PowerShell modules are installed and up to date" -ForegroundColor White
Write-Host " 5. Try running with a different root management group ID if the current one is invalid" -ForegroundColor White
Write-Host ""
Write-Host "📞 For additional support, contact the Cloud Engineering team" -ForegroundColor Cyan
Write-Host "======================================================================================================================================================================"
# Ensure we exit with error code for automation scenarios
exit 1
} finally {
# Reset progress preference
$ProgressPreference = "Continue"
}