わたねこコーリング

野良プログラマ発、日々のアウトプット

【AWS】EC2 インスタンスでの定時処理を cron から EventBridge スケジューラに移行した

EC2 インスタンスでちょっとしたシェルスクリプトを cron で定時実行していたのを、SSM Run Command + EventBridge スケジューラに移行してみたという話です。以下、ざっくりと手順を。極力 AWS CLI を使ってコマンドベースで進められるように書いてます(古いバージョンでは動かないことがあるので最新版を推奨)。アカウント番号 999999999999 と インスタンス ID i-0123456789abcdef はお手持ちのものに読み替えて下さい。

EC2 インスタンス設定

何はともあれ定時実行したいシェルスクリプトを。本例では下記のようなローカルディスクの使用率を出力するスクリプトを用意しておきます。

$ cat <<'EOS' > /tmp/local-disk-usage.sh
#!/bin/bash
echo "$(hostname) のディスク使用率は $(df | awk '/nvme0n1p1/ {print $5}') です。"
EOS

上記スクリプトをリモートで実行する為に Systems Manager の Run Command を使うので、当該インスタンスには SSM Agent が必要です。が、本件インスタンスの OS である Amazon Linux 2 にはデフォルトでインストールされているので、下記を実行して、Active: active (running) と表示されるなら OK。そうでないなら systemctl start しておきます。

$ sudo systemctl status amazon-ssm-agent

また、当該 EC2 インスタンスを Systems Manager から制御する旨の許可として、適用している IAM ロールに IAM ポリシー AmazonEC2RoleforSSM をアタッチしておきます。

EventBridge 用 IAM ロールの作成

EventBridge スケジューラが Run Command する時に必要な IAM ロール my-eventbridge-role を作成しておきます。ロールには、スケジューラからの利用を許可した信頼関係を持たせます。

$ __TRUSTPOLICY__=$(cat <<EOS
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "scheduler.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
EOS
)

$ aws iam create-role \
    --role-name my-eventbridge-role \
    --assume-role-policy-document "$__TRUSTPOLICY__"

さらに、当該 EC2 インスタンスへの Run Command 実施を許可したポリシーを作成して、上記ロールにアタッチ。

$ __POLICY__=$(cat <<EOS
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "ssm:SendCommand",
            "Effect": "Allow",
            "Resource": [
                "arn:aws:ec2:ap-northeast-1:999999999999:instance/i-0123456789abcdef",
                "arn:aws:ssm:ap-northeast-1:*:document/AWS-RunShellScript"
            ]
        }
    ]
}
EOS
)

$ aws iam create-policy \
    --policy-name my-eventbridge-policy \
    --policy-document "$__POLICY__"
$ aws iam attach-role-policy \
    --role-name my-eventbridge-role \
    --policy-arn arn:aws:iam::999999999999:policy/my-eventbridge-policy

CloudWatch ログの設定

EventBridge スケジューラが Run Command 出力を保存する CloudWatch ログのロググループを作っておきます。

$ aws logs create-log-group \
    --log-group-name my-eventbridge-shcedule-loggroup

EventBridge スケジューラを作成

これで準備が出来たので、EventBridge スケジューラを作成します。スケジュールのターゲットに Run Command を定義するには、コマンドをエスケープされた JSON 文字列で指定する必要があるので、予めコマンド定義をシェル変数にセットしておきます。

$ __COMMAND_INPUT__=$(cat <<EOS
{
    "DocumentName": "AWS-RunShellScript",
    "InstanceIds": ["i-0123456789abcdef"],
    "Parameters": {
        "commands": ["bash /tmp/local-disk-usage.sh"]
    },
    "CloudWatchOutputConfig": {
        "CloudWatchLogGroupName": "my-eventbridge-shcedule-loggroup",
        "CloudWatchOutputEnabled": true
    }
}
EOS
)

EventBridge スケジュール定義を作成。下記は上記で定義したターゲットを毎日 22:30 に実行するという内容です。StartDate は最初の実行日時から5分前以内でなければならないとのこと。JSON 文字列のエスケープ処理には jq を使います。尚、set -f はヒアドキュメント中のワイルドカード展開を抑止するおまじない。

$ set -f
$ __EB_SCHEDULE__=$(cat <<EOS
{
  "EndDate": "2100-01-01T00:00:00+09:00",
  "FlexibleTimeWindow": {
    "Mode": "OFF"
  },
  "GroupName": "default",
  "Name": "my-eventbridge-schedule-sample",
  "ScheduleExpression": "cron(30 22 * * ? *)",
  "ScheduleExpressionTimezone": "Asia/Tokyo",
  "StartDate": "2023-03-04T22:25:00+09:00",
  "State": "ENABLED",
  "Target": {
    "Arn": "arn:aws:scheduler:::aws-sdk:ssm:sendCommand",
    "Input": $(echo $__COMMAND_INPUT__ | jq '@json'),
    "RetryPolicy": {
      "MaximumEventAgeInSeconds": 86400,
      "MaximumRetryAttempts": 185
    },
    "RoleArn": "arn:aws:iam::999999999999:role/my-eventbridge-role"
  }
}
EOS
)
$ set +f

上記で作成したスケジュールを EventBridge スケジューラに登録して設定は完了です。

$ aws scheduler create-schedule --cli-input-json "$__EB_SCHEDULE__"

確認

最初の実行時刻が過ぎたところで、CloudWatch ロググープ my-eventbridge-shcedule-loggroup にログストリームが生成されていて、/tmp/local-disk-usage.sh の出力が記録されているか確認します。

余談

定時実行を cron から AWS サービスに移行するにあたり、ネットで調べると EventBridge ルールの用例が多くヒットするのですが、それだとログを残せないのが不満でした。昨年末にリリースされた EventBridge スケジューラは自由度が高く、上記不満も解消されていてイイっすね。ターゲットが SSM Run Command だとコマンド定義を JSON で書かなければいけないのでちょっと敷居が高いですが、本件のようなユースケースには合っていたと思います。

dev.classmethod.jp