LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

ECSを動かすEventBridge SchedulerをTerraformで構築してみた


こんにちは、インフラストラクチャーグループのyjszkです。2月から入社しました。
リブセンスにはバッチをECSとEventBridge Ruleで動かしている実装があります。EventBridge Ruleがなかなかの曲者で、UTCでしか時間を指定できません。
UTCで指定されたルールはいつ動くのかがわかりづらいですし、JSTでは1つのルールで済んだものがUTCでは2つ以上に分割されてしまうこともあります。
例えば、JSTで特定の曜日に10分ごとに実行するタスクがあるとします。

*/10 * * * 0-3,5-6 *

これがUTCだと3つになります。これはなかなかつらいです。

0/10 0-15 ? * 4 *
0/10 15-0 ? * 5 *
0/10 * ? * 1-3,6,7 *

一方、2022年11月にリリースされたAWSの新機能EventBridge Schedulerではタイムゾーン指定ができます。つまりJST指定が可能です!
また、あらゆるAPIをキックできECSの発火にも使えるので、こちらに移行することにしました。

変更点

以前はEventBridge RuleでECSをキックしていました。
これらをコードで管理しており、ルールの管理についてはecscheduleを採用していました。こちらはYAMLで設定を管理でき、既存のルールのエクスポート、差分出力、変更、実行ができる素晴らしいツールです。
しかし、ルールの削除に関して手動のオペレーションが必要となり、完全にコード化というわけには行きませんでした。 今回はこちらをTerraformにして完全にコード化します。

実装例

想定環境

VPC、サブネット、クラスター、タスク定義はあらかじめ作成されていることとします。

コード

Terraform

  • Schedulerが使用するIAMロール
resource "aws_iam_role" "event_scheduler" {
  name               = "event-scheduler"
  assume_role_policy = data.aws_iam_policy_document.sts_for_eventbridge_scheduler.json

  tags = {
    Name = "event-scheduler"
  }
}
  • IAMロールのassume_role_policy
data "aws_iam_policy_document" "sts_for_eventbridge_scheduler" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["scheduler.amazonaws.com"]
    }
  }
}
  • 管理ポリシーの定義とロールへのアタッチ
data "aws_iam_policy" "ecs_events_role" {
  arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole"
}

data "aws_iam_policy" "ecs_scheduler_role" {
  arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

resource "aws_iam_role_policy_attachment" "event_scheduler_run_ecs" {
  role       = aws_iam_role.event_scheduler.name
  policy_arn = data.aws_iam_policy.ecs_events_role.arn
}

resource "aws_iam_role_policy_attachment" "event_scheduler_get_ecr" {
  role       = aws_iam_role.event_scheduler.name
  policy_arn = data.aws_iam_policy.ecs_scheduler_role.arn
}
  • 今回タスク定義はすでに存在している想定のためdataで取り込む
data "aws_ecs_task_definition" "batch_task_definition" {
  task_definition = "batch-task"
}
  • Scheduler本体
resource "aws_scheduler_schedule" "batch_rules" {
  # 珍妙な書き方だがfor_eachはmap型しか受け取れないのでそのための処理
  # https://qiita.com/minamijoyo/items/3785cad0283e4eb5a188
  for_each = {
    for i in local.scheduler_rules : i.name => i
  }
  name                         = each.value.name
  state                        = var.scheduler_state
  schedule_expression          = each.value.schedule_expression
  schedule_expression_timezone = "Asia/Tokyo"
  flexible_time_window {
    mode = "OFF"
  }
  target {
    arn      = aws_ecs_cluster.main.arn
    role_arn = aws_iam_role.event_scheduler.arn
    ecs_parameters {
      task_definition_arn     = data.aws_ecs_task_definition.batch_task_definition.arn
      launch_type             = "FARGATE"
      platform_version        = "LATEST"
      enable_ecs_managed_tags = false
      enable_execute_command  = false
      network_configuration {
        subnets         = module.main_vpc.private_subnet_ids  # 適宜自分の環境のサブネット類と置き換えてください
        security_groups = [aws_security_group.hoge.id] # 適宜自分の環境のセキュリティグループ類と置き換えてください
      }
    }
    input = jsonencode({
      "containerOverrides" : [{
        "name" : data.aws_ecs_task_definition.batch_task_definition.family,
        "command" : each.value.command
      }]
    })
  }
}
  • 可変の値はYAMLに記載してForEach+Mapで回すことにより可読性をあげている
locals {
  scheduler_rules = yamldecode(file("${path.module}/scheduler.yaml"))
}

Terraformに食わせるYAML

  • Terraformに素でコードを書くと、JSONになって読みにくいので、可変の値についてはYAMLで記載をしています。
- name: task1
  schedule_expression: cron(5 10 ? * SUN-WED,FRI-SAT *)
  command:
    - sh
    - /home/hoge/foo.sh
- name: task2
  schedule_expression: cron(10 11 ? * THU *)
  command:
    - sh
    - /home/bar/foo.sh
yamldecodeについて

yamldecode(file("${path.module}/scheduler.yaml"))についてですが、terraform consoleで動きを確認することができます。
上記のYAMLがどうTerraformに取り込まれるか簡単に確認することができます。

$ terraform console
> yamldecode(file("test.yaml"))
[
  {
    "command" = [
      "sh",
      "/home/hoge/foo.sh",
    ]
    "name" = "task1"
    "schedule_expression" = "cron(5 10 ? * SUN-WED,FRI-SAT *)"
  },
  {
    "command" = [
      "sh",
      "/home/bar/foo.sh",
    ]
    "name" = "task2"
    "schedule_expression" = "cron(10 11 ? * THU *)"
  },
]
>  

このようにデコード結果を確認することができます。便利ですね。

やってみて

リリースされたばかりのサービスということもあり、サンプルコードが日本語英語問わずほぼありませんでした。この場合は公式ドキュメントしか勝たんというのがあります。なので、日本語の情報を増やすために書きました、参考になれば幸いです。
また、入社したばかりかつリモートワークなのでビクビクしながらのコーディングでしたが、Slackで1を聞けば100くらい返ってくることもある優秀なエンジニアが揃っている会社なので、ガンガン聞いて非常に多くのことを吸収しました。
ぜひ皆さんも一緒に働きましょう!!!

hrmos.co