AWS CDKでAWS::CloudFormation::Init タイプを使ってEC2インスタンスの環境構築ができるようにしてみた

前回、AWS Cloud Development Kit(AWS CDK)を利用してEC2インスタンスを立ち上げてみたのですが、AWS CDKでAWS::CloudFormation::Initタイプが利用できるのかも確認してみました。

AWS Cloud Development Kit(AWS CDK)でEC2インスタンスを立ち上げてみる - Qiita
https://qiita.com/kai_kou/items/e35fd8c6af7dff9f2624


AWS::CloudFormation::Init タイプについては下記をご参考ください。

AWS::CloudFormation::Init タイプを使ってEC2インスタンスの環境構築ができるようにしてみた - Qiita
https://qiita.com/kai_kou/items/e1ef9fa6fb0375dcf15f


## 前提

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


## 実装

前回記事の実装をベースにしてAWS::CloudFormation::Initタイプの定義を追加しました。

AWS Cloud Development Kit(AWS CDK)でEC2インスタンスを立ち上げてみる - Qiita
https://qiita.com/kai_kou/items/e35fd8c6af7dff9f2624


```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')
    });

    ec2Instance.addOverride('Metadata', {
      'AWS::CloudFormation::Init': {
        'config': {
          'commands': {
            'test': {
              'command': "echo $STACK_NAME test",
              'env': {
                'STACK_NAME': this.stackName
              }
            }
          },
        }
      }
    });

    let userData = ec2.UserData.forLinux();
    userData.addCommands(
      '/opt/aws/bin/cfn-init',
      `--region ${this.region}`,
      `--stack ${this.stackName}`,
      `--resource ${ec2Instance.logicalId}`
    );
    userData.addCommands('echo', 'hoge!');
    ec2Instance.userData = cdk.Fn.base64(userData.render());

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

公式ドキュメントを漁ってみたものの良い情報が得られず、下記Issueを参考にしました。

Add support for AWS::CloudFormation::Init · Issue #777 · aws/aws-cdk
https://github.com/aws/aws-cdk/issues/777

ec2: cfn-init support in ASGs · Issue #1413 · aws/aws-cdk
https://github.com/aws/aws-cdk/issues/1413

feat(aws-ec2): add support for CloudFormation::Init by rix0rrr · Pull Request #792 · aws/aws-cdk
https://github.com/aws/aws-cdk/pull/792

追加した実装は以下となります。
ポイントとして`ec2Instance.addOverride()` でメタデータを追加して`AWS::CloudFormation::Init` タイプで定義を追加します。
`/opt/aws/bin/cfn-init` の`--resource` オプションでリソース名を指定するのに`ec2Instance` を作ってから`userData` を設定することで、`ec2Instance.logicalId` が利用できるようにしています。ベタ書きでもいいっちゃいいですね。


```ts
    ec2Instance.addOverride('Metadata', {
      'AWS::CloudFormation::Init': {
        'config': {
          'commands': {
            'test': {
              'command': "echo $STACK_NAME test",
              'env': {
                'STACK_NAME': this.stackName
              }
            }
          },
        }
      }
    });

    let userData = ec2.UserData.forLinux();
    userData.addCommands(
      '/opt/aws/bin/cfn-init',
      `--region ${this.region}`,
      `--stack ${this.stackName}`,
      `--resource ${ec2Instance.logicalId}`
    );
    userData.addCommands('echo', 'hoge!');
    ec2Instance.userData = cdk.Fn.base64(userData.render());
(略)
```


## デプロイしてみる

```console
> 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 | 14:30:29 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata      | CDKMetadata
 0/4 | 14:30:30 | CREATE_IN_PROGRESS   | AWS::EC2::SecurityGroup | SecurityGroup (SecurityGroupDD263621)
 0/4 | 14:30:32 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata      | CDKMetadata Resource creation Initiated
 1/4 | 14:30:32 | CREATE_COMPLETE      | AWS::CDK::Metadata      | CDKMetadata
 1/4 | 14:30:35 | CREATE_IN_PROGRESS   | AWS::EC2::SecurityGroup | SecurityGroup (SecurityGroupDD263621) Resource creation Initiated
 2/4 | 14:30:37 | CREATE_COMPLETE      | AWS::EC2::SecurityGroup | SecurityGroup (SecurityGroupDD263621)
 2/4 | 14:30:39 | CREATE_IN_PROGRESS   | AWS::EC2::Instance      | myInstance
 2/4 | 14:30:40 | CREATE_IN_PROGRESS   | AWS::EC2::Instance      | myInstance Resource creation Initiated
 3/4 | 14:30:56 | CREATE_COMPLETE      | AWS::EC2::Instance      | myInstance
 4/4 | 14:30:59 | 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:xxxxxxxxxxxx:stack/UseCdkEc2Stack/72304c90-b41d-11e9-b604-129cd46a326a
```

デプロイできたらSSHアクセスして実行ログを確認してみます。

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


$ cat /var/log/cfn-init.log

2019-08-01 05:31:11,740 [INFO] -----------------------Starting build-----------------------
2019-08-01 05:31:11,740 [INFO] Running configSets: default
2019-08-01 05:31:11,741 [INFO] Running configSet default
2019-08-01 05:31:11,742 [INFO] Running config config
2019-08-01 05:31:11,746 [INFO] Command test succeeded
2019-08-01 05:31:11,746 [INFO] ConfigSets completed
2019-08-01 05:31:11,746 [INFO] -----------------------Build complete-----------------------


$ cat /var/log/cfn-init-cmd.log

2019-08-01 05:31:11,742 P2090 [INFO] ************************************************************
2019-08-01 05:31:11,742 P2090 [INFO] ConfigSet default
2019-08-01 05:31:11,743 P2090 [INFO] ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2019-08-01 05:31:11,743 P2090 [INFO] Config config
2019-08-01 05:31:11,743 P2090 [INFO] ============================================================
2019-08-01 05:31:11,743 P2090 [INFO] Command test
2019-08-01 05:31:11,746 P2090 [INFO] -----------------------Command Output-----------------------
2019-08-01 05:31:11,746 P2090 [INFO]    UseCdkEc2Stack test
2019-08-01 05:31:11,746 P2090 [INFO] ------------------------------------------------------------
2019-08-01 05:31:11,746 P2090 [INFO] Completed successfully.


$ cat /var/log/cloud-init-output.log

(略)
Updated:
  bind-libs.x86_64 32:9.8.2-0.68.rc1.60.amzn1
  bind-utils.x86_64 32:9.8.2-0.68.rc1.60.amzn1
  kernel-tools.x86_64 0:4.14.133-88.105.amzn1
  python27-jinja2.noarch 0:2.7.2-3.16.amzn1
  vim-common.x86_64 2:8.0.0503-1.46.amzn1
  vim-enhanced.x86_64 2:8.0.0503-1.46.amzn1
  vim-filesystem.x86_64 2:8.0.0503-1.46.amzn1
  vim-minimal.x86_64 2:8.0.0503-1.46.amzn1

Complete!
Cloud-init v. 0.7.6 running 'modules:final' at Thu, 01 Aug 2019 05:31:11 +0000. Up 18.18 seconds.
hoge!
Cloud-init v. 0.7.6 finished at Thu, 01 Aug 2019 05:31:11 +0000. Datasource DataSourceEc2.  Up 18.77 seconds
```

ユーザーデータの`/opt/aws/bin/cfn-init` コマンド実行でメタデータに`AWS::CloudFormation::Init` タイプで指定したコマンドが実行されました。やったぜ。


## まとめ

メタデータの指定について、もっと良い実装ができそうですが、ひとまずAWS CDKでも`AWS::CloudFormation::Init` タイプを利用できるのが確認できたので満足です。


## 参考

AWS Cloud Development Kit(AWS CDK)でEC2インスタンスを立ち上げてみる - Qiita
https://qiita.com/kai_kou/items/e35fd8c6af7dff9f2624

AWS::CloudFormation::Init タイプを使ってEC2インスタンスの環境構築ができるようにしてみた - Qiita
https://qiita.com/kai_kou/items/e1ef9fa6fb0375dcf15f

Add support for AWS::CloudFormation::Init · Issue #777 · aws/aws-cdk
https://github.com/aws/aws-cdk/issues/777

ec2: cfn-init support in ASGs · Issue #1413 · aws/aws-cdk
https://github.com/aws/aws-cdk/issues/1413

feat(aws-ec2): add support for CloudFormation::Init by rix0rrr · Pull Request #792 · aws/aws-cdk
https://github.com/aws/aws-cdk/pull/792