AWS LambdaでSSM RunCommandを実行する（AWS SDK for JavaScript v3）

# 概要
LambdaからSSMのRunCommandを実行する処理の作成方法について記述します。言語は`JavaScript`でSDKのv3（バージョン3）で書いてあります。

v3では、サービスのインスタンスを作成して、非同期で`send`メソッドを呼び出すというのが全サービス共通の書き方になります。`send`メソッドはPromise型を返します。何のアクションを行うかは`send`メソッドの引数の型によって判断されます。

## 公式ドキュメント

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/

# サンプルプログラム

## 処理の流れ

SSMClient生成 → SendCommandCommand生成 → send実行 → ListCommandsCommand生成 → send実行 （RunCommandが終了するまで繰り返し） → send実行 → send実行 → ...

注意点としては、RunCommandを実行した後その処理がサーバーで正常終了したのか異常終了したのかすぐにわからない点です。RunCommandの`send`メソッドの実行直後の`then`ではステータス[^1]は`Pending`や`In Progress`になることが多いと思います。その為、RunCommand実行後その実行したコマンドのrunCommandIdを退避し、ステータスがSuccessかそれ以外になるまで繰り返し実行結果を取得する処理[^2]を入れています。

```javascript:handler.js
'use strict';

const { SSMClient, SendCommandCommand } = require('@aws-sdk/client-ssm');

const startSsmRunCommandHandler = async (event) => {
    // SSMインスタンスを生成
    const client = new SSMClient({ region: process.env.AWS_REGION });
    
    // RunCommandのコマンドインスタンスを生成
    const commandRunCommand = new SendCommandCommand({
        DocumentName: 'AWS-RunShellScript',
        InstanceIds: ['i-01234567'],
        Parameters: {
            commands: ["touch test"],
            workingDirectory: ['/root']
        },
    });

    // 実行したRunCommandのIDを格納するための変数
    let runCommandId;
    // RunCommandを実行
    await client
        .send(commandRunCommand)
        .then((data) => {
            console.info(`SSM RunCommand Success: ${data}`);
            // RunCommandのIDを取得します。
            runCommandId = data.Command.CommandId;
        })
        .catch((error) => {
            console.error(`SSM RunCommand Failed: ${error.message}`);
            throw error;
        });

    // 実行したRunCommandのリスト取得のコマンドインスタンを生成
    const commandList = new ListCommandsCommand({
        CommandId: runCommandId
    });

    // ListCommandを実行する関数
    const runCommandStatusGetFunction = async () => {
        await client
            .send(commandList)
            .then((data) => {
                console.info(`RunCommand List: ${JSON.stringify(data)}`);
                const cmd = data.Commands[0];
                const status = cmd.Status;
                switch (status) {
                    case 'Pending':
                    case 'InProgress':
                    case 'Delayed':
                        // Pending, InProgress, Delayedの場合はエラーをスローして再実行する。
                        throw new Error('ListCommand Re-run.');
                    case 'Success':
                        console.info(`SSM RunCommand Finished.`);
                    default:
                        // 上記ステータス以外はlambdaを異常終了させる。
                        context.fail(new Error(`SSM RunCommand Failed.`));
                }
            });
    };

    // エラーが発生した場合再実行を行う関数
    const retry = (func, onError) => {
        const _retry = (e) => {
            return onError(e)
              .catch((e) => {
                throw e;
              })
              .then(func)
              .catch(_retry);
        };
        return func().catch(_retry);
    };

    // 実行結果が特定できるまで繰り返し処理する。
    await retry(runCommandStatusGetFunction, (e) => {
        console.error(e);
        return new Promise((resolve) => setTimeout(resolve, 2000));
    });
};

module.exports = {
    startSsmRunCommandHandler: startSsmRunCommandHandler
};
```


[^1]:[公式　コマンドのステータスについて](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/monitor-commands.html)

[^2]:[JavaScript で非同期処理のリトライ](https://qiita.com/hoto17296/items/614a809939007813761b)