Step Functionsから、EC2上でコマンドを実行する

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

# 概要

- StateMachineでEC2のコマンドを実行する場合は、Systems Managerの[SendCommand](https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_SendCommand.html)を使用
    - 対象のEC2には`AmazonSSMManagedInstanceCore`を付与
- SendCommandは実行のみで、完了を待たない
    - そのため、[GetCommandInvocation](https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetCommandInvocation.html)で状態をチェックし、処理が終わるまでループする

コールバックを待つ方法もあるようですが、今回は上記の方法になります。

# 参考

https://tech.nri-net.com/entry/20_types_of_ssm

https://repost.aws/ja/questions/QUqwhQMLAuRhSxbuRCLvX48Q/ssm-sendcommand%E3%81%AE%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%97%E3%81%91%E5%8F%96%E3%82%8B%E6%96%B9%E6%B3%95

https://medium.com/@mahmoudabdelrrahman/aws-step-functions-systems-manager-for-multi-ec2-management-616b6c9b17d1

# ソース
概要の仕組みを作成するCloudFormationテンプレートは以下になります。
今回動かすコマンドは`bash /home/ssm-user/scripts/start.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
                  - ssm:GetCommandInvocation
                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
            Parameters:
              DocumentName: AWS-RunShellScript
              DocumentVersion: "1"
              InstanceIds: 
                - !Ref InstanceId
              MaxConcurrency: "50"
              MaxErrors: "50"
              Parameters:
                workingDirectory:
                  - ""
                executionTimeout:
                  - "3600"
                commands:
                  - "bash /home/ssm-user/scripts/start.sh"
              TimeoutSeconds: 60
            ResultPath: $.SendCommandOut
            Next: Wait
          Wait: 
            Type: Wait
            Seconds: 5
            Next: GetCommandInvocation
          GetCommandInvocation: 
            Type: "Task"
            Parameters: 
              CommandId.$: $.SendCommandOut.Command.CommandId
              InstanceId: !Ref InstanceId
            Resource: arn:aws:states:::aws-sdk:ssm:getCommandInvocation
            ResultPath: $.GetCommandInvocationOut
            Next: Is InProgress
          Is InProgress: 
            Type: Choice
            Choices: 
            - Variable: $.GetCommandInvocationOut.Status
              StringEquals: "InProgress"
              Next: Wait
            Default: Success
          Success: 
            Type: Succeed
```
作成されるState Machineは以下になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/c0906387-225b-3a0b-e438-4ac4ea5ad810.png)

1. SendCommandToEc2でコマンドを実行
1. 数秒待機
1. GetCommandInvocationで、1.の状況を取得
1. 3.の結果が実行中の場合、2.に遷移。それ以外は正常終了

# 実行
## 実行前
概要にも書いたとおり、対象のEC2にはAmazonSSMManagedInstanceCoreを付与しておきます。
また、EC2の中に以下のファイルを作っておきます。

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

exit 0
```
:::note
echoを書き込むファイルout.txtは、実行されたかのチェックのために作っているだけです。確認の際、内部から`tail -f out.txt`で監視していました。
不要であれば削除してください。
:::

## State Machine実行結果
実行した結果は以下です。7秒のSleepがかかっており、5秒間隔でチェックしているので、Wait->GetCommandInvocationが2回実行されているのがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/ee41d658-e2ee-9c8a-e4b4-289871cf49e3.png)

コマンドの結果もStatusで確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/0f47e5fa-ceb9-0b71-67d3-a1d9d0250663.png)

エラーも確認できるか確認します。bashを修正して`exit 1`にします。

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

- exit 0
+ exit 1
```
出力を見るとStatusがFailedであり、エラーがStep Functions側で確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/71f901a3-5a32-50fc-2d37-a8394891bc4b.png)

# おわりに
今回はStep FunctionsからEC2上のコマンドを実行し、結果を確認するStateMachineを作ってみました。
EC2側のコマンドは最小限で済みますが、Step Functionsが若干複雑なものになります。

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