AWS Systems Managerを用いてEC2上のコマンドを動かしてみた

こんにちは。streampackのfadoです。厳しい暑さが続いておりますがみなさん、いかがお過ごしでしょうか。まだまだ油断は禁物ですのでうがいと手洗いを忘れないようにしましょう。さて、今回は表題にも書きました通り、`AWS　Systems Manager`を利用し、`AWS Lambda`及び`Amazon API Gateway`と連携したシステムで`Amazon EC2`インスタンス上でコマンドを実行させる方法を簡単に説明していきたいと思います。

#注意事項
* Linuxコマンド及び`AWS Lambda`,`Amazon API Gateway`,`AWS Systems Manager`の知見がある方を対象にしています。
* `AWS Lambda`はPython3で記載しています。
* ffmpegのRTMP送信先は`AWS Elemental MediaLive` -> `AWS Elemental MediaPackage`にしていますがスコープ外ですのでセットアップ等は割愛させていただきます。

#リソース
* Amazon EC2　(以下 EC2)
* AWS Systems Manager（以下 SSM)
* AWS Lambda　(以下 Lambda)
* Amazon API Gateway（以下 API Gateway)
* AWS IAM　(IAM ロールとIAM ポリシー)

#手順
##EC2
* OSは`Amazon Linux 2`を使っています。`AWS Systems Manager`エージェントとffmpegが動く環境であればどのOSでも問題ありません。
* `AmazonSSMManagedInstanceCore`ポリシーがアタッチされた`IAM ロール`を対象`EC2`インスタンスにつけます。
`AmazonSSMManagedInstanceCore`は`EC2`インスタンスが`SSM`のメイン機能を取り扱えるように許可してくれるポリシーです。
* 例ではffmpegコマンドを利用しますので事前にインストールしておきましょう。 
 * https://ffmpeg.org/download.html
* 動画はフリー素材を使っています。
 * http://bbb3d.renderfarming.net/download.html

##SSM
`AWS Systems Manager`コンソールにて「インスタンスとノード」 -> 「マネージドインスタンス」のタブで対象EC2インスタンスが登録されていることを確認します。表示に出ていない場合は`EC2`にアタッチされているロールに`AmazonSSMManagedInstanceCore`ポリシーが漏れている可能性があります。
![スクリーンショット 2020-09-18 1.59.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/1fa29dad-7fc5-6ad6-3486-bb5e47aa38a1.png)

##Lambda
`Lambda`の`IAM ロール`に`AWS Systems Manager`の`ssm:SendCommand`と`ssm:ListCommandInvocations`アクションを利用しているのでこの二つのアクションが含まれるポリシーを付けます。`Lambda`の`IAM ロール`専用のポリシーにしたいので、インランポリシーでの作成になります。

関数コードは次の通りです。

```python:lambda_handler.py
import boto3
import time
import json

apigw = boto3.client('apigateway')
ssm = boto3.client('ssm')

## 対象インスタンスを定義
instance_id = ['i-XXXXXXXXX']

## 実行するコマンドを定義
start_cmd = "/usr/local/bin/ffmpeg -re -stream_loop -1  -i /home/ec2-user/media/bbb_sunflower_1080p_30fps_normal.mp4 -c:v copy -c:a aac -flags +loop-global_header -f flv rtmp://XX.XX.XX.XX:1935/test/live < /dev/null > /tmp/ffmpeg.log 2>&1 &"
stop_cmd = "/usr/bin/killall ffmpeg"

def lambda_handler(event, context):
    try:
        ## API GatewayのURL pathによって実行するコマンドを区別
        api_path = event['path']
        if "start" in api_path:
            cmd_to_execute = {'commands': [start_cmd]}
        elif "stop" in api_path:
            cmd_to_execute = {'commands': [stop_cmd]}
        else:
            return "No matches,exiting..."
        
        ### send-commandを実行
        response = ssm.send_command(
            DocumentName = "AWS-RunShellScript",
            InstanceIds = instance_id,
            Comment = 'Executing ffmpeg command...', 
            TimeoutSeconds = 600, 
            Parameters = cmd_to_execute
        )
        command_id = response['Command']['CommandId']
        
        ## send_commandのCommandIdid値が取得できるまで数秒かかることもあるため5秒間隔をおく
        time.sleep(5)
        
        ## ステータスを取得
        list_invocations = ssm.list_command_invocations(
            CommandId = command_id,
            Details = True
            )

        ### Api Gatewayに渡すレスポンスを定義
        body = list_invocations['CommandInvocations'][0]['Status']
        
        return { 
            'statusCode': 200,
            'headers': {'Content-Type': 'application/json'},
            'body': body
            }
        
    except Exception as e:
        raise e
```
詳細な説明は省きますが上記は`API Gateway`から送られてきたURLを判別し、対象`EC2`インスタンスに対しstartまたはstopコマンドの実行命令を送ります。コマンド実行が成功ならSuccess,失敗ならFailedのレスポンスが戻ってくる流れとなっています。

##API Gateway
次のように設定します。リソース名は分かりやすいものにしましょう。
尚、メソッドはGETではなくPOSTにします。APIのURLを叩くだけで実行されないようにするためです。

| プロトコル | メソッド | リソース名 |
|:-----------|------------:|:------------:|
| REST      | POST        | /start         |
| REST     | POST     | /stop       |

メソッドを設定する際に「Lambda プロキシ統合の使用」にチェックを入れます。これは`API Gateway`が受信リクエストを`Lambda`関数の入力eventパラメータにマッピングさせるためです。

![スクリーンショット 2020-09-18 0.20.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/c04132cc-91a2-4686-3643-249a56d26168.png)

#検証
###startコマンド

1.設定したstart用APIでPOSTメソッドを用いて実行します。「Success」レスポンスが返ってきたら問題なくコマンドが実行されたことになります。「Failed」レスポンスの場合は、EC2インスタンスのamazon-ssm-agent.logをご確認ください。

```
$ curl -X POST https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/ffmpeg/start
Success
```
2.`EC2`インスタンス上でffmpegプロセスが起動できていることを確認できました。

```
$ ps axu|grep ffmpeg
root      3119  9.9  1.0  91388 21100 ?        S    01:23   0:00 /usr/local/bin/ffmpeg -re -stream_loop -1 -i /home/ec2-user/media/bbb_sunflower_1080p_30fps_normal.mp4 -c:v copy -c:a aac -flags +loop-global_header -f flv rtmp://54.249.218.4:1935/test/live
ec2-user  3121  0.0  0.1 110536  2236 pts/0    S+   01:23   0:00 grep --color=auto ffmpeg
```
3.配信が問題なくできていることも確認できました。
（配信はスコープ外なのでできていなくてもffmpegプロセスさえ動いていれば問題ありません）

![スクリーンショット 2020-09-18 1.39.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/e43f3bae-fb9e-f567-7092-c6c129439d24.png)
###stopコマンド
1.設定したstop用APIでPOSTメソッドを用いて実行します。「Success」レスポンスが返ってきたら問題なくコマンドが実行されたことになります。「Failed」レスポンスの場合は、EC2インスタンスのamazon-ssm-agent.logをご確認ください。

```
$ curl -X POST https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/ffmpeg/stop
Success
```
2.`EC2`インスタンス上でffmpegプロセスが起動できていることを確認できました。

```
$ ps axu|grep ffmpeg
ec2-user  3108  0.0  0.1 110536  2236 pts/0    S+   01:22   0:00 grep --color=auto ffmpeg
```

`SSM`からのコマンド実行に問題がないか`EC2`インスタンスのログでも確認することができます。

/var/log/amazon/ssm/amazon-ssm-agent.logの一部

```
  },
  "documentStatus": "Success",
  "documentTraceOutput": "",
  "runtimeStatus": {
    "aws:runShellScript": {
      "status": "Success",
      "code": 0,
      "name": "aws:runShellScript",
      "output": "",
      "startDateTime": "2020-09-17T16:08:17.477Z",
      "endDateTime": "2020-09-17T16:08:17.488Z",
      "outputS3BucketName": "",
      "outputS3KeyPrefix": "",
      "stepName": "",
      "standardOutput": "",
      "standardError": ""
    }
```

#結論
`AWS Systems Manager`,`AWS Lambda`と`Amazon API Gateway`を活用すれば`EC2`インスタンス上でコマンドを簡単に実行させることができます。今回は単純な処理を例に挙げて説明しましたが踏み込んだコマンドを実行するスクリプトを用意すればさらに高度な処理ができ、かつその実行結果を表示させることも可能です。それではまた次回！

#参考文献
https://aws.amazon.com/jp/systems-manager/
https://aws.amazon.com/jp/lambda/
https://aws.amazon.com/jp/api-gateway/
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html