AWS CDKを使ってVPC/EC2/S3バケットを構築する

AWS CDKを使った基本的な環境を構築したくなったので、記事を投稿してみました。
また、退職して少しの間ニートになったので、CDK関連の記事を今後投稿してみようと思ってます。

## 作成する環境構成

以下のような環境を構築するためのCDKソースコードを作成します。
基本的な構成なのですが、CDKでコーディングすると楽しいのでおすすめです。

![SgF9dAJ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203061/9f158faf-53dc-c30c-09a4-ba314965b63b.png)


- CDKバージョン: v1.18.0
- TypeScript: v3.7.3


## AWS CDKのインストール

```
sudo npm install -g aws-cdk
```

```
cdk ---version
1.18.0 (build bc924bc)
```


```
mkdir mini-cdk
cd mini-cdk
cdk init app --language=typescript
```

```
npm install @aws-cdk/aws-ec2 @aws-cdk/aws-s3 @aws-cdk/aws-s3-deployment @aws-cdk/aws-iam
```

```
# 作成するスタッックのファイル
touch lib/mini-vpc-stack.ts
touch lib/mini-ec2-stack.ts
touch lib/mini-s3-stack.ts

# 都合上削除
rm test/mini-cdk.test.ts
```

## VPCスタックの作成

VPCスタックを作成します。
**注意点として、利用するAZの数とNatGatewayに気を付けた方がよいです。`natGateways: 0`として明示しないと、AZごとにNatGatewayが作成されてしまいます**。個人で使う分には利用コストが高くなってしまうので注意が必要です。

```lib/mini-vpc-stack.ts
import cdk = require("@aws-cdk/core");
import ec2 = require("@aws-cdk/aws-ec2");

export class MiniVpcStack extends cdk.Stack {
  public readonly vpc: ec2.Vpc;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPCを作成する
    this.vpc = this.createVpc();

    // GatewayEndpointをセットアップする
    this.setupGatewayEndpoint();

    // InterfaceEndpointをセットアップする
    this.setupInterfaceEndpoint();
  }

  private createVpc() {
    return new ec2.Vpc(this, "MiniVpc", {
      cidr: "172.17.0.0/16",
      natGateways: 0, // NatGatewayはお金がかかるので使わない
      maxAzs: 1, // Availability Zoneの数
      subnetConfiguration: [
        { name: "Public", cidrMask: 20, subnetType: ec2.SubnetType.PUBLIC },
        { name: "Isolated", cidrMask: 20, subnetType: ec2.SubnetType.ISOLATED }
      ]
    });
  }
  private setupGatewayEndpoint() {
    // IsolatedのサブネットからS3に直接アクセスできるようにVPCエンドポイントを利用する
    this.vpc.addGatewayEndpoint("S3EndpointForIsolatedSubnet", {
      service: ec2.GatewayVpcEndpointAwsService.S3,
      subnets: [{ subnetType: ec2.SubnetType.ISOLATED }]
    });

    // IsolatedのサブネットからDynamoDBに直接アクセスできるようにVPCエンドポイントを利用する
    // 今回は利用しません。
    this.vpc.addGatewayEndpoint("DynamoDbEndpointForIsolatedSubnet", {
      service: ec2.GatewayVpcEndpointAwsService.DYNAMODB,
      subnets: [{ subnetType: ec2.SubnetType.ISOLATED }]
    });
  }

  private setupInterfaceEndpoint() {
    // InterafceEndpoint用のセキュリティグループ
    const interfaceEndpointSecurityGroup = new ec2.SecurityGroup(
      this,
      "InterfaceEndpointSecurityGroup",
      {
        securityGroupName: "InterfaceEndpointSecurityGroup",
        vpc: this.vpc
      }
    );
    interfaceEndpointSecurityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(443)
    );

    // Isolated SubnetからECRに接続するためのエンドポイント
    // 今回は利用しません。
    this.vpc.addInterfaceEndpoint("ECRInterfaceEndpoint", {
      securityGroups: [interfaceEndpointSecurityGroup],
      service: ec2.InterfaceVpcEndpointAwsService.ECR,
      subnets: {
        subnetType: ec2.SubnetType.ISOLATED
      }
    });
    this.vpc.addInterfaceEndpoint("ECR_DockerInterfaceEndpoint", {
      securityGroups: [interfaceEndpointSecurityGroup],
      service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER
    });
  }
}
```

`setupInterfaceEndpoint`メソッドでECRにアクセスするためのエンドポイントを作成していますが、今回は関係ありません。実装の参考情報として解釈してください。

## EC2スタックの作成

Publicサブネットに1つ、Isolatedサブネットに1つインスタンスを作成するスタックを作成します。
IsolatedサブネットのEC2インスタンスからS3バケットにアクセスできるようにロールをアタッチしています。

```lib/mini-ec2-stack.ts
import cdk = require("@aws-cdk/core");
import ec2 = require("@aws-cdk/aws-ec2");
import iam = require("@aws-cdk/aws-iam");

interface MiniEc2StackProps extends cdk.StackProps {
  readonly vpc: ec2.Vpc; // MiniVpcStackのプロパティをvpcを受け取りたいので設定している
  readonly keyName: string; // キーペア
}

export class MiniEc2Stack extends cdk.Stack {
  private readonly vpc: ec2.Vpc;
  private readonly keyName: string;

  private publicEc2SecurityGroup: ec2.SecurityGroup;
  private isolatedEc2SecurityGroup: ec2.SecurityGroup;
  private isolatedEc2Role: iam.Role;

  constructor(scope: cdk.Construct, id: string, props: MiniEc2StackProps) {
    super(scope, id, props);

    this.vpc = props.vpc;
    this.keyName = props.keyName;

    // PublicSubnetのEC2インスタンスをセットアップする
    this.setupPublicEc2Instance();

    // IsolatedSubnetのEC2インスタンスをセットアップする
    this.setupIsolatedEc2Role();
    this.setupIsolatedEc2Instance();
  }

  // PublicSubnetにEC2インスタンスを作成する
  private setupPublicEc2Instance() {
    this.publicEc2SecurityGroup = this.createPublicEc2InstanceSecurityGroup();

    const userData = ec2.UserData.forLinux();
    userData.addCommands("sudo yum update -y");

    new ec2.Instance(this, "MiniPublicEc2Instance", {
      vpc: this.vpc,
      instanceName: "MiniPublicEc2Instance",
      keyName: this.keyName,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.NANO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        edition: ec2.AmazonLinuxEdition.STANDARD,
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2
      }),
      vpcSubnets: {
        subnetType: ec2.SubnetType.PUBLIC
      },
      userData,
      securityGroup: this.publicEc2SecurityGroup
    });
  }

  // IsolatedSubnetにEC2インスタンスを作成する
  private setupIsolatedEc2Instance() {
    this.isolatedEc2SecurityGroup = this.createIsolatedEc2InstanceSecurityGroup();

    const userData = ec2.UserData.forLinux();
    userData.addCommands("sudo yum update -y");

    new ec2.Instance(this, "MiniIsolatedEc2Instance", {
      instanceName: "MiniIsolatedEc2Instance",
      vpc: this.vpc,
      keyName: this.keyName,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.NANO
      ),
      machineImage: new ec2.AmazonLinuxImage({
        edition: ec2.AmazonLinuxEdition.STANDARD,
        generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2
      }),
      vpcSubnets: {
        subnetType: ec2.SubnetType.ISOLATED
      },
      securityGroup: this.isolatedEc2SecurityGroup,
      role: this.isolatedEc2Role
    });
  }

  private setupIsolatedEc2Role() {
    this.isolatedEc2Role = new iam.Role(this, "IsolatedEc2InstanceRole", {
      roleName: "IsolatedEc2InstanceRole",
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com")
    });

    // ロールにS3へのアクセスするポリシーをアタッチする
    this.isolatedEc2Role.addToPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: [
          "s3:GetObject",
          "s3:PutObject",
          "s3:ListBucket",
          "s3:ListAllMyBuckets"
        ],
        resources: ["*"]
      })
    );
  }

  // PublicサブネットのEC2インスタンスのセキュリティグループを作成する
  private createPublicEc2InstanceSecurityGroup() {
    const sg = new ec2.SecurityGroup(this, "MiniPublicEc2InstanceSg", {
      vpc: this.vpc,
      securityGroupName: "MiniPublicEc2InstanceSg"
    });
    sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22));
    return sg;
  }

  // IsolatedサブネットのEC2インスタンスのセキュリティグループを作成する
  private createIsolatedEc2InstanceSecurityGroup() {
    const sg = new ec2.SecurityGroup(this, "MiniIsolatedEc2InstanceSg", {
      vpc: this.vpc,
      securityGroupName: "MiniIsolatedEc2InstanceSg"
    });

    sg.addIngressRule(this.publicEc2SecurityGroup, ec2.Port.allTraffic());

    return sg;
  }
}
```

## S3バケットスタックの作成

ここでは、バケットを2つ作成することにしましたw。
１つはホスティング用のバケット、もうひとつは内部用のバケットです。

データをあとで入れるのがめんどくさかったので、都合上、`@aws-s3-deployment`を使うことにしました。
以下のコマンドで適当なファイルを初期生成しています。

```
mkdir -p data/hosting
mkdir -p data/internal
echo "Hello MiniCdk!" > data/hosting/index.html
echo "This is Internal Bucket!" > data/internal/test.txt
```

S3バケットスタックを作成するソースコードは以下の通りです。

```lib/mini-s3-stack.ts
import cdk = require("@aws-cdk/core");
import s3 = require("@aws-cdk/aws-s3");
import s3Deploy = require("@aws-cdk/aws-s3-deployment");

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

    this.setupInternalBucket();
    this.setupHostingBucket();
  }

  // 内部で使うバケットを作成する
  setupInternalBucket() {
    const bucket = new s3.Bucket(this, "InternalBucket");

    new s3Deploy.BucketDeployment(this, "DeployInternalBucket", {
      sources: [s3Deploy.Source.asset("./data/internal")],
      destinationBucket: bucket
    });
  }

  // ホスティング用に使うバケットを作成する
  setupHostingBucket() {
    const bucket = new s3.Bucket(this, "HostingBucket", {
      websiteIndexDocument: "index.html",
      publicReadAccess: true,
      versioned: true,
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });

    new s3Deploy.BucketDeployment(this, "DeployHostingBucket", {
      sources: [s3Deploy.Source.asset("./data/hosting")],
      destinationBucket: bucket
    });
  }
}
```

## エンドポイントの作成

ここまで作成したスタックのエンドポイントとなるファイルを作成します。


```bin/mini-cdk.ts
#!/usr/bin/env node
import "source-map-support/register";
import cdk = require("@aws-cdk/core");
import { MiniVpcStack } from "../lib/mini-vpc-stack";
import { MiniEc2Stack } from "../lib/mini-ec2-stack";
import { MiniS3BucketStack } from "../lib/mini-s3-stack";

main();

function main() {
  const app = new cdk.App();

  // EC2インスタンスにアクセスするためのキーペアを指定する
  // マネジメントコンソールから事前にキーペアを作成しておいてください。
  const keyName = app.node.tryGetContext("keyName");

  // VPCスタック
  const miniVpcStack = new MiniVpcStack(app, "MiniVpcStack");

  // EC2スタック
  new MiniEc2Stack(app, "MiniEc2Stack", {
    vpc: miniVpcStack.vpc,
    keyName
  });

  // S3バケットスタック
  new MiniS3BucketStack(app, "MiniS3BucketStack");

  app.synth();
}
```

キーペアをデプロイ時に指定するので、マネジメントコンソールかCLIから作成しておいてください。
ここでは、`mini-cdk`という名前で作成しました。

<img width="925" alt="スクリーンショット 2019-12-15 20.50.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203061/050607a0-2049-a385-44c2-e8e3349364b5.png">


## ビルドとデプロイ

ビルドして`js`ファイルを生成します。

```
npm run build
```

デプロイ対象のスタック一覧を確認します。

```
cdk list

MiniS3BucketStack
MiniVpcStack
MiniEc2Stack
```

ここまでで準備が整ったので、スタックをまとめてデプロイして環境を構築します。

```
export AWS_DEFAULT_REGION=ap-northeast-1
cdk -c keyName=mini-cdk deploy *Stack --require-approval never --profile dev
```

- `-c`オプションでコンテキストパラメータとしてキーペア名を指定しています。
- `--require-approval never`は[y/n]を手動で承認するのが手間だった（CodeBuildとかを使った自動化などを想定）ので付けています。
- `--profile`は各自のものに読み替えてください。


## 構築したあとの確認作業

**IsolatedサブネットのEC2インスタンス**から確認します。

```
$ aws s3 ls --region ap-northeast-1
2019-12-15 12:03:53 minis3bucketstack-hostingbucket5dac2127-4r11qt9ir0vr
2019-12-15 12:03:27 minis3bucketstack-internalbucketda21a343-1hy903e2t0zu7

# 内部用に作ったバケット
$ aws s3 ls s3://minis3bucketstack-internalbucketda21a343-1hy903e2t0zu7/ --region ap-northeast-1
2019-12-15 12:04:52         24 test.txt

# ホスティング用に作ったバケット
$ aws s3 ls s3://minis3bucketstack-hostingbucket5dac2127-4r11qt9ir0vr/ --region ap-northeast-1
2019-12-15 12:04:52         15 index.html
```

他にもホスティングURLでバケットにアクセスできるか等確認がありますが、割愛させていただきます。

以上、CDKを使ったVPC/EC2/S3の構築でした。
次回は、Fargateについて書いてみようと思います。