Organizations管理のアカウントリストと組織構造をCLIでCSV出力する

# はじめに
私は株式会社GENEROSITYのSREエンジニアです。
AWSアカウントをOrganizationsで一元管理しており、定期的に一覧を取得する必要があります。
アカウント情報と併せて所属するOU情報(組織構造)も取得したいのですが、残念ながらAWSコンソールでのCSV出力では欲しい情報が得られないため、コマンド一発でCSV出力できるシェルスクリプトを作成しました。

# 出力結果
まずは出力CSVの内容を展開します。
(内容はサンプルです)
`アカウントID`, `アカウント名`, `OU情報`が取得できます。
```csv
000000000001,AccountName1,Root,ParentOU1,ChildOU1-1
000000000002,AccountName2,Root,ParentOU2,ChildOU2-1,grandchildOU2-1
100000000001,AccountName3,Root,ParentOU3,ChildOU3-1
```
「アカウント情報(特にOU情報)の一覧が欲しい」という同じ悩みをお持ちの方は、ぜひ以降も読み進めてみてください。

# こんな人に読んで欲しい
- Organizationsでアカウント管理している
- アカウント名を一覧化したい
- 組織情報を一覧化したい

# コンソールでの出力内容
「AWS Organizations」サービスよりCSVのエクスポートが可能ですが、ここにはOU情報は含まれていないようです。
CSVのヘッダーはこちら
`Account ID`, `ARN`, `Email`, `Name`, `Status`, `Joined method`, `Joined timestamp`

![console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952770/adbc9f85-5b53-5d83-be39-205765cc47cc.png)

# 組織構造
弊社の組織構造はRootを起点にしてこのようになっています。
(内容はサンプルです)
この最下層のOUまで取得することが目的です。
- Root
  - ParentOU1
    - ChildOU1-1
    - ChildOU1-2
    - ChildOU1-3
  - ParentOU2
    - ChildOU2-1
      - grandchildOU2-1-1 ← ここまで取りたい
    - ChildOU2-2
  - ParentOU3
    - ChildOU3-1
    - ChildOU3-2

# 前提条件
以下を準備しておく必要がありますが、設定方法は割愛します。
参考リンクを載せておきますので、ぜひご活用ください。
- [IAMユーザ作成](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html)
- [ローカルPCへのAWS CLI(ver2以上)のインストール](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html)
- [AWS CLIのconfigure profile設定](https://docs.aws.amazon.com/ja_jp/cli/v1/userguide/cli-configure-options.html)

# 事前準備
ではシェルスクリプトを実行する前に以下を準備しましょう。
- 必要な情報へのアクセス権限をまとめたポリシー作成
- IAMユーザーへポリシーのアタッチ

## 必要な情報へのアクセス権限をまとめたポリシー作成
IAMコンソールからポリシーを作成しましょう。
今回はポリシー名を`OrganizationsInfoViewerRole`としました。

![スクリーンショット 2024-12-12 19.38.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952770/16311f50-2018-dfda-c730-e0d9caa2b417.png)

以下内容をJSONで貼り付けてください。

```json:OrganizationsInfoViewerRole
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "organizations:ListParents",
                "organizations:ListRoots",
                "organizations:ListAccounts",
                "organizations:DescribeOrganizationalUnit"
            ],
            "Resource": "*"
        }
    ]
}
```

## IAMユーザーへポリシーのアタッチ
IAMコンソールから先ほど作成したポリシーをIAMユーザーへアタッチしましょう。

![スクリーンショット 2024-12-12 19.36.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952770/3a061f29-1b35-db74-0cac-144eec5723c7.png)

# スクリプト解説
今回作成したスクリプトは2つあり、それぞれの概要を記載します。

## 組織構造を取得する
引数で受け取ったアカウントIDを元に、アカウントが所属するOUをRootまで再起的に取得しています。
```get_organization_structure.sh
#!/bin/bash

# 対象アカウントID
ACCOUNT_ID="$1"

# 現在の子要素
CURRENT_ID=$ACCOUNT_ID

# 結果の格納用
STRUCTURE=""

# 組織構造を再起的に取得
while [ "$CURRENT_ID" != "" ]; do
    # アカウントが所属するOUを取得
    PARENT=$(aws organizations list-parents --child-id "$CURRENT_ID" --query 'Parents[0]' --output json)
    PARENT_ID=$(echo "$PARENT" | jq -r '.Id')
    PARENT_TYPE=$(echo "$PARENT" | jq -r '.Type')

    # 親がルートの場合は終了
    if [ "$PARENT_TYPE" == "ROOT" ]; then
        ROOT_NAME=$(aws organizations list-roots --query 'Roots[0].Name' --output text)
        STRUCTURE="$ROOT_NAME,$STRUCTURE"
        break
    fi

    # 親がOUの場合、名前を取得
    if [ "$PARENT_TYPE" == "ORGANIZATIONAL_UNIT" ]; then
        OU_NAME=$(aws organizations describe-organizational-unit --organizational-unit-id "$PARENT_ID" --query 'OrganizationalUnit.Name' --output text)
        STRUCTURE="$OU_NAME,$STRUCTURE"
    fi

    # 次の親を設定
    CURRENT_ID=$PARENT_ID
done

# 結果を表示
echo "$STRUCTURE"
```

本シェルスクリプト内では３つのCLIコマンドを呼び出しています。
- [list-parents](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/organizations/list-parents.html)：アカウントIDが所属するOU情報(ID,タイプ)を取得
- [list-roots](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/organizations/list-roots.html)：組織内で定義されているルート情報を取得
- [describe-organizational-unit](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/organizations/describe-organizational-unit.html)：OUのIDからOU名を取得

## アカウント名の取得とCSVの作成
アカウントIDの一覧を取得し、各アカウントIDに紐づくOU情報を取得します。
最後にそれぞれ取得した情報をカンマ(,)で連結してCSVへ出力しています。
```list_aws_account_to_csv.sh
#!/bin/bash

# PROFILE
PROFILE="$1"

# 最大同時実行プロセス数
MAX_JOBS=5
PIDS=() # 実行中のプロセスIDを保持する配列

# 引数でprofileの指定がなかった場合はエラーメッセージを出力して終了
if [ -z "$PROFILE" ]; then
  echo "Error: The AWS profile is required. Please specify the profile as an argument and try again."
  exit 1
fi

# アカウント情報一覧取得
ACCOUNTS=$(aws organizations list-accounts --output json | jq -c '.Accounts[]')

# 配列サイズを取得
ACCOUNTS_SIZE=$(echo "$ACCOUNTS" | jq -s 'length')
echo "The number of accounts: $ACCOUNTS_SIZE"

# ループカウント
COUNT=0
# 取得したJSON配列をループ処理
echo "$ACCOUNTS" | while read -r account; do
  # IDとNameを抽出して変数に格納
  ID=$(echo "$account" | jq -r '.Id')
  NAME=$(echo "$account" | jq -r '.Name')

  # アカウントが所属するOUを取得してバックグラウンドで実行
  {
    OUTPUT="${ID},${NAME},$(./get_organization_structure.sh "$ID")"
    if [ ! $? -eq 0 ]; then
      echo "An error occurred."
      exit 1
    fi

    # CSVに追記
    echo "$OUTPUT" >> aws_accounts.csv
  } & # バックグラウンド実行
  PIDS+=($!) # PIDを配列に保存

  COUNT=$((COUNT + 1))
  echo "$COUNT/$ACCOUNTS_SIZE have been done."

  # 並列プロセスの上限に達したら完了を待つ
  while [ "${#PIDS[@]}" -ge "$MAX_JOBS" ]; do
    for i in "${!PIDS[@]}"; do
      if ! kill -0 "${PIDS[$i]}" 2>/dev/null; then
        unset PIDS[$i] # 終了したプロセスを配列から削除
      fi
    done
    PIDS=("${PIDS[@]}") # 配列を詰め直す
    sleep 1 # 少し待つ
  done
done

# 残りのプロセスが終了するまで待つ
wait

echo "All accounts processed. Output written to aws_accounts.csv."
```

本シェルスクリプト内では１つのCLIコマンドを呼び出しています。
- [list-accounts](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/organizations/list-accounts.html?highlight=list%20accounts)：Organizationsで管理しているアカウント一覧情報(ID,ARN,名前 など)を取得

# 実行方法
さいごにシェルスクリプトの実行方法です。
profile名を引数へ設定してください。

```bash
bash list_aws_account_to_csv.sh {profile名}
```

弊社環境では100以上のアカウントを管理しており、出力には約15分かかりました。
みなさんの環境によって前後するとは思いますが、参考にしてください。
並列処理をすることで実行時間短縮も図ることができそうなので、今後取り組みたいと思います。

# さいごに
今回は「アカウント名と紐づくOU情報を一覧化したい」という悩みを解決すべく、CLIを使ったシェルスクリプトを作成しました。
他のコマンドを組み合わせることで、さらに必要な情報を出力することも可能ですのでお好きにカスタマイズしてご活用ください。

ここまで読んでいただきありがとうございました！