Fusic Tech Blog

Fusion of Society, IT and Culture

Go+serverlessで祝日を考慮したSlackリマインダー
2020/12/08

Go+serverlessで祝日を考慮したSlackリマインダー

本記事はFusic Advent Calendar 2020の8日目の記事です。

昨日は@kozoさんの記事でした。 「LaravelでSQLを直接実行したい場合にSQLをどこに置くのか?」 Laravelはディレクトリ構成が自由に設計でき、逆に困ることもあるのですごく参考になりました。

さて、「毎週月曜日にSlackで何かしらのリマインダを送っているけれども、祝日や振替休日にまでchannelやhereの通知が来て煩わしい」なんてことはよくあると思います。 今回、月曜日に定期実行され、月曜が祝日だったら送信しないSlackリマインダーをGo+serverlessで作成してみました。

環境

Serverless Framework: 2.14.0

構成

Cloudwatch Eventsの定期実行でGoで記述されたLambdaを呼び出し、LambdaからSlackのincoming webhookを呼び出します。

go-lambda-diagram

実装

SlackのIncoming Webhook URLをドキュメントに従って取得します。

https://api.slack.com/messaging/webhooks

Serverless Framework

Serverless Frameworkを導入して、設定を記述していきます。

serverless create -u https://github.com/serverless/serverless-golang/ -p go-lambda-slack

go.modを作成して、必要なパッケージを導入します。

cd go-lambda-slack
go mod init go-lambda-slack
go get github.com/aws/aws-lambda-go
go get github.com/najeira/jpholiday
go get github.com/slack-go/slack
go get github.com/stretchr/testify

最終的なディレクトリ構成は以下のようにします。

.
├── README.md
├── config
│   └── setting.yml
├── main.go
├── go.mod
├── go.sum
├── notification
│   ├── notification.go
│   └── notification_test.go
└── serverless.yml

config/setting.ymlには先程取得したSlackのIncoming Webhook URLを記載します。このファイルは.gitignoreに記述してgitの管理から外したりする処置が必要です。

WEB_HOOK_URL: https://hooks.slack.com/services/xxxxxxxxx/xxxxxxxxx/xxxxxxxxx

serverless.ymlです。作成するLambda内で環境変数として扱えるよう、ファイルから読み出したURLを環境変数として設定しています。

CloudWatch Eventsでのcronの時間指定はJSTではなく、UTCになるので注意が必要です。今回は月曜の朝9時に実行されるよう設定しています。これが例えば、月曜の朝8時に実行したいという場合には、UTCでは日曜日の23時に実行する必要があります。

service: go-lambda-slack

provider:
  name: aws
  runtime: go1.x
  stage: dev
  region: ap-northeast-1
  logRetentionInDays: 1
  versionFunctions: false

package:
 exclude:
   - ./**
 include:
   - ./bin/**

functions:
  notification:
    handler: bin/main
    environment: ${file(./config/setting.yml)}
    events:
      - schedule: cron(0 0 * * MON *)

Lambda

祝日の判定にはnajeira/jpholiday、Slackの送信にはslack-go/slackを使用します。 Goで祝日を扱うライブラリはいくつかあるようですが、2021東京オリンピック等の祝日判定に対応しているものを探してこちらを選択しました。振替休日にも対応されています。

// main.go
package main

import (
	"fmt"
	"os"

	"go-lambda-slack/notification"

	"github.com/aws/aws-lambda-go/lambda"
)

// Handler ...
func Handler() error {
	url := os.Getenv("WEB_HOOK_URL")
	err := notification.Send(url)

	if err != nil {
		fmt.Println(err)
	}

	return nil
}

func main() {
	lambda.Start(Handler)
}
// notification/notification.go
package notification

import (
	"fmt"
	"time"

	holiday "github.com/najeira/jpholiday"
	"github.com/slack-go/slack"
)

// Send ...
func Send(url string) error {
	today := time.Now()
	if isHolyday(today) {
		fmt.Println("今日は" + holiday.Name(today) + "!")
		return nil
	}

	text := "<!here>\n今日は平日!"

	msg := slack.WebhookMessage{
		Text: text,
	}

	return slack.PostWebhook(url, &msg)
}

func isHolyday(t time.Time) bool {
	return holiday.Name(t) != ""
}

祝日判定に問題ないかだけテストを行います。

// notification/notification_test.go
package notification

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
)

func TestIsHolyday(t *testing.T) {
	tests := []struct {
		input    time.Time
		expected bool
	}{
		{time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC), true},    // 2021-01-01 元日
		{time.Date(2021, time.February, 23, 0, 0, 0, 0, time.UTC), true},  // 2021-02-23 天皇誕生日
		{time.Date(2021, time.July, 22, 0, 0, 0, 0, time.UTC), true},      // 2021-07-22 海の日
		{time.Date(2021, time.July, 23, 0, 0, 0, 0, time.UTC), true},      // 2021-07-23 スポーツの日
		{time.Date(2021, time.August, 8, 0, 0, 0, 0, time.UTC), true},     // 2021-08-08 山の日
		{time.Date(2021, time.August, 9, 0, 0, 0, 0, time.UTC), true},     // 2021-08-09 山の日振替休日
		{time.Date(2021, time.December, 23, 0, 0, 0, 0, time.UTC), false}, // 2021-12-23 天皇誕生日ではない
		{time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC), true},    // 2022-01-01 元日
		{time.Date(2022, time.July, 18, 0, 0, 0, 0, time.UTC), true},      // 2022-07-18 海の日
		{time.Date(2022, time.July, 22, 0, 0, 0, 0, time.UTC), false},     // 2022-07-22 海の日ではない
		{time.Date(2022, time.July, 23, 0, 0, 0, 0, time.UTC), false},     // 2022-07-23 スポーツの日ではない
		{time.Date(2022, time.August, 8, 0, 0, 0, 0, time.UTC), false},    // 2022-08-08 山の日ではない
		{time.Date(2022, time.August, 11, 0, 0, 0, 0, time.UTC), true},    // 2022-08-11 山の日
		{time.Date(2022, time.October, 10, 0, 0, 0, 0, time.UTC), true},   // 2022-10-10 スポーツの日
	}

	for _, tt := range tests {
		assert.Equal(t, isHolyday(tt.input), tt.expected)
	}
}
$ go test ./notification/
ok      go-lambda-slack/notification    0.530s

大丈夫そうですね。

デプロイ

Goのbuildを実行して、デバッグ用にcronの設定を毎分実行されるようにしてからデプロイを実行します。

GOOS=linux go build -o bin/main
sls deploy -v

本日は祝日ではないので、Slack通知が実行されます。

notification/notification.go中のtodayを2021/01/01に書き換えて再度デプロイしてみます。Slackの通知が来ず、CloudWatch Logsにメッセージが出力されているのが確認できます。

cloudwatch-log

まとめ

祝日には実行されないSlackリマインダをCloudWatch Events+Lambda(Go)で作成しました。Serverless Frameworkだとサクッと作れて良いですね。

makihara

makihara

2020年からFusicでWebエンジニアをしています。 仕事ではPHPを主に扱っていて、最近はGoが楽しいです。