Terraform Providerを使ったcron式のチェック

SREの菅原です。

カンムのサービスのバッチ処理は基本的にEventBridge Scheduler+ECSで動いており、バッチのスケジュールはterraformで以下のように定義されています。

module "kanmu_batch" {
  # バッチまわりはモジュール化
  source = "../modules/batch"

  for_each = {
    hogehoge-batch = {
      schedule_expression = "cron(0 0 * * ? *)"
      command             = ["/batch/bin/hoge", "hikisu"]
      is_enabled          = true
    }
    fugafuga-batch = {
      schedule_expression = "cron(5 0 * * ? *)"
      command             = ["/batch/bin/fuga", "hikisu"]
      is_enabled          = true
    }
    # ...
  }

  schedule_expression = each.value.schedule_expression
  command             = each.value.command
  # ...

schedule_expressionに指定するのはEventBridgeのcron式、またはrate式なのですが、cron式でDay-of-monthに値を指定した場合、Day-of-weekは?にする必要があるなど、いくつか間違いやすいポイントがあります。

aws_scheduler_scheduleリソースがterraform plan実行時にチェックしてくれると助かるのですがそのような機能はないため、terraform applyを実行してはじめて間違いに気付くことになります。

単純なミスがあるのにterraform planが通ってしまいレビュー後のterraform applyで手戻りが発生すると余計な時間を割くことになるので、terraform providerを使ってschedule_expressionをチェックできるようにしてみました。

terraform-provider-cronplan

schedule_expressionのチェック用に自作したterraform-provider-cronplanは、Data SourceとFunctionが一つずつ定義されているシンプルなproviderです。

github.com

provider::cronplan::exprというFunctionを使って、terraform plan実行時にschedule_expressionをチェックすることができます。

module "kanmu_batch" {
  source = "../modules/batch"

  for_each = {
    hogehoge-batch = {
      schedule_expression = provider::cronplan::expr("cron(10 * * * ? *)")
      command             = ["/batch/bin/hoge", "hikisu"]
      is_enabled          = true
    }

上記の例ではschedule_expressionに問題がなければ普通にスケジュールが作成されますが、schedule_expressionに間違いがあるとエラーになります。

cron式の些細なシンタックスエラーはterraform planで分かるようになったので、DXがそれなりに改善できたと思います。

EventBridgeのcron式の仕様について

terraform-provider-cronplanのcron式のチェックに使っているのは、趣味で開発しているcronplanというGolangのライブラリです。

開発するにあたってEventBridgeのcron式の仕様を調べたのですが、マニュアルドキュメントがあるぐらいできちんとした仕様は見つけられませんでした。

そもそもcron式全般についてきちんとした仕様を見つけられることができず*1、特にvixie-cron等のUnixのcron式のSUN(日曜日)が0であるのに対し、一部Javaライブラリのcron式ではSUNが1になっている点にだいぶ混乱しました。

EventBridgeは後者のJavaライブラリを踏襲しているようで分かりにくい部分もあるのですが、LWのような特殊な(かつ業務上必要になる)拡張が入っているのはありがたかったです。

Day-of-weekのLについて

EventBridgeのcron式の仕様は見つけられなかったのですがcronplanの実装を続けているうちに、バックエンドの実装はこれではないか?あるいは仕様が準拠しているのではないか?というライブラリがありました。

www.quartz-scheduler.org

Quartzは古くからあるJavaのJob Schedulingライブラリのようで、Secondsがある点を除けばEventBridgeのcron式の動作と一致しています。 特にDay-of-weekのLの変わった仕様がEventBridgeと同じです。


Lは末日を表す文字でDay-of-monthフィールドでLL-1を指定することで月末、月末の1日前を表すことができます。 LはDay-of-weekフィールドにも指定することができ、たとえば6Lと指定すると月の最終週の金曜日を表すことができます。

このLはDay-of-weekフィールドに単体で指定することができるのですが

cron(0 0 ? * L *)

というcron式が何を意味しているか分かりますか?

正解は

cron(0 0 ? * SAT *)

になります。Lを単体で指定した場合、SATを指定したのと同じ動作になります。

この動作を初めて見つけたとき、だいぶ変わった仕様だと思っていたのですがQuartzのドキュメントにはまさにその仕様が書かれていました。

If used in the day-of-week field by itself, it simply means “7” or “SAT”. https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html

それ以外にもいくつかのエッジケース、たとえば「Day-of-monthに31を指定したときに31日がない月はどうなるのか?」とか「Day-of-weekに6#5を指定したときに第五週目の金曜日がない月はどうなるのか?」などの動作が一致しているように見えたので、準拠している可能性は高そうなのですが公式の情報は見つけられていないので、気になっているところではあります…

cronplanに関してはAWSのEventBridgeコンソールのプレビューを見つつできるだけ動作を合わせているつもりですが、さすがに数日・数年かけたテストはできていないので、もし実際の動作と異なる仕様がありましたらIssueを起票していただけましたら対応しますので、ぜひぜひご利用ください。

*1:未確認ですがUnixに関してはPOSIXの仕様があるのかもしれないです