はじめに
私は株式会社GENEROSITYのSREです。
日々の業務では複数アカウントに対する同時操作やOU(Organizational Unit)に応じた操作が必要になることが多々あります。
AWS OrganizationsにはCSVエクスポート機能がありますが、毎回必要な情報を欲しい形に加工するのが手間でした。
そこで定期的にOU情報を取得し、必要な情報のみをJSON形式で出力するシステムを構築しましたので共有します。
前提条件
弊社の環境は以下のような状況です。
- 常時100を超えるAWSアカウントを運用している
- アカウントは月に数個から十数個のペースで作成される
- すべてのアカウントはOrganizations管理されており、いずれかのOUに属している
- 情報取得用アカウントと取得対象アカウントは同一で、Organizationsの情報取得権限が委譲されている
システム概要
構築したシステムの概要は以下の通りです。
- EventBridge:定期的(毎週月曜日00:00)にターゲットのLambdaを実行
- Lambda:OU情報取得および作成したJSONをS3へアップロード処理を実行
システム構築手順
以下の手順に従ってシステムを作成いたします。今回はすべて同一アカウント内にシステムを構築しています。
1. IAMロールの準備
システムに必要な2つのIAMロールを事前に準備します。
EventBridge実行ロール
Lambda実行権限を持つポリシーをアタッチしたロールを作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
"arn:aws:lambda:ap-northeast-1:{アカウントID}:function:{任意のLambda関数名}:*",
"arn:aws:lambda:ap-northeast-1:{アカウントID}:function:{任意のLambda関数名}"
]
}
]
}
Lambda実行ロール
Organizations情報取得、S3オブジェクトアップロード、ログ出力権限を持つポリシーをアタッチしたロールを作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:ap-northeast-1:{AWSアカウントID}:*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:ap-northeast-1:{AWSアカウントID}:log-group:/aws/lambda/{任意のLambda関数名}:*"
]
},
{
"Effect": "Allow",
"Action": [
"organizations:ListRoots",
"organizations:ListOrganizationalUnitsForParent",
"organizations:ListChildren",
"organizations:ListAccounts",
"organizations:DescribeAccount"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::{S3バケット名}/*"
}
]
}
2. Lambdaの作成
処理の流れは以下のようにシンプルな構成になっています。
- OUの必要情報を取得
- 親OUから最下層の子OUまで情報を再帰的に取得
- 必要情報を加工してJSONオブジェクトを生成
- JSONオブジェクトをS3へアップロード
import { OrganizationsClient, ListRootsCommand, ListOrganizationalUnitsForParentCommand, ListChildrenCommand, DescribeAccountCommand } from "@aws-sdk/client-organizations";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { format, addHours } from "date-fns";
const client = new OrganizationsClient({ region: "ap-northeast-1" });
const s3Client = new S3Client({ region: "ap-northeast-1" });
// S3バケット名(環境に応じて変更してください)
const BUCKET_NAME = '{任意のS3バケット名}';
// 再帰的にOUとアカウントを取得する関数
const getOUHierarchy = async (parentId, depth = 0) => {
let result = [];
// OU一覧を取得
const { OrganizationalUnits } = await client.send(new ListOrganizationalUnitsForParentCommand({ ParentId: parentId }));
for (const ou of OrganizationalUnits) {
console.log(`${" ".repeat(depth)}OU: ${ou.Name} (${ou.Id})`);
// OU内のアカウントをページネーションで取得
let accounts = [];
let nextToken;
do {
const { Children, NextToken } = await client.send(new ListChildrenCommand({
ParentId: ou.Id,
ChildType: "ACCOUNT",
NextToken: nextToken
}));
// 子アカウント情報を取得
for (const child of Children) {
const accountInfo = await client.send(new DescribeAccountCommand({ AccountId: child.Id }));
console.log(`${" ".repeat(depth + 1)}- Account: ${accountInfo.Account.Name} (ID: ${accountInfo.Account.Id})`);
accounts.push({
Name: accountInfo.Account.Name,
Id: accountInfo.Account.Id
});
}
nextToken = NextToken;
} while (nextToken);
// 再帰的に子OUを取得
const subOUs = await getOUHierarchy(ou.Id, depth + 1);
result.push({
OUName: ou.Name,
OUId: ou.Id,
Accounts: accounts,
SubOUs: subOUs
});
}
return result;
};
// Lambdaハンドラー
export const handler = async () => {
try {
// Root IDを取得
const { Roots } = await client.send(new ListRootsCommand({}));
const rootId = Roots[0].Id;
console.log(`Root ID: ${rootId}`);
// Root OU配下の階層構造を取得
const hierarchy = await getOUHierarchy(rootId);
console.log("Final Structure:", JSON.stringify(hierarchy, null, 2));
// 現在の日時を取得してJSTに変換
const jstDate = addHours(new Date(), 9);
const fileName = `${format(jstDate, 'yyyyMMddHH')}_organizations_info.json`;
// S3にアップロードするオブジェクトを作成
const putObjectParams = {
Bucket: BUCKET_NAME,
Key: fileName,
Body: JSON.stringify(hierarchy, null, 2),
ContentType: 'application/json'
};
// S3にアップロード
const uploadResult = await s3Client.send(new PutObjectCommand(putObjectParams));
console.log(`File uploaded successfully to S3: ${uploadResult.ETag}`);
return {
statusCode: 200,
body: JSON.stringify({
message: 'Organizations info successfully exported',
fileName: fileName
})
};
} catch (error) {
console.error("Error fetching Organizations data:", error);
throw error;
}
};
作成したLambda関数には、先ほど準備したLambda実行ロールをアタッチしてください。
3. EventBridge Schedulerの作成
定期実行のためのEventBridge Schedulerを設定します。
スケジュール設定
毎週月曜日00:00(UTC)に実行するよう、以下のCron式を設定します。
0 0 ? * MON *
Cron式の構成:
- 分:0
- 時間:0
- 日:?(任意)
- 月:*(毎月)
- 曜日:MON(月曜日)
- 年:*(毎年)
ターゲット設定
- ターゲットに先ほど作成したLambda関数を指定
- 先ほど作成したEventBridge実行ロールをアタッチ
これでシステムの構築は完了です!
出力結果のサンプル
システムが実行されると、以下のようなJSON形式のファイルがS3にアップロードされます。
[
{
"OUName": "ExceptionsOU",
"OUId": "ou-0000-01234567",
"Accounts": [
{
"Name": "Account1",
"Id": "000011112222"
},
{
"Name": "Account2",
"Id": "111122223333"
}
],
"SubOUs": []
},
{
"OUName": "Workloads",
"OUId": "ou-0000-12345678",
"Accounts": [],
"SubOUs": [
{
"OUName": "Development",
"OUId": "ou-0000-23456789",
"Accounts": [
{
"Name": "Account3",
"Id": "222233334444"
}
],
"SubOUs": []
}
]
}
]
運用上のメリット
このシステムを導入することで、以下のようなメリットを感じています。
- 作業時間の削減:毎回手動でOU情報を取得・加工する必要がなくなった
- 情報の鮮度向上:定期的に最新情報が自動取得されるため、古い情報での作業ミスが減った
- 即座の情報取得:必要に応じてLambdaを直接実行することで、リアルタイムでの情報取得も可能
おわりに
定期出力されることで、毎回OU情報を手動取得する手間が省け、業務効率が大幅に改善されました。
現時点の最新情報を取得したい場合には、Lambdaを直接実行することで即座に情報を取得することも可能です。
これからも日々のちょっとした繰り返し業務を自動化し、トイル削減に努めていきます。
同じような課題をお持ちの方々の参考になれば幸いです。
最後までお読みいただき、ありがとうございました!