SSM経由でEC2にS3にファイルをDL＆実行させるLambda関数

本記事に掲載しているLambdaは、以下のよう構成を想定したものです。

![SSM経由でEC2にS3にファイルをDL＆実行させるLambda関数.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1959433/f9fea884-09c2-bd7c-a5a0-3aebb0694f8b.png)

# 本記事のLambdaを作るようになった経緯

この記事のLambdaを作る前に元々やりたかったことはEC2にある各種ログファイルを定期的にS3に移動させることです。
当初EventBridgeルールにファイル移動コマンドを定数で書いていましたが、
移動対象が増えるにつれコマンドがたくさん増えてしまいEventBridgeの画面でコマンド編集をするのが辛くなってきました。
こんな感じです。

![EventBridgeのルールに定数で多数のコマンドが指定されている.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1959433/51b76a9e-0e20-2283-723b-4a92703065f4.png)

EventBridgeのルールでコマンドを送る方法はお手軽なのはいいのですが、変更しようとするとコマンドを一旦削除して追加するしかないのでやりにくさがあります。
また、この方法ではワンライナーのコマンドを積み重ねることしかできないため、分岐が書きづらいです。

以上の理由により、ログファイルの移動関連の処理を見直すことになったのですが、
しっかりプログラムを書くのもちょっと・・・だし、処理内容を運用チームの方でちょこちょこ直すのでデプロイが伴うような構成も避けたいということで今後の利便性を考えて本記事のLamdaを作るに至りました。

# Lambdaソースコード（Python）

```python
import boto3
import random
import string
import datetime

def lambda_handler(event, context):

    s3_bucket = event['s3_bucket']
    s3_key = event['s3_key']
    instance_id = event['instance_id']
    timeout = int(event['timeout'])

    # EC2 Systems Managerのクライアントを作成
    ssm_client = boto3.client('ssm')
    
    try:
        # ランダム文字列を生成
        random_string = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(10))

        # 現在の日時を取得し、年月日時分秒をプレフィックスに加える
        current_datetime = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
        script_name = current_datetime + '_' + random_string + '_script.sh'

        # EC2インスタンスに対してRunCommandを発行
        # S3からスクリプトをダウンロードして実行させる
        response = ssm_client.send_command(
            InstanceIds=[instance_id],
            DocumentName='AWS-RunShellScript',
            Parameters={'commands': ['aws s3 cp s3://{}/{} /tmp/{} && chmod +x /tmp/{} && /tmp/{}'.format(s3_bucket, s3_key, script_name, script_name, script_name)]},
            TimeoutSeconds=timeout,
        )

        # コマンドの実行結果を取得
        command_id = response['Command']['CommandId']

        # コマンドの実行状況をポーリングして待機
        waiter = ssm_client.get_waiter('command_executed')
        waiter.wait(
            InstanceId=instance_id,
            CommandId=command_id,
            WaiterConfig={
                'Delay': 10,  # ポーリング間隔を10秒に設定
                'MaxAttempts': 60  # 最大ポーリング回数を60回に設定 (合計10分)
            }
        )

        # コマンドの実行結果を取得
        command_result = ssm_client.get_command_invocation(
            InstanceId=instance_id,
            CommandId=command_id
        )
        print(command_result)

        # コマンドの実行結果判定
        if command_result['Status'] != 'Success':
            raise Exception('Command execution failed.')

        # 正常終了
        return {
            'statusCode': 200,
            'body': 'Command executed successfully'
        }

    except Exception as e:
        # 異常終了
        return {
            'statusCode': 500,
            'body': str(e)
        }
```

https://github.com/satoshi256kbyte/aws-code/blob/master/lambda/execute_scripts_ec2.py

SAMなどは使ってないのでビルドは不要です。
単純にLambdaのコード画面で貼り付けるだけで動くコードです。

このLambda関数はEC2に対し、S3からファイルをDLして実行するように命令します。
ログファイルの移動などのちょっとしたシェルスクリプトを書いてS3に置いてもらえば、これを用いてEC2に定期的に実行させられますよという想定です。

スクリプトをダウンロードするためのバケット名・キー、スクリプトを実行させるEC2のインスタンスID、タイムアウト時間はEventのJSONで渡すようにします。
こんな感じで指定します。

```json
{
  "s3_bucket": "バケット名",
  "s3_key": "スクリプトファイルのバケット上のキー",
  "instance_id": "EC2のインスタンスID",
  "timeout": "3600"
}
```

RunCommandの実行結果を一応見てはいますが、
実際のところエラーが発生すると殆どここに来ないようです。
実行させるスクリプトでエラーが起きると殆ど`waiter.wait`の時点で例外が発生して`except Exception as e:`の部分に飛びます。

```python
# コマンドの実行結果判定
if command_result['Status'] != 'Success':
    raise Exception('Command execution failed.')
```

# このLambdaに必要な権限

このLambdaを実行するのに必要な権限はこちらです。
Resourceの部分を緩めに書いてるので実際使う場合は対象がもっと絞られるようにした方が良いと思います。

本記事のLambdaはSessionManager経由でEC2にコマンドを送っています。
`ssm:SendCommand`は、コマンドを送るのに必要な権限で、
`ssm:GetCommandInvocation`はコマンドの実行結果を得るために必要な権限です。

なお、S3からファイルをダウンロードしてるのはEC2側なので、EC2の方にS3のGetObjectが必要になります。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "policy1",
            "Effect": "Allow",
            "Action": [
                "ssm:SendCommand",
                "ssm:GetCommandInvocation"
            ],
            "Resource": "*"
        },
        {
            "Sid": "policy2",
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:us-east-1:アカウントID:*"
        },
        {
            "Sid": "policy3",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:us-east-1:アカウントID:*:*:*"
        }
    ]
}
```

# EventBridgeの設定

EventBridgeスケジュールでの例です。

ターゲットはLambdaを呼ぶので、AWS LambdaのInvokeです。
![EventBridgeスケジュールのターゲット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1959433/3409a563-d2fb-7049-44d1-390d2e8a0f07.png)
Invokeの部分でLambda関数とバケット名などのパラメータを指定します。
![EventBridgeスケジュールの入力.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1959433/cd1f5207-e5a4-bb88-2ddd-92d252294187.png)
この他の設定には特別に書くようなものはなく、デフォルトのままで大丈夫です。