AWS CDKでEC2 Auto Scalingをデプロイしてみる

## はじめに
Auto Scalingのテスト環境の構築をAWS CDKの勉強も兼ねて行ってみました。
EC2にCPUの負荷を与えて、想定通りにEC2がスケールアウトすることを確認したかったため、作業内容を備忘録のつもりでまとめます。

## 環境
- OS: macOS Cataline 10.15.5
- VSCode: 1.46.0
- Node.js: v14.4.0
- npm: 6.14.5
- TypeScript: 3.9.5
- CDK: 1.45.0

## AWS CDKのインストール
[公式](https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/getting_started.html)の情報を参考にインストールします。

```
$ npm install -g aws-cdk
```

インストールできているか、念のため確認します。

```
$ cdk --version
1.45.0 (build 0cfab15)
```

## プロジェクトの作成 

CDKのプロジェクトを[公式](https://docs.aws.amazon.com/ja_jp/cdk/latest/guide/hello_world.html)に沿って作成します。

```
$ mkdir test-asg
$ cd test-asg
$ cdk init app --language=typescript
```

## コードの実装
必要なCDKのモジュールをインストールします。

```
$ npm install @aws-cdk/aws-autoscaling @aws-cdk/aws-ec2 @aws-cdk/aws-elasticloadbalancingv2
```

なお、自分が検証した際には[このエラー](https://qiita.com/shootacean/items/91e2666f1cf244828047)に遭遇したため、`package.json`を編集して`@aws-cdk/core`と`@aws-cdk/assert`のバージョンを合わせます。


```package.json
{

  : (省略)

  "devDependencies": {
    // 1.45.0 -> 1.46.0
    "@aws-cdk/assert": "^1.46.0",
    "@types/jest": "^25.2.1",
    "@types/node": "10.17.5",
    "jest": "^25.5.0",
    "ts-jest": "^25.3.1",
    "aws-cdk": "1.45.0",
    "ts-node": "^8.1.0",
    "typescript": "~3.7.2"
  },
  "dependencies": {
    "@aws-cdk/aws-autoscaling": "^1.46.0",
    "@aws-cdk/aws-ec2": "^1.46.0",
    "@aws-cdk/aws-elasticloadbalancingv2": "^1.46.0",
    // 1.45.0 -> 1.46.0
    "@aws-cdk/core": "^1.46.0",
    "source-map-support": "^0.5.16"
  }
}
```

`node_modules`ディレクトリを削除し、再度インストールします。これでエラーが解消され、ビルドも可能になりました。(CDKのバージョンを最初から上げていれば、このようなことはしなくても良かったかもしれません…)

```
$ rm -rf node_modules
$ npm install
```


`lib/test-asg-stack.ts`を編集します。以下の点に留意して作成しました。

- 予めAWSのコンソールで`TestAsgEnv`という名前のキーペアを作成しておき、SSHでログインできるようにする。
- EC2起動時に`sudo yum update -y`が実行されるようにする。
- CPU使用率が50%でスケールアウトするようにする。
- スタック作成完了時にALBのDNS名が表示されるようにする。

```lib/test-asg-stack.ts
import * as cdk from '@aws-cdk/core';
import * as autoscaling from '@aws-cdk/aws-autoscaling';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2';

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

        const cidr = '10.0.0.0/16';

        const vpc = new ec2.Vpc(
            this,
            'test-asg-vpc', {
            cidr,
            natGateways: 1,
            subnetConfiguration: [
                {
                    cidrMask: 18,
                    name: 'public',
                    subnetType: ec2.SubnetType.PUBLIC,
                },
                {
                    cidrMask: 18,
                    name: 'private',
                    subnetType: ec2.SubnetType.PRIVATE,
                },
            ],
        }
        );

        const securityGroup = new ec2.SecurityGroup(
            this,
            'test-asg-security-group',
            {
                vpc,
                securityGroupName: 'test-asg-security-group',
                allowAllOutbound: true
            }
        );

        securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'SSH from anywhere');
        securityGroup.addEgressRule(ec2.Peer.anyIpv4(), ec2.Port.allTraffic());

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

        const alb = new elbv2.ApplicationLoadBalancer(this, 'TestAsgALB', {
            vpc,
            vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
            internetFacing: true,
            loadBalancerName: 'test-asg-alb'
        });

        const asg = new autoscaling.AutoScalingGroup(this, 'TestAutoScalingGroup', {
            vpc,
            instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
            machineImage: new ec2.AmazonLinuxImage(),
            desiredCapacity: 1,
            maxCapacity: 3,
            keyName: 'TestAsgEnv',
            associatePublicIpAddress: true,
            vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
            userData,
        });

        asg.addSecurityGroup(securityGroup);

        asg.scaleOnCpuUtilization('CPU50Percent', {
            targetUtilizationPercent: 50
        });

        const listener = alb.addListener('Listener', {
            port: 80
        });

        listener.addTargets('Target', {
            targets: [asg],
            port: 80
        });

        new cdk.CfnOutput(this, 'Application LoadBalancer DNS', { value: alb.loadBalancerDnsName });
    }
}

```


## デプロイ
アプリケーションをビルドします。

```
$ npm run build
```

AWS CloudFormationのスタックをデプロイします。デプロイが完了するまで待ちます。

```
$ cdk deploy
```


## Auto Scalingのテスト
CloudWatch Alarmに2つ登録されていました。もっと細かく設定すれば、条件をカスタマイズできると思います。
<img width="1290" alt="スクリーンショット 2020-06-21 11.10.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/347723/cad5819b-f777-5876-de5f-7be20b187acc.png">


Chaos Engineeringのツールである[Gremlin](https://www.gremlin.com/)でEC2にCPUの負荷(CPU 51%の攻撃を300秒間実行)を与えてみたところ、無事にスケールアウトできました。
(結果のスクリーンショットを取るのを忘れていました… 既に環境は削除済みで、GremlinのFreeアカウントの制約でターゲット数が限られているようなので事象の再現は控えます…)


## おわりに
CloudFormationのyamlファイルよりも、コードで記述できるため作りやすかったですね。
ただ、思いの外「これだ！」といえる情報を見つけられなかったので、公式の仕様のドキュメントを眺めてパラメータを推測しながら、そしてIDEの補完を駆使してゴリ押ししながら作成しました。
EKSの環境もCDKで作れるので、こういうものにもチャレンジしていきたいです。


## 参考
[AWS CDKでAutoScaling環境を作成（Single Stack / Nested Stack）](https://wp-kyoto.net/create-autoscaling-stack-by-aws-cdk-with-single-or-nested-stack-2/)
[Deploy your Auto-Scaling Stack with AWS-CDK](https://medium.com/swlh/deploy-your-auto-scaling-stack-with-aws-cdk-abae64f8e6b6)
[Gremlinを使ってCPU負荷を注入する](http://blog.serverworks.co.jp/tech/2020/02/16/gremlin/)