AWS CDKで作るおひとりさまランディングゾーン
はじめに
会社で AWS を利用する際、AWS アカウントは AWS Control Tower を導入して管理することが一般的です。
これを個人環境にも導入したいです。
個人環境でもマルチアカウントの検証やアカウント作成自体も簡単にしたいのです。
更に最近 AWS CDK も気になっています。
二つとも一緒にやってしまおうと考え、AWS CDK を利用して AWS Control Tower のランディングゾーンを作成していきます。
AWS Control Tower とは? や AWS CDK とは?という方は、下記の AWS Black Belt Online Seminar で詳しく解説されているので、そちらをご覧ください。
-
AWS Control Tower
-
AWS CDK
方針
下記の AWS ドキュメントを元に AWS CloudFormation の部分を AWS CDK に置き換える形で作成していきます。
事前準備
AWS のベストプラクティスでは、管理アカウントは管理アカウントでしかできないタスクのみに使用することが推奨されています。
既存の AWS アカウントを使っても良いですが、管理アカウントをよりクリーンにしておくために新たに AWS アカウントを開設しておきます。
新規アカウント作成およびアカウントの初期設定については、下記のクラスメソッドさんの記事を参考に実施しました。
実装
つくるもの
AWS CDK アプリケーションとして下記のリソースを作成していきます。
- CtLandingZoneStack
- 各コンストラクトを束ねて、ランディングゾーン を作成するスタック
- Iam
- 下記の前提条件の IAM 部分を切り出したコンストラクト
- Organizations
- 下記の前提条件の Organizations 部分を切り出したコンストラクト
- LandingZone
- 下記のランディングゾーンに対応するコンストラクト
環境設定
下記の環境で実施しました。
- Node.js v22.11.0
- AWS CDK v2.167.2
AWS CDK アプリケーションの作成
プロジェクトの初期化
AWS CDK 用のプロジェクトを作成します。
mkdir ct-landing-zone
cd ct-landing-zone && cdk init app --language=typescript
ログアカウントや監査アカウントのメールアドレスを環境変数で扱いたいので、そのためのパッケージを追加していきます。
npm i -D dotenv
cdk.json
を編集して追加したパッケージを利用するようにします
{
- "app": "npx ts-node --prefer-ts-exts bin/ct-landing-zone.ts",
+ "app": "npx ts-node -r dotenv/config --prefer-ts-exts bin/ct-landing-zone.ts",
.
.
}
コンストラクトの作成
下記の 3 つのコンストラクトを作成していきます。
- Iam
- Organizations
- LandingZone
ランディングゾーンを起動の前提条件となる IAM 関連のリソースから作成していきます。
import { aws_iam as iam, Stack } from "aws-cdk-lib";
import { Construct } from "constructs";
export class Iam extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);
// AWSControlTowerAdmin
const ctAdminPolicyJSON = {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: "ec2:DescribeAvailabilityZones",
Resource: "*",
},
],
};
const ctAdminManagedPolicy = new iam.ManagedPolicy(this, "CtAdminPolicy", {
managedPolicyName: "AWSControlTowerAdminPolicy",
document: iam.PolicyDocument.fromJson(ctAdminPolicyJSON),
});
new iam.Role(this, "CtAdminRole", {
roleName: "AWSControlTowerAdmin",
assumedBy: new iam.ServicePrincipal("controltower.amazonaws.com"),
path: "/service-role/",
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSControlTowerServiceRolePolicy"
),
ctAdminManagedPolicy,
],
});
// AWSControlTowerCloudTrail
const ctCloudTrailPolicyJSON = {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: ["logs:CreateLogStream", "logs:PutLogEvents"],
Resource: `arn:${
Stack.of(this).partition
}:logs:*:*:log-group:aws-controltower/CloudTrailLogs:*`,
},
],
};
const ctCloudTrailManagedPolicy = new iam.ManagedPolicy(
this,
"CtCloudTrailPolicy",
{
managedPolicyName: "AWSControlTowerCloudTrailPolicy",
document: iam.PolicyDocument.fromJson(ctCloudTrailPolicyJSON),
}
);
new iam.Role(this, "CtCloudTrailRole", {
roleName: "AWSControlTowerCloudTrailRole",
assumedBy: new iam.ServicePrincipal("cloudtrail.amazonaws.com"),
path: "/service-role/",
managedPolicies: [ctCloudTrailManagedPolicy],
});
// AWSControlTowerConfigAggregatorRoleForOrganizations
new iam.Role(this, "CtConfigAggregatorRoleForOrg", {
roleName: "AWSControlTowerConfigAggregatorRoleForOrganizations",
assumedBy: new iam.ServicePrincipal("config.amazonaws.com"),
path: "/service-role/",
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSConfigRoleForOrganizations"
),
],
});
// AWSControlTowerStackSet
const ctStackSetJSON = {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: "sts:AssumeRole",
Resource: `arn:${
Stack.of(this).partition
}:iam::*:role/AWSControlTowerExecution`,
},
],
};
const ctStackSetManagedPolicy = new iam.ManagedPolicy(
this,
"CtStackSetPolicy",
{
managedPolicyName: "AWSControlTowerStackSetRolePolicy",
document: iam.PolicyDocument.fromJson(ctStackSetJSON),
}
);
new iam.Role(this, "CtStackSetRole", {
roleName: "AWSControlTowerStackSetRole",
assumedBy: new iam.ServicePrincipal("cloudformation.amazonaws.com"),
path: "/service-role/",
managedPolicies: [ctStackSetManagedPolicy],
});
}
}
次に Organizations のコンストラクトを作成します。
import { aws_organizations as organizations, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
interface OrganizationsConstructProps extends StackProps {
loggingAccountEmail: string;
securityAccountEmail: string;
}
export class Organizations extends Construct {
public readonly loggingAccountId: string;
public readonly securityAccountId: string;
constructor(
scope: Construct,
id: string,
props: OrganizationsConstructProps
) {
super(scope, id);
// Organization
const org = new organizations.CfnOrganization(this, "Organization", {
featureSet: "ALL",
});
// Logging Account
const loggingAccount = new organizations.CfnAccount(
this,
"LoggingAccount",
{
accountName: "log-archive",
email: props.loggingAccountEmail,
}
);
this.loggingAccountId = loggingAccount.attrAccountId;
// Security Account
const securityAccount = new organizations.CfnAccount(
this,
"SecurityAccount",
{
accountName: "audit",
email: props.securityAccountEmail,
}
);
this.securityAccountId = securityAccount.attrAccountId;
}
}
L2 Construct がないので、L1 Construct で記述していきます。
もともと記述量が少ないので、L1 Construct でも特段困りません。
最後に LandingZone のコンストラクトを作成します。
import { aws_controltower as controltower } from "aws-cdk-lib";
import { Construct, IDependable } from "constructs";
export interface LandingZoneConstructProps {
version: string;
governedRegions: string[];
centralizedLoggingAccountId: string;
enableCentlizedLogging: boolean;
securityAccountId: string;
}
export class LandingZone extends Construct {
public readonly landingZone: controltower.CfnLandingZone;
constructor(scope: Construct, id: string, props: LandingZoneConstructProps) {
super(scope, id);
const manifest = {
governedRegions: props.governedRegions,
organizationStructure: {
security: {
name: "Security",
},
sandbox: {
name: "Sandbox",
},
},
centralizedLogging: {
accountId: props.centralizedLoggingAccountId,
configurations: {
loggingBucket: {
retentionDays: 365,
},
accessLoggingBucket: {
retentionDays: 3650,
},
},
enabled: props.enableCentlizedLogging,
},
securityRoles: {
accountId: props.securityAccountId,
},
accessManagement: {
enabled: false,
},
};
this.landingZone = new controltower.CfnLandingZone(this, "LandingZone", {
version: props.version,
manifest: manifest,
});
}
public addDependencies(dependencies: IDependable[]): this {
dependencies.forEach((dependency) => {
this.landingZone.node.addDependency(dependency);
});
return this;
}
}
manifest
の記述については、AWS CloudFormation のドキュメントにも詳細な設定について確認できないので、下記ドキュメントのサンプルを元に記述していきます。
このコンストラクト作成で重要な点は、依存関係を追加できるようにすることです。
ランディングゾーンの作成は必要な IAM ロールや AWS Organizations を作成した後でないとできません。
(依存関係の追加に気づかず、どうしてデプロイに失敗するのか格闘していました。。。)
スタックの作成
作成するスタックは下記の 1 つです
- CtLandingZoneStack
コンストラクトの作成で作ったものを取り込んでいきます。
ここでは、ログアカウントや監査アカウントのメールアドレス等は App から注入できるようにパラメーター化を行なっています。
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import { Iam } from "../construct/iam";
import { Organizations } from "../construct/organizations";
import { LandingZone } from "../construct/landingzone";
export interface CtLandingZoneStackProps extends StackProps {
ctSecurityAccountEmail: string;
ctLoggingAccountEmail: string;
ctVersion: string;
ctGovernedRegions: string[];
ctEnableCentlizedLogging: boolean;
}
export class CtLandingZoneStack extends Stack {
constructor(scope: Construct, id: string, props: CtLandingZoneStackProps) {
super(scope, id, props);
/**
* Preparation
* ref: https://docs.aws.amazon.com/ja_jp/controltower/latest/userguide/lz-apis-cfn-setup.html
*/
const iam = new Iam(this, "Iam");
const organization = new Organizations(this, "Organizations", {
loggingAccountEmail: props.ctLoggingAccountEmail,
securityAccountEmail: props.ctSecurityAccountEmail,
});
/**
* Create AWS Control Tower Landing Zone
* ref: https://docs.aws.amazon.com/ja_jp/controltower/latest/userguide/lz-apis-cfn-launch.html
*/
new LandingZone(this, "LandingZone", {
version: props.ctVersion,
governedRegions: props.ctGovernedRegions,
centralizedLoggingAccountId: organization.loggingAccountId,
enableCentlizedLogging: props.ctEnableCentlizedLogging,
securityAccountId: organization.securityAccountId,
}).addDependencies([iam, organization]);
}
}
エントリーポイントの作成
CDK アプリケーションのエントリーポイントを作成していきます。
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { CtLandingZoneStack } from "../lib/stack/ct-landing-zone-stack";
import { prodParameter } from "../parameter";
if (!process.env.CT_SECURITY_ACCOUNT_EMAIL) {
throw new Error("CT_SECURITY_ACCOUNT_EMAIL is not set");
}
if (!process.env.CT_LOGGING_ACCOUNT_EMAIL) {
throw new Error("CT_LOGGING_ACCOUNT_EMAIL is not set");
}
const app = new cdk.App();
// Create stack for "Prod" environment.
new CtLandingZoneStack(app, "Prod-CtLandingZone", {
description: "AWS Control Tower Landing Zone",
env: {
account: prodParameter.env?.account || process.env.CDK_DEFAULT_ACCOUNT,
region: prodParameter.env?.region || process.env.CDK_DEFAULT_REGION,
},
tags: {
Environment: "Prod",
},
ctSecurityAccountEmail: process.env.CT_SECURITY_ACCOUNT_EMAIL,
ctLoggingAccountEmail: process.env.CT_LOGGING_ACCOUNT_EMAIL,
ctVersion: prodParameter.ctVersion,
ctGovernedRegions: prodParameter.ctGovernedRegions,
ctEnableCentlizedLogging: prodParameter.ctEnableCentlizedLogging,
});
App に注入するパラメータは環境変数の他に別途パラメータファイルを作成しています。
このあたりは Baseline Environtmnet on AWS の記述を参考にしています。
import { Environment } from "aws-cdk-lib";
export interface AppParameter {
env?: Environment;
envName: string;
/**
* AWS Control Tower Landing Zone Parameter
*
* The following email addresses are loaded from environment variables, so they are not listed in the parameters:
* - Security Account Email
* - Logging Account Email
*/
ctVersion: string;
ctGovernedRegions: string[];
ctEnableCentlizedLogging: boolean;
}
export const prodParameter: AppParameter = {
envName: "Prod",
ctVersion: "3.3",
ctGovernedRegions: ["ap-northeast-1", "us-east-1"],
ctEnableCentlizedLogging: true,
};
デプロイ
まず、CDK プロジェクト配下にデプロイのための環境変数ファイルを作成し、CDK のデプロイ環境と監査アカウントおよびロギングアカウントのメールアドレスを設定します。
CDK_DEFAULT_ACCOUNT=012345678901
CDK_DEFAULT_REGION=ap-northeast-1
CT_SECURITY_ACCOUNT_EMAIL="username+audit@example.com"
CT_LOGGING_ACCOUNT_EMAIL="username+log-archive@example.com"
CDK アプリケーションを初めてデプロイする際には、ブートストラップスタックのデプロイが必要なので実施していきます。
cdk bootstrap
下記のように表示されればブートストラップ完了です。
※ AWS CLI の認証情報を設定していないとエラーとなるので、ブートストラップに失敗する場合はそちらを確認します。
CDKToolkit: creating CloudFormation changeset...
[█████████▋················································] (2/12)
10:54:09 PM | CREATE_IN_PROGRESS | AWS::CloudFormation::Stack | CDKToolkit
10:54:12 PM | CREATE_IN_PROGRESS | AWS::IAM::Role | ImagePublishingRol
✅ Environment aws://999999999999/ap-northeast-1 bootstrapped.
準備が整ったところで、CDK アプリケーションをデプロイします。
cdk deploy
このとき、機密性が高いデプロイである旨の警告メッセージが表示されます。
ランディングゾーンを作成しようとしているので、そうですよねということでy
を入力して続行です。
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
IAM Statement Changes
┌───┬─────────────────────────────────────────┬────────┬────────────────┬──────────────────────────────────────┬───────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼─────────────────────────────────────────┼────────┼────────────────┼──────────────────────────────────────┼───────────┤
│ + │ ${Iam/CtAdminRole.Arn} │ Allow │ sts:AssumeRole │ Service:controltower.amazonaws.com │ │
├───┼─────────────────────────────────────────┼────────┼────────────────┼──────────────────────────────────────┼───────────┤
│ + │ ${Iam/CtCloudTrailRole.Arn} │ Allow │ sts:AssumeRole │ Service:cloudtrail.amazonaws.com │ │
├───┼─────────────────────────────────────────┼────────┼────────────────┼──────────────────────────────────────┼───────────┤
│ + │ ${Iam/CtConfigAggregatorRoleForOrg.Arn} │ Allow │ sts:AssumeRole │ Service:config.amazonaws.com │ │
├───┼─────────────────────────────────────────┼────────┼────────────────┼──────────────────────────────────────┼───────────┤
│ + │ ${Iam/CtStackSetRole.Arn} │ Allow │ sts:AssumeRole │ Service:cloudformation.amazonaws.com │ │
└───┴─────────────────────────────────────────┴────────┴────────────────┴──────────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼─────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Iam/CtAdminRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSControlTowerServiceRolePol │
│ │ │ icy │
│ + │ ${Iam/CtAdminRole} │ ${Iam/CtAdminPolicy} │
├───┼─────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Iam/CtCloudTrailRole} │ ${Iam/CtCloudTrailPolicy} │
├───┼─────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Iam/CtConfigAggregatorRoleForOrg} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSConfigRoleForOrganizations │
├───┼─────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Iam/CtStackSetRole} │ ${Iam/CtStackSetPolicy} │
└───┴─────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Do you wish to deploy these changes (y/n)?
デプロイが終わるのを待ちます。(30 分くらい待ちました。)
つくったものの確認
まず、管理アカウントの AWS CloudFormation コンソールを開いて、スタックがデプロイされていることを確認します。
ブートストラップ用の CDKToolkit スタックと ランディングゾーンである Prod-CtLandingZone のスタックがデプロイされていることが確認できます。
また、ランディングゾーン作成の中で下記の Control Tower 関連のスタックも一緒にデプロイされていることが分かります。
- AWSControlTowerBP-BASELINE-CONFIG-MASTER
- AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER
管理アカウントの AWS Control Tower コンソールを開いて、ランディングゾーン設定も確認しておきます。
組織単位 (OU) が 2 つ作成されて、Security OU の配下に監査アカウントとロギングアカウントが存在することが分かります。
さいごに
無事に AWS CDK を利用してランディングゾーンを作成できました。
これで個人でもマルチアカウントや Organizations の検証がしやすくなるはずです。
私たち BABY JOB は、子育てを取り巻く社会のあり方を変え、「すべての人が子育てを楽しいと思える社会」の実現を目指すスタートアップ企業です。圧倒的なぬくもりと当事者意識をもって、こどもと向き合う時間、そして心のゆとりが生まれるサービスを創出します。baby-job.co.jp/
Discussion