Step Functionsから、EC2上でコマンドを実行する（コールバックを待つ版）

# はじめに
以前、Step Functionsを使って、EC2上でコマンドを実行する方法の一つを記事としてまとめました。

https://qiita.com/a_b_/items/85a2c63bf8d577b490d8

上記記事ではEC2上のsuccess/failureを、StateMachine側からスリープを挟んでチェックしていました。

今回はEC2側からsuccess/failureを返す方法を記事にします。

# 概要

- StateMachineでEC2のコマンドを実行する方法として、Systems Managerの[SendCommand](https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_SendCommand.html)を使用
    - EC2側からのコールバックを待つようにします
    - StateMachie側からTaskTokenを渡します
        - コールバックを待つ設定にすると、コンテキストオブジェクトに追加されます
- EC2側からは、`send-task-success`または`send-task-failure`を実行し応答
    - その際にTaskTokenが必要です
    - 対象のEC2には以下を付与します
        - アクション：`states:SendTaskSuccess`、`states:SendTaskFailure`の許可
        - ポリシー：`AmazonSSMManagedInstanceCore`
            - SendCommandの実行に必要です
# 参考

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/input-output-contextobject.html

https://qiita.com/napam/items/354f86ea0342399ab1d8

https://awscli.amazonaws.com/v2/documentation/api/latest/reference/stepfunctions/send-task-success.html

https://awscli.amazonaws.com/v2/documentation/api/latest/reference/stepfunctions/send-task-failure.html

# ソース
概要の仕組みを作成するCloudFormationテンプレートは以下になります。
stop.shを実行するコマンドを指定しています。

```yaml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  InstanceId:
    Type: String

Resources:
  SMRole: 
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - states.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: "/"
      Policies:
        - PolicyName: allowSsm
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ssm:SendCommand
                Resource: '*'

  SendCommandToEc2StateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      RoleArn: !GetAtt SMRole.Arn
      Definition:
        StartAt: SendCommandToEc2
        States: 
          SendCommandToEc2: 
            Type: Task
            Resource: arn:aws:states:::aws-sdk:ssm:sendCommand.waitForTaskToken
            Parameters: 
              DocumentName: AWS-RunShellScript
              DocumentVersion: "1"
              InstanceIds: 
                - !Ref InstanceId
              MaxConcurrency: "50"
              MaxErrors: "50"
              Parameters: 
                workingDirectory: 
                - ""
                executionTimeout: 
                - "3600"
                commands.$:
                  "States.Array( States.Format( 'bash /home/ssm-user/scripts/stop.sh {}', $$.Task.Token ))"
              TimeoutSeconds: 60
            ResultPath: $.SendCommandOut
            Next: Success
            Catch: 
            -
              ErrorEquals: 
              - States.ALL
              Next: Fail
          Success: 
            Type: Succeed
          Fail: 
            Type: Fail
```
作成されるState Machineは以下になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/94ab90b5-94af-c647-8c5f-cbb470b26724.png)

- SendCommandToEc2でコマンドを実行
    - `SendTaskSuccess`が帰ってきたらSuccessに遷移
    - `SendTaskFailure`を含む例外発生時はFailに遷移

## コマンド部分について補足

```yaml
commands.$:
  "States.Array( States.Format( 'bash /home/ssm-user/scripts/stop.sh {}', $$.Task.Token ))"
```

- `commands`は配列で指定する必要があるため`States.Array`を使います
    - `$`を使う場合は`[ ]`を配列として使えないです
- `stop.sh`の中で`send-task-success`/`send-task-failure`を使う際にTaskeTokenを使うので、引数にTaskTokenを指定します
    - `States.Format`を使ってコマンドを生成しています

# 実行

## success

EC2上で実行するファイルは以下になります。引数にTaskeTokenを指定して実行し、`aws stepfunctions send-task-success`で用いて応答します。

```shell:/home/ssm-user/scripts/stop.sh
echo "Start Scripts:"`date '+%Y-%m-%d %H:%M:%S'` | tee -a /home/ssm-user/scripts/out.txt
sleep 7

aws stepfunctions send-task-success --task-token "$1" --task-output {}
echo "Stop Scripts:"`date '+%Y-%m-%d %H:%M:%S'` | tee -a /home/ssm-user/scripts/out.txt

exit 0
```

正常にSuccessに遷移しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/3f82f59d-11d8-f22f-57ad-7eb1cc78ee96.png)

## failure

`aws stepfunctions send-task-failure`で応答して、例外扱いになるのかも確認するため、ファイルを一部修正します。

```diff_shell:/home/ssm-user/scripts/stop.sh
echo "Start Scripts:"`date '+%Y-%m-%d %H:%M:%S'` | tee -a /home/ssm-user/scripts/out.txt
sleep 7

- aws stepfunctions send-task-success --task-token "$1" --task-output {}
+ aws stepfunctions send-task-failure --task-token "$1"
echo "Stop Scripts:"`date '+%Y-%m-%d %H:%M:%S'` | tee -a /home/ssm-user/scripts/out.txt

exit 0
```
実行結果から、正常にFailに遷移しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/c41a4f66-6f4c-b248-5593-89582ae663f4.png)

# おわりに
今回はStep FunctionsからEC2上のコマンドを実行し、結果を待つStateMachineを作ってみました。
[以前の記事](https://qiita.com/a_b_/items/85a2c63bf8d577b490d8)と比べて、かなりシンプルになりました。その分EC2側で配慮が必要になります。
どちらを使うかは、その時々の状況に合わせて選べばよいかと思います。

この記事がどなたかのお役に立てれば幸いです。