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 をインストールします。
リリース後も頻繁にアップデートされていますので、インストール済みの方も最新バージョンか確認しておくと良いかもです。
> 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を利用します。
> 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 コマンドを実行すると以下のようにファイルが自動生成されました。
コマンド実行したディレクトリの名前が反映されました。
> 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
> 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、サブネット、セキュリティグループが必要になります。
# !/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
  }
});
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の情報を取得してくれます。
# !/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 を指定しないと以下のようなエラーが発生します。
> 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 を指定します。
(略)
    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 とあるので、つい指定してしまいがちですが、エラーになります。

詳細は下記を参考ください。
TODO:リンク貼る
> 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) オプションを複数指定します。
> 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 を忘れないようにしましょう。(大敗
> 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アクセスしてみます。
> 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 ~]$
やったぜ。
後片付け
検証が済んだらスタックを削除しておきます。
> 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 が走ってしまうためみたいです。チョットフベン
> 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