ECSクラスターオートスケーリング(CAS)を使ってみた

# はじめに
2019/11に追加されたECSクラスターオートスケーリング(CAS)がなかなか使えそうだったので試してみた。

# ECSクラスターオートスケーリング(CAS)とは
2019/11のre:Invent 2019で発表された、ECSのスケーリング管理を楽にしてくれる仕組みです。

>本日、AWS ECS Cluster Auto Scalingを発表します。この機能は、スケールアウトを高速化し信頼性を向上させる、クラスター内の空きキャパシティ管理の提供と、スケールイン時に終了されるインスタンスの自動管理を提供し、クラスターの自動スケーリングをより使いやすいものにします。

引用元：[新機能 – AWS ECS Cluster Auto ScalingによるECSクラスターの自動スケーリング](https://aws.amazon.com/jp/blogs/news/aws-ecs-cluster-auto-scaling-is-now-generally-available/)

これを使うとタスクやサービスとそのホストとなるコンテナインスタンスのスケーリングをまとめてやってくれます。

>つい最近まで、ECS クラスター内での EC2 インスタンスの数を、タスクとサービスに合わせてスケーリングさせようとすると、難しい問題の発生することがありました。

引用元：[Amazon ECS クラスターの Auto Scaling を深く探る](https://aws.amazon.com/jp/blogs/news/deep-dive-on-amazon-ecs-cluster-auto-scaling/)

詳しくは↓でわかりやすく説明してくれてます。
[Capacity Providerとは？ECSの次世代スケーリング戦略を解説する](https://dev.classmethod.jp/articles/regrwoth-capacity-provider/)

# 構築
公式ブログを参考に実際に構築していきます。
公式ブログの手順は説明がなかったり、値が誤っていたりするので、適宜補足してあります。
公式の手順だとAmazon Linux2のコンテナでコマンドを実行させるだけなのですが、せっかくなのでWebサーバを動かしてみます。

### 0. 事前準備
ECS自体利用したことがない場合はまず3つのIAMロールから作成する必要があります。

**1. ECSコンテナインスタンス用のロール**
下記の「コンテナインスタンスの`ecsInstanceRole`IAMロールを作成するには」を参考にロールを作成
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/instance_IAM_role.html

**2. ECSタスク用のロール**
下記の「タスク用の IAM ロールを作成するには」を参考にロールを作成
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task-iam-roles.html

**3. ECSタスク実行用のロール**
下記の「Creating the task execution IAM role」を参考にロールを作成
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_execution_IAM_role.html



### 1. 起動設定の作成
まずは、オートスケーリンググループがどんなインスタンスを立ち上げるかを指定する「起動設定」を作成します。

#### 1-1. 起動設定の定義ファイルを作成
ローカルで下記ファイルを作成します。

```json:demo-launchconfig.json
{
    "LaunchConfigurationName": "demo-launchconfig",
    "ImageId": "ami-0bea16de11596d6ba",
    "KeyName": "xxx_ec2_key",
    "SecurityGroups": [
        "sg-XXXXXXXXXXXXXXX"
    ],
    "InstanceType": "t2.micro",
    "BlockDeviceMappings": [
        {
            "DeviceName": "/dev/xvdcz",
            "Ebs": {
                "VolumeSize": 30,
                "VolumeType": "gp2",
                "DeleteOnTermination": true,
                "Encrypted": true
                }
        }
    ],
    "InstanceMonitoring": {
        "Enabled": false
    },
    "IamInstanceProfile": "arn:aws:iam::XXXXXXXXXXX:instance-profile/ecsInstanceRole",
    "AssociatePublicIpAddress": true
}
```

補足：
１. `ImageId`はECS最適化AMIを利用するか、それ以外のAMIを利用する場合はECSコンテナエージェントのインストールが必要になります。今回はECS最適化AMIを利用します。
東京リージョンの最新のECS最適化AMIのAMI IDは下記から取得できます。
https://ap-northeast-1.console.aws.amazon.com/systems-manager/parameters/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id/description?region=ap-northeast-1#

2. `KeyName`はインスタンスに埋め込むキーペア名を指定してください。SSHしない場合なくても大丈夫です。

3. `SecurityGroups`は事前にマイIPの22と80を許可で作成したものを指定してください。

4. `IamInstanceProfile`はこのインスタンスにアタッチするIAMロールを指定します。ここでは手順0で作成した`ecsInstanceRole`の"インスタンスプロファイルARN"を指定します。
※公式ブログではロールARNを指定していますが誤りです、注意してください。

#### 1-2. ユーザーデータを作成
ローカルで下記ファイルを作成します。

```bash:demo-userdata.sh
#!/bin/bash
echo ECS_CLUSTER=demo-cluster >> /etc/ecs/ecs.config
```

補足：
１. 先頭の`#!/bin/bash` (シバン)は必須です。
2. `echo ECS_CLUSTER=demo-cluster >> /etc/ecs/ecs.config`はECSコンテナインスタンスに必須の設定です。この後作成するクラスター名に紐付きます。
3. もし起動時に実行させたいコマンドがある場合3行目以降に書きます。ECS最適化AMIにはAWC CLIが入っていないので必要な場合インストールのコマンドから書く必要があります。

#### 1-3. 起動設定の作成実行
1-1. 1-2. で作成したファイルがある場所で下記コマンドを実行します。

```bash:Terminal
aws autoscaling create-launch-configuration --cli-input-json file://demo-launchconfig.json --user-data file://demo-userdata.sh
```

### 2. オートスケーリンググループの作成

次に先ほどの設定でインスタンスを立ち上げるオートスケーリンググループを作ります。

#### 2-1. オートスケーリンググループの設定ファイルを作成
AWS CLIで指定する設定内容のJSONを作成します。

```json:demo-asgconfig.json
{
    "LaunchConfigurationName": "demo-launchconfig", 
    "MinSize": 0,
    "MaxSize": 2,
    "DesiredCapacity": 0,
    "DefaultCooldown": 300,
    "AvailabilityZones": [ 
        "ap-northeast-1a" ], 
    "HealthCheckType": "EC2", 
    "HealthCheckGracePeriod": 300, 
    "VPCZoneIdentifier": "subnet-abcd1234", 
    "TerminationPolicies": [ 
        "DEFAULT" 
    ],
    "NewInstancesProtectedFromScaleIn": true, 
    "ServiceLinkedRoleARN": "arn:aws:iam::111122223333:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling"
} 
```

補足：
1. `MaxSize`はあまり大きくすると消し忘れた時のリスクがあるので`2`にしておきます。
2. `AbailabilityZones`は東京リージョン`ap-northeast-1a`を指定します。
3. `VPCZoneIdentifier`は自分のアカウント内のVPCにあるサブネットのIDを指定します。
4. `ServiceLinkedRoleARN`は`111122223333`の部分を自分のアカウントIDに置き換えます。

#### 2-2. オートスケーリンググループ作成実行
2-1. で作成したJSONのある場所で下記コマンドを実行します。

```bash:Terminal
aws autoscaling create-auto-scaling-group --auto-scaling-group-name demo-asg --cli-input-json file://demo-asgconfig.json
```

### 4. キャパシティプロバイダーの作成

オートスケーリンググループとクラスターを紐付けるキャパシティプロバイダーを作成します。

#### 4-1. キャパシティプロバイダーの設定ファイルを作成
AWS CLIで指定する設定内容のJSONを作成します。

```json:demo-capacityprovider.json
{
    "name": "demo-capacityprovider", "autoScalingGroupProvider": {
    "autoScalingGroupArn": "arn:aws:autoscaling:ap-northeast-1:XXXXXXXXXX:autoScalingGroup:XXXXXX-XXXXXXXX-XXXXXXXX:autoScalingGroupName/demo-asg",
            "managedScaling": {
                "status": "ENABLED",
                "targetCapacity": 100,
                "minimumScalingStepSize": 1,
                "maximumScalingStepSize": 100
            },
            "managedTerminationProtection": "ENABLED"
    }
}
```

補足：
1. `autoScalingGroupArn`は手順2で作成したものオートスケーリンググループのARNを指定します。

#### 4-2. キャパシティプロバイダー作成実行
4-1. で作成したJSONのある場所で下記コマンドを実行します。

```bash:Terminal
aws ecs create-capacity-provider --cli-input-json file://demo-capacityprovider.json
```

補足：
1. `AWS CLI`のバージョンが古いと`create-capacity-provider`サブコマンドに対応していないので、コマンドが使えない場合は`sudo pip install --upgrade awscli`でバージョンアップしてください。

### 5. クラスターの作成

#### 5-1. クラスター作成実行
下記コマンドでECSのクラスターを作成します。
オプションで先ほどのキャパシティプロバイダーも紐付けます。

```bash:Terminal
aws ecs create-cluster --cluster-name demo-cluster --capacity-providers demo-capacityprovider --default-capacity-provider-strategy capacityProvider=demo-capacityprovider,weight=1
```

#### 5-2. クラスターのステータスを確認
下記コマンドの結果に`"status": "ACTIVE"`が含まれていれば、作成が終わっています。

```bash:Terminal
aws ecs describe-clusters --clusters demo-cluster --include ATTACHMENTS
```

### 5. タスク定義の作成
ECS上で実行させるコンテナの情報を「タスク定義」として作成します。

#### 5-1. タスク定義の設定ファイルを作成
AWS CLIで指定する設定内容のJSONを作成します。

```json:demo-web-task.json
{
    "family": "demo-web-task",
    "taskRoleArn": "arn:aws:iam::XXXXXXXXXX:role/ecsTaskRole",
    "executionRoleArn": "arn:aws:iam::XXXXXXXXX:role/ecsTaskExecutionRole",
    "networkMode": "bridge",
    "containerDefinitions": [
        {
            "name": "web",
            "image": "nginx:latest",
            "memory": 512,
            "essential": true,
            "portMappings": [
                {
                    "containerPort": 80,
                    "hostPort": 80,
                    "protocol": "tcp"
                },
                {
                    "containerPort": 443,
                    "hostPort": 443,
                    "protocol": "tcp"
                }
            ]
        }],
    "requiresCompatibilities": ["EC2"] 
}
```

補足：
1. `taskRoleArn`には手順0で作成したタスクロールのARNを指定します。
2. `executionRoleArn`には手順0で作成したタスク実行ロールのARNを指定します。
3. `containerDefinitions`でコンテナについて定義します。今回はNGINXイメージをメモリ1GiBで実行し、ホストの80ポートをコンテナの80ポートにマッピングします。

#### 5-2. タスク定義作成実行
5-1. で作成したJSONのある場所で下記コマンドを実行します。

```bash:Terminal
aws ecs register-task-definition --cli-input-json file://demo-web-task.json
```

### 6. サービスの作成

#### 6-1. サービス定義ファイルを作成
AWS CLIで指定する設定内容のJSONを作成します。

```json:demo-web-service.json
{
    "cluster": "demo-cluster",
    "serviceName": "demo-web-service",
    "taskDefinition": "demo-web-task:1",
    "desiredCount": 1,
    "capacityProviderStrategy": [
        {
            "capacityProvider": "demo-capacityprovider",
            "weight": 1,
            "base": 1
        }
    ]
}
```

#### 6-2. サービス作成実行
6-1. で作成したJSONのある場所で下記コマンドを実行します。

```bash:Terminal
aws ecs create-service --cli-input-json file://demo-web-service.json
```

### 7. 動作確認
これで、キャパシティプロバイダによりサービスを実行するために希望インスタンスサイズが1以上に変更され、インスタンスが立ち上がり、その中でNGINXのコンテナが立ち上がるはず。
初回のインスタンスが立ち上がるまでは少し時間がかかるので、マネジメントコンソールでタスクが`PROVISIONING`になっていることとキャパシティプロバイダーの希望するサイズが1以上になっていることを確認し、5分ほど待つ。

![スクリーンショット 2020-08-22 17.18.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140013/050bbd9d-6e1e-550f-e669-12c43a7aef12.png)

![スクリーンショット 2020-08-22 17.18.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140013/65fbc746-24aa-2d92-e08e-3f283991de85.png)

タスクが`RUNNING`になったら、EC2インスタンスのパブリックDNSの80番ポートへアクセスするとNGINXでホストされているHTMLが表示される。
![スクリーンショット 2020-08-22 18.32.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/140013/b46d5b7e-33b3-6b4f-f308-936f9a3296fe.png)

## おわりに
正直、Webサービスの場合ELBかRoute53でエンドポイントをつけないとスケーリングする意味がないのでそこまでやりたかったんですが、コンテナインスタンスの定義で結構はまって時間もかかってしまったので、今回はNGINXの画面が見られてひとまず満足したので、続きは後日。。。正直ユーザーデータとかでいろいろ専用の設定を入れたいという要件がなければ、`capacityProvider`は`FARGATE`にしたほうが大分楽そう。