<# .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 5-level management group structure (Level 0-4). 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 5-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: " 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) - Level 3 Management Group (workload/application level) - Level 4 Management Group (environment/instance 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 1.2 - Extended to 4-level management group hierarchy (added Level 3) 1.3 - Extended to 5-level management group hierarchy (added Level 4) .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 (5-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] $Level3_ManagementGroupId = "" [string] $Level4_ManagementGroupId = "" [string] $Level0_ManagementGroupName = "" [string] $Level1_ManagementGroupName = "" [string] $Level2_ManagementGroupName = "" [string] $Level3_ManagementGroupName = "" [string] $Level4_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 } } # Process Level 3 management groups (nested within Level 2) $level3Groups = $level2ManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups' if ($level3Groups.Count -gt 0) { Write-Host " 🔍 Found $($level3Groups.Count) Level 3 management groups" -ForegroundColor Green foreach ($level3ManagementGroupLister in $level3Groups) { try { $managementGroupCount++ $level3ManagementGroup = Get-AzManagementGroup -Group $level3ManagementGroupLister.Name -Expand -ErrorAction Stop Write-Host "" Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────" Write-Host " 📁 LEVEL 3: $($level3ManagementGroup.DisplayName)" -ForegroundColor DarkCyan Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────" $subscriptions = $level3ManagementGroup.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.Level3_ManagementGroupId = $level3ManagementGroup.Id $resourceCheck.Level3_ManagementGroupName = $level3ManagementGroup.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 4 management groups (nested within Level 3) $level4Groups = $level3ManagementGroup.Children | Where-Object Type -EQ 'Microsoft.Management/managementGroups' if ($level4Groups.Count -gt 0) { Write-Host " 🔍 Found $($level4Groups.Count) Level 4 management groups" -ForegroundColor Green foreach ($level4ManagementGroupLister in $level4Groups) { try { $managementGroupCount++ $level4ManagementGroup = Get-AzManagementGroup -Group $level4ManagementGroupLister.Name -Expand -ErrorAction Stop Write-Host "" Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────" Write-Host " 📂 LEVEL 4: $($level4ManagementGroup.DisplayName)" -ForegroundColor Blue Write-Host " ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────" $subscriptions = $level4ManagementGroup.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.Level3_ManagementGroupId = $level3ManagementGroup.Id $resourceCheck.Level3_ManagementGroupName = $level3ManagementGroup.DisplayName $resourceCheck.Level4_ManagementGroupId = $level4ManagementGroup.Id $resourceCheck.Level4_ManagementGroupName = $level4ManagementGroup.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 4 management group '$($level4ManagementGroupLister.Name)': $($_.Exception.Message)" -ForegroundColor Red } } } else { Write-Host " ℹ️ No Level 4 management groups found" -ForegroundColor DarkGray } } catch { Write-Host " ❌ Error accessing Level 3 management group '$($level3ManagementGroupLister.Name)': $($_.Exception.Message)" -ForegroundColor Red } } } else { Write-Host " ℹ️ No Level 3 management groups found" -ForegroundColor DarkGray } } 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) -and [string]::IsNullOrEmpty($_.Level3_ManagementGroupId) }).Count $level3Subs = ($Result | Where-Object { -not [string]::IsNullOrEmpty($_.Level3_ManagementGroupId) -and [string]::IsNullOrEmpty($_.Level4_ManagementGroupId) }).Count $level4Subs = ($Result | Where-Object { -not [string]::IsNullOrEmpty($_.Level4_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 " Workload/Application Level (Level 3): $level3Subs subscriptions" -ForegroundColor DarkCyan Write-Host " Environment/Instance Level (Level 4): $level4Subs subscriptions" -ForegroundColor Blue 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" }