📅

SSM Change Calendarで日跨ぎバッチをよしなに制御する

2024/09/15に公開

TL;DR

日次イベントをインポートしたChange Calendarを使用して日跨ぎのバッチの実行可否を判定する場合は、意図的にカレンダーインポート時のタイムゾーンをずらしてみるのもありかもしれません。なお、このやり方は日次イベント以外のイベントが含まれているとおかしなことになる可能性があるのでご注意ください。

Systems Manager Change Calendarとは

Change Calendarはその名の通り、重要な業務中の意図せぬ変更を防止したり、カレンダーに基づいて変更アクションを実行するためのものですが、CLIやAPIで日時を指定してアクションを実行可能かどうか状態(OPEN or CLOSED)を取得することができ、変更管理以外にも活用することできます。
営業時間外に使用しないEC2インスタンスをシャットダウンしたい、営業日以外は日次バッチをスキップしたい等、スケジュールにもとづいて何らかの処理を行うユースケースはよく見られ、EventBridgeのcron式やrate式で実現可能な範囲であればよいのですが、祝日や組織固有の業務計画、営業日カレンダーを考慮したい等、EventBridgeのみで要求を実現することが難しいケースも少なくありません。そんなときはChange Calendarの出番かもしれません。

やりたいこと

ここで毎営業日の業務終了後にバッチ処理を行う場合を考えてみましょう。営業終了時刻やデータ量によっては0時を跨いで翌日にかけて処理を行うことになり、金曜日の営業後の処理が非営業日の土曜日に開始する可能性が考えられます。Change Calendarはイベントをひとつずつ手で登録することも可能ですが、祝日などの情報はicsファイルを指定してインポートすることができます。休業日を手で登録する場合は、日付だけでなく開始時刻や終了時刻を指定することができるので、休日の開始時刻をずらせばよいのですが、インポートしたカレンダーに休業日が日次イベントとして登録されていると、日付が変わった瞬間にCLOSEDと判定されてしまいます。
これではバッチ処理のジョブ起動時に支障がでるので、インポートした日次イベントをうまく扱う方法を考えてみました。簡単な確認のつもりが、前準備がで思ったより時間くったのでメモを残しておきます。

【前準備】 Google カレンダー 「日本の休日」 の取り込み

iCalendar形式のファイルをインポートできるのですが、RFC 5545に準拠していれば何でも良いというわけではなく、特定のカレンダープロバイダーのみがサポートされているようです。今回はみなさんお馴染み?のGoogleカレンダーの「日本の祝日」を利用しました。

ユーザガイドからリンクされている手順に従ってエクスポートしたものをインポートしたのですが、失敗します。
Import Failed
結論からいうと、上記URLから取得したicsファイルは108kBあり、ユーザガイドに記載された上限サイズを超えていたのが問題だったようで、icsファイルから不要なイベントを削除してサイズを60kB程度に抑えたところインポートに成功しました。

任意の数の有効な .ics ファイルをインポートできます。ただし、各カレンダーでインポートされたファイルすべての合計サイズは 64 KB を超えられません。

なお、トラブルシューティングガイドには、次のような記載があるのですが、大きいから失敗したと言った記載は見当たりませんでした。

解決策 4 — .ics が大きすぎるためにインポートが失敗したと報告された場合は、カレンダーのエントリに関する基本的な詳細のみをエクスポートしていることを確認してください。必要に応じて、エクスポート期間を短縮します。

【前準備】 icsファイルのサイズ削減

icsファイルはBEGIN VEVENTからEND VEVENTの間にイベントの詳細情報が含まれるフラットなテキストファイルなので、イベントが日時順にソートされていれば特定範囲の行を削除するだけでよかったのですが、取得したicsファイルは2019年等の古いイベントが含まれており、時間順にソートもされていませんでした。ちょっと調べた限りではicsファイルをダイエットする良い方法が見当たらず、ics.pyというライブラリを使用して2024年1月1日以前のイベントをフィルタしました。参考までにコードを貼っときますが、かなりやっつけです。

from ics import Calendar
import arrow
import requests

url = 'https://calendar.google.com/calendar/ical/ja.japanese%23holiday%40group.v.calendar.google.com/public/basic.ics'
c = Calendar(requests.get(url).text)
start = arrow.get('2024-01-01T00:00:00.00+09:00')
c2 = Calendar(creator='-//Google Inc//Google Calendar 70.9054//EN')
for e in c.timeline.start_after(start):
    c2.events.add(e)
with open('japan_holidays.ics', 'w') as f:
    f.writelines(c2.serialize_iter())

出来上がったファイルを見るとちょっとメタデータ等怪しいところがありましたが、今回は影響がないので目を瞑りました。今回は古いイベントを除外してファイルサイズを削減しましたが、icsファイルにはイベントに関する様々な情報が含まれているので、Change Calendarとして不要な情報を除去するともっと減らせるかもしれません。

どうやらChange CalendarはPRODIDX-WR-CALNAMEが含まれていないicsファイルを受け付けないようで、PRODIDにもとづいてサポートするカレンダープロバイダか否かを判別しているようだったので、上記コードでは-//Google Inc//Google Calendar 70.9054//ENを指定しています。また、ics.pyでX-WR-CALNAMEを指定する方法がわからなかったので、吐き出したicsファイルに次の行を手で加えています。

X-WR-CALNAME:日本の祝日

タイムゾーンをずらしてインポート

Google カレンダーから取得したicsファイルはタイムゾーンにUTCが指定されています。

X-WR-TIMEZONE:UTC

多くのブログ等がこれをAsia/Tokyoに変更してからインポートするよう紹介しており、通常はそうすべきなのですが、Asia/Tokyo指定すると日時イベントも日本時間の0時から開始することになります。今回はあえてタイムゾーンをずらすことで、日時イベントの開始時刻をずらしてみようと思います。

なお、icsファイルにX-WR-TIMEZONEが指定されていると、インポート時にタイムゾーンを選択するプルダウン自体が表示されないのでご注意ください。
Import without Timezone Selection

営業日のジョブは翌日5時までに完了すると仮定として、タイムゾーンにGMT+04:00を指定してみました。
Import with GMT+04:00

勤労感謝の日が11/23の朝5時から開始しているのがわかります。
Calendar with GMT+04:00

CLIでも、日本時間の11/23午前1時がOPENであることがわかります。

$ aws ssm get-calendar-state \
    --calendar-names "arn:aws:ssm:us-west-2:<AccountId>:document/test" \
    --at-time "2024-11-23T01:00:00+09:00"
{
    "State": "OPEN",
    "AtTime": "2024-11-22T16:00:00Z",
    "NextTransitionTime": "2024-11-22T20:00:00Z"
}

ちなみに、Asia/Tokyoを指定してインポートしてみると、日本時間の0時から勤労感謝の日が始まり、想定通りCLOSEDになります。
Import with GMT+09:00
Calendar with GMT+09:00

$ aws ssm get-calendar-state \
    --calendar-names "arn:aws:ssm:us-west-2:<AccountId>:document/test" \
    --at-time "2024-11-23T01:00:00+09:00"
{
    "State": "CLOSED",
    "AtTime": "2024-11-22T16:00:00Z",
    "NextTransitionTime": "2024-11-23T15:00:00Z"
}

Discussion