AWS

ECSタスクを定期実行するEventBridge SchedulerをTerraformで実装してみる

kohachan

はじめに

最近の業務で、ECSタスクを定期実行するEventBridge Schedulerを実装する機会があったので、Terraformでどのように実装できるかハンズオン形式で紹介したいと思います。

ECRイメージ、タスク定義の準備

まず、実行するタスクの元になるコンテナイメージをECRに格納して準備しておきます。

本記事では、以下のリポジトリでビルドしたイメージを使ってタスクを起動する前提のため、実際に試してみたい方は参考にしてみてください。
https://github.com/khomma0312/tf-eventbridge-ecs-run-task-script

※必要なければ、次の「設定ファイルの内容」から本題のコードを見ることができるのでスキップしてください。

ECRをecs-scheduler-example-repositoryというリポジトリ名で作成した上で、以下の手順でイメージのビルド、ECRへのプッシュを実施しておきます。

cd tf-eventbridge-run-ecs-task-script
echo "ECR_IMAGE_URI=ECRのイメージURI:latest" > .env
# イメージをプッシュ
scripts/build-and-push-image.sh

次に、以下の手順でタスク定義もプッシュしておきます。
S3_BUCKETで指定する名前のS3バケットは、先に作成しておきます。

また、ECS_ROLE_ARNECS_EXECUTION_ROLE_ARNは今回のサンプルコードに合わせて特定の名前のものにしていますが、
自分で作ったECSタスクロール、ECSタスク実行ロールと同じ名前であればなんでもOKです。

# プロジェクトルートに移動
cd tf-eventbridge-run-ecs-task-script
echo "ECS_ROLE_ARN=arn:aws:iam::アカウントID:role/ecs-scheduler-example-ecs-task-role" >> .env
echo "ECS_EXECUTION_ROLE_ARN=arn:aws:iam::アカウントID:role/ecs-scheduler-example-ecs-task-execution-role" >> .env
echo "S3_BUCKET=結果格納先バケット名" >> .env
# タスク定義をプッシュ
scripts/push-task-definition.sh

設定ファイルの内容

最初に、どのように設定を書けば良いかコードをお見せします。

実際にterraform applyして試したい方は、以下にサンプルリポジトリを用意しているので、こちらで試すこともできます。
https://github.com/khomma0312/tf-eventbridge-run-ecs-task

eventbridge.tf

data "aws_caller_identity" "current" {}

locals {
  aws_account_id        = data.aws_caller_identity.current.account_id
}

resource "aws_scheduler_schedule" "ecs_run_task_scheduler" {
  name       = "${local.project_name}-scheduler"
  group_name = "default"

  flexible_time_window {
    mode = "OFF"
  }

  # 毎月1日の4時に実行する
  schedule_expression          = "cron(0 4 1 * ? *)"
  schedule_expression_timezone = "Asia/Tokyo"

  target {
    arn      = aws_ecs_cluster.main.arn
    role_arn = aws_iam_role.ecs_task_scheduler.arn

    ecs_parameters {
      task_definition_arn = "arn:aws:ecs:ap-northeast-1:${local.aws_account_id}:task-definition/${local.project_name}"
      launch_type         = "FARGATE"
      platform_version    = "LATEST"
      network_configuration {
        subnets         = [aws_subnet.private.id]
        security_groups = [aws_security_group.ecs.id]
      }
    }

    input = jsonencode({
      containerOverrides = [
        {
          name = local.project_name
          environment = [
            {
              name  = "JOB_NAME"
              value = "bar"
            }
          ]
        }
      ]
    })
  }
}

iam.tf

data "aws_iam_policy_document" "ecs_task_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

# ECSタスク実行Role
resource "aws_iam_role" "ecs_task_execution" {
  name               = "${local.project_name}-ecs-task-execution-role"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_assume_role.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# ECSタスクRole用ポリシー
data "aws_iam_policy_document" "ecs_task" {
  statement {
    effect = "Allow"

    actions = [
      "s3:PutObject",
      "s3:GetObject",
    ]
    resources = ["*"]
  }
}

resource "aws_iam_policy" "ecs_task" {
  name   = "${local.project_name}-ecs-task-policy"
  policy = data.aws_iam_policy_document.ecs_task.json
}

# ECSタスクRole
resource "aws_iam_role" "ecs_task" {
  name               = "${local.project_name}-ecs-task-role"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_assume_role.json
}

resource "aws_iam_role_policy_attachment" "ecs_task" {
  role       = aws_iam_role.ecs_task.name
  policy_arn = aws_iam_policy.ecs_task.arn
}


data "aws_iam_policy_document" "ecs_run_task" {
  statement {
    effect = "Allow"

    actions = [
      "ecs:RunTask"
    ]
    resources = ["*"]
  }
  statement {
    actions = [
      "iam:PassRole"
    ]
    resources = ["*"]
    condition {
      test     = "StringLike"
      variable = "iam:PassedToService"
      values   = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_policy" "ecs_task_scheduler" {
  name   = "${local.project_name}-scheduler-policy"
  policy = data.aws_iam_policy_document.ecs_run_task.json
}

# EventBridge Scheduler Role
resource "aws_iam_role" "ecs_task_scheduler" {
  name = "${local.project_name}-scheduler-role"
  assume_role_policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Effect" : "Allow",
        "Principal" : {
          "Service" : ["scheduler.amazonaws.com"]
        },
        "Action" : "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ecs_task_scheduler_policy_attachment" {
  role       = aws_iam_role.ecs_task_scheduler.name
  policy_arn = aws_iam_policy.ecs_task_scheduler.arn
}

ecs.tf

resource "aws_ecs_cluster" "main" {
  name = "${local.project_name}-cluster"
}

内容の解説

上記のコードを順に解説していきます。

ECSを定期起動するEventBridgeの実装

eventbridge.tf内のaws_scheduler_scheduleにて、ECSを起動するEventBridge Schedulerを作成しています。
ポイントはecs_parametersの部分で、ここでECSの起動設定を行っています。

特に注意が必要なのがtask_definition_arnで、常に最新のタスク定義を指すようにするには、「:2」などの後ろのリビジョン番号を何も指定しないでおく必要があります。

task_definition_arn = "arn:aws:ecs:ap-northeast-1:${local.aws_account_id}:task-definition/${local.project_name}"

ecs_parametersの部分はマネジメントコンソールだと以下のような画面になっており、Terraformでもこれらの項目を指定できるようになっています。

また、コンテナの環境変数やコマンドなどの設定を上書きしたい時には、inputパラメータを使ってcontainerOverridesで項目を指定すれば任意の値を設定できます。

今回は環境変数(JOB_NAME)を上書きするケースを想定して実装しています。

target {
  arn      = aws_ecs_cluster.main.arn
  role_arn = aws_iam_role.ecs_task_scheduler.arn

  ecs_parameters {
    task_definition_arn = "arn:aws:ecs:ap-northeast-1:${local.aws_account_id}:task-definition/${local.project_name}"
    launch_type         = "FARGATE"
    platform_version    = "LATEST"
    network_configuration {
      subnets         = [aws_subnet.private.id]
      security_groups = [aws_security_group.ecs.id]
    }
  }

  input = jsonencode({
    containerOverrides = [
      {
        name = local.project_name
        environment = [
          {
            name  = "JOB_NAME"
            value = "bar"
          }
        ]
      }
    ]
  })
}

ECSで使用するロールの実装

iam.tfにて、ECSで使用するECSタスクロール、ECSタスク実行ロールを作成しています。

ECSタスクロールでは、タスクで実行する処理内で必要な権限を持ったロールを設定します。
今回は、S3にオブジェクトを追加する処理がある前提で、s3:PutObjectなどを指定しています。

ECSタスク実行ロールでは、タスクを実行・起動する際に必要となる権限(CloudWatch Logsへの書き出しなど)を持ったロールを設定します。今回はAWSマネージドのAmazonECSTaskExecutionRolePolicyをポリシーとして設定しました。

EventBridgeスケジューラー用ロールの実装

iam.tfではその他に、EventBridgeスケジューラー用のロールも実装しています。

EventBridgeスケジューラー用のロールでは、ecs:RunTaskiam:PassRoleの権限が必要になります。

詳細は以下の公式ドキュメントに記載されていますが、こちらの通り、上記2つの権限が付与されたEventBridgeスケジューラー用のロールも作成しておきます。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/CWE_IAM_role.html

ECSクラスターの実装

ターゲット先となるECSクラスターも作成しておきます。
こちらで作成したクラスターを、前述のEventBridge Schedulerの定義時に指定しています。

実行結果

以上のコードをterraform applyで反映すると、以下のようなECSタスクを定期実行するEventBridgeが完成します!

タスク実行後にS3を見ると、オブジェクトのputもできていることが確認できます。

終わりに

ECSタスクを起動するEventBridge SchedulerをTerraformで実装する方法を紹介しました。

シンプルな仕組みではありますが、各ロールに必要な権限やEventBridge SchedulerのECS起動の設定値など、直感的にわからない部分もあったので参考になれば幸いです。

AUTHOR
kohachan
kohachan
記事URLをコピーしました