AWS Organizations example: list OUs

It’s rare that I come away from an in-depth experience with an AWS service thinking it’s inelegant, but AWS Organizations is that infrequent exception. It leaves me cold.

Not because AWS Organizations is a bad idea. For enterprise users of AWS, it’s a an obvious requirement. Still, I have issues with AWS Organizations. Two of them in fact.

First, it’s an attempt to pretty the pig. In this case, the pig is AWS’s original sin of relying on Amazon accounts. That means AWS accounts are inextricably tied to a one-to-one embrace of email addresses (unlike Azure’s more elegant subscription in which an email address can be associated with multiple subscriptions). For example, my root AWS Organizations account is an Amazon retail account from back in the horse and buggy days — and to this day, AWS cannot break the link between the two. I’ve asked. Worse, if I want a new AWS Organizations account in my organization (or any AWS account for that matter), I need a new email address. The one-on-one relationship between AWS accounts and email addresses persists even after the account is closed: when you close the account you agree you can never create a new AWS account with the now-burned email address. Talk about deadly embraces.

My second issue is that AWS Organizations’ console UI stinks.

Fortunately, using PowerShell in combination with the AWS PowerShell cmdlets can at least partly remedy the fugly AWS Organizations console design. Check out this animation of the AWS Organizations console showing the organization’s tree view of organizational units. Notice that the tree pane cannot be resized and even expanding the browser window horizontally does not display the AWS Organizations OU tree completely. You can never get a really comprehensive view of your organizational unit structure.

AWS Organizations console (click to enlarge)

My solution to UX design flaw was to write a PowerShell script that traverses the organization’s OUs and produces a convenient tabular output either to the console or to a .csv file in your home directory.

Here a sample of what the output looks like on the console.

AWS Organizations OU table
AWS Organizations OU table (click to enlarge)

And here’s what the .csv output looks like.

AWS Organizations OU table csv output
AWS Organizations OU table csv output (click to enlarge)

As always, the script is written in PowerShell Core, in this case with version 6.2.2 and the latest AWSPowerShell.NetCore cmdlets — which at the time of this post were at version 3.3.542.0.

There are two ways to invoke the script:

Change to the directory where you have stored the script and enter:

./Function-DisplayAWSOrganizationOUsAsTable.ps1 -AWSStoredProfile where the single parameter is a credential profile previously created by Set-AWSCredential -StoreAs <profilename>. The user or role in the stored profile must have access to the root OU in the AWS Organizations root account.

Or you can add the optional parameter -CSV $true which will produce a nicely-formatted CSV file of all your AWS Organizations OUs. The script stores the listing in the path pointed to by the PowerShell $HOME automatic variable.

I hope this is of use to you. It may also be useful to users of the newly available AWS Control Tower product. It should work with OUs created from that tool as well, though I haven’t tested it yet.

As always, I look forward to hearing from you on how this worked for you.

<#
    .SYNOPSIS
        Displays a formatted table of AWS Organization OUs on the console
    
    .DESCRIPTION
        Starting at the root OU, this function displays a formatted list of OUs in an AWS Organization. The user MUST be a user with admin access to the Organization's root account and this function MUST be run in that root account
    
    .PARAMETER AWSStoredProfile
        A stored profile containing the AWS credentials providing admin access to the AWS Organization's master account
    
    .PARAMETER CSV
        This parameter allows you to send the output of the function in CSV format to a file in your home directory. Specify $true; defaults to $false
    
    .EXAMPLE
        PS C:\> ./AddNewOrgAccounToMFSMaster.ps1 -AWSStoredProfile <AWSStoredProfile> # Displays OUs as table on console
        PS C:\> ./AddNewOrgAccounToMFSMaster.ps1 -AWSStoredProfile <AWSStoredProfile> | Out-File <path> # Writes text to file
        PS C:\> ./AddNewOrgAccounToMFSMaster.ps1 -AWSStoredProfile <AWSStoredProfile> -CSV $true 

    
    .NOTES
        Alex Neihaus 2019-07-15
        (c) 2019 Air11 Technology LLC -- licensed under the Apache OpenSource 2.0 license, https://opensource.org/licenses/Apache-2.0
        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at
        http://www.apache.org/licenses/LICENSE-2.0
        
        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS" BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.
        
        Author's blog: https://www.yobyot.com
#>
[CmdletBinding()]
param
(
    [Parameter(Mandatory = $false,
        Position = 1,
        HelpMessage = 'Supply a stored AWS credential profile authorized in the master account to create new accounts')]
    [Alias('creds')]
    [System.String]$AWSStoredProfile,
    [Parameter(Position = 2,
        HelpMessage = 'Enter TRUE to pipe output to a file in .csv format')]
    [ValidateSet($true, $false, IgnoreCase = $true)]
    [boolean]$CSV = $false
)
<# This function recursively lists all OUs starting from the root and places the ARN, ID and name 
    of the OU into an array to be presented to the user as a formatted table
#>
function Get-OrgChildOUs ($ParentOUId, $ParentName) {
    $childouids = Get-ORGChild -ParentId $ParentOUId -ChildType ORGANIZATIONAL_UNIT
    foreach ($childouid in $childouids.Id) {
        $obj = New-Object -TypeName PSObject -Property @{
            "Arn"    = "";
            "Id"     = "";
            "Name"   = "";
            "Parent" = "";
        }
        $t = Get-ORGOrganizationalUnit -OrganizationalUnitId $childouid
        # If we assign output of Get-ORGOrganizationUnit to a variable, it becomes type Amazon.Organizations.Model.OrganizationalUnit which we cannot modify
        # So, we manually copy the properties into our object, which is then added to the array of all OUs
        $obj.Arn = $t.Arn
        $obj.Id = $t.Id
        $obj.Name = $t.Name
        $obj.Parent = $ParentName
        $Script:AllOUs += $obj
        Start-Sleep -Seconds 2 # For some reason, AWS cmdlet Get-OrgOrganizationalUnit fails if lots of IDs are passed via pipeline, so this foreach loop slows it down
        # Using the current OU id, see if there are any child OUs and recursively call this function.
        do {
            Get-OrgChildOUs -ParentOUId $obj.Id -ParentName $obj.Name
        }
        until ($null -eq $($AWSHistory.LastServiceResponse.Children)) # $AWSHistory contains $null when there are no more child OUs
        
    }
}
function Get-AllOrgOus {
    Begin {
        switch ($PSVersionTable.PSEdition) {
            # Find out which version of pwsh is running and load the proper AWS cmdlet module
            "Core" {
                Import-Module AWSPowerShell.NetCore
            }
            "Desktop" {
                Import-Module AWSPowerShell
            }
            default {
                "There's a big problem; this version of PowerShell is unknown"
                "Returned edition of PowerShell is: $PSVersionTable.PSEdition"
                exit
            }
        }
        $error.clear() # Reset the error variable
        # On macOS using the VSCode debugger as of 2019-07-19, a simple Set-AWSCredential causes an error
        # If so, Initialize-AWSDefaultConfiguration works. See https://github.com/PowerShell/vscode-powershell/issues/2050
        switch (Test-Path Variable:PSDebugContext -IsValid) {
            $true {
             Initialize-AWSDefaultConfiguration -ProfileName $AWSStoredProfile   
            }
            $false {
                Set-AWSCredential -ProfileName $AWSStoredProfile -ErrorAction SilentlyContinue # Set the profile to be used
                if ($null -ne $error[0]) {
                    # Profile was NOT set correctly; Set-AWSCredentals does NOT store errors in $AWSHistory variable, so check $error array varaiable
                    "Your AWS stored profile was incorrect in some way"
                    "Set-AWSCredential returned: $error[0].Exception"
                    exit
                }
            }
        }
    }
    Process {
        # Start by getting the OUs off the root.
        $Script:AllOUs = @()
        $RootOUId = (Get-ORGRoot).Id # Get the four-character root OU ID
        Get-OrgChildOUs -ParentOUId $RootOUId -ParentName "Root" # Get the OU IDs at level one below the root
    }
    End {
        $objarray = @()
        foreach ($id in $Script:AllOUs.id) {
            $obj = New-Object -TypeName PSObject -Property @{
                "OUName" = "$($Script:AllOUs.Name[($Script:AllOUs.id).IndexOf($id)])";
                "OUId"   = "$($Script:AllOUs.Id[($Script:AllOUs.id).IndexOf($id)])";
                "Parent" = "$($Script:AllOUs.Parent[($Script:AllOUs.id).IndexOf($id)])";
                "Arn"    = "$($Script:AllOUs.Arn[($Script:AllOUs.id).IndexOf($id)])";
            }
            $objarray += $obj
        }
        switch ($CSV) {
            $false {
                Write-Host -ForegroundColor Yellow "Displaying all OUs in this AWS Organization"
                $objarray | Sort-Object -Property "OUName" | Format-Table Parent, OUName, OUId, Arn -AutoSize
            }
            $true {
                $csvfile = "$HOME/AWSOUs-$(Get-Date -Format "yyyy-mm-dd-THH-MM-ss").csv"
                Write-Host -ForegroundColor Yellow "Writing a CSV file to $csvfile with all OUs in this AWS Organization"
                $objarray | Sort-Object -Property "OUName" | Export-Csv -Path $csvfile
            }
        }
        
    }
}
Get-AllOrgOus

 


Posted

in

, ,

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *