EC2を自動起動停止させるLambdaをNode.js18に移植した

## はじめに

EC2を自動起動停止させるLambdaをたぶんNode.js v6かv8のころから使ってきました。
Node.js v14まではコードを修正せずに動いたのですが、v18に上げたらそのままでは動かなかったので、やったことを残しておきます。

## Node.js v14まで

### ソースコード

元々使っていたコードは下記です。

```typescript:index.js
var aws = require('aws-sdk');
var ec2 = new aws.EC2();

exports.handler = (event, context, callback) => {
    'use strict';

    let filter_status;
    switch (event.type) {
        case 'start':
            filter_status = 'stopped';
            break;
        case 'stop':
            filter_status = 'running';
            break;
        default:
            callback({ 'exception': 'unexpected_argument' });
            return;
    }

    const tagValue = event.tagvalue || process.env.TAG_VALUE;

    ec2.describeInstances({
        'Filters': [
            {
                Name: 'tag:' + process.env.TAG_TITLE,
                Values: [tagValue]
            },
            {
                Name: 'instance-state-name',
                Values: [filter_status]
            }
        ]
    }, (err, data) => {
        if (err) {
            callback(err);
            return;
        }

        if (data.Reservations.length === 0) {
            callback(null, { 'result': 'There is no instance to be operated.' });
            return;
        }

        let target_instance_ids = [];
        data.Reservations.forEach(reservation => {
            reservation.Instances.forEach(instance => {
                target_instance_ids.push(instance.InstanceId);
            });
        });

        if (event.type === 'start') {
            ec2.startInstances({ InstanceIds: target_instance_ids }, instance_operate_callback(callback));
        } else {
            ec2.stopInstances({ InstanceIds: target_instance_ids }, instance_operate_callback(callback));
        }
    });
};

function instance_operate_callback(lambda_callback) {
    return (err, data) => {
        if (err) {
            lambda_callback(err);
            return;
        }
        let operated_instances = data.StartingInstances || data.StoppingInstances;

        let message = '\n' + operated_instances.length + 'instance(s)\n\n';
        operated_instances.forEach(instance => {
            message += '* ' + instance.InstanceId + '\n';
        });
    };
}
```

### 設定

必要な設定は下記です。

* アクセス権限・・・`AmazonEC2FullAccess`
* 環境変数
    * TAG_TITLE・・・`AutoRun`
        * このタグを持つEC2を自動起動停止の対象とする
    * TAG_VALUE・・・`weekday`
        * AutoRunタグの値がこれだったら自動起動させる
* トリガー
    * EventBridge・・・`stop_ec2`
        * スケジュール・・・`cron(05 12 ? * MON-FRI *)`
        * ターゲット1・・・入力定数：
            * ```json
              {
                "type": "stop",
                "tagvalue": "autostop"
              }
              ```
        * ターゲット2・・・入力定数：
            * ```json
              {
                "type": "stop",
                "tagvalue": "weekday"
              }
              ```
    * EventBridge・・・`start_ec2`
        * スケジュール・・・`cron(00 01 ? * MON-FRI *)`
        * ターゲット1・・・入力定数：
            * ```json
              {
                "type": "start"
              }
              ```

### 使い方

自動起動停止させたいEC2に`AutoRun`というタグを設定してください。
値による挙動の違いは下記の通りです。
weekday=自動起動停止, autostop=自動停止のみ, none=何もしない

上記は手順通りに設定した場合で、EventBridgeの入力定数を変えることで、任意のタグを利用できます。
例えば「このEC2だけ午前4時に起動させたい」という場合は、

```json
{
  "type": "start"
  "tagvalue": "start0400"
}
```

という入力定数を持つトリガーと、このトリガーを持ち午前4時に起動するEventBridgeを作成し、対象のEC2に`AutoRun:start0400`というタグを付与することで実現可能です。

「none=何もしない」に関しては特にそのように実装しているわけではなく、トリガーに使われていない値なら何でも良いし、AutoRunタグ自体を削除するでも良いです。明示的に何もして欲しくないという意思表示のためにnoneを採用しただけです。


## Node.js v18

### v18で変わったこと

AWS SDK for JavaScript v2からAWS SDK for JavaScript v3に変わったらしいです。
importの仕方が変わりました。

元々importではなくrequire使ってましたが、これも`require is not defined in ES module scope, you can use import instead`と言われてしまうので書き換えました。

主な変更点は以下の通りです。

* 必須
    * requireをimportに置き換え、AWS SDK for JavaScript v3を使用しました。
    * AWS SDKのメソッド名をv3に対応したものに変更しました。
* オプション
    * 非同期操作の処理をasync/awaitを利用して変更しました。(v14以降サポート)
    * カスタムのinstance_operate_callback関数を削除し、AWS SDKのメソッドとasync/awaitを直接利用するようにしました。

### ソースコード

修正後のソースコードは下記です。
index.mjsとpackage.jsonを同じディレクトリに配置してください。

```typescript:index.mjs
import { EC2Client, DescribeInstancesCommand, StartInstancesCommand, StopInstancesCommand } from '@aws-sdk/client-ec2';
const ec2 = new EC2Client();

export const handler = async (event) => {
    'use strict';

    let filter_status;
    switch (event.type) {
        case 'start':
            filter_status = 'stopped';
            break;
        case 'stop':
            filter_status = 'running';
            break;
        default:
            throw new Error('unexpected_argument');
    }


    const tagValue = event.tagvalue || process.env.TAG_VALUE;

    try {
        const data = await ec2.send(new DescribeInstancesCommand({
            Filters: [
                {
                    Name: 'tag:' + process.env.TAG_TITLE,
                    Values: [tagValue]
                },
                {
                    Name: 'instance-state-name',
                    Values: [filter_status]
                }
            ]
        }));

        if (data.Reservations.length === 0) {
            return { result: '操作対象のインスタンスが見つかりません。' };
        }

        let target_instance_ids = [];
        data.Reservations.forEach(reservation => {
            reservation.Instances.forEach(instance => {
                target_instance_ids.push(instance.InstanceId);
            });
        });

        if (event.type === 'start') {
            await ec2.send(new StartInstancesCommand({ InstanceIds: target_instance_ids }));
        } else {
            await ec2.send(new StopInstancesCommand({ InstanceIds: target_instance_ids }));
        }

        let message = '\n' + target_instance_ids.length + '台のインスタンスが操作されました。\n\n';
        target_instance_ids.forEach(instanceId => {
            message += '* ' + instanceId + '\n';
        });

        return { message };
    } catch (err) {
        throw err;
    }
};
```

```json:package.json
{
  "type": "module"
}
```

### 設定

設定はそのまま流用できます。

### 使い方

使い方も同じです。

### テスト手順

Lambda関数の設定画面上、コードの右にある「テスト」タブで即時実行させられます。
※殆どの人には関係ない話ですが、zscaler経由だとテストが実行できません。

1. テストに使うEC2のタグを`AutoRun:autostop-test`に書き換える
1. Lambda関数 > テスト
1. 自動停止テスト
    1. イベントアクションをテスト・・・新しいイベントを作成
    1. イベント名・・・`autostop-test`
    1. イベント共有の設定・・・プライベート
    1. テンプレート - オプション・・・何でも良い。一般>hello-worldとかで可
    1. イベント JSON
        1. ```json
           {
             "type": "stop",
             "tagvalue": "autostop-test"
           }
           ```
    1. 「保存」ボタン押下
    1. 「テスト」ボタン押下
1. 自動起動テスト
    1. イベントアクションをテスト・・・新しいイベントを作成
    1. イベント名・・・`autostart-test`
    1. イベント共有の設定・・・プライベート
    1. テンプレート - オプション・・・`autostop-test`。何でも良い
    1. イベント JSON
        1. ```json
           {
             "type": "start",
             "tagvalue": "autostop-test"
           }
           ```
    1. 「保存」ボタン押下
    1. 「テスト」ボタン押下

保存した`autostop-test`、`autostart-test`は繰り返し使えます。
1. イベントアクションをテスト・・・保存されたイベントを編集
1. イベント名・・・`autostop-test` or `autostart-test`
1. 「テスト」ボタン押下

テストが終わったら、テストに使ったEC2のAutoRunタグを元に戻してください。