はじめに

皆さんはSystems ManagerのChange Calendarという機能をご存知でしょうか?

https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-change-calendar.html

名前の通り、SSMで使用するカレンダーを管理することができる機能です

このChange Calendarには日本の祝日データがないため、サードパーティからエクスポートしてきたものを使用する必要があります
その際に、Change Calendarにインポートすると時間設定がずれてしまう事象が発生したので、解決方法を含めて記載しようと思います

Change Calendarについての概要

Change Calendarは上記の通り、SSMで使用するカレンダーをユーザーが作成・管理できる機能です

AutomationやRun Commandと組み合わせると、カレンダーの状態に応じて異なるアクションを実行させることができます
例えば、Change Calendarに日本の祝日データを登録しておいて、祝日の場合はインスタンスを起動させないタスクを定義することなどができます

非常に便利な機能なのですが、各国の祝日データはSSMから提供されてないためサードパーティから入手する必要があります

公式ドキュメントでは、以下の3つをサポートしているとの記載があります

  • Google カレンダー
  • Microsoft Outlook
  • iCloud カレンダー

実施した作業

今回はGoogle カレンダーからの祝日データエクスポートと、Change Calendarへのデータインポートを実施しました
その手順を、簡単に紹介します

Google カレンダーからの祝日データエクスポート

まずはGoogleカレンダーの画面を開き、「関心のあるカレンダーを探す」をクリックします

「日本の祝日」にチェックを入れます

再度Googleカレンダーのトップ画面に戻り、追加した「日本の祝日」データの設定を開きます

カレンダーの統合の欄にある「iCal形式の公開URL」にアクセスすると、カレンダーのデータがダウンロードされます

Change Calendarへのデータインポート

AWSマネジメントコンソールでSystems Managerの画面を開き、左のメニューから「Change Calendar」をクリックします

新規にカレンダーを作成する場合は「カレンダーを作成」をクリックします
すでに存在するカレンダーを使う場合は、対象のカレンダーをクリックした後に「Action」から「編集」をクリックします

「Import calendar」という箇所でインポートさせたいカレンダーデータを選択したら「保存」をクリックします

発生した事象について

今回、Googleカレンダーからエクスポートした祝日データをChange Calendarにインポートしましたが、2つの問題がありました

  • タイムゾーンがUTCになってしまう
  • タイムゾーンを変更しても、各祝日の時間幅がズレてしまう

タイムゾーンがUTCになってしまう

タイムゾーンがUTCとしてインポートされてしまうので、時間が9時間ずれてしまいます
例えば、11/23(木)の勤労感謝の日が「11/23 AM9:00(JST)~11/24 AM9:00(JST)」と認識されてしまっていました

これの何が困るかというと、例えば時間を指定してタスクを実行する時に問題が発生します

例えば、祝日以外の朝8時にインスタンスを自動起動させたいとします
ただし、カレンダーデータのタイムゾーンがUTCだと、11/24(金)の朝9時までは祝日だと判定されてしまいます
となると、「11/24(金)の8:00は祝日だからインスタンスを起動しない」と判定されてしまい、望んだ挙動をしてくれません

祝日の時間幅がズレてしまう

「タイムゾーンがUTCになってしまう」の解決方法は下記に示しますが、これを解決しても「祝日の時間幅がズレてしまう」という問題が発生しました
タイムゾーンをAsia/Tokyoに変更しても、各祝日の詳細を見てみると日付を跨いでしまっていることがわかります

解決方法

AWSサポートへの問い合わせを元に調査を行った結果、Google カレンダーのデータそのものを編集する必要があると判明しました
具体的には以下の修正が必要とわかりました

  • X-WR-TIMEZONEをAsia/Tokyoに変更する
  • DTSTARTとDTENDに時刻を入力する

X-WR-TIMEZONEをAsia/Tokyoに変更する

Google カレンダーからそのままエクスポートすると.icsという拡張子のデータが手に入ります
これをテキストエディタで開くと、以下のようになっています(長いので上10行だけ抜粋します)

BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:日本の祝日
X-WR-TIMEZONE:UTC
X-WR-CALDESC:日本の祝日と行事
BEGIN:VEVENT
DTSTART;VALUE=DATE:20220503

タイムゾーンを示すX-WR-TIMEZONEUTCになっているので、Asia/Tokyoに変更しました

BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:日本の祝日
X-WR-TIMEZONE:Asia/Tokyo
X-WR-CALDESC:日本の祝日と行事
BEGIN:VEVENT
DTSTART;VALUE=DATE:20220503

すると、タイムゾーンが東京に変わりました!

DTSTARTとDTENDに時刻を入力する

タイムゾーンを変更しても、各日にちの時間幅にはズレが発生しています

ここで再度ICSファイルを深掘りしてみると、DTSTARTおよびDTENDで始まる行の記載を編集する必要があるとわかりました

そもそも、ICSファイルはBEGIN:VEVENTEND:VEVENTで挟まれた箇所が、各祝日のデータになっています
このBEGIN:VEVENTEND:VEVENTで挟まれた箇所の塊が複数列挙されているのがICSファイルなのです
さらに、DTSTARTおよびDTENDは各祝日の開始日と終了日を示しています

Googleカレンダーからダウンロードしてきたデータを確認すると、以下のように日付を跨ぐような記載になっていました

BEGIN:VEVENT
DTSTART;VALUE=DATE:20230101
DTEND;VALUE=DATE:20230102
DTSTAMP:20240119T004943Z
UID:20230101_bi9rgohetsotqlilg2ou523bh8@google.com
CLASS:PUBLIC
CREATED:20220927T105018Z
DESCRIPTION:祝日
LAST-MODIFIED:20220927T105018Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:元日
TRANSP:TRANSPARENT
END:VEVENT

ということで、ICSファイルに以下のような処理を行えれば正確に時間幅を指定できると考えました

  • BEGIN:VEVENTとEND:VEVENTで囲まれた範囲それぞれで以下を実施
    • 現状のDTSTARTの末尾8文字を変数として定義
    • DTSTARTとDTENDを以下に置き換える
      • DTSTART;TZID=Asia/Tokyo:T000000
      • DTEND;TZID=Asia/Tokyo:T235959
    • ただし、最初の7行は処理の対象外にする

このような処理を行なってくれるPythonスクリプトを、ChatGPTにも手伝ってもらいながら作成しました

import re

def process_vevent_block(block):
    date_match = re.search(r"DTSTART;VALUE=DATE:(\d{8})", block)
    if date_match:
        date = date_match.group(1)
        block = re.sub(r"DTSTART;VALUE=DATE:\d{8}", f"DTSTART;TZID=Asia/Tokyo:{date}T000000", block)
        block = re.sub(r"DTEND;VALUE=DATE:\d{8}", f"DTEND;TZID=Asia/Tokyo:{date}T235959", block)
    return block

def process_calendar(input_text):
    lines = input_text.split('\n')
    output_lines = []

    in_vevent_block = False
    current_block = ""

    for line in lines:
        if line.startswith("BEGIN:VEVENT"):
            in_vevent_block = True
            current_block = line + '\n'
        elif line.startswith("END:VEVENT"):
            in_vevent_block = False
            current_block += line
            processed_block = process_vevent_block(current_block)
            output_lines.append(processed_block)
        elif in_vevent_block:
            current_block += line + '\n'
        else:
            output_lines.append(line)

    result = '\n'.join(output_lines).rstrip('\n')  # 末尾の改行を削除
    result = re.sub(r"^\n", "-", result)  # 先頭の改行を - に置換

    return result

file_path = "basic.ics"
with open(file_path, "r", encoding="utf-8") as file:
    input_text = file.read()

output_text = process_calendar(input_text)
print(output_text)

これは作業ディレクトリにあるbasic.icsという名前のファイルを読み込んで標準出力する形になっています
なので、以下のコマンドを実行することで別ファイルに書き込めます

$ python3 ics_time_change.py > output.ics

Change Calendarでインポートできるようにする

これで作成できたICSファイルをChange Calendarに読み込もうとすると、エラーになってしまいます


原因ははっきりとはわかりませんが、おそらく上記の処理でICSファイルが壊れてしまったと考えられます

なので、ICSファイルをGoogleカレンダーにインポートし、再度エクスポートするという手順を踏みました
具体的には以下の通りです

  • Googleカレンダーで新しいカレンダーを作成する
  • 新規作成したカレンダーに、編集したICSファイルをインポートする
  • インポートが完了したら、新規作成したカレンダーをエクスポートする
  • エクスポートしたICSファイルをChange Calenderでインポートする

この手順でうまくインポートすることができました!

時間幅のズレも解消されていることがわかります

注意点

Googleカレンダーの祝日データは、ダウンロードしてきたタイミングのデータしかないです
今後追加される祝日データについては反映されていないため、新しい祝日が追加されたら同じことをする必要があります

また、Googleの祝日データは約1年分しかないようです
実際、2023/12/15にダウンロードしてきたデータの中には2024/11までのデータしか存在しませんでした

なので、年に1度くらいのタイミングで祝日データを更新する必要がありそうです

まとめ

最初は、Change Calendar側で何か問題か設定漏れがあるのかと思いましたが、今回はICSファイルの中身を変更する必要がありました
ICSファイルの中身は長いですが、比較的簡単な構造をしているので使用する前に確認してみると良いと思います

なお、「もっといい方法あるよ!」など意見あれば是非教えて欲しいです!