Public/Invoke-ScriptMultiAccountRegion.ps1
|
function Invoke-ScriptMultiAccountRegion { <# .SYNOPSIS Invokes AWS commands across multiple accounts and regions to gather data. .DESCRIPTION Executes a ScriptBlock against multiple AWS accounts (via profiles) and regions, collecting the output from each invocation. Each output object is optionally enriched with the AccountId and Region it came from, making it easy to aggregate and compare data across your AWS estate. Designed for read-only data gathering (e.g., Get-EC2SecurityGroup, Get-S3Bucket, Get-IAMUser). The ScriptBlock receives -ProfileName and -Region via PSDefaultParameterValues so AWS cmdlets inside the block automatically use them. .PARAMETER ProfileName One or more AWS profile names to iterate over. Accepts pipeline input. If not specified, uses the current default AWS profile. .PARAMETER Region One or more AWS regions to query per profile. Defaults to the current default region. .PARAMETER ScriptBlock The ScriptBlock to execute for each account/region combination. .PARAMETER IncludeAccountId When specified, adds an AccountId property to each output object. .PARAMETER IncludeRegion When specified, adds a Region property to each output object. .PARAMETER IncludeProfileName When specified, adds a ProfileName property to each output object. .PARAMETER ThrottleLimit Seconds to wait between calls to avoid throttling. Defaults to 0. .PARAMETER AccessKey AWS access key for explicit credentials. Optional. .PARAMETER SecretKey AWS secret key for explicit credentials. Optional. .PARAMETER SessionToken AWS session token for temporary credentials. Optional. .PARAMETER Credential Pre-built AWS credential object. Optional. .PARAMETER ProfileLocation Custom credential file path. Optional. .PARAMETER EndpointUrl Custom AWS service endpoint URL. Optional. .EXAMPLE Invoke-ScriptMultiAccountRegion -ProfileName 'dev','prod' -Region 'us-east-1' ` -ScriptBlock { Get-STSCallerIdentity } -IncludeRegion -IncludeProfileName .EXAMPLE Get-AWSCredential -ListProfileDetail | Select-Object -ExpandProperty ProfileName | Invoke-ScriptMultiAccountRegion -Region 'us-east-1' ` -ScriptBlock { Get-S3Bucket } -IncludeAccountId -IncludeProfileName #> [CmdletBinding()] param( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [string[]]$ProfileName, [Parameter()] [string[]]$Region, [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [Parameter()] [switch]$IncludeAccountId, [Parameter()] [switch]$IncludeRegion, [Parameter()] [switch]$IncludeProfileName, [Parameter()] [ValidateRange(0, 60)] [int]$ThrottleLimit = 0, # AWS common parameters (credential passthrough for Get-STSCallerIdentity validation) [Parameter()] [string]$AccessKey, [Parameter()] [string]$SecretKey, [Parameter()] [string]$SessionToken, [Parameter()] $Credential, [Parameter()] [string]$ProfileLocation, [Parameter()] [string]$EndpointUrl ) begin { # Build base AWS splat from credential parameters then remove ProfileName/Region # since those are arrays used for iteration in this function, not single-value # credential params to pass to AWS cmdlets directly. $awsParams = New-AWSParamSplat -BoundParameters $PSBoundParameters $awsParams.Remove('ProfileName') | Out-Null $awsParams.Remove('Region') | Out-Null if (-not $ProfileName) { # Try the shell's current stored credential profile name $currentProfile = $null if ($StoredAWSCredentials) { $currentProfile = $StoredAWSCredentials } if (-not $currentProfile) { # Fall back: check if there's a default profile in the credential store $defaultProfile = (Get-AWSCredential -ListProfileDetail | Where-Object { $_.ProfileName -eq 'default' } | Select-Object -First 1 -ExpandProperty ProfileName) if ($defaultProfile) { $currentProfile = $defaultProfile } } if ($currentProfile) { $ProfileName = @($currentProfile) } else { Write-Error "No ProfileName specified and no current AWS profile found. Use -ProfileName or Set-AWSCredential." return } } if (-not $Region) { $defaultRegion = (Get-DefaultAWSRegion).Region if ($defaultRegion) { $Region = @($defaultRegion) } else { Write-Error "No region specified and no default AWS region set. Use -Region or Set-DefaultAWSRegion." return } } $profileCount = 0 $regionTotal = $Region.Count } process { foreach ($prof in $ProfileName) { $profileCount++ Write-Progress -Id 1 -Activity "Processing AWS Profiles" ` -Status "Profile: $prof (#$profileCount)" ` -CurrentOperation "Authenticating..." # Validate credentials before doing any work for this profile # Override ProfileName per iteration; base awsParams carries other credential params $iterParams = $awsParams.Clone() $iterParams['ProfileName'] = $prof try { $identity = Get-STSCallerIdentity @iterParams -ErrorAction Stop $accountId = $identity.Account Write-Verbose "Profile '$prof' resolved to AccountId: $accountId" } catch { Write-Warning "Skipping profile '${prof}': unable to authenticate - $_" continue } $regionIndex = 0 foreach ($r in $Region) { $regionIndex++ $regionPercent = [int](($regionIndex / $regionTotal) * 100) Write-Progress -Id 2 -ParentId 1 -Activity "Processing Regions for '$prof'" ` -Status "Region: $r ($regionIndex of $regionTotal)" ` -PercentComplete $regionPercent Write-Verbose "Executing against Profile='$prof', Region='$r'" try { $originalDefaults = $PSDefaultParameterValues.Clone() $PSDefaultParameterValues['*:ProfileName'] = $prof $PSDefaultParameterValues['*:Region'] = $r # Module-scoped functions cannot modify the caller's $PSDefaultParameterValues. # Use [scriptblock]::Create() to build an unbound scriptblock (not tied to any # module's session state) that sets $PSDefaultParameterValues and then invokes # the user's original ScriptBlock. Unbound scriptblocks execute in the global/ # caller scope where AWS cmdlets will see the default parameter values. $invoker = [scriptblock]::Create(@" `$PSDefaultParameterValues['*:ProfileName'] = '$($prof -replace "'", "''")' `$PSDefaultParameterValues['*:Region'] = '$($r -replace "'", "''")' & `$args[0] "@) $results = & $invoker $ScriptBlock if ($results) { foreach ($item in $results) { $props = [ordered]@{} foreach ($p in $item.PSObject.Properties) { $props[$p.Name] = $p.Value } if ($IncludeAccountId) { $props['AccountId'] = $accountId } if ($IncludeRegion) { $props['Region'] = $r } if ($IncludeProfileName) { $props['ProfileName'] = $prof } $enriched = [PSCustomObject]$props $propNames = [string[]]$props.Keys $propSet = [System.Management.Automation.PSPropertySet]::new( 'DefaultDisplayPropertySet', $propNames ) $enriched | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $propSet -Force $enriched } } else { Write-Verbose "No results returned for Profile='$prof', Region='$r'" } } catch { Write-Warning "Error executing ScriptBlock for Profile='${prof}', Region='${r}': $_" } finally { # Restore module-scoped defaults $PSDefaultParameterValues.Clear() foreach ($key in $originalDefaults.Keys) { $PSDefaultParameterValues[$key] = $originalDefaults[$key] } # Clean up caller/global scope defaults set by the unbound invoker scriptblock $cleanup = [scriptblock]::Create( "`$PSDefaultParameterValues.Remove('*:ProfileName'); " + "`$PSDefaultParameterValues.Remove('*:Region')" ) & $cleanup } if ($ThrottleLimit -gt 0) { Write-Verbose "Throttling: waiting $ThrottleLimit second(s)" Start-Sleep -Seconds $ThrottleLimit } } Write-Progress -Id 2 -ParentId 1 -Activity "Processing Regions for '$prof'" -Completed } } end { Write-Progress -Id 1 -Activity "Processing AWS Profiles" -Completed } } |