組織をコードで縛り上げろ:PulumiとControl Towerで実現する、AWSマルチアカウント自動化の深淵
世の中の多くのチームは、AWS Control TowerのコンソールをポチポチとクリックしてOrganizational Unit (OU) を作り、SCP(サービスコントロールポリシー)を祈りながら適用している。
愚か者のやる仕事だ。
真のSRE、真のインフラストラクチャー・アーティストにとって、数千に及ぶAWSアカウントの群れと組織階層(OU)は、「コードによって完全に決定論的に支配されるべき巨大なトポロジー」でなければならない。コンソール職人の手作業などという不確実性の温床は、セキュリティインシデントとコンプライアンス違反の温床でしかないのだ。
今回は、Pulumiを使い、AWS Organizations、Control Tower、そしてライフサイクルイベントを完全に結合させ、人間が一切介在しない「真のマルチアカウント自動化基盤」を構築する極限の知見を授けよう。
—
1. 組織階層(OU)とSCPの宣言的コード管理
TerraformでOrganizationsを管理したことがある者なら、状態ファイルのロック競合や、循環依存関係に起因するapply地獄に泣いた経験があるはずだ。
PulumiのTypeScript/Python SDKを用いることで、オブジェクト指向の抽象化と、厳密な型安全性を持った「真に再利用可能な組織トポロジー」をコード化できる。
組織設計のベストプラクティス:コードによる階層表現
以下のコードは、単なるリソースの羅列ではない。企業のガバナンス境界をコードとして表現したものである。
import as pulumi from “@pulumi/pulumi”;
import as aws from “@pulumi/aws”;
// 1. ルート組織情報の取得
const currentOrg = aws.organizations.getOrganization({}).then(org => org);
// 2. 統制されたOU構造の定義
interface OrganizationalUnitDefinition {
name: string;
scps: pulumi.Output
childOus?: OrganizationalUnitDefinition[];
}
const orgTopology: OrganizationalUnitDefinition[] = [
{
name: “Security”,
scps: [/ ログ改ざん防止SCPのARN /],
childOus: [
{ name: “LogArchive”, scps: [] },
{ name: “Auditing”, scps: [] }
]
},
{
name: “Workloads”,
scps: [/ 許可されたリージョン以外の制限SCP /],
childOus: [
{ name: “Production”, scps: [] },
{ name: “Staging”, scps: [] }
]
}
];
// 再帰的にOUとSCPアタッチメントを生成する関数(冪等性を完全に担保)
function provisionOuRecursive(
parentId: pulumi.Output
def: OrganizationalUnitDefinition,
rootId: string
) {
const ou = new aws.organizations.OrganizationalUnit(`ou-${def.name}`, {
parentId: parentId,
name: def.name,
});
// SCPの関連付け
def.scps.forEach((scpArn, index) => {
new aws.organizations.PolicyAttachment(`scp-attach-${def.name}-${index}`, {
policyId: scpArn,
targetId: ou.id,
});
});
// 子OUの再帰的プロビジョニング
if (def.childOus) {
def.childOus.forEach(child => {
provisionOuRecursive(ou.id, child, rootId);
});
}
}
このアプローチの美しさは、「インフラストラクチャの状態がコードの抽象木(AST)と1:1で同期する」点にある。構成の変更はGitOpsを通じてのみ行われ、手動変更は次のPulumi Preview/Up実行時に容赦なく検知され、元の正しい状態に収束(Reconcile)される。
—
2. AWS Organizationsプロバイダを活用した新規アカウント自動発行パイプライン
Control TowerのAccount Factoryは便利だが、組織独自の認可フローや、IPAM(IP Address Management)との連携、独自のタグ付け強制などを挟むには拡張性が乏しい。
ここで、`aws.organizations.Account` リソースを用いた「完全自製アカウント発行パイプライン」の出番だ。
競合とスロットリングを回避する非同期設計
AWS Organizations APIは、アカウント作成の同時実行数に厳しいスロットリング(Rate Limit)を課している。これを回避し、かつ冪等なアカウント発行を実現する設計が以下だ。
export class ManagedAccount extends pulumi.ComponentResource {
public readonly accountId: pulumi.Output
constructor(name: string, args: {
email: string;
accountName: string;
parentId: pulumi.Output
roleName?: string;
}, opts?: pulumi.ComponentResourceOptions) {
super(“custom:aws:ManagedAccount”, name, {}, opts);
// アカウントの作成(Control Towerの管理下に配置するためのタグも付与)
const account = new aws.organizations.Account(`${name}-account`, {
email: args.email,
name: args.accountName,
parentId: args.parentId,
roleName: args.roleName || “OrganizationAccountAccessRole”,
// コスト配分のためのタグを最初から強制注入
tags: {
“ManagedBy”: “Pulumi”,
“Environment”: name,
},
}, { parent: this });
this.accountId = account.id;
this.registerOutputs({ accountId: this.accountId });
}
}
このカスタムコンポーネントを組織定義のループ内で呼び出すことで、「組織図の宣言」と「物理アカウントの生成」が単一のトランザクション(Pulumiのグラフ評価)として完結する。
—
3. Control Towerライフサイクルイベントと連携したPulumi自動デプロイ設計
ここからが本題だ。アカウントが新規作成され、Control Towerによるプロビジョニング(Baselineの適用)が完了した瞬間、そのアカウントに対して基盤共通リソース(CloudWatch Agent、Security Hub、GuardDuty、VPCフローログ、IAM Identity Centerの割り当てなど)を瞬時にデプロイしなければならない。
これを「手動」や「重たい中央集権スクリプト」でやってはならない。イベント駆動型・分散型のPulumiエグゼキューターを構築する。
アーキテクチャの全貌
1. Control Tower / AWS Organizations: 新規アカウント作成完了(`CreateManagedAccount` 成功等)
2. Amazon EventBridge: ライフサイクルイベント (`AWS Service Event via CloudTrail`) を検知
3. AWS Lambda / Step Functions: イベントペイロードから `account_id` と `account_name` を抽出
4. Pulumi Automation API (Lambda内実行): ターゲットアカウントに対してクロスアカウントAssumeRoleし、動的にPulumiスタックを実行
Automation APIを用いたセルフホスト・プロビジョニングLambda
Lambdaの内部でPulumiのCore Engineを動かす。これにより、外部のCI/CDパイプラインサーバーを待つことなく、イベント発生から数秒以内にアカウントの初期化が完了する。
import as pulumi from “@pulumi/pulumi”;
import as auto from “@pulumi/pulumi/automation”;
import as aws from “@aws-sdk/client-sts”;
export async function handler(event: any) {
const detail = event.detail;
const accountId = detail.serviceEventDetails.managedAccountAddedDetails.accountId;
const accountEmail = detail.serviceEventDetails.managedAccountAddedDetails.accountEmail;
const stackName = `account-baseline-${accountId}`;
// Automation APIを使ったインメモリでのスタック実行
const workDir = “./child-stack”; // 子アカウントに適用するリソース定義が含まれるディレクトリ
try {
const s = await auto.LocalWorkspace.createOrSelectStack({
stackName: stackName,
workDir: workDir,
});
await s.setAllConfig({
“aws:region”: { value: “ap-northeast-1” },
“targetAccountId”: { value: accountId },
“targetAccountEmail”: { value: accountEmail },
});
// ターゲットアカウントへのクロスアカウントAssumeRoleプロバイダを設定してUpを実行
console.log(`Starting Pulumi Up for account ${accountId}…`);
const upResult = await s.up({ onOutput: console.log });
return {
statusCode: 200,
body: JSON.stringify(`Successfully provisioned account ${accountId}. Summary: ${upResult.summary.result}`),
};
} catch (e) {
console.error(`Failed to provision account ${accountId}:`, e);
throw e;
}
}
—
4. 低レイヤ&エキスパート知見:メモリ消費、並行性、ステート分離の極意
このアーキテクチャをプロダクションスケール(数千アカウント規模)で運用するにあたり、初心者が必ず踏み抜く「地雷」が存在する。伝説的エンジニアとしての知見をここに残す。
① Lambda上でのPulumi Automation APIのメモリ枯渇対策
LambdaでPulumiエンジンを動かす際、デフォルトのメモリ割り当て(128MB〜512MB)では確実にOOM (Out of Memory) で死ぬ。PulumiのNode.jsランタイムとGo製エンジンのバイナリ、そしてAWS SDKのオーバーヘッドを考慮し、最低でも1024MB、出来れば2048MBを割り当てろ。
また、`/tmp` ディレクトリの容量(デフォルト500MB〜10GB)を圧迫しないよう、プラグイン(`aws` プロバイダ等)はビルド時にLambda Layerとして焼き込むか、極力キャッシュ機構を働かせろ。
② ステートファイル(Backend)のパーティショニング
数千アカウントの基盤を一つのPulumiスタックで管理しようとする愚行は避けたことだ。グラフが巨大化しすぎると、計画(Preview)フェーズだけで数分を要し、メモリを食い潰す。
OU単位、あるいはアカウント単位でスタックを完全に分離(Sharding)しろ。
s3://my-pulumi-state-bucket/
├── org-root/
│ └── pulumi.yaml
├── ou-security/
│ └── pulumi.yaml
└── ou-workloads/
└── pulumi.yaml
ルート組織のスタックはOU構造とアカウントの“メタデータ”のみを管理し、個別アカウント内のリソース群は、イベント駆動で動く子スタックに完全に委譲する。この疎結合な設計こそが、大規模IaCの生存戦略である。
③ 最終防衛ライン:プロバイダのエイリアスとマルチリージョン耐性
Organizations APIはグローバルエンドポイント(`us-east-1`)を要求する一方で、個別のSCPやリソースはリージョンごとに挙動が異なる。
PulumiでAWSプロバイダを明示的に複数定義し、ガバナンス統制を行うスタックでは必ず `region` を明示的にバインドせよ。
const usEast1Provider = new aws.Provider(“us-east-1”, {
region: “us-east-1”,
});
// Organizationsは常にus-east-1で操作することを強制
const orgRoot = new aws.organizations.Organization(“org”, {
featureSet: “ALL”,
}, { provider: usEast1Provider });
—
結び:インフラストラクチャを「コードの神殿」に昇華させろ
AWS OrganizationsとControl TowerをPulumiで完全にコード化し、ライフサイクルイベントとAutomation APIで結合させた瞬間から、あなたのインフラは「人間の手による気まぐれ」から解放される。
コードは嘘をつかない。ドキュメント化されていない手動変更は、明日には消え去る。
さあ、エディターを開き、組織のすべてをコードの支配下に置くのだ。健闘を祈る。