AWS Lambdaでなんちゃってオートスケーリング

現在運用中のELB配下にあるEC2インスタンスの負荷が高くなってきたため、負荷の高い時間帯のみEC2インスタンスを追加してELBに登録させるという処理をLambdaを使って行った際の備忘録です。

※運用中のELBはNLBですが、ALBでもだいたい同じだと思います。

・負荷の高い時間帯になったら：EC2インスタンスの起動＞ELBへの登録
・負荷の低い時間帯になったら：ELBから削除＞EC2インスタンスの停止

## 1. ポリシーの作成(EC2インスタンスの起動・停止用)
「IAM > ポリシー > ポリシーの作成」に進みます。

サービスからEC2を選択して下記アクションを許可します。
```
ec2:Describe
ec2:Start
ec2:Stop
```

```
ポリシー名：Auto_EC2-Policy
```
※分かりやすい適当な名前を入力して保存

## 2. ポリシーの作成(ELBへの登録・削除用)
再度「IAM > ポリシー > ポリシーの作成」に進みます。

サービスからELB v2を選択して下記アクションを許可します。
```
RegisterTargets
DeregisterTargets
```

```
ポリシー名：Auto_ELB-Policy
```
※ポリシー名に分かりやすい適当な名前を入力して保存

## 3. ロールの作成(EC2インスタンスの起動・停止用)
「IAM > ロール > ロールの作成」に進みます。

下記を選択して次へ
```
信頼されたエンティティタイプ：AWSのサービス
ユースケース：Lambda
```

```
許可を追加で「Auto_EC2-Policy」を選択して次へ
```

```
ロール名：Auto_EC2-Role
```
※分かりやすい適当な名前を入力して保存

## 4. ロールの作成(ELBへの登録・削除用)
再度「IAM > ロール > ロールの作成」に進みます。

下記を選択して次へ
```
信頼されたエンティティタイプ：AWSのサービス
ユースケース：Lambda
```

```
許可を追加で「Auto_ELB-Policy」を選択して次へ
```

```
ロール名：Auto_ELB-Role
```
※分かりやすい適当な名前を入力して保存

## 5. Lambdaの設定(EC2インスタンスの起動用)

「Lambda > 関数 > 関数の作成」に進みます。

下記を入力・選択して次へ
```
関数名：EC2_Start_Service (※分かりやすい適当な名前を入力)
ランタイム：Node.js 18.xを選択
アーキテクチャ：x86_64
```

```
デフォルトの実行ロールの変更：既存のロールを使用する
既存のロール：Auto_EC2-Role
```

コードのindex.mjsタブに下記を入力

```
import { EC2Client, DescribeInstancesCommand, StartInstancesCommand, StopInstancesCommand } from '@aws-sdk/client-ec2';

const INSTANCE_ID = ['インスタンスID1', 'インスタンスID2']; 

const ec2 = new EC2Client({
    region: "ap-northeast-1",
});

export const handler = async (event) => {
    await ec2.send(new StartInstancesCommand({ InstanceIds: INSTANCE_ID }));

    let message = INSTANCE_ID.length + '台のインスタンスが起動されました。( ';
    message += INSTANCE_ID.join(" / ") + ' )';

    return { message };
};
```

## 6. Lambdaの設定(EC2インスタンスの停止用)

「Lambda > 関数 > 関数の作成」に進みます。

下記を入力・選択して次へ

```
関数名：EC2_Stop_Service (※分かりやすい適当な名前を入力)
ランタイム：Node.js 18.xを選択
アーキテクチャ：x86_64
```

```
デフォルトの実行ロールの変更：既存のロールを使用する
既存のロール：Auto_EC2-Role
```

コードのindex.mjsタブに下記を入力

```
import { EC2Client, DescribeInstancesCommand, StartInstancesCommand, StopInstancesCommand } from '@aws-sdk/client-ec2';

const INSTANCE_ID = ['インスタンスID1', 'インスタンスID2']; 

const ec2 = new EC2Client({
    region: "ap-northeast-1",
});

export const handler = async (event) => {
    await ec2.send(new StopInstancesCommand({ InstanceIds: INSTANCE_ID }));

    let message = INSTANCE_ID.length + '台のインスタンスが停止されました。( ';
    message += INSTANCE_ID.join(" / ") + ' )';

    return { message };
};
```

## 7. Lambdaの設定(ELBへの登録用)

「Lambda > 関数 > 関数の作成」に進みます。

下記を入力・選択して次へ
```
関数名：ELB_Register_Service (※分かりやすい適当な名前を入力)
ランタイム：Node.js 18.xを選択
アーキテクチャ：x86_64
```

```
デフォルトの実行ロールの変更：既存のロールを使用する
既存のロール：Auto_ELB-Role
```

コードのindex.mjsタブに下記を入力

```
import { ElasticLoadBalancingV2Client, RegisterTargetsCommand , DeregisterTargetsCommand } from '@aws-sdk/client-elastic-load-balancing-v2';

const ARN = [
    {
        arn: 'ターゲットグループ1',
        ec2: [
            {
                id: 'インスタンスID1',
                port: 80
            },
            {
                id: 'インスタンスID2',
                port: 80
            }
        ]
    },
    {
        arn: 'ターゲットグループ2',
        ec2: [
            {
                id: 'インスタンスID1',
                port: 443
            },
            {
                id: 'インスタンスID2',
                port: 443
            }
        ]
    }
];

const elbv2 = new ElasticLoadBalancingV2Client({
    region: "ap-northeast-1",
});

export const handler = async (event) => {
    let message = "RegisterTargets : ";

    for (let i = 0; i < ARN.length; i++) {
        if (i > 0) message += ' / ';
        message += ARN[i].arn + ' -> ';

        let input = {
            TargetGroupArn: ARN[i].arn, 
            Targets: []
        };

        const ec2 = ARN[i].ec2;

        message += '[ ';

        for (let j = 0; j < ec2.length; j++) {
            input.Targets.push(
                { 
                    Id: ec2[j].id,
                    Port: ec2[j].port
                }
            );

            if (j > 0) message += ', ';
            message += ec2[j].id+':'+ec2[j].port;
        }

        message += ' ]';

        await elbv2.send(new RegisterTargetsCommand(input));
    }

    return { message };

};
```

※ターゲットグループ(ARN)は「EC2 > ロードバランサー > 対象のLB名 > リスナー」にあるターゲットグループ名をクリックするとARNの詳細を確認できます。

## 8. Lambdaの設定(ELBからの削除用)

「Lambda > 関数 > 関数の作成」に進みます。

下記を入力・選択して次へ
```
関数名：ELB_Deregister_Service (※分かりやすい適当な名前を入力)
ランタイム：Node.js 18.xを選択
アーキテクチャ：x86_64
```

```
デフォルトの実行ロールの変更：既存のロールを使用する
既存のロール：Auto_ELB-Role
```

コードのindex.mjsタブに下記を入力

```
import { ElasticLoadBalancingV2Client, RegisterTargetsCommand , DeregisterTargetsCommand } from '@aws-sdk/client-elastic-load-balancing-v2';

const ARN = [
    {
        arn: 'ターゲットグループ1',
        ec2: [
            {
                id: 'インスタンスID1',
                port: 80
            },
            {
                id: 'インスタンスID2',
                port: 80
            }
        ]
    },
    {
        arn: 'ターゲットグループ2',
        ec2: [
            {
                id: 'インスタンスID1',
                port: 443
            },
            {
                id: 'インスタンスID2',
                port: 443
            }
        ]
    }
];

const elbv2 = new ElasticLoadBalancingV2Client({
    region: "ap-northeast-1",
});

export const handler = async (event) => {
    let message = "DeregisterTargets : ";

    for (let i = 0; i < ARN.length; i++) {
        if (i > 0) message += ' / ';
        message += ARN[i].arn + ' -> ';

        let input = {
            TargetGroupArn: ARN[i].arn, 
            Targets: []
        };

        const ec2 = ARN[i].ec2;

        message += '[ ';

        for (let j = 0; j < ec2.length; j++) {
            input.Targets.push(
                { 
                    Id: ec2[j].id,
                    Port: ec2[j].port
                }
            );

            if (j > 0) message += ', ';
            message += ec2[j].id+':'+ec2[j].port;
        }

        message += ' ]';

        await elbv2.send(new DeregisterTargetsCommand(input));
    }

    return { message };

};
```

※ターゲットグループ(ARN)は「EC2 > ロードバランサー > 対象のLB名 > リスナー」にあるターゲットグループ名をクリックするとARNの詳細を確認できます。

## 9. テストの実施
それぞれ設定が終わったらコードタブにある「Test」を実行してエラーが無いか確認します。

※実際に実行されるので慎重に行いましょう。


## 10. トリガーの登録(EC2インスタンスの起動用)

「Lambda > 関数 > EC2_Start_Service」にアクセスして「トリガーを追加」をクリックします。

下記を入力・選択して追加をクリック
```
トリガーの設定：EventBridge (CloudWatch Events)を選択
既存のルール：EC2_Start_Service (作成したEC2インスタンス起動用関数名)
スケジュール式：cron(0 23 * * ? *)
```
※毎日23時にEC2インスタンスの起動を実施

## 11. トリガーの登録(ELBへの登録用)

「Lambda > 関数 > ELB_Register_Service」にアクセスして「トリガーを追加」をクリックします。

下記の入力・選択して追加をクリック
```
トリガーの設定：EventBridge (CloudWatch Events)を選択
既存のルール：ELB_Register_Service (作成したELB登録用関数名)
スケジュール式：cron(50 23 * * ? *)
```
※毎日23時50分にELBへの登録を実施

## 12. トリガーの登録(ELBからの削除用)

「Lambda > 関数 > ELB_Deregister_Service」にアクセスして「トリガーを追加」をクリックします。

下記を入力・選択して追加をクリック
```
トリガーの設定：EventBridge (CloudWatch Events)を選択
既存のルール：ELB_Deregister_Service (作成したELB削除用関数名)
スケジュール式：cron(10 6 * * ? *)
```
※毎日6時10分にELBからの削除を実施

## 13. トリガーの登録(EC2インスタンスの停止用)

「Lambda > 関数 > EC2_Stop_Service」にアクセスして「トリガーを追加」をクリックします。

下記を入力・選択して追加をクリック
```
トリガーの設定：EventBridge (CloudWatch Events)を選択
既存のルール：EC2_Stop_Service (作成したEC2インスタンス停止用関数名)
スケジュール式：cron(0 7 * * ? *)
```
※毎日7時にEC2インスタンスの停止を実施


これで
23時00分：EC2インスタンスが2台が起動
23時50分：ELBへEC2インスタンス2台を登録
06時10分：ELBからEC2インスタンス2台を削除
07時00分：EC2インスタンス2台を停止
という時限式なんちゃってオートスケーリングの完成です！

※参考
https://qiita.com/mighty-n/items/e667f4380c372e5cef40
https://qiita.com/mighty-n/items/f6dd823a26a6809dca80
https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/ec2/
https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/elastic-load-balancing-v2/