モチベーション
人事の方が毎月最終営業日(平日)に月締めの連絡(勤怠や経費精算)を社内全員にSlackで通知していたので自動化したい
条件など
- 主に文字列だけ送信(複雑なことはない)
- 毎月最終営業日(平日)は月によって日数や曜日、日本の祝日があるので変化する
案出し
-
Slack Workflow
-
毎月最終営業日(平日)は月によって日数や曜日、日本の祝日があるので変化する
- 上記の制約により細かいスケジュール設定がSlack Workflowではできなかったのでボツ
-
-
cron-job
- 無料で使えるcron-jobを利用しクラウドに上げたリソースを叩く方針
- スケジュールがSlack Workflowと同じような感じにしか設定できなかったためボツ
- AWS Lambda & EventBridge
- 上記の案よりスケジュールの設定が詳細にでき、リソースも管理できるかつ料金はほぼかからないため、今回こちらを採用
- デプロイするときにLambda Layersを使用して共通のパッケージをシェアできるようにすることやリソースサイズを下げることができるが今回は126MBほどだったことやシェアするパッケージが特にないので全てビジネスロジックにまとめてデプロイすることにします
実装(コード)
完成コード
PythonとTypeScriptの検討をしましたが、今回はPythonで実装します。日本の祝日判定ができるライブラリjpholidayの更新がjapanese-holidaysやholidays-jpより最近だったためです。どちらでも実装できそうです。
Slackに表示したいtextを環境変数に入れてLambdaの環境変数をいじるだけで通知内容を変更できるように設計しました。
main.py (file名と関数名は任意です。importは省略してます)
def lambda_handler(event=None, context=None):
load_dotenv(verbose=True)
slack_token = os.getenv("SLACK_API_BOT_TOKEN")
slack_channel = os.getenv("SLACK_CHANNEL")
chat_text = os.getenv("CHAT_TEXT")
sClient = SClient(slack_token, slack_channel)
today = sClient.today().strftime('%d')
# today = '31' # for test
check_day = sClient.get_last_weekday()
if today == check_day:
sClient.post_message(chat_text)
else:
pprint.pprint(f'unmatch {today}')
slack_client.py(importは省略してます)
class SClient:
def __init__(self, token, channel):
self.client = WebClient(token=token)
self.channel = channel
def post_message(self, text) -> Union[dict, None]:
"""
Post a message to Slack
Parameters
----------
text : str
The message to post
"""
# Convert \\n to newline for lambda_handler()
replaced_text = text.replace('\\n', '\n')
try:
response = self.client.chat_postMessage(
channel=self.channel,
text=replaced_text,
mrkdwn=True,
link_names=True,
)
return response
except SlackApiError as e:
print(f"Error posting message: {e.response['error']}")
return None
def get_last_weekday(self) -> str:
"""
Get the last weekday of the month
Returns
-------
day : str
The last weekday of the month, e.g., '31'
"""
today = self.today()
next_month = today.replace(day=28) + timedelta(days=4)
last_day = next_month - timedelta(days=next_month.day)
while last_day.weekday() >= 5 or jpholiday.is_holiday(last_day):
last_day -= timedelta(days=1)
day = last_day.strftime('%d')
return day
def today(self) -> datetime:
"""
Get today's date and time in JST
Returns
-------
today : datetime
Today's date and time
"""
JST = timezone(timedelta(hours=+9))
today = datetime.now(JST)
return today
特記
- 毎月の最終営業日の判定はシンプル
- 閏年の影響がある2月を起点に考慮し毎月26日以降(土日を避けたい)に関数を実行する
- 関数実行日を28日に変換し、4日足して強制的に次の月のどこかの日にする
- 次の月からその日数分を引いて特定したい月の最終日を割り出す
- 月の最終日が土曜、日曜、祝日だった場合1日戻る(平日になるまで行う)
- (p.s AIに聞いてもほぼ同じコードが出てきた。。。)
- TimezoneをJSTにする
- AWS Lambdaの実行環境はUTCになっているので直す必要がある
- 環境変数の任意の文字列の改行
\n
を\\n
にする(改行がある場合)- うまく変換されず改行されない文がSlackに送られてしまう
Slack Appのセッティング
Slack ApiでCreate New APPを押してbotを作成する。
こちらの記事を参考に作成しました。ありがとうございます。
必要なもの
- OAuth TokensのBot User OAuth Token
- OAuth & PermissonのScopesでBot Token Scopeに
chat:write
を付与する
Slack Channelに作成したアプリ(bot)を追加
- botアプリを追加したいSlackチャンネルを開いて右上の
その他
を押す。(点が3つあるやつ) - チャンネル詳細を開くを押す
- インテグレーションを押す
- APPのアプリを追加するから先ほど作成したbotアプリを追加する
zip fileの作成
AWS Lambdaにコードやパッケージをデプロイするときにzip fileにする必要があるのでVertual Environmentを作成し、そこにパッケージをinstallしてzip fileを作る。
$ python -m venv venv
$ pip install -r requirements.txt
make_zip.sh
#!/bin/bash
# Define the name of the zip file
ZIP_FILE="lambda_function.zip"
# Remove the existing zip file if it exists
if [ -f "$ZIP_FILE" ]; then
rm "$ZIP_FILE"
fi
# Create a new zip file with the contents of the current directory
mkdir -p build
cp -r venv/lib/python3.13/site-packages/* build/
cp *.py build/
cp requirements.txt build/
cd build || exit
zip -r "../$ZIP_FILE" .
cd ..
echo "Created $ZIP_FILE for AWS Lambda function."
lambda_function.zipというfileが作成されます。
AWS Lambda & EventBrigeの設定
AWS accountやIAMの設定などはここでは記述しません。
AWS Lambda関数の作成
- 関数を作成を押す
- 一から作成
- 関数名を任意で決める
- ランタイムはPython3.13を指定
- アーキテクチャはx86_64
- 関数の作成を押す
EventBrigeの設定
- 上記で作成したLambda関数のダッシュボードに遷移
- トリガーを追加を押す
- トリガーをEventBrigeを指定する
- ルール
- 新規作成
- ルール名と説明をわかりやすく任意で書いておく
- ルールタイプ
- スケジュール式を選択
- 公式を参考にcron式を設定
-
cron(0 6 26-31 * ? *)
カッコ内左から(分, 時間, 日, 月, 曜日, 年)
*
はワイルドカードで全て、?
は特に指定なしの意味
つまり、全ての年の毎月(全ての月)26~31日の{6時(UTC) == 15時(JST)}に曜日指定なし
に実行の意味
※ cron式に矛盾や書式ミスがあった場合、警告が出ます - 追加を押す
コードデプロイ
- AWS Lambdaで作成した関数のダッシュボードに遷移
- コードタブを押す(デフォルトで開いている)
- コードソース内の deploy を押して先ほど作成した lambda_function.zip をuploadする(しばらく待つとデプロイ成功など通知が画面に出る)
- 画面下にスクロールしてランタイム設定の編集を押す
- handerを
file名.関数名
に設定する。今回の場合main.lambda_handler
- handerを
環境変数を設定
- AWS Lambdaで作成した関数のダッシュボードに遷移
- 設定を押す
- 左ペインから環境変数を押す
-
CHAT_TEXT
: Slackに表示させたい任意の文字 -
SLACK_API_BOT_TOKEN
: 先ほど取得したSLACK_API_BOT_TOKEN -
SLACK_CHANNEL
: 文字列を表示させたいSlackチャンネルのidを指定する- Slackを開いて表示させたいチャンネルを選択
- 右上の
その他
を押す - チャンネル詳細を押す
- Window の一番下に表示されているものが channel_id
-
完成
テストはlambda_handler内の下記を修正し、コードソース内のテストまたはテストタプを実行すると実行できる。
うまくいけば指定チャンネルにCHAT_TEXT
で指定した文字列が投稿されます。
# today = sClient.today().strftime('%d')
today = '31' # for test ← ここを実行月の最終営業日に設定
APPの画像はAIで作りました
calendarを使用した方が綺麗に書けたかも。