😽

ECS + Fargate のタスクを EventBridge Scheduler で実行する

2023/03/08に公開

やりたいこと

平日の特定時間にタスク定義を実行する
※この際常時実行ではなくスポットでの実行をする

実装方法

EventBridge の Scheduler を使用することでタスク定義を cron ベースで実行できる

はまったこと

Scheduler でスケジュールを作成するときに自動作成出来る IAM ロールにやられました
※自動作成だから大丈夫と思っていたら修正しないと正常に動作が出来なかったです(今考えれば当たり前ですが)

修正が必要なポイント

  1. タスク定義で使用している Task role と Task execution role に対して iam:PassRole を許可する
  2. リビジョン番号の指定を行わないよう修正する場合、リソース内の指定から":"を削除する必要がある
  3. 既存のロールを使用する場合、Trusted entities にイベント名を追加してあげる必要がある

タスク定義で使用している Task role と Task execution role に対して iam:PassRole を許可する

Scheduler が使用するロールにも以下赤枠の Task role と Task execution role で指定している IAM ロールの iam:PassRole を許可する

リビジョン番号の指定を行わないよう修正する場合、リソース内の指定から":"を削除する必要がある

以下 「タスク定義のリビジョンを Latest にする方法」 の注意に記載

既存のロールを使用する場合、Trusted entities にイベント名を追加してあげる必要がある

別のスケジュールで作成したロールを使いまわす場合、Trusted entities にあらかじめ新しく作るスケジュール名を設定しておかないと以下のエラーが出力されてイベントが作成できない

自動作成される IAM ロールのポリシーとTrusted entities

ポリシー

Amazon_EventBridge_Scheduler_ECS_7fb9c96715

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecs:RunTask"
            ],
            "Resource": [
                "arn:aws:ecs:ap-northeast-1:xxx:task-definition/<タスク定義名>:*"
            ],
            "Condition": {
                "ArnLike": {
                    "ecs:cluster": "arn:aws:ecs:ap-northeast-1:xxx:cluster/<クラスター名>"
                }
            }
        }
    ]
}

Trusted entities

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "scheduler.amazonaws.com"
            },
            "Action": "sts:AssumeRole",
            "Condition": {
                "StringEquals": {
                    "aws:SourceArn": "arn:aws:scheduler:ap-northeast-1:xxx:schedule/<スケジュールグループ名>/<スケジュール名>",
                    "aws:SourceAccount": "xxx"
                }
            }
        }
    ]
}

エラーの確認方法

Cloudtrail で Event name から RunTask を検索することで確認可能

"eventSource": "ecs.amazonaws.com",
"eventName": "RunTask",
"awsRegion": <リージョン名>",
"sourceIPAddress": "events.amazonaws.com",
"userAgent": "events.amazonaws.com",
"errorCode": "AccessDenied",
"errorMessage": "User: arn:aws:sts::xxx:assumed-role/Amazon_EventBridge_Invoke_ECS_xxx/<ロール名> is not authorized to perform: ecs:RunTask on resource: arn:aws:ecs:xxx:task-definition/<タスク定義名> because no identity-based policy allows the ecs:RunTask action",

タスク定義のリビジョンを Latest にする方法

現在はコンソール上からタスク定義のリビジョンの選択を Latest とした場合、EcsParameters 内で自動で最新のリビジョン番号が付与されてしまう
※新しいリビジョンが作成されても手動で更新する必要がある
https://github.com/aws/aws-cdk/issues/21782

以下スクリプトを実行することで EcsParameters 内のタスク定義の ARN でリビジョン番号を削除することで常に最新のリビジョンを実行することが可能になる

注意

  • boto3 の最新バージョンが必要(pip3 install -U boto3)
  • スケジュール作成時に自動作成されたポリシーはリソースが以下となっており、リビジョン番号が含まれるようになっているので ":" を削除してあげる必要がある(これにはまった。。。)
    "arn:aws:ecs:ap-northeast-1:xxx:task-definition/<タスク定義名>:*"

実行環境

※以下私の環境バージョン

boto3 1.26.80
botocore 1.29.80

コード

以下のコードで修正が必要なイベントやタスク定義の情報を設定し実行することでタスク定義の上書きが可能です

import boto3

client = boto3.client("scheduler")

group_name = ""  # ScheduleGroup-name
schedule_name = ""  # ScheduleName
taskdefarn = ""  # 設定したい ARN

def gen_params():
    # Create params from current_params
    current_params = client.get_schedule(Name=schedule_name, GroupName=group_name)
    unnecessaries = ["ResponseMetadata", "Arn", "CreationDate", "LastModificationDate"]
    keys = [x for x in current_params if x not in unnecessaries]

    params = {}
    for key in keys:
        params[key] = current_params[key]

    if params["Target"]["EcsParameters"]["TaskDefinitionArn"]:
        taskdefarn_before = params["Target"]["EcsParameters"]["TaskDefinitionArn"]
        print("TaskDefinitionArn Before: {}".format(taskdefarn_before))

        params["Target"]["EcsParameters"]["TaskDefinitionArn"] = taskdefarn

    return params

def main():
    params = gen_params()
    response = client.update_schedule(**params)
    if response["ResponseMetadata"]["HTTPStatusCode"] == 200:
        print("{}: Update success.".format(schedule_name))

if __name__ == "__main__":
    client = boto3.client("scheduler")
    response = main()

Discussion