AWS Systems Manager - RunCommand の定期実行日を判定して、祝日だった場合は翌営業日に延期させてみる(Change Calendar × Lambda)
こんにちは。
食べ放題でバッファオーバーフロー
オペレーション部のかわいです。
みなさんは Systems Manager の RunCommand、使ってますか?
私は使ったり使ってなかったりですが、OS 寄りの機能なので結構好きだったりします。
さて、今回は「特定曜日にジョブを実行してるけど、その日が祝日だった場合は次の営業日にズラしたい」という要件に対して、デフォルトの機能だけでは実装できなさそうだったので検証してみました。
AWS Systems Manager の Change Calendar と Document、あとは Lambda と EventBridge を組み合わせて実現しています。それでは早速いってみましょう!
おおまかな流れ
まずは概要ということで、簡単に処理の流れです。
- EventBridge から定期的に Lambda 関数を起動
- Lambda が Change Calendar を参照し、祝日かどうかを判定
- 指定日が祝日であればそのまま実行せずスキップ。祝日でなければ Systems Manager ドキュメント から Automation → RunCommand を実行
本記事では Systems Manager から RunCommand を実行し、AWS-RunPatchBaseline を該当インスタンスに流すまでがゴールです。
(事前準備)カレンダーのインポート
事前準備として、まずは祝日を定義するために .ics ファイルから Change Calendar カレンダーを作成します。
※カレンダーのインポート方法については以下ブログを参照
以下、.ics カレンダーファイルより冒頭部分抜粋
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:20200101
DTEND;VALUE=DATE:20200102
DTSTAMP:20251003T020254Z
UID:20200101_uatigv0ek4n6d02niftsgule6g@google.com
CLASS:PUBLIC
CREATED:20240522T184449Z
DESCRIPTION:祝日
LAST-MODIFIED:20240522T184449Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:元日
TRANSP:TRANSPARENT
END:VEVENT
~~(以下略)~~
筆者環境では Google Calendar から「日本の祝日」をエクスポートしました。
検証時、そのままではファイルサイズが大きすぎてインポートできなかったので、カレンダーファイルを分割して2つの Change Calendar を作成しました
※ .ics ファイル分割の際は、ファイル内の「BEGIN:VEVENT」から「END:VEVENT」が1つの祝日イベントとなることに留意しつつ、2ファイル目の冒頭にも「BEGIN:VCALENDAR」から「X-WR-CALDESC:日本の祝日と行事」の部分を記載することで、カレンダーファイルとして Change Calendar にアップロードが可能になります。
任意の数の有効な .ics ファイルをインポートできます。ただし、各カレンダーでインポートされたファイルすべての合計サイズは 64 KB を超えられません。
また、カレンダー作成時には「DEFAULT_OPEN」を選択することで、イベントがある日を 閉じた状態 として判定させます。
DEFAULT_OPEN、またはデフォルトでオープン
カレンダーイベント中を除き、デフォルトですべてのアクションの実行が可能です。
Systems Manager - Document の作成
まずは Lambda 関数から呼び出すドキュメントを作成します。
「Systems Manager」→「ドキュメント」から、任意のドキュメント名で以下のようなコンテンツで作成します。
ドキュメントタイプは「コマンド」を指定しました。
{
"schemaVersion": "0.3",
"description": "AWS-RunPatchBaseline",
"parameters": {
"ScheduledDate": {
"type": "String"
},
"InstanceIds": {
"type": "StringList"
},
"Operation": {
"type": "String",
"default": "Install"
}
},
"mainSteps": [
{
"name": "runPatchBaseline",
"action": "aws:runCommand",
"isEnd": true,
"inputs": {
"DocumentName": "AWS-RunPatchBaseline",
"InstanceIds": [
"{{ InstanceIds }}"
],
"Parameters": {
"Operation": [
"{{ Operation }}"
]
}
}
}
]
}
Lambda関数の設定
次に、祝日判定~ジョブ実行指示用の Lambda関数 を設定します。
今回は以下のようなコードを作成しました。
検証タイミングの兼ね合いで、本記事では 火曜日(TARGET_WEEKDAY 箇所で1を指定)を指定 しています。
この数字は月曜日を起点の0とし、日曜日であれば7になります。
- コード全文(ランタイムには Python 3.13 を使用しました)
# lambda_function.py
import os
import json
import hashlib
from datetime import datetime, timedelta, timezone
import boto3
import uuid
ssm = boto3.client('ssm')
CALENDAR_NAMES = [c.strip() for c in os.environ.get('CALENDAR_NAMES', '').split(',') if c.strip()]
TARGET_WEEKDAY = int(os.environ.get('TARGET_WEEKDAY', '1')) # 0=Mon,1=Tue...
LOOKBACK_DAYS = int(os.environ.get('LOOKBACK_DAYS', '7'))
AUTOMATION_DOCUMENT = os.environ.get('AUTOMATION_DOCUMENT', '<ドキュメント名を記入します>')
AUTOMATION_PARAMETERS_JSON = os.environ.get('AUTOMATION_PARAMETERS_JSON', '{}')
DRY_RUN = os.environ.get('DRY_RUN', 'true').lower() == 'true'
TEST_DATE = os.environ.get('TEST_DATE') # optional override YYYY-MM-DD
def now_jst():
return datetime.now(timezone.utc).astimezone(timezone(timedelta(hours=9)))
def iso_at_midnight_jst(date_obj):
return date_obj.strftime('%Y-%m-%dT00:00:00+09:00')
def is_calendar_open_for_date(date_obj):
# date_obj is date or datetime
if isinstance(date_obj, datetime):
dt = date_obj
else:
dt = datetime.combine(date_obj, datetime.min.time())
at_time = dt.strftime('%Y-%m-%dT00:00:00+09:00')
resp = ssm.get_calendar_state(CalendarNames=CALENDAR_NAMES, AtTime=at_time)
state = resp.get('State')
print(f"GetCalendarState at {at_time} => State={state}")
return state == 'OPEN'
def client_token_for_date(date_obj):
if isinstance(date_obj, datetime):
date_iso = date_obj.date().isoformat()
else:
date_iso = date_obj.isoformat()
token_input = f"{date_iso}|{AUTOMATION_DOCUMENT}"
return str(uuid.uuid5(uuid.NAMESPACE_DNS, token_input))
def start_automation(scheduled_date_str, parameters):
token = client_token_for_date(datetime.fromisoformat(scheduled_date_str))
if DRY_RUN:
print(f"[DRY_RUN] Would start automation {AUTOMATION_DOCUMENT} for scheduled_date={scheduled_date_str} token={token} params={parameters}")
return None
resp = ssm.start_automation_execution(
DocumentName=AUTOMATION_DOCUMENT,
Parameters=parameters,
ClientToken=token
)
exec_id = resp.get('AutomationExecutionId')
print(f"Automation を開始します: {exec_id}")
return exec_id
def lambda_handler(event, context):
# TEST_DATE を使うのは DRY_RUN が true のときだけ(誤運用防止)
if DRY_RUN and TEST_DATE:
today = datetime.fromisoformat(TEST_DATE).date()
else:
today = now_jst().date()
print(f"Lambda invoked for today (JST): {today.isoformat()}")
# 今日が実行日かどうかを判定
if today.weekday() == TARGET_WEEKDAY:
print("今日はターゲット曜日です")
if is_calendar_open_for_date(today):
print("今日は OPEN です -> execute scheduled_date = today")
try:
parameters = json.loads(AUTOMATION_PARAMETERS_JSON or "{}")
except Exception as e:
print(f"ERROR: Failed to parse AUTOMATION_PARAMETERS_JSON: {e}")
return
parameters.setdefault('ScheduledDate', [today.isoformat()])
start_automation(today.isoformat(), parameters)
return
else:
print("今日は CLOSED です -> skip and leave pending")
return
# 実行日ではない場合は過去の日付をチェック
for d in range(1, LOOKBACK_DAYS + 1):
sd = today - timedelta(days=d)
if sd.weekday() != TARGET_WEEKDAY:
continue
print(f"スケジュール候補日: {sd.isoformat()}")
if not is_calendar_open_for_date(sd):
print(f"scheduled_date {sd.isoformat()} は CLOSED でした")
if not is_calendar_open_for_date(today):
print("今日も CLOSED です-> cannot execute now.")
continue
try:
parameters = json.loads(AUTOMATION_PARAMETERS_JSON or "{}")
except Exception as e:
print(f"ERROR: Failed to parse AUTOMATION_PARAMETERS_JSON: {e}")
return
parameters.setdefault('ScheduledDate', [sd.isoformat()])
start_automation(sd.isoformat(), parameters)
return
else:
print(f"scheduled_date {sd.isoformat()} was OPEN -> assume handled.")
print("実行対象の scheduled_date は見つかりませんでした")
Lambda 関数の IAM ポリシー権限と環境変数について
ここで結構重要なのが IAM ポリシーの権限と環境変数です。
まず Lambda 関数には、以下の IAM ポリシー権限を付与しました。
※本番環境の場合は各リソースレベルでの制限推奨です。
SSM の各種アクションと、CLoudWatch Logs へのログ出力権限を付与します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSSMActionsForLambda",
"Effect": "Allow",
"Action": [
"ssm:GetCalendarState",
"ssm:StartAutomationExecution",
"ssm:DescribeAutomationExecutions",
"ssm:SendCommand",
"ssm:ListCommands",
"ssm:ListCommandInvocations",
"ssm:GetCommandInvocation",
"ssm:DescribeInstanceInformation"
],
"Resource": "*"
},
{
"Sid": "AllowCloudWatchLogsForLambda",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
また、コード内の各値をオーバーライドする場合は、下記のように環境変数を設定します。
例えば「DRY_RUN」の部分を「true」にすることで、本実行なしにテストできます。
「TARGET_WEEKDAY」は任意の曜日を指定してください。
「AUTOMATION_DOCUMENT」には、今回作成したドキュメント名を指定します。
手動テスト
念のため、Lambda 関数の手動テストを実施します。
START RequestId: aef13037-e0c1-4596-bacd-7d75c4141f2a Version: $LATEST
Lambda invoked for today (JST): 2025-10-06
スケジュール候補日: 2025-09-30
GetCalendarState at 2025-09-30T00:00:00+09:00 => State=OPEN
scheduled_date 2025-09-30 was OPEN -> assume handled.
実行対象の scheduled_date は見つかりませんでした
END RequestId: aef13037-e0c1-4596-bacd-7d75c4141f2a
REPORT RequestId: aef13037-e0c1-4596-bacd-7d75c4141f2a Duration: 443.41 ms Billed Duration: 988 ms Memory Size: 128 MB Max Memory Used: 86 MB Init Duration: 544.00 ms```
→ この実行結果では、10/6(月)にテスト
→ 前回の火曜日(9/30)を実施済み判定とし今回の実行はスキップされたので、想定内の挙動となりました。
あとは EventBridge ルールを指定し、定期実行を仕掛ければ準備完了です。
EventBridge ルールの作成
今回は平日の深夜12:00am に実行させたいので、以下の cron 式でルールを作成しました。
※UTC 表記な点に注意
ターゲットには先ほど追加した Lambda 関数を指定します。
実行結果の確認
朝起きて RunCommand の実行履歴から結果を確認します。
無事実行されてますね(GMT 15:00 なので、10/7の深夜0時付近に実行)。
また、意図的に祝日としてスキップさせたい場合は、Change Calendar から手動でイベントを追加します。
以下は Lambda の手動テスト結果ですが、祝日判定され「今日は CLOSED です -> skip and leave pending」、想定どおり実行はスキップされました。
START RequestId: ee89ae0d-71a7-41ec-bd0b-4eabaf2ee935 Version: $LATEST
Lambda invoked for today (JST): 2025-10-08
今日はターゲット曜日です
GetCalendarState at 2025-10-08T00:00:00+09:00 => State=CLOSED
今日は CLOSED です -> skip and leave pending
END RequestId: ee89ae0d-71a7-41ec-bd0b-4eabaf2ee935
REPORT RequestId: ee89ae0d-71a7-41ec-bd0b-4eabaf2ee935 Duration: 471.48 ms Billed Duration: 984 ms Memory Size: 128 MB Max Memory Used: 88 MB Init Duration: 512.38 ms
まとめ的な
本記事では、Systems Manager の Change Calendar と Lambda を組み合わせて、「定期ジョブを、その日が祝日なら翌営業日にスライドして実行する方法」を紹介しました(他にも良いやり方あるかも)。
カレンダーファイルは1年に1回手動更新しないといけない点が運用面での注意点ですが、比較的容易に実装できるのが良いポイントだと感じました。応用すれば色んなパターンに活用できそうですね。
ではまた!