祝日にリマインドしないSlackリマインダーを作りました【Slack Reminder as Code】

Slackの公式リマインダーが（日本の）祝日を解釈してくれないので、祝日を除外したSlackリマインダーを作りました。

[tksugimoto/aws-lambda_slack-reminder-except-holiday](https://github.com/tksugimoto/aws-lambda_slack-reminder-except-holiday "https://github.com/tksugimoto/aws-lambda_slack-reminder-except-holiday")

# 特徴
* 土日祝日はリマインドしない
    * 年末年始など任意休日も設定可能
* リマインダーをテキスト形式で管理可能（**Slack Reminder as Code**）
* リマインダーの作成 / 変更の反映はコマンド一発
    * `terraform apply`

```bash:11時30分にお昼を通知するリマインダーのレシピ
module "slack_reminder_sample" {
	source = "./slack_reminder"
	prefix = "${var.prefix}"
	iam_for_lambda_arn = "${aws_iam_role.iam_for_lambda.arn}"

	# ↓ ここから設定 ↓
	name = "lunch"
	reminder_text = "そろそろお昼です"

	schedule_name = "every_day_11-30am_JST"
	schedule_expression = "cron(30 2 * * ? *)"

	slack_webhook_url = "${var.slack_webhook_url}"

	# 祝日追加（年末年始など）
	additional_holidays = "12/30-31 1/1-7"
}
```

# 経緯
1. Slackのリマインダー機能で平日の夕方にリマインド投稿を設定していた
2. 2016年の勤労感謝の日(11/23)は水曜日だったのでSlackで設定した平日のみのリマインダーはいつもどおり実行された
3. BOSS :angry: :anger:     
    ![祝日のSlack reminderに激おこ.png](https://qiita-image-store.s3.amazonaws.com/0/92375/1c78fec9-6fa8-0203-3866-9d839ec5f120.png)


# 仕組み
## 毎日のリマインダー
1. [AWS CloudWatch Events](http://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html "http://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html")のスケジューラーを使って毎日定刻に[AWS Lambda](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html "http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html")起動
2. AWS LambdaからSlackの[Incoming WebHooks](https://api.slack.com/incoming-webhooks "https://api.slack.com/incoming-webhooks")を使ってSlackに投稿
    * AWS Lambda関数内で土日・祝日・独自定義休日の判定を行ない平日のみ投稿

## 上記環境の自動構築
* [Terraform](https://www.terraform.io/ "https://www.terraform.io/")を使用
    * Terraformを使うことでリマインダーをテキスト形式で管理可能
    * AWS Lambdaの環境変数とTerraformのmodule機能でリマインダーの複数定義をサポート

# 設定例・書式
```bash:11時30分にお昼を通知するリマインダーのレシピ(30_main.tf)
module "slack_reminder-2" {
	source = "./slack_reminder"
	prefix = "${var.prefix}"
	iam_for_lambda_arn = "${aws_iam_role.iam_for_lambda.arn}"

	# ↓ ここから設定 ↓
	name = "lunch" # AWSのマネジメントコンソールに表示されるのでわかりやすく一意なもの
	reminder_text = "そろそろお昼です"

	schedule_name = "every_day_11-30am_JST" # AWSのマネジメントコンソールに表示されるのでわかりやすいもの
	schedule_expression = "cron(30 2 * * ? *)" # ※ UTCなので日本時間-9時間

	slack_webhook_url = "${var.slack_webhook_url}"

	# 祝日追加（年末年始など）
	additional_holidays = "12/30-31 1/1-7"

	# オプション設定（設定なし/空文字の場合はWebhookのデフォルト値）
	channel = "#random"
	username = "reminder bot"
	icon_emoji = ":bento:"
}
```
※ 適切なシンタックスハイライトが無かったので、一番見やすいと思ったbashにしてあります

↑のリマインダーは↓のようになります。 
2016年の天皇誕生日12/23(金)にはリマインドされてないことが確認できます。
![image](https://qiita-image-store.s3.amazonaws.com/0/92375/0756c3dc-3b86-e13e-4364-1f0bfb742ced.png)

設定のサンプル [30_main.tf.sample](https://github.com/tksugimoto/aws-lambda_slack-reminder-except-holiday/blob/master/30_main.tf.sample "https://github.com/tksugimoto/aws-lambda_slack-reminder-except-holiday/blob/master/30_main.tf.sample") を見てもらえればどんな書き方があるのか分かりやすいと思います。
注意点は、スケジューラーはUTCなので、 `schedule_expression` に書く時刻は日本時間-9時間である必要があることです。
スケジュール書式の詳細は → [Rate または Cron を使用したスケジュール式 - AWS Lambda](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html "http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html")

# 使い方
[tksugimoto/aws-lambda_slack-reminder-except-holiday](https://github.com/tksugimoto/aws-lambda_slack-reminder-except-holiday "https://github.com/tksugimoto/aws-lambda_slack-reminder-詳しくはexcept-holiday")のREADME参照。

# 料金
通常のリマインダーなら無料枠内で収まります。

## 実行環境 [AWS Lambda](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html "http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html")
無料枠が多いのでこのリマインダーによって課金されることは通常は無いと思われます。

> **100 万件** 1 か月あたりの無料リクエスト数
> **1 か月あたり最大 320 万秒** 1 か月あたりのコンピューティング時間
> 12 か月間の AWS 無料利用枠期間終了後にも期限切れになりません。

[無料のクラウドサービス | AWS 無料利用枠](https://aws.amazon.com/jp/free/ "https://aws.amazon.com/jp/free/")

実際の実行時間のログ（一例）を以下に示します。

```text:休日（Slackへの通信なし）
Duration:  6.08 ms	Billed Duration:  100 ms Memory Size: 128 MB	Max Memory Used: 17 MB
Duration:  2.08 ms	Billed Duration:  100 ms Memory Size: 128 MB	Max Memory Used: 12 MB
Duration: 49.61 ms	Billed Duration:  100 ms Memory Size: 128 MB	Max Memory Used: 16 MB
```
```text:平日（Slackへの通信あり）
Duration: 749.63 ms	Billed Duration:  800 ms Memory Size: 128 MB	Max Memory Used: 18 MB
Duration: 983.10 ms	Billed Duration: 1000 ms Memory Size: 128 MB	Max Memory Used: 20 MB
Duration: 719.58 ms	Billed Duration:  800 ms Memory Size: 128 MB	Max Memory Used: 20 MB
```

Slack（Incoming Webhooks）へ通信しない休日は`100 ms`以下で、通信する平日は`800m s`程度でした。

簡単のために毎日が平日で`1000 ms / day / reminder`のLambda実行がされたとすると、無料枠を超えるには約10万リマインダーの設定が必要です。これは1秒に1回以上リマインドされる計算になります。
なお、無料枠を既に超えている場合は、メモリの最小単位128MBで `$ 0.000000208 / 100 ms` なので、1種類のリマインダーを1ヶ月動かすと、

```math
$\, 0.000000208 / 100\, \mathrm{ms} \times 1\, \mathrm{s} / \mathrm{reminder} \times 31\, \mathrm{reminder} / \mathrm{month} = $\, 0.00006448 / \mathrm{month}
```
日本円で約 0.007円となります。

## 定期的な実行 [AWS CloudWatch Events](http://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html "http://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html")
とても安いので無視してよいと思います。
100万リクエストで `$1` だそうなので、約300種類のリマインダーを1ヶ月動かすと1円になります。

> **Amazon CloudWatch Events - Custom Events**
> $1.00 per million custom events generated

[料金 - AWS CloudWatch | AWS](https://aws.amazon.com/jp/cloudwatch/pricing/ "https://aws.amazon.com/jp/cloudwatch/pricing/")


# 祝日判定
Node.jsのpackage [japanese-holidays](https://www.npmjs.com/package/japanese-holidays "https://www.npmjs.com/package/japanese-holidays") を使っています。
npmでインストールできて、依存が少ないものを選択しました。
その年の祝日をすべて計算しているので若干計算量が多いですが、AWS Lambdaが十分高速＆1リマインダーで1回しか呼ばないので計算時間は誤差と判断しました。

* Qiita: [日本の休日を求める（休日判定） - Qiita](http://qiita.com/osamu_takeuchi/items/0623c8aceda43b57914c "http://qiita.com/osamu_takeuchi/items/0623c8aceda43b57914c")
* npmjs: [japanese-holidays](https://www.npmjs.com/package/japanese-holidays "https://www.npmjs.com/package/japanese-holidays")
* ソースコード(GitHub): [osamutake/japanese-holidays-js: Provides utilities to manipulate japanese holidays.](https://github.com/osamutake/japanese-holidays-js "https://github.com/osamutake/japanese-holidays-js")