※こちらは社内に2024年1月頃に公開したメモを少し改変したものです
※相変わらずどんくさい方法かもしれませんが、当時はとても有効でした。
ざっくりと!何をしたのか
Before
今までは全祝日を各Lambdaに記載し、記載した祝日と実行日を照らし合わせ、「祝日orそうでない」で場合分けしていたのですが…

(なのでコードとして良くないですし、メンテナンスもめちゃ面倒でした…
)
After
公開されている祝日(一般的な日本の祝日)はライブラリから取得し、その他設定したい祝日をファイルに記載しS3に保管。祝日判定時にそのS3のファイルを見に行く、としました。

この方法だと、S3内に配置されたファイルだけメンテすれば良いので、ソースを汚すことも無ければ複数ソースを確認する必要も無くなるので良いですよね。この図だけ見ても、Beforeでは3か所を修正・確認しなければならなかったところ、Afterでは1箇所の修正・確認だけで済みます![]()
もう少し詳しく…何をしたのか
一般的な「日本の祝日」を取得するライブラリ
この問題に直面した際にその内容を聞いてくださっていた他のエンジニアさんが「調べたらこんなのありましたよ」って教えてくださり…これがめちゃ便利でした。
上記以外の「祝日としたい日」をS3に保管する
このライブラリだけだと、例えば「1月2日」とかは祝日とはならないのです。でも、そういう日も祝日として扱いたい!とかあると思うのですよね。
ライブラリのREADMEにも「独自の休日」という形で載っていますが、holidays.ini というファイルにそういった日付をまとめました。で、これをS3に保管していつでも呼べるようにする、と。
下記はサンプルです。
[HOLIDAYS]
2024-01-02: 元旦休暇*
2024-01-03: 元旦休暇*
2024-12-29: 年末休暇*
2024-12-30: 年末休暇*
2024-12-31: 年末休暇*
2023-12-29: 年末休暇*
2023-12-30: 年末休暇*
2023-12-31: 年末休暇*
2022-02-22: test猫の日*
コードサンプル
ライブラリのREADMEを真似ればいろいろできますが、私はこんなコードを記述して使っています。使用しているソースから今回の記事に不要な内容はテキトーに削除していることもあり、コレをまるっとコピペしても動きません&余計な記述がまだ残っているかもしれませんが、流れを何となくでも感じていただければなと。
祝日判定関数などなど
import configparser
import logging
from datetime import datetime, timedelta, timezone
import boto3
import jpholiday
from botocore.exceptions import ClientError, NoCredentialsError
from constants import ResponsecdValues
s3_client = boto3.client("s3")
# 日本のタイムゾーンを定義
JST = timezone(timedelta(hours=+9))
logger = logging.getLogger()
# INFO、WARNING、ERROR、CRITICALレベルのメッセージを出力する
logger.setLevel(logging.INFO)
# 指定した祝日を取得する
def get_original_holidays(bucket_name, file_name):
original_holidays = {}
config = configparser.ConfigParser()
# S3クライアントの初期化
s3_client = boto3.client("s3")
try:
# S3からholidays.iniファイルをダウンロード
s3_client.download_file(bucket_name, file_name, "/tmp/" + file_name)
config.read("/tmp/" + file_name)
if "HOLIDAYS" in config:
original_holidays = config["HOLIDAYS"]
return original_holidays
# エラーハンドリング
except NoCredentialsError as e:
handle_error(e, ResponsecdValues.NO_CREDENTIALS_ERROR.value, 500)
except ClientError as e:
handle_error(e, ResponsecdValues.CLIENT_ERROR.value, 500)
except Exception as e:
handle_error(e, ResponsecdValues.FAIL.value, 503)
# 祝日か否かを判定する
def _is_holiday(date, bucket_name, file_name):
original_holidays = get_original_holidays(bucket_name, file_name)
if date in [
datetime.strptime(holiday, "%Y-%m-%d").date()
for holiday in original_holidays.keys()
]:
# 設定した祝日 に当てはまっているか
return True, _is_holiday_name(original_holidays, date)
elif jpholiday.is_holiday(date):
# 日本の祝日 に当てはまっているか
return True, jpholiday.is_holiday_name(date)
return False, ""
# 祝日名を取得する
def _is_holiday_name(original_holidays, date):
if date.strftime("%Y-%m-%d") in original_holidays.keys():
return original_holidays[date.strftime("%Y-%m-%d")]
else:
return None
- メインで使用するソースから呼び出すのは
_is_holidayです。- 初めに「独自の休日(祝日として指定したい日)」を
get_original_holidaysにて取得します。- ここではS3にアクセスし、ファイルを呼び出しています。
- まずは「独自の休日」に当てはまっているか、確認します。
- 当てはまっていた場合は「True」と、
_is_holiday_nameを用いて「祝日名(これも独自指定)」を返却します。
- 当てはまっていた場合は「True」と、
- 次に「日本の祝日」に当てはまっているか、確認します。
- 当てはまっていた場合は「True」と、
_is_holiday_nameを用いて「祝日名」を返却します。
- 当てはまっていた場合は「True」と、
- 「独自の休日」「日本の祝日」どちらにも当てはまっていない場合は「False」「(無)」を返却します。
- 初めに「独自の休日(祝日として指定したい日)」を
メイン…祝日判定関数を呼び出す側
import logging
from datetime import datetime, timedelta, timezone
import check_holiday
import get_data
from constants import ResponsecdValues
from retry.api import retry_call
# 日本のタイムゾーンを定義
JST = timezone(timedelta(hours=+9))
logger = logging.getLogger()
# INFO、WARNING、ERROR、CRITICALレベルのメッセージを出力する
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
try:
# SecretsManager から, この Lambda 内で使用したい特有の値を取得する
secret_dict_lambda = retry_call(
get_data.get_secret,
fargs=["lambda-info-hogehoge"],
fkwargs=None,
exceptions=Exception,
tries=3,
delay=1,
backoff=2,
jitter=0.3,
)
# 実行日付取得
today_datetime = datetime.now(JST)
today = today_datetime.date()
message = ""
# 祝日判定
holiday_checker = check_holiday._is_holiday(
today, secret_dict_lambda["S3_BUCKET"], secret_dict_lambda["S3_FILE"]
)
if holiday_checker[0]:
print("祝日(" + holiday_checker[1] + ")のため, 以降は実施しない")
message = "祝日のため、以降は実施しない。"
else:
message = "祝日ではないため、以降を実施。"
# ここに処理を記載★
# 正常系ログ を出力
detail = {
"statusCode": 200,
"responsecd": ResponsecdValues.SUCCESS.value,
"message": message,
}
logger.info(detail)
# エラーハンドリング
except KeyError as e:
handle_error(e, ResponsecdValues.KEY_ERROR.value, 500)
except Exception as e:
handle_error(e, ResponsecdValues.FAIL.value, 503)
- SecretsManager から, この Lambda 内で使用したい特有の値を取得します。
- (ここは祝日判定に限らず…普通に環境変数に登録しても良い内容ですが、Lambda関数の管理方法として試みた結果この書き方になりました。また別の機会で触れます。)
- 実行日付を取得し、その日付と
_is_holidayを用いて祝日判定を行います。- 「True」が返却された場合は祝日のため、以降の処理は実施しません。
- 「False」が返却された場合は祝日ではないため、処理を実施します。
- 実行したい処理を
# ここに処理を記載★部分に記載するのが良いです。インデントを間違えないように気を付けて。
- 実行したい処理を
おわりに
ちなみに以下の方法も検討したのですが(これは日付をカレンダーUIで見られるのが良いですよね)、この方法だと主に以下の懸念点があり断念しました。
- 祝日を一度に全て登録できなさそう
- 1件1件、画面遷移しながら登録することに
- 複数Lambdaに反映させたい際、祝日判定Lambdaから複数Lambdaを呼び出すことになる
ことになる?- もし最初の祝日判定Lambdaが何かしらの原因で焦げたら、以降の処理のLambdaは全て実施されない…?
これでカンペキ!とは言えませんが、年末年始が近づいているということで祝日設定のメンテナンスをさっそく実行したところ、以前よりずっと管理がしやすくなったな~と実感を得られまして(実際は実感という感覚だけでなく、工数もしっかり減っています)やった甲斐がありました。
皆さんは祝日対応、どんな方法を採用していますか?良ければぜひ共有してくださいね![]()