0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

AWS Organizations 内のアカウントリストを組織構造と一緒に csv 出力する

Last updated at Posted at 2022-01-30

はじめに

AWS Organizations でマルチアカウント管理していると、アカウントの一覧を定期的に取得したいときはありませんか?

アカウント情報だけではなく、所属する OU の情報も一緒に出力する方法を紹介します。

実はコンソールから csv をダウンロードできる

コンソール上でも通知が出ているように、最近組織内のアカウントリストを csv 出力することができるようになりました。アクションから「アカウントリストをエクスポート」でダウンロード可能です。

image.png

これはこれでとても便利なのですが、2022年1月時点では以下の問題があります。

コンソールからしかダウンロードできない

AWS CLI や SDK 経由でダウンロードができないため、定期的に取得したいといったケースでは自動化が困難です。以下のドキュメントに記載があります。

The only way to export the .csv file with account details is by using the AWS Management Console. You can't export the account list .csv file using the AWS CLI.

取得できる情報が限られている

csv には以下の情報が含まれますが、OU など組織構造の情報は取得できません。

  • Account ID
  • ARN
  • Email
  • Name
  • Status
  • Joined method
  • Joined timestamp

そのため AWS Lambda で Organizations の API を叩いて、OU 情報を含むアカウントリストを出力できるようにしました。

EventBrdige & Lambda でアカウントリストを自動更新

そのアカウントが所属する OU ID と、親 OU を含む OU 名を出力します。例えば以下のような出力になります。

accounts.csv
Id,Name,Email,Status,Joined Method,Joined Timestamp,OU Id,1st Level OU,2nd Level OU,3rd Level OU,4th Level OU,5th Level OU
000000000000,account-mgmt,account+mgmt@example.com,ACTIVE,INVITED,2022-01-31 07:19:57,r-xxxx
111111111111,account-0001,account+0001@example.com,ACTIVE,INVITED,2022-01-31 07:25:38,ou-xxxx-yyyyyyyy,Suspended
222222222222,account-0002,account+0002@example.com,ACTIVE,CREATED,2022-01-31 07:31:28,ou-xxxx-zzzzzzzz,Sample System,Additional,Workloads,Prod
333333333333,account-0003,account+0003@example.com,ACTIVE,CREATED,2022-01-31 08:15:49,ou-xxxx-zzzzzzzz,Sample System,Additional,Workloads,SDLC
444444444444,account-0004,account+0004@example.com,ACTIVE,CREATED,2022-01-31 09:18:50,ou-xxxx-zzzzzzzz,Sample System,Foundational,Security,Prod
555555555555,account-0005,account+0005@example.com,ACTIVE,CREATED,2022-01-31 10:21:30,ou-xxxx-zzzzzzzz,Sample System,Foundational,Infrastructure,Prod
666666666666,account-0006,account+0006@example.com,ACTIVE,CREATED,2022-01-31 11:21:05,ou-xxxx-zzzzzzzz,Sample System,Foundational,Infrastructure,SDLC

EventBrdige には以下のルールを登録しておき、アカウント作成時に Lambda 関数が起動するようにします。

{
  "detail-type": ["AWS API Call via CloudTrail"],
  "source": ["aws.organizations"],
  "detail": {
    "eventSource": ["organizations.amazonaws.com"],
    "eventName": ["CreateAccount"]
  }
}

Lambda 関数のコードは以下です。

  • ListChildren API を再帰処理し、組織内の OU リストを作成
  • リスト内の各 OU に対し以下を実行
    • ListParents API を再起処理し、自 OU と上位 OU の名前を取得
    • ListAccountsForParent API で OU に所属するアカウントの情報を取得し、レコード追加
  • csv ファイルを書き出し、環境変数 BUCKET で指定した S3 バケットにアップロード

というのがざっくりな処理の流れです。もう少し簡潔に書けそうな気がするのですが、かなり長くなってしまいました。

lambda_function.py
"""
AWS Organizations list accounts

Environment variables:
    BUCKET: S3 Bucket Name
"""
from logging import getLogger, INFO
import csv
import operator
import os
from botocore.exceptions import ClientError
import boto3

logger = getLogger()
logger.setLevel(INFO)

def upload_s3(output, key, bucket):
    """Upload csv to S3"""
    try:
        s3_resource = boto3.resource('s3')
        s3_bucket = s3_resource.Bucket(bucket)
        s3_bucket.upload_file(output, key, ExtraArgs={'ACL': 'bucket-owner-full-control'})
    except ClientError as err:
        logger.error(err.response['Error']['Message'])
        raise

def get_ou_ids(org, parent_id):
    """Recursively process list_children to create a list of OUs."""
    ou_ids = []
    try:
        paginator = org.get_paginator('list_children')
        iterator = paginator.paginate(
            ParentId=parent_id,
            ChildType='ORGANIZATIONAL_UNIT'
        )
        for page in iterator:
            for ou in page['Children']:
                ou_ids.append(ou['Id'])
                ou_ids.extend(get_ou_ids(org, ou['Id']))
    except ClientError as err:
        logger.error(err.response['Error']['Message'])
        raise
    else:
        return ou_ids

def get_ou_name(org, ou_id):
    """Return OU name"""
    try:
        ou_info = org.describe_organizational_unit(OrganizationalUnitId=ou_id)
    except ClientError as err:
        logger.error(err.response['Error']['Message'])
        raise
    else:
        return ou_info['OrganizationalUnit']['Name']

def get_ou_structure(org, child_id):
    """Recursively process list_parents and return the OU structure."""
    ou_structure = []
    try:
        parent_ou = org.list_parents(ChildId=child_id)['Parents'][0]
        if parent_ou['Id'][:2] == 'ou':
            ou_structure.append(get_ou_name(org, parent_ou['Id']))
            ou_structure.extend(get_ou_structure(org, parent_ou['Id']))
    except ClientError as err:
        logger.error(err.response['Error']['Message'])
        raise
    else:
        return ou_structure

def list_accounts():
    """Create accounts list"""
    org = boto3.client('organizations')
    accounts = []
    ou_structure = []

    try:
        root_id = org.list_roots()['Roots'][0]['Id']
        ou_id_list = [root_id]
        ou_id_list.extend(get_ou_ids(org, root_id))

        for ou_id in ou_id_list:
            if ou_id[:2] == 'ou':
                ou_structure = get_ou_structure(org, ou_id)
                ou_structure.reverse()
                ou_structure.append(get_ou_name(org, ou_id))
            paginator = org.get_paginator('list_accounts_for_parent')
            page_iterator = paginator.paginate(ParentId=ou_id)
            for page in page_iterator:
                for account in page['Accounts']:
                    item = [
                        account['Id'],
                        account['Name'],
                        account['Email'],
                        account['Status'],
                        account['JoinedMethod'],
                        account['JoinedTimestamp'].strftime('%Y-%m-%d %H:%M:%S'),
                        ou_id,
                    ]
                    accounts.append(item + ou_structure)
    except ClientError as err:
        logger.error(err.response['Error']['Message'])
        raise
    else:
        return sorted(accounts, key=operator.itemgetter(5))

def lambda_handler(event, context):
    """Lambda function to output the list of accounts in AWS organization in CSV format."""
    key = 'accounts.csv'
    output_file = '/tmp/accounts.csv'
    bucket = os.environ['BUCKET']
    header = [[
        'Account Id',
        'Account Name',
        'Account Email',
        'Account Status',
        'Joined Method',
        'Joined Timestamp',
        'OU Id',
        '1st Level OU',
        '2nd Level OU',
        '3rd Level OU',
        '4th Level OU',
        '5th Level OU'
    ]]
    account_list = list_accounts()

    with open(output_file, 'w', newline='', encoding='utf-8') as output:
        writer = csv.writer(output)
        writer.writerows(header)
        writer.writerows(account_list)

    upload_s3(output_file, key, bucket)

IAM ロールには以下のようなポリシーをアタッチします。

iam_policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::<your_bucket_name>/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "organizations:DescribeOrganizationalUnit",
                "organizations:ListAccountsForParent",
                "organizations:ListChildren",
                "organizations:ListParents",
                "organizations:ListRoots"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

CDK でデプロイする

上記を AWS Construct Library 化して npm/pypi に公開しました。詳細は Construct Hub でご確認ください。

例えば TypeScript の場合、以下の手順でデプロイできます。

インストール

$ yarn add cdk-organizations-list-accounts

使い方

import * as cdk from 'aws-cdk-lib';
import { OrganizationsListAccounts } from 'cdk-organizations-list-accounts';

const App = new cdk.App();
const stack = new cdk.Stack(App, 'Stack', { env: { region: 'us-east-1' } });
new OrganizationsListAccounts(stack, 'Organizations-List-Accounts');

Deploy!

$ cdk deploy

以上です。
参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?