AWSの社内研修歴5年の講師が、研修環境準備のノウハウを公開します

# 初めに
この記事は、[NTTテクノクロス Advent Calendar 2021](https://qiita.com/advent-calendar/2021/ntt-tx) の23日目です。

NTT テクノクロスの渡邉です。
普段はAWSやコンテナ関連の業務に携わっております。ときおり[社外向けブログ](https://www.ntt-tx.co.jp/column/aws_gameday_online_apn_vol2/211122/)執筆や、ソフト道場のAWS分野(IaaS, PaaS)の講師なども担当しております。
今年は新しくECS、EKSを中心としたコンテナ研修を立ち上げ、12/15に無事開催し、**この原稿を落としそうになっています。**

という訳で当初の予定を変更し、今回は社内研修の準備のノウハウを公開します。

## 準備したい環境
(ざっくりした)絵と共に、研修実施に必要な環境構成を一緒に考えてみましょう。
![qiita_sekkei.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528454/623ce802-fb10-e2b7-ed30-bbe1f2a59e23.png)

- 受講者は自宅/自社からAWSマネジメントコンソール経由で、AWSの各リソースへアクセス。
- 受講者ごとにAWS CLI, Docker, Kubectl等のコマンドの実行環境が用意済である。
- 受講者が利用するVPCはそれぞれ準備された状態とし、VPC内に必要なクラスター・ノードを構築してもらう。
   - VPC作成は設定が煩雑かつ、十数人で一斉に構築するとスロットリングの関係でエラーが発生し、研修時間を浪費するため。

## 課題

1. ユーザに付与する権限をどのように検討するか？
   - 理想は利用するAPIを一覧にし、それだけを許可するようなIAMポリシーを提供する形式。
   - しかし、IAMポリシーのActionをホワイトリスト方式とする運用は、検討期間・コストが非常にかかります。(検討の経験ある方ほどわかるはず)
2. 受講者用のCLI環境は何にするか。
   - Cloudshell, Cloud9, EC2どれを選ぶべきか。
3. 大量のVPCをどのように作るか
   - 愚直にやるならCloudFormationテンプレートを作成し、シェルなどで「Create Stack」をループさせれば作れることは作れるが、昨今のトレンドも鑑みてAWS CDKで完結させたい。
 
# 1. 研修受講者向けのIAM設計

## IAMユーザ、ポリシーの運用方針
基本的な設計方針は下図です。リソースの参照・操作が必要な場合のみIAMユーザの権限を昇格させる運用です。

![qiita_sekkei_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528454/6e1cbce5-3fa2-a88a-b04d-e46793beb35c.png)

サービス操作用のIAMポリシーを、払いだすIAMユーザに直接付与しない理由は以下の3点です。

- 受講者に払い出す強めの権限は、一時的な権限としたい。
   - 研修後に講師がリソース一式を削除するため、IAMユーザは8時間強でローテートされる前提。しかし、本業の緊急対応などでの作業遅れ・漏れを考慮する。
- 講師の操作権限を受講者と統一したい。
   - 研修中のデモは受講者権限で実施したい、でもトラブル時の解析はAdmin権限がすぐに必要。
   - 権限切り替えのために2段階認証(MFA)を含むサインインを何度も繰り返すより、IAM Roleによるスイッチロール・スイッチバックで済ますと、何かと効率的です。
- 拡張性を担保したい。
   - 今回は検討外ですが、受講者側のIAMユーザにIP制御を加えたい場合、IAMユーザ側のポリシーで制御し、認証が通ったエンティティからIP制御のない強めの権限にスイッチロールを実施する運用がおすすめです。

## ポリシーの権限選定

前述のとおり、IAMポリシーの権限をホワイトリスト方式で検討するのは骨が折れます。
特に研修立ち上げ期に関しては座学テキストやハンズオン設計に注力したい事情があり、あまり時間をかけられないのも本音。

今回の受講者は幸運にも自社の社員のみであったため、ある程度は性善説で割り切って設計しました。

- 利用サービスのActionは”ec2:*”のようにラフに設定。
- 取り返しの付かない操作についてはPermissions Boundaryで限定的に制御。

## 参考：Permissions Boundary(アクセス許可の境界)

利用経験のない方向けに簡単に説明すると、Permissions BoundaryはIAMポリシーで付与する権限に対し、アクセス許可の上限を設けるサービスです。

![permissions_boundary.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528454/edb5de27-ea79-89f1-a29e-1418d2f91cfb.png)
※出展：[IAM エンティティのアクセス許可境界](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_policies_boundaries.html)

Webで事例を検索すると、AWS OrganizationsのSCP(Service Control Policy)に準ずる使い方、例えば不要な権限昇格の防止などに利用されていますね。
正直、IAMポリシーで設計し切ってしまえば不要では……？SCPだけあればいいのでは……？と考えていた時期もありましたが、「”明示的Deny”を共通モジュールとして扱える」と捉えると、カジュアルに利用できて良いことに気付きました。

- 明示的Allow(, 暗黙的Deny)：IAM ポリシー
- 明示的Deny：Permissions Boundary

と分離することでIAMポリシーの可読性を高められ、禁止操作を明に表現しやすいのがメリットです。

## CDKで実装する

一部見せたくない設定を隠したサンプルコードはこちら。
「student」「signinPassword」はユースケースに応じて変更しましょう。

``` sample1.ts
import * as cdk from "@aws-cdk/core";
import * as iam from "@aws-cdk/aws-iam";
import * as ec2 from "@aws-cdk/aws-ec2";

export class IamStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 受講者ロール用のポリシーステートメント定義
    const PolicyStatementForStudy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      resources: ["*"],
      actions: [
        "ecr:*",
        "ecs:*",
        "eks:*",
        "ec2:*",
        "iam:*",
      ],
    });

    // 受講者ロール用のポリシー定義
    const policyName = "dojo-xxxx-policy";
    const policy = new iam.Policy(this, policyName, {
      policyName,
      statements: [PolicyStatementForStudy],
    });
    // 受講者ロール用のPermissions Boundary
    const permissionsBoundary = new iam.ManagedPolicy(
      this,
      "dojo-xxxx-permissions-boundary",
      {
        statements: [
          // IAMエンティティ（Role以外）の増減、RI系購入操作の無効化
          new iam.PolicyStatement({
            effect: iam.Effect.DENY,
            actions: [
              "iam:CreateAccessKey",
              "iam:CreateUser",
              "iam:CreateLoginProfile",
              "iam:DeleteAccessKey",
              "iam:DeleteUser",
              "iam:DeleteLoginProfile",
              "iam:DeleteRolePermissionsBoundary",
              "iam:DeleteUserPermissionsBoundary",
              "ec2:PurchaseReservedInstancesOffering",
              "ec2:ModifyReservedInstances",
              "ec2:GetReservedInstancesExchangeQuote",
              "ec2:DescribeReservedInstancesOfferings",
              "ec2:DescribeReservedInstancesModifications",
              "ec2:DescribeReservedInstancesListings",
              "ec2:DescribeReservedInstances",
              "ec2:CreateReservedInstancesListing",
              "ec2:CancelReservedInstancesListing",
              "ec2:AcceptReservedInstancesExchangeQuote",
              "ec2:CancelCapacityReservation",
              "ec2:CreateCapacityReservation",
              "ec2:DescribeCapacityReservations",
              "ec2:DescribeHostReservationOfferings",
              "ec2:DescribeHostReservations",
              "ec2:ModifyCapacityReservation",
              "ec2:ModifyInstanceCapacityReservationAttributes",
              "ec2:PurchaseHostReservation",
            ],
            resources: ["*"],
          }),
          // 特定リージョン外操作の無効化
          new iam.PolicyStatement({
            effect: iam.Effect.DENY,
            actions: ["*"],
            resources: ["*"],
            conditions: {
              StringNotEquals: {
                "aws:RequestedRegion": [
                    "ap-northeast-1",
                    "us-east-1"
                ],
              },
            },
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: ["*"],
            resources: ["*"],
          }),
        ],
      }
    );
    // 受講者ロール用のIAM Role
    const studentRole = new iam.Role(this, "Role", {
      assumedBy: new iam.AccountPrincipal(this.account),
      roleName: "dojo-xxxx-role",
      permissionsBoundary,
    });
    studentRole.attachInlinePolicy(policy);

    // 受講者ユーザ用のポリシー(SwitchRole)
    const PolicyStatementForSwitch = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ["sts:AssumeRole"],
      resources: [studentRole.roleArn],
    });
    // 受講者ユーザ用のポリシー(password変更)
    const PolicyStatementForChangePassword = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      resources: ["*"],
      actions: [
          "iam:ChangePassword",
          "iam:GetAccountPasswordPolicy"
        ],
    });
    // 受講者ユーザ用のグループ
    const groupName = "dojo-container-student-group";
    const group = new iam.Group(this, groupName, {
      groupName: groupName,
    });
    group.attachInlinePolicy(
      new iam.Policy(this, "InlinePolicy", {
        statements: [
          PolicyStatementForSwitch,
          PolicyStatementForChangePassword,
        ],
      })
    );
    const student = 1;
    const signinPassword = "Hogehoge-hugahuga1234";
    for (let num = 1; num <= student; num++) {
      const username = `dojo-xxxx-user-${num}`;
      const user = new iam.User(this, username, {
        userName: username,
        permissionsBoundary,
        groups: [group],
        password: cdk.SecretValue.plainText(signinPassword),
        passwordResetRequired: true,
      });
    }
  }
}

const app = new cdk.App();
const result = new IamStack(app, "iamstack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});
app.synth();
```

## 実装のポイント

- 受講者のIAMユーザは「初期パスワードをユーザ自身で変更させる」前提。あとはSwitchRole前提の最小権限を持つIAM Groupに所属させる。
- 性善説運用ではあるが、IAM Role作成が必須のカリキュラムのためIAM系は比較的緩め。最低限のCreate,Delete,PermissionsBoundaryの削除は意識。
- EC2も緩め。RIなどはさすがに取り返しがつかないので塞ぐ。

# 2. 研修受講者向けのCLI環境設計

## CloudShellの盲点
マネジメントコンソールから利用するユーザにCLIインターフェースを提供する、という要件ではまずCloudShellが思い浮かびますが、**今回の研修はコンテナを題材にしているため、候補から真っ先に外れます。**
というのも、CloudShellの公式ドキュメントを見るとわかるのですが、CloudShellはDockerコンテナに対応しておりません。
https://docs.aws.amazon.com/cloudshell/latest/userguide/vm-specs.html

> Currently, the AWS CloudShell compute environment doesn't support Docker containers.

明に書かれていませんが環境自体コンテナなのかも？という感触もあります。
また実際に動かすとDockerコマンドもうまく実行できないため、正直言って今回の用途では使用感がよくありません。候補からは外しました。

## Cloud9特有の課題

Cloudshellが候補として消える場合、Cloud9 or EC2どちらを採用するか。という話になります。
ちなみにCloud9は**AWSアカウント管理者がインスタンスを準備するのにあまり向かないサービスです。**

というのも、Cloud9で作成したインスタンスへ他のIAM User・Roleをアクセスさせるには、各環境内の設定値(Cloudformation非対応)で明示的に許可する必要があります。
ちなみに初めにサインインできるのは「所有者」インスタンスを作成したIAM User・Roleなので、管理者（≒私）が環境作成後に20人弱分の環境へサインインして招待操作を都度実施する必要があります。め、面倒すぎる……！

またCloud9は便利な反面、権限が強すぎてしまう傾向にあり、実務での商用/ステージング環境へは配置されないことが多いです。折角の研修の機会なので覚えてほしい運用パターンである**EC2＋Session Manager**を選択しました。

::: note info
※ちなみに、AWS公式のハンズオン集のAWS Workshops(https://workshops.aws/)にて多用されているのをみて分かるように、こういった用途でCloud9を選定するのはそこまで悪手ではありません。本章での判断は、あくまで私の趣味という点をご理解ください。
:::

## CDKで実装する

- 基本的には1。のサンプルコードの差分のみ抜粋しているので~~コピペ~~参考にする際はご注意を。

```  sample2.ts
import * as cdk from "@aws-cdk/core";
import * as iam from "@aws-cdk/aws-iam";
import * as ec2 from "@aws-cdk/aws-ec2";

export class IamStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 受講者ロール＆インスタンス用のポリシーステートメント定義
    const PolicyStatementForStudy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      resources: ["*"],
      actions: [
        "ecr:*",
        "ecs:*",
        "eks:*",
        "ec2:*",
        "iam:*",
      ],
    });
    // 受講者ロール用のポリシーステートメント定義
    const PolicyStatementForSession = new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: [
          "ssm:DescribeSessions",
          "ssm:DescribeInstanceInformation",
          "ssm:DescribeInstanceProperties",
          "ssm:StartSession",
          "ssm:SendCommand",
          "ssm:ResumeSession",
          "ssm:TerminateSession",
          "ssm:GetConnectionStatus"
        ],
        ],
        resources: ["*"],
      })
    // 受講者インスタンス用のポリシーステートメント定義   
    const PolicyStatementForConn = new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: [
          "ssm:DescribeAssociation",
          "ssm:UpdateInstanceInformation",
          "ssmmessages:CreateControlChannel",
          "ssmmessages:CreateDataChannel",
          "ssmmessages:OpenControlChannel",
          "ssmmessages:OpenDataChannel",
          "s3:GetEncryptionConfiguration"
        ],
        resources: ["*"],
      }) 

    // 受講者ロール用のポリシー定義
    const iamPolicyName = "dojo-xxxx-iam-policy";
    const dojoIamPolicy = new iam.Policy(this, iamPolicyName, {
      policyName: iamPolicyName,
      statements: [
        PolicyStatementForStudy,
        PolicyStatementForSession,
      ],
    });
    
    // 受講者インスタンス用のポリシー定義
    const instancePolicyName = "dojo-xxxx-instance-policy";
    const dojoInstancePolicy = new iam.Policy(this, instancePolicyName, {
      policyName: instancePolicyName,
      statements: [
        PolicyStatementForStudy,
        PolicyStatementForConn
      ],
    });
    
    // 受講者ロール＆インスタンス用のPermissions Boundary
    const permissionsBoundary = new iam.ManagedPolicy(
      this,
      "dojo-xxxx-permissions-boundary",
      {
        statements: [
          // IAMエンティティ（Role以外）の増減、RI系購入操作の無効化
          new iam.PolicyStatement({
            effect: iam.Effect.DENY,
            actions: [
              "iam:CreateAccessKey",
              "iam:CreateUser",
              "iam:CreateLoginProfile",
              "iam:DeleteAccessKey",
              "iam:DeleteUser",
              "iam:DeleteLoginProfile",
              "iam:DeleteRolePermissionsBoundary",
              "iam:DeleteUserPermissionsBoundary",
              "ec2:PurchaseReservedInstancesOffering",
              "ec2:ModifyReservedInstances",
              "ec2:GetReservedInstancesExchangeQuote",
              "ec2:DescribeReservedInstancesOfferings",
              "ec2:DescribeReservedInstancesModifications",
              "ec2:DescribeReservedInstancesListings",
              "ec2:DescribeReservedInstances",
              "ec2:CreateReservedInstancesListing",
              "ec2:CancelReservedInstancesListing",
              "ec2:AcceptReservedInstancesExchangeQuote",
              "ec2:CancelCapacityReservation",
              "ec2:CreateCapacityReservation",
              "ec2:DescribeCapacityReservations",
              "ec2:DescribeHostReservationOfferings",
              "ec2:DescribeHostReservations",
              "ec2:ModifyCapacityReservation",
              "ec2:ModifyInstanceCapacityReservationAttributes",
              "ec2:PurchaseHostReservation",
            ],
            resources: ["*"],
          }),
          // 特定リージョン外操作の無効化
          new iam.PolicyStatement({
            effect: iam.Effect.DENY,
            actions: ["*"],
            resources: ["*"],
            conditions: {
              StringNotEquals: {
                "aws:RequestedRegion": [
                    "ap-northeast-1",
                    "us-east-1"
                ],
              },
            },
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: ["*"],
            resources: ["*"],
          }),
        ],
      }
    );

    // 受講者ロール用のIAM Role
    const studentIamRole = new iam.Role(this, "StudentIamRole", {
      assumedBy: new iam.AccountPrincipal(this.account),
      roleName: "dojo-xxxx-iam-role",
      permissionsBoundary,
    });
    studentIamRole.attachInlinePolicy(dojoIamPolicy)
        
    // 受講者インスタンス用のIAM Role
    const studentInstanceRole = new iam.Role(this, "StudentInstanceRole", {
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
      roleName: "dojo-xxxx-instance-role",
      permissionsBoundary,
    });
    studentInstanceRole.attachInlinePolicy(dojoInstancePolicy)
    
    // 受講者ユーザ用のポリシー(SwitchRole)
    const PolicyStatementForSwitch = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ["sts:AssumeRole"],
      resources: [studentIamRole.roleArn],
    });
    // 受講者ユーザ用のポリシー(password変更)
    // ～～ 以下 1．と同様～～
```

## 実装のポイント

- スイッチロール先のロールと**ほぼ**同じロールをEC2用に作っています。
    - CDKだとStatementの差分が書きやすいですね。

- Session Managerの権限設定は公式ドキュメントを参考に実装。
    - 入る側（IAMユーザ）： [クイックスタート のデフォルト IAM ポリシーSession Manager](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/getting-started-restrict-access-quickstart.html)
    - 入られる側（EC2インスタンス）：[Session Manager のカスタム IAM インスタンスプロファイルを作成する](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/getting-started-create-iam-instance-profile.html)


# 3. 研修受講者向けのVPC設計

本章については、1. 2.で作った環境からアクセスするための環境を作れれば良いので、Typescriptでテンプレートを書くだけです。簡単ですね。
……と研修5日前には思っていたんですけどね(苦笑)

## CDKで実装する(失敗作)

このテンプレートは机上では動きそうに見えます。というよりVPC数が少ない限りは実際に動きます。

``` sample3.ts
import * as cdk from "@aws-cdk/core";
import * as ec2 from "@aws-cdk/aws-ec2";
import * as iam from "@aws-cdk/aws-iam";
import * as elbv2 from "@aws-cdk/aws-elasticloadbalancingv2";

export class dojonwStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const [bastionVpc, bastionSecurityGroup] = this.bastionVpc();
    const student = 1;
    for (let num = 1; num <= student; num++) {
        this.bastion(bastionVpc, num);
        const [vpc, albSecurityGroup] = this.systemVpc(num);
    }
  }
  private bastionVpc(): [ec2.Vpc, ec2.SecurityGroup] {
    const vpc = new ec2.Vpc(this, "vpc-bastion", {
      natGateways: 0,
      maxAzs: 3,

      subnetConfiguration: [
        {
          name: "public",
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          name: "isolated",
          subnetType: ec2.SubnetType.ISOLATED,
        },
      ],
    });
    vpc.addFlowLog("bastionFlowLogCloudWatch", {
      trafficType: ec2.FlowLogTrafficType.REJECT,
    });
    const bastionSecurityGroup = new ec2.SecurityGroup(
      this,
      "bastionSecurityGroup",
      {
        securityGroupName: `bastion-sg`,
        allowAllOutbound: true,
        vpc,
      }
    );
    return [vpc, bastionSecurityGroup];
  }
  private bastion(vpc: ec2.Vpc, student: number) {
    const ebsSetting = {
      encrypted: true,
      deleteOnTermination: true,
      volumeType: ec2.EbsDeviceVolumeType.GP3,
    };

    const rolename = "dojo-xxxx-role";

    const role = iam.Role.fromRoleArn(
      this,
      "ExistingDojoRole",
      `arn:aws:iam::${this.account}:role/${rolename}`
    );

    const bastion = new ec2.Instance(this, `bastionhost-${student}`, {
      vpc,
      instanceName: `bastionhost-${student}`,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MICRO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
        cpuType: ec2.AmazonLinuxCpuType.X86_64,
      }),
      blockDevices: [
        {
          deviceName: `/dev/xvda`,
          volume: ec2.BlockDeviceVolume.ebs(10, ebsSetting),
        },
      ],
      vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
      role,
    });

    bastion.addUserData(
      "amazon-linux-extras install -y docker",
      "systemctl start docker",
      "systemctl enable docker",
      "usermod -aG docker ec2-user"
    );
  }
  private systemVpc(student: number): [ec2.Vpc, ec2.SecurityGroup] {
    const vpc = new ec2.Vpc(this, `vpc-student-${student}`, {
      natGateways: 1,
      maxAzs: 3,
    });
    vpc.addFlowLog(`FlowLogCloudWatch-${student}`, {
      trafficType: ec2.FlowLogTrafficType.REJECT,
    });

    const albSecurityGroup = new ec2.SecurityGroup(
      this,
      `albSecurityGroup-${student}`,
      {
        securityGroupName: `alb-sg-${student}`,
        allowAllOutbound: true,
        vpc,
      }
    );
    const alb = new elbv2.ApplicationLoadBalancer(
      this,
      `alb-student-${student}`,
      {
        vpc,
        vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }),
        loadBalancerName: `alb-${student}`,
        internetFacing: true,
        securityGroup: albSecurityGroup,
      }
    );
    return [vpc, albSecurityGroup];
  }

}

const app = new cdk.App();
new dojonwStack(app, "stack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});
app.synth();
```

どこがマズいのか？ それは、**CloudFormationの制限を考慮していない点です。**

## VPCのサブリソース数、イメージできますか？

[AWS CloudFormation のクォータ](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html)を見てみましょう。
> リソース AWS CloudFormation テンプレートで宣言できるリソースの最大数。 500 個のリソース

このクォータを意識することは通常時はあまりありませんが、大量VPCの作成時には実はかなり大切なのです。**何故なら、このクォータは2021/12時点で上限緩和申請を行うことはできません。** CDKでの準備を後悔した瞬間です。

研修中にAZ障害が発生する前提で3AZ構成のVPCを検討した場合、下表のように数えた結果、1VPCあたり30弱のリソースが作成されるのがわかります。

|リソース(cloudformation)|VPCあたりのリソース数|備考|
|:---|:---|:---|
|AWS::EC2::VPC|1|VPC|
|AWS::EC2::Subnet|6|サブネット。2subnet×3az|
|AWS::EC2::RouteTable|6|ルートテーブル。2subnet×3az|
|AWS::EC2::SubnetRouteTableAssociation|6|サブネットとrtbの関係性を表現するリソース。2subnet×3az
|AWS::EC2::Route|6|各az|
|AWS::EC2::EIP|1|
|AWS::EC2::NatGateway|1|AZ冗長の問題はあるが、費用が高額の為※一つのみ。|
|AWS::EC2::InternetGateway|1|インターネットとの接続点|
|AWS::EC2::VPCGatewayAttachment|1|InternetGatewayとVPCの関係性を表現するリソース|

20人で600リソースという訳で、1スタックに押し込むのは不可能ですね。

この時ばかりは諦めかけましたが、もしや？と思って調べた結果、公式ドキュメントに[回避策](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_core.NestedStack.html)が載っていました！ こんな場合は**ネストされたスタックをCDKで実装します。**

## CDKで実装する(成功例)

動的にネストされたスタックを作成するサンプルです。

``` sample3_r2.ts
import * as cdk from "@aws-cdk/core";
import * as ec2 from "@aws-cdk/aws-ec2";
import * as ecs from "@aws-cdk/aws-ecs";
import * as iam from "@aws-cdk/aws-iam";
import * as elbv2 from "@aws-cdk/aws-elasticloadbalancingv2";

// Nested Stack用のInterfaceを定義
interface ResourceNestedStackProps extends cdk.NestedStackProps {
  readonly resourceNumber: number;
  readonly vpc: ec2.Vpc;
}
// Nested Stackとして各VPCを定義する
class containerVpc extends cdk.NestedStack {
  constructor(
    scope: cdk.Construct,
    id: string,
    props: ResourceNestedStackProps
  ) {
    super(scope, id, props);
    this.bastion(props.vpc, props.resourceNumber);
    const [vpc, albSecurityGroup] = this.systemVpc(props.resourceNumber);
  }

  private bastion(vpc: ec2.Vpc, student: number) {
    const ebsSetting = {
      encrypted: true,
      deleteOnTermination: true,
      volumeType: ec2.EbsDeviceVolumeType.GP3,
    };

    const rolename = "dojo-xxxx-role";

    const role = iam.Role.fromRoleArn(
      this,
      "ExistingDojoRole",
      `arn:aws:iam::${this.account}:role/${rolename}`
    );

    const bastion = new ec2.Instance(this, `bastionhost-${student}`, {
      vpc,
      instanceName: `bastionhost-${student}`,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MICRO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
        cpuType: ec2.AmazonLinuxCpuType.X86_64,
      }),
      blockDevices: [
        {
          deviceName: `/dev/xvda`,
          volume: ec2.BlockDeviceVolume.ebs(10, ebsSetting),
        },
      ],
      vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
      role,
    });

    bastion.addUserData(
      "amazon-linux-extras install -y docker",
      "systemctl start docker",
      "systemctl enable docker",
      "usermod -aG docker ec2-user"
    );
  }
  private systemVpc(student: number): [ec2.Vpc, ec2.SecurityGroup] {
    const vpc = new ec2.Vpc(this, `vpc-student-${student}`, {
      natGateways: 1,
      maxAzs: 3,
    });
    vpc.addFlowLog(`FlowLogCloudWatch-${student}`, {
      trafficType: ec2.FlowLogTrafficType.REJECT,
    });

    const albSecurityGroup = new ec2.SecurityGroup(
      this,
      `albSecurityGroup-${student}`,
      {
        securityGroupName: `alb-sg-${student}`,
        allowAllOutbound: true,
        vpc,
      }
    );
    const alb = new elbv2.ApplicationLoadBalancer(
      this,
      `alb-student-${student}`,
      {
        vpc,
        vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PUBLIC }),
        loadBalancerName: `alb-${student}`,
        internetFacing: true,
        securityGroup: albSecurityGroup,
      }
    );
    return [vpc, albSecurityGroup];
  }
}
// 呼び出し元のスタック
export class dojonwStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const [bastionVpc, bastionSecurityGroup] = this.bastionVpc();
    const student = 1;
    for (let num = 1; num <= student; num++) {
      new containerVpc(this, `containerVpc-${num}`, {
        resourceNumber: num,
        vpc: bastionVpc,
      });
    }
  }
  private bastionVpc(): [ec2.Vpc, ec2.SecurityGroup] {
    const vpc = new ec2.Vpc(this, "vpc-bastion", {
      natGateways: 0,
      maxAzs: 3,

      subnetConfiguration: [
        {
          name: "public",
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          name: "isolated",
          subnetType: ec2.SubnetType.ISOLATED,
        },
      ],
    });
    vpc.addFlowLog("bastionFlowLogCloudWatch", {
      trafficType: ec2.FlowLogTrafficType.REJECT,
    });
    const bastionSecurityGroup = new ec2.SecurityGroup(
      this,
      "bastionSecurityGroup",
      {
        securityGroupName: `bastion-sg`,
        allowAllOutbound: true,
        vpc,
      }
    );
    return [vpc, bastionSecurityGroup];
  }

}

const app = new cdk.App();
new dojonwStack(app, "nwstack", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});
app.synth();

```

## 実装のポイント

- ループで渡したいパラメータは、事前にcore.NestedStackPropsを継承したinterfaceで定義する。
- 運用面としては大量VPC作成を試みると、スロットリングで失敗しやすい。デプロイ時は「--no-rollback」オプションが推奨、というより必須。
    - 「--no-rollback」はCloudFormationのロールバック防止機能に準ずる、CDKのオプション。この機能がないといわゆる「VPC作成ガチャ」のような余分な準備工数バッファが必要。
        - 参考：[AWS CloudFormation がロールバック前のプロビジョニングエラーのトラブルシューティングをして、デプロイを加速するのためのオプションをリリース](https://aws.amazon.com/jp/about-aws/whats-new/2021/08/aws-cloudformation-troubleshoot-errors/)



## 準備したい環境(完成版)

色々凝った構成にしようとした時期もありましたが、最終的には王道な構成へ。
![qiita_sekkei_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528454/bb4a681c-6830-1fa8-a010-13e473cf861f.png)

# おわりに
研修準備の裏側をここまで解説できたのは初めてです。いかがでしたか？
今回説明してきた部分は受講者含めあまり注目されないので、徹底的に手を抜くこともできますし、**逆に徹底的に作りこむことも可能です！**  オーバーエンジニアリング最高っっ！！！！！ (後ろ回しにした業務と関係各所から目を背けながら)

……これだけは5年分のノウハウを持って断言できますが、ほどほどにするのが本当にいいと思います。


明日は@uehajがReact QueryとPleasanterのはなしを書いてくれるようですよ。
引き続き、NTTテクノクロス Advent Calendar 2021をお楽しみください。もうすぐメリークリスマス！🎄


## Appendix

今年は[弊社のアドベントカレンダー](https://qiita.com/advent-calendar/2021/ntt-tx)にもAWSの流れが！ 
この記事を読んだ人はこんな記事を読んでいます(読んでね)

- [【AWS】ひとりでもOrganizationsを使うと便利で勉強になる話(AWS SSOの始め方を手厚く)](https://qiita.com/kzmqiita/items/55d34245f5adb7ecc4b7)
- [【AWS】利用料金の詳細通知＆自動化](https://qiita.com/inoue-mn/items/d28a2bb6091bbc7bbec5)
- [AWSの分散負荷テストソリューション及び負荷テストのノウハウ](https://qiita.com/horietakehiro/items/36f6f9b129fa3b65b225)
- [【AWS】QuickSightの便利機能を検証してみた](https://qiita.com/yukimuraki/items/fc20b3474242ddf6c9f6)