AWS Cloud Development Kit(AWS CDK)でEC2インスタンスを立ち上げてみる

AWS Cloud Development Kit(AWS CDK)の利用ノウハウを増やすべく、EC2インスタンスを立ち上げてみました。
公式ドキュメントやGitHubのソースを眺めたらだいたいは実装できるのですが、ハマりポイントがちらほらとありました。


AWS Cloud Development Kit(AWS CDK)ってなんぞ？という方は下記をご参考ください。

AWS クラウド開発キット (CDK) – TypeScript と Python 用がご利用可能に | Amazon Web Services ブログ
https://aws.amazon.com/jp/blogs/news/aws-cloud-development-kit-cdk-typescript-and-python-are-now-generally-available/


### 前提

- AWSアカウントがある
- AWS CLIが利用できる
- Node.jsがインストール済み


### AWS CDKのインストール

AWS CDKのコマンドが利用できるようにするため、`aws-cdk` をインストールします。
リリース後も頻繁にアップデートされていますので、インストール済みの方も最新バージョンか確認しておくと良いかもです。

```console
> node -v
v10.11.0

> npm -v
6.10.1


> npm i -g aws-cdk

# fishの場合
> exec fish -l

> cdk --version
1.2.0 (build 6b763b7)
```


### AWS CDKプロジェクト作成

`cdk` コマンドでプロジェクトを作成します。言語はTypeScriptを利用します。

```console
> mkdir use-cdk-ec2
> cd use-cdk-ec2

> cdk init app --language=typescript

Applying project template app for typescript
Initializing a new git repository...
Executing npm install...
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN use-cdk-ec2@0.1.0 No repository field.
npm WARN use-cdk-ec2@0.1.0 No license field.

# Useful commands

 * `npm run build`   compile typescript to js
 * `npm run watch`   watch for changes and compile
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk synth`       emits the synthesized CloudFormation template
```


`cdk init` コマンドを実行すると以下のようにファイルが自動生成されました。
コマンド実行したディレクトリの名前が反映されました。

```console
> tree . -L 2
.
├── README.md
├── bin
│   └── use-cdk-ec2.ts
├── cdk.json
├── lib
│   └── use-cdk-ec2-stack.ts
├── node_modules
(略)
├── package-lock.json
├── package.json
└── tsconfig.json

3 directories, 5 files
```

### `@aws-cdk/aws-ec2` のインストール

`@aws-cdk/aws-ec2` をインストールして利用できるようにします。

aws-cdk/packages/@aws-cdk/aws-ec2 at master · aws/aws-cdk
https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-ec2


```console
> npm install -s @aws-cdk/aws-ec2

+ @aws-cdk/aws-ec2@1.1.0
added 4 packages from 1 contributor and audited 538 packages in 8.417s
found 0 vulnerabilities
```

### 実装する

`@aws-cdk/aws-ec2` を利用してEC2インスタンスが立ち上がるように実装します。
EC2インスタンスを立ち上げるには、VPC、サブネット、セキュリティグループが必要になります。

```ts:bin/use-cdk-ec2.ts
#!/usr/bin/env node
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import { UseCdkEc2Stack } from '../lib/use-cdk-ec2-stack';

const app = new cdk.App();
new UseCdkEc2Stack(app, 'UseCdkEc2Stack', {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION
  }
});
```

```ts:lib/use-cdk-ec2-stack.ts
import cdk = require('@aws-cdk/core');
import ec2 = require('@aws-cdk/aws-ec2/lib');

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

    let vpc = ec2.Vpc.fromLookup(this, 'VPC', {
      vpcId: this.node.tryGetContext('vpc_id')
    });

    const cidrIp = this.node.tryGetContext('cidr_ip');
    const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
      vpc
    });
    securityGroup.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.allTraffic());
    securityGroup.addIngressRule(ec2.Peer.ipv4(cidrIp), ec2.Port.tcp(22));

    let ec2Instance = new ec2.CfnInstance(this, 'myInstance', {
      imageId: new ec2.AmazonLinuxImage().getImage(this).imageId,
      instanceType: new ec2.InstanceType('t3.small').toString(),
      networkInterfaces: [{
        associatePublicIpAddress: true,
        deviceIndex: '0',
        groupSet: [securityGroup.securityGroupId],
        subnetId: vpc.publicSubnets[0].subnetId
      }],
      keyName: this.node.tryGetContext('key_pair')
    });

    new cdk.CfnOutput(this, 'Id', { value: ec2Instance.ref });
    new cdk.CfnOutput(this, 'PublicIp', { value: ec2Instance.attrPublicIp });
  }
}
```


実装のポイントをいくつか上げてみます。


#### 既存VPCをインポートする

VPCはAWS CDKで作成することもできますが、既存のVPCをインポートすることもできます。下記は実装例となります。

aws-cdk/integ.import-default-vpc.lit.ts at master · aws/aws-cdk
https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts


`ec2.Vpc.fromLookup()` を利用してインポートしますが、その場合スタックのインスタンス作成時にアカウントとリージョン情報を渡す必要があります。そうするとAWS CDKが指定されたアカウント、リージョンからVPCの情報を取得してくれます。

```ts:bin/use-cdk-ec2.ts
#!/usr/bin/env node
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import { UseCdkEc2Stack } from '../lib/use-cdk-ec2-stack';

const app = new cdk.App();
new UseCdkEc2Stack(app, 'UseCdkEc2Stack', {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION
  }
});
```

`env` を指定しないと以下のようなエラーが発生します。

```console
> cdk synth

Cannot retrieve value from context provider vpc-provider since account/region are not specified at the stack level. Either configure 'env' with explicit account and region when you define your stack, or use the environment variables 'CDK_DEFAULT_ACCOUNT' and 'CDK_DEFAULT_REGION' to inherit environment information from the CLI (not recommended for production stacks)
Subprocess exited with error 1
```

`env` について詳しくはこちらが参考になります。

Environments - AWS Cloud Development Kit (AWS CDK)
https://docs.aws.amazon.com/cdk/latest/guide/environments.html

`ec2.Vpc.fromLookup` の第3パラメータでVPCの絞り込み条件が指定できます。詳細は下記が詳しいです。

interface VpcLookupOptions · AWS CDK
https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.VpcLookupOptions.html

今回は、`vpcId` を指定します。

```ts:lib/use-cdk-ec2-stack.ts
(略)
    let vpc = ec2.Vpc.fromLookup(this, 'VPC', {
      vpcId: this.node.tryGetContext('vpc_id')
    });
(略)
```


#### `scope` でなく`this` を指定する

`cdk.Stack` を継承したクラス内で各リソースを定義するのに、第一パラメータに`scope: cdk.Construct` を指定する必要がありますが、個々で指定するべきは、クラスの`constructor` にある`scope` ではなく、`this` を渡す必要があります。
VS Codeを利用しているとメソッドの説明で`scope` とあるので、つい指定してしまいがちですが、エラーになります。
<img width="505" alt="スクリーンショット 2019-07-30 14.10.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48549/23ae7332-5295-cc65-2d95-950e2ea165e6.png">


詳細は下記を参考ください。

TODO:リンク貼る

```console
> cdk synth -v
(略)
No stack could be identified for the construct at path
Subprocess exited with error 1
Error: Subprocess exited with error 1
    at ChildProcess.proc.on.code (/Users/xxx/.anyenv/envs/ndenv/versions/v10.11.0/lib/node_modules/aws-cdk/lib/api/cxapp/exec.ts:110:23)
    at ChildProcess.emit (events.js:182:13)
    at ChildProcess.EventEmitter.emit (domain.js:442:20)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:240:12)
```


#### 外部から値を指定するには`Context` を利用する

実装に含めたくない値がある場合、`cdk` コマンドの`--context(または -c)` オプションで指定することができます。
実装では`this.node.tryGetContext('KEY')` で値が取得できます。

値が複数ある場合、`--context(または -c)` オプションを複数指定します。

```console
> cdk synth \
  -c KEY=VALUE \
  -c KEY2=VALUE2
```


#### EC2インスタンスのパラメータ指定

EC2インスタンスは`ec2.CfnInstance` クラスを利用して定義します。パラメータについては下記が詳しかったです。
この辺を把握するにはCFnの利用経験がないとちょっと厳しいかもしれません。

Interface CfnInstanceProps
https://docs.aws.amazon.com/cdk/api/latest/typescript/api/aws-ec2/cfninstanceprops.html#aws_ec2_CfnInstanceProps



### デプロイしてみる

実装ができたらデプロイしてみます。環境変数に設定するAWSアカウント番号(12桁の数値)とリージョン、`--context` オプションで指定する既存VPCのIDやキーペア、SSHアクセスを許可するIPアドレスについては各自のを指定してください。

`cdk` コマンドを実行する前に`npm run build` を忘れないようにしましょう。(大敗

```console
> npm run build

 > use-cdk-ec2@0.1.0 build /Users/kai/dev/aws/cdk/use-cdk-ec2
 > tsc

> export CDK_DEFAULT_ACCOUNT=999999999999
> export CDK_DEFAULT_REGION=us-east-1

> cdk deploy \
  -c vpc_id=vpc-xxxxxxxx \
  -c key_pair=cdk-test-ec2-key \
  -c cidr_ip=xxx.xxx.xxx.xxx/32

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:

Security Group Changes
┌───┬──────────────────────────┬─────┬────────────┬────────────────────┐
│   │ Group                    │ Dir │ Protocol   │ Peer               │
├───┼──────────────────────────┼─────┼────────────┼────────────────────┤
│ + │ ${SecurityGroup.GroupId} │ In  │ TCP 22     │ xxx.xxx.xxx.xxx/32 │
│ + │ ${SecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4)    │
└───┴──────────────────────────┴─────┴────────────┴────────────────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Do you wish to deploy these changes (y/n)? y
UseCdkEc2Stack: deploying...
UseCdkEc2Stack: creating CloudFormation changeset...
 0/4 | 15:22:17 | CREATE_IN_PROGRESS   | AWS::EC2::SecurityGroup | SecurityGroup (SecurityGroupDD263621)
 0/4 | 15:22:17 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata      | CDKMetadata
 0/4 | 15:22:20 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata      | CDKMetadata Resource creation Initiated
 1/4 | 15:22:20 | CREATE_COMPLETE      | AWS::CDK::Metadata      | CDKMetadata
 1/4 | 15:22:22 | CREATE_IN_PROGRESS   | AWS::EC2::SecurityGroup | SecurityGroup (SecurityGroupDD263621) Resource creation Initiated
 2/4 | 15:22:23 | CREATE_COMPLETE      | AWS::EC2::SecurityGroup | SecurityGroup (SecurityGroupDD263621)
 2/4 | 15:22:26 | CREATE_IN_PROGRESS   | AWS::EC2::Instance      | myInstance
 2/4 | 15:22:27 | CREATE_IN_PROGRESS   | AWS::EC2::Instance      | myInstance Resource creation Initiated
 3/4 | 15:22:43 | CREATE_COMPLETE      | AWS::EC2::Instance      | myInstance
 4/4 | 15:22:45 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | UseCdkEc2Stack

 ✅  UseCdkEc2Stack

Outputs:
UseCdkEc2Stack.PublicIp = xxx.xxx.xxx.xxx
UseCdkEc2Stack.Id = i-xxxxxxxxxxxxxxxxx


Stack ARN:
arn:aws:cloudformation:us-east-1:999999999999:stack/UseCdkEc2Stack/59b2a500-b292-11e9-8257-12505ef78976

```

デプロイできたらSSHアクセスしてみます。

```console
> ssh -i cdk-test-ec2-key \
  ec2-user@xxx.xxx.xxx.xxx

The authenticity of host 'xxx.xxx.xxx.xxx (xxx.xxx.xxx.xxx)' can't be established.
ECDSA key fingerprint is SHA256:xxx.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'xxx.xxx.xxx.xxx' (ECDSA) to the list of known hosts.

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2018.03-release-notes/
10 package(s) needed for security, out of 13 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-xxx-xxx-xxx-xxx ~]$

```

やったぜ。


### 後片付け

検証が済んだらスタックを削除しておきます。

```console
> cdk destroy \
  -c vpc_id=vpc-xxxxxxxx
Are you sure you want to delete: UseCdkEc2Stack (y/n)? y
UseCdkEc2Stack: destroying...
   0 | 15:27:57 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | UseCdkEc2Stack User Initiated
   0 | 15:27:59 | DELETE_IN_PROGRESS   | AWS::CDK::Metadata      | CDKMetadata
   0 | 15:27:59 | DELETE_IN_PROGRESS   | AWS::EC2::Instance      | myInstance
   1 | 15:28:01 | DELETE_COMPLETE      | AWS::CDK::Metadata      | CDKMetadata

 ✅  UseCdkEc2Stack: destroyed
```

`cdk destroy` コマンドの`-c` オプションで`vpc_id` をつけないと下記のエラーが発生しました。
おそらく、`cdk destroy` コマンド実行時にも`ec2.Vpc.fromLookup` が走ってしまうためみたいです。ﾁｮｯﾄﾌﾍﾞﾝ


```console
> cdk destroy

The filter 'null' is invalid
```

## まとめ

`@aws-cdk/aws-ec2` パッケージのREADMEにEC2インスタンスについて記載がなく、それがむしろ気になって実装してみましたが、情報を調べつつの実装となり手間取りました。ただ、日頃からプログラミングする人であれば、慣れたらYAMLやJSONで定義するよりもスムーズに実装できる感がありました。



## 参考

AWS クラウド開発キット (CDK) – TypeScript と Python 用がご利用可能に | Amazon Web Services ブログ
https://aws.amazon.com/jp/blogs/news/aws-cloud-development-kit-cdk-typescript-and-python-are-now-generally-available/

aws-cdk/packages/@aws-cdk/aws-ec2 at master · aws/aws-cdk
https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-ec2

aws-cdk/integ.import-default-vpc.lit.ts at master · aws/aws-cdk
https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-ec2/test/integ.import-default-vpc.lit.ts

Environments - AWS Cloud Development Kit (AWS CDK)
https://docs.aws.amazon.com/cdk/latest/guide/environments.html

Vpc.from_lookup in v0.36 (python): Cannot retrieve value from context provider vpc-provider since account/region are not specified at the stack level · Issue #3082 · aws/aws-cdk
https://github.com/aws/aws-cdk/issues/3082

interface VpcLookupOptions · AWS CDK
https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.VpcLookupOptions.html

TODO:リンク貼る

Interface CfnInstanceProps
https://docs.aws.amazon.com/cdk/api/latest/typescript/api/aws-ec2/cfninstanceprops.html#aws_ec2_CfnInstanceProps