Amazon ECS でタスクをスケジューリングして定期的に実行する

この記事は個人ブログと同じ内容です

www.ritolab.com


Amazon ECS には タスクをスケジューリングして動作させることのできる機能があり、これを用いることで毎日走らせたい処理など定期的に行いたい処理を実行する事ができます。

今回は AWS ECS の「タスクのスケジューリング」を使って、タスクを定期的に実行してみます。

ECS タスクのスケジューリング

ECS の画面から設定が可能ですが、内部的には CloudWatch Events Rule を作成してスケジュールを構成しているようですね。

Amazon ECS タスクのスケジューリング

docs.aws.amazon.com

ECS の構築

まずはベースとなるタスク定義やクラスタを作成しておきます。

  • Terraform (v0.14.3) で行います。
  • 起動タイプは FARGATE です。
  • コンテナのイメージは ECR に登録済みの前提です。

IAM の作成

ECS でタスクを実行する IAM Role 作成します。

main.tf

# IAM Role - ECS Task Execution for Scheduler
resource "aws_iam_role" "ecs_scheduler_task_execution" {
  name               = "EcsTaskExecutionRole-sample"
  assume_role_policy = file("policies/iam_role/ecs_task_execution.json")
}
# IAM Role Policy - ECS Task Execution for Scheduler
resource "aws_iam_policy" "ecs_scheduler_task_execution" {
  name        = "EcsTaskExecutionPolicy-sample"
  description = "Ecs Task Execution"
  policy      = file("policies/iam_policy/ecs_task_execution.json")
}
# Attach Policy to Role / Scheduler
resource "aws_iam_role_policy_attachment" "ecs_scheduler_task_execution" {
  role       = aws_iam_role.ecs_scheduler_task_execution.name
  policy_arn = aws_iam_policy.ecs_scheduler_task_execution.arn
}

読み込んでいる各ファイルの内容は以下です

policies/iam_role/ecs_task_execution.json

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

policies/iam_policy/ecs_task_execution.json

{
    "Version": "2008-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

タスク定義・クラスタ作成

ECS のタスク定義・クラスタを作成します。

main.tf

# CloudWatch Logs - log group
resource "aws_cloudwatch_log_group" "ecs_scheduler" {
  name = "/ecs-scheduler"
}

# Task Definition
resource "aws_ecs_task_definition" "task_scheduler" {
  family                   = "scheduler"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "256"
  memory                   = "512"
  container_definitions    = templatefile("task-definitions/scheduler.json", {
    log_group_name      = aws_cloudwatch_log_group.ecs_scheduler.name
  })
  execution_role_arn       = aws_iam_role.ecs_scheduler_task_execution.arn
}

# ECS Cluster
resource "aws_ecs_cluster" "task_scheduler" {
  name = "scheduler-cluster"
}

読み込んでいる各ファイルの内容は以下です

[
    {
        "name": "<< CONTAINER-NAME >>",
        "image": "<< AWS-ID >>.dkr.ecr.<< REGION >>.amazonaws.com/<< CONTAINER-IMAGE-NAME >>:<< CONTAINER-IMAGE-TAG >>",
        "cpu": 128,
        "memory": null,
        "memoryReservation": 128,
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-group": "${log_group_name}",
                "awslogs-region": "<< REGION >>",
                "awslogs-stream-prefix": "scheduler",
                "awslogs-datetime-format": "%Y-%m-%d %H:%M:%S"
            }
        }
    }
]

<< ... >> としている部分は各々で必要な値が入ります。

これでベースとなる ECS 環境が構築できました。

タスクのスケジューリング設定

ここから ECS のタスクスケジューリングを設定していきます。

Cloudwatch Events を設定することでタスクスケジューリングを実現します。

IAM Role の作成

CloudWatch Events の IAM Role を作成します。

main.tf

# IAM Role - ECS Events
resource "aws_iam_role" "ecs_events" {
  name               = "EcsEventsRole-sample"
  assume_role_policy = file("policies/iam_role/ecs_events.json")
}
# IAM Role Policy
resource "aws_iam_policy" "ecs_events" {
  name   = "EcsEventsPolicy-sample"
  policy = templatefile("policies/iam_policy/ecs_events.json", {
    // リビジョンは固定しない
    task_definition_arn = replace(aws_ecs_task_definition.task_scheduler.arn, "/:\\d+$/", "")
  })
}
# Attach Policy to Role
resource "aws_iam_role_policy_attachment" "ecs_events" {
  policy_arn = aws_iam_policy.ecs_events.arn
  role       = aws_iam_role.ecs_events.name
}

1 点だけ注意するポイントがあります。IAM Policy 作成時のポリシー定義で、ECS タスク定義の ARN を指定しますが、この値はリビジョンを除いたものを渡します。

リビジョンが指定されたままの ARN で ポリシーを作成してしまうと当然ながらそのリビジョンでのみ実行可能なポリシーになってしまうため、例えばアプリケーションの新たなリリースを行ってイメージを更新した(イメージのタグを更新した、など)際にはタスク定義のリビジョンが一つ上がるので、作成したポリシーではスケジューリング実行ができなくなってしまいます。

他、読み込んでいる各ファイルの内容は以下です。

policies/iam_role/ecs_events.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": "events.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

policies/iam_policy/ecs_events.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "iam:PassRole",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "ecs:RunTask",
            "Resource": "${task_definition_arn}"
        }
    ]
}

タスクスケジューリング

スケジュールを設定します。

main.tf

# CloudWatch Event Rule
resource "aws_cloudwatch_event_rule" "ecs_scheduled_task" {
  name                = "ecs-scheduled-task"
  schedule_expression = "cron(20 2 * * ? *)"
}

# CloudWatch Event Target
resource "aws_cloudwatch_event_target" "ecs_scheduled_task" {
  arn       = aws_ecs_cluster.task_scheduler.arn
  rule      = aws_cloudwatch_event_rule.ecs_scheduled_task.name
  role_arn  = aws_iam_role.ecs_events.arn
  target_id = "scheduler-target"
  input     = file("container-overrides/ecs_scheduled_task.json")

  ecs_target {
    // リビジョンなしで渡すことで常に最新のバージョンを使用するようにする
    task_definition_arn = replace(aws_ecs_task_definition.task_scheduler.arn, "/:\\d+$/", "")
    task_count          = 1
    launch_type         = "FARGATE"
    platform_version    = "1.4.0"

    network_configuration {
      subnets          = [aws_subnet.private_1.id,aws_subnet.private_2.id]
      assign_public_ip = false
    }
  }
}

以下、いくつかポイントがあります。

スケジュールの設定

CloudWatch Event Rule の設定において schedule_expression を指定していますが、ここでどういったスケジュールで動作させるのかを指定します。

上記のような cron 式、または rate 式で記述できます。

ルールのスケジュール式

docs.aws.amazon.com

1 つ注意なのが、 cron 式を使う場合 AWS 上では UTC で実行されるため、日本のタイムゾーンで考えると時刻指定は -9h で行う必要があります。

今回は、毎日 11:20 に実行されるように設定しました。

サブネットの指定

network_configuration の値について、ここではマルチ AZ かつプライベートサブネットに ECS を展開しているため subnets および assign_public_ip は上記のような指定になっています。

サブネットの指定などは自身の環境に合わせて設定を行ってください。

Resource: aws_cloudwatch_event_target
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target

ContainerOverride

読み込んでいる各ファイルの内容は以下です

container-overrides/ecs_scheduled_task.json

{
    "containerOverrides": [
        {
            "name": "<< CONTAINER-NAME >>",
            "command": ["php", "artisan", "sample:logger"]
        }
    ]
}

ここでは ContainerOverride の値を設定しています。コンテナ起動時のデフォルトコマンドがここで指定したもので上書きされます。

タスク起動時にここで指定したコマンドが実行されるイメージです。

ContainerOverride

docs.aws.amazon.com

今回はサンプルとして、PHP フレームワークである Laravel のコマンドを動かす想定として、artisan コマンドを記述しています。

コンテナ起動時のデフォルトコマンドを上書きしたことによって動作は以下のようになります。

  1. スケジューリングによって指定した時間にコンテナが起動する
  2. コマンド php artisan sample:logger を実行する
  3. 処理が終了したらタスクが終了する

動作確認

ECS でのタスクスケジューリングの設定が完了したので、AWS コンソール画面から確認してみます。

ECS クラスタの画面からタスクのスケジューリングタブを選択すると、スケジュールが設定されている事が確認できます。

f:id:ro9rito:20210215084907p:plain

また、CloudWatch Events の Rule を確認すると、指定の通りに毎日 11:20 にトリガーが設定されている事が確認できます。

f:id:ro9rito:20210215084926p:plain

時間になったらタスクが起動しました

f:id:ro9rito:20210215084948p:plain

タスクを実行した際にログを出力するようにしておいたのでそちらも確認してみます。

f:id:ro9rito:20210215085012p:plain

スケジューリングでのタスク実行が動作している事を確認できました。

まとめ

ECS のタスクスケジューリングを使う事で、タスク実行をスケジュール化できました。

定期的に実行するような処理は ECS のタスクのスケジューリングでいい感じに行えそうでした。