
オンライン雑談会の日程調整で 「調整さんの回答期限は今日だよ!!」 をSlackに自動投稿する仕組みをサーバーレスで作りました
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
定期開催しているオンライン雑談会があります。日時の調整に調整さんが大活躍しています。 おおよその作業フローは下記です。
- 月初に「調整さんを作ってね」のリマインダーがSlackに自動投稿される(以前に作成した仕組み)
- 人間が調整さんを作成する
- 人間がSlackのチャンネルで案内する(URLと締切)
- 人間が締切日に「今日が締切だよ!」と案内する
地味にめんどくさい、かつ、忘れがちな 3.と4.をテコ入れしてサーバーレスによる自動化に挑戦しました。
なお、下記で「開催日は今日だよ!」と案内する仕組みも導入しました。
おすすめの方
- AWS SAMを使いたい
- SlackでAppを作りたいい
- SlackでEvents APIを使いたい
- SlackでIncoming Webhookを使いたい
- LambdaでSlackに投稿したい
最初に作ったものをご紹介(完成版)
作業フロー(ユーザ視点)
- 毎月初日に「調整さん作ってね」がチャンネルに自動投稿される(以前に作成)
- 人間が調整さんを作成し、Slackのワークフローで登録する(URLと締切日)
- 締切日の10時に「今日が締切だよ」がチャンネルに自動投稿される
1. 毎月初日に「調整さん作ってね」がチャンネルに自動投稿される
コレ自体は以前に作成した仕組みです。
2. 人間が調整さんを作成し、Slackのワークフローで登録する(URLと締切日)
調整さんを作成したあと、Slackのワークフローを人間が起動し、「調整さんのURL」と「回答期限」を入力します。 このワークフローはチャンネルに投稿してお知らせします。
3. 締切日の10時に「今日が締切だよ」がチャンネルに自動投稿される
全体概要
SlackのEvents APIを利用してチャンネルのメッセージを監視し、ワークフロー経由で投稿した「調整さんのURLと期限」をDynamoDBへ保存します。 その後、1日1回の頻度でDynamoDBを確認し、期限が今日の項目があればSlackにその旨を投稿しています。
DynamoDBのハッシュキーは年月日のunixtime(0時0分0秒)を使っています。これは処理を楽にするため、かつ、同じ日に複数の調整さんを登録しないためです。 また、DynamoDBのデータ削除はTTL機能を使って自動削除させています。最大でも48時間後の削除になりえますが、少ないサンプル数での実績としてほぼすぐ消えたこと、および、2日連続で通知されることを許容します。
環境&前提
| 項目 | バージョン |
|---|---|
| macOS | Mojave 10.14.6 |
| Python | 3.7 |
| SAM CLI | version 0.37.0 |
下記の続きで作成していきます。
目次(ここから下)
- 期日をDynamoDBに保存するAPI(Lambda)を作成する(verify用)
- Slackのアプリを作成する
- Slackアプリをワークスペースに追加する
- Slackのチャンネルにアプリを追加する
- Slackのワークフローを作成する
- 期日をDynamoDBに保存するAPI(Lambda)を作成する
- 締め切りを告知するLambdaを作成する
- さいごに
期日をDynamoDBに保存するAPI(Lambda)を作成する(verify用)
チャンネルのメッセージを受け取るAPIを作成しますが、「そのAPIは有効なの?」を確認する必要があるため、まずはSlackの仕様に基づいた応答を返すAPIを作成します。
AWS SAMテンプレート
前回の内容も含んでいます。また、この時点で下記も追加しています。
- DynamoDBの定義
- LambdaにDynamoDBFullAccessを付与
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Chouseisan-reminder
Parameters:
ChouseisanNotifySlackUrl:
Type : AWS::SSM::Parameter::Value<String>
Resources:
ChouseisanReminderFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: chouseisan-reminder-function
CodeUri: src/notify_1st_message/
Handler: app.lambda_handler
Runtime: python3.7
Timeout: 10
Environment:
Variables:
TZ: Asia/Tokyo
INCOMMING_WEBHOOK_URL: !Ref ChouseisanNotifySlackUrl
Events:
NotifySlack:
Type: Schedule
Properties:
Schedule: cron(0 0 1-7 * ? *) # 日本時間で毎月1-7日のAM9時
ChouseisanReminderFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${ChouseisanReminderFunction}"
ChouseisanSaveDeadlineFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: chouseisan-save-deadline-function
CodeUri: src/save_deadline/
Handler: app.lambda_handler
Runtime: python3.7
Timeout: 10
Environment:
Variables:
TZ: Asia/Tokyo
REMINDER_TABLE_NAME: !Ref ChouseisanReminderTable
Policies:
- arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
Events:
WorkflowApi:
Type: Api
Properties:
Path: /reminder
Method: post
ChouseisanSaveDeadlineFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${ChouseisanSaveDeadlineFunction}"
ChouseisanReminderTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: chouseisan-reminder-table
KeySchema:
- AttributeName: deadline
KeyType: HASH
AttributeDefinitions:
- AttributeName: deadline
AttributeType: N
TimeToLiveSpecification:
AttributeName: expiration
Enabled: true
BillingMode: PAY_PER_REQUEST
Outputs:
SaveDeadlineApi:
Description: "save deadline api"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/reminder/"
Lambdaコード
src/save_deadline/app.pyを新規作成します。
Events APIのドキュメントに従って、受け取ったパラメータのchallengeを返しています。
import json
def lambda_handler(event, context):
body = json.loads(event['body'])
return {
'statusCode': 200,
'body': json.dumps(
{'challenge': body['challenge']},
),
}
ビルド&デプロイ
sam build
sam package \
--output-template-file packaged.yaml \
--s3-bucket cm-fujii.genki-chouseisan-reminder-deploy-bucket
sam deploy \
--template-file packaged.yaml \
--stack-name Chouseisan-Reminder-Stack \
--capabilities CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset \
--parameter-overrides ChouseisanNotifySlackUrl=/Slack/INCOMING_WEBHOOK_URL/channel_name/choseisan_reminder
Slackのアプリを作成する
下記を持つアプリを作成します。
- Event Subscriptions (Events API)
- Incomming Webhook
まずはアプリを新規作成する
SlackのBasic app setupを開き、アプリを新規作成します。Create a new slack appを選択します。
続いてCreate New Appを選択します。
「App Name」を適当に記入して、Create Appを選択します。
Event Subscriptionsを設定する
Event Subscriptionsを選択します。
有効化したあと、Requests URLを入力します。VerifiedになればOKです!
続いてSubscribe to bot eventsにmessage.channelsを追加し、Saveします。
Incomming Webhookを設定する
Incoming Webhooksを選択してOnにします。
続いてAdd New Webhook to Workspaceを選択して追加します。
Slackアプリをワークスペースに追加する
OAuth & Permissionsを選択し、Install App to Workspaceを選択します。
Slackのチャンネルにアプリを追加する
任意のチャンネルの詳細メニューから追加できます。
Slackのワークフローを作成する
ワークフローを新規作成
Slackの左上を選択し、ワークフロービルダーを起動します。
適当に名前をつけます。
人間が起動するためショートカットを選択します。
適当に入力します。
ワークフローのステップを追加(フォーム入力)
ステップを追加を選択します。
フォームを作成するを選択します。
調整さんのURLと回答期限を入力するフォームを作成します。
それぞれは下記です。
ワークフローのステップを追加(メッセージ送信)
さらにステップを追加します。
今度はメッセージを送信を選択します。
そして保存しましょう。
ワークフローを公開する
右上の公開するボタンを選択すればOKです!
JSONメモ
このワークフローを実行したとき、Lambdaには次のJSONが来ました。
{
"token": "xxxxx",
"team_id": "xxxxx",
"api_app_id": "xxxxx",
"event": {
"type": "message",
"subtype": "bot_message",
"text": "調整さんに記入をお願いします!\n期限は *2020/04/06* です!\n<https://chouseisan.com/s?h=xxxxx>",
"ts": "1586171318.003000",
"username": "reminder_misc_join_201901_workflow",
"bot_id": "xxxxx",
"channel": "xxxxx",
"event_ts": "1586171318.003000",
"channel_type": "channel"
},
"type": "event_callback",
"event_id": "xxxxx",
"event_time": 1586171318,
"authed_users": [
"xxxxx"
]
}
期日をDynamoDBに保存するAPI(Lambda)を作成する
AWS SAMにはDynamoDBの定義を追加済みなのでLambdaコードのみを修正します。
Lambdaコードを変更
Lambdaコード(src/save_deadline/app.py)を下記にします。
import boto3
import json
import logging
import os
import re
from datetime import datetime
logger = logging.getLogger()
logger.setLevel(logging.INFO)
dynamodb = boto3.resource('dynamodb')
def lambda_handler(event, context):
main(event)
return {
'statusCode': 200
}
def main(event):
logger.info(json.dumps(event))
body = json.loads(event['body'])
logger.info(json.dumps(body))
if 'username' not in body['event']:
logger.info('No username.')
return
if 'reminder_misc_join_201901_workflow' != body['event']['username']:
logger.info('No workflow message.')
return
deadline = parse_timestamp(body['event']['text'])
url = parse_url(body['event']['text'])
logger.info(f'deadline: {deadline}, url: {url}')
# 当日11時をDynamoDBのTTL期限とする
expiration = deadline + 60*60*11
put_item(deadline, url, expiration)
def parse_timestamp(text):
pattern = r'.+\n期限は \*(\d{4}/\d{1,2}/\d{1,2})\* です!'
res = re.match(pattern, text)
if res:
# 0時のunixtimeを返す
return int(datetime.strptime(res.group(1), '%Y/%m/%d').timestamp())
raise ValueError
def parse_url(text):
pattern = r'.+\n.+\n<(.+)>'
res = re.match(pattern, text)
if res:
return res.group(1)
raise ValueError
def put_item(deadline, url, expiration):
table_name = os.environ['REMINDER_TABLE_NAME']
table = dynamodb.Table(table_name)
res = table.put_item(Item={
'deadline': deadline,
'expiration': expiration,
'url': url
})
logger.info(res)
ビルド&デプロイ
sam build
sam package \
--output-template-file packaged.yaml \
--s3-bucket cm-fujii.genki-chouseisan-reminder-deploy-bucket
sam deploy \
--template-file packaged.yaml \
--stack-name Chouseisan-Reminder-Stack \
--capabilities CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset \
--parameter-overrides ChouseisanNotifySlackUrl=/Slack/INCOMING_WEBHOOK_URL/channel_name/choseisan_reminder
動作確認(簡易)
Slackのワークフローを実行します。
そうすると、DynamoDBにデータが保存されました!
締め切りを告知するLambdaを作成する
1日1回起動し、今日が締め切りなら告知するLambdaを作成していきます。
AWS SAM(全部)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Chouseisan-reminder
Parameters:
ChouseisanNotifySlackUrl:
Type : AWS::SSM::Parameter::Value<String>
Resources:
ChouseisanReminderFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: chouseisan-reminder-function
CodeUri: src/notify_1st_message/
Handler: app.lambda_handler
Runtime: python3.7
Timeout: 10
Environment:
Variables:
TZ: Asia/Tokyo
INCOMMING_WEBHOOK_URL: !Ref ChouseisanNotifySlackUrl
Events:
NotifySlack:
Type: Schedule
Properties:
Schedule: cron(0 0 1-7 * ? *) # 日本時間で毎月1-7日のAM9時
ChouseisanReminderFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${ChouseisanReminderFunction}"
ChouseisanSaveDeadlineFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: chouseisan-save-deadline-function
CodeUri: src/save_deadline/
Handler: app.lambda_handler
Runtime: python3.7
Timeout: 10
Environment:
Variables:
TZ: Asia/Tokyo
REMINDER_TABLE_NAME: !Ref ChouseisanReminderTable
Policies:
- arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
Events:
WorkflowApi:
Type: Api
Properties:
Path: /reminder
Method: post
ChouseisanSaveDeadlineFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${ChouseisanSaveDeadlineFunction}"
ChouseisanReminderTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: chouseisan-reminder-table
KeySchema:
- AttributeName: deadline
KeyType: HASH
AttributeDefinitions:
- AttributeName: deadline
AttributeType: N
TimeToLiveSpecification:
AttributeName: expiration
Enabled: true
BillingMode: PAY_PER_REQUEST
ChouseisanNotifyDeadlineFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: chouseisan-notify-deadline-function
CodeUri: src/notify_deadline_message/
Handler: app.lambda_handler
Runtime: python3.7
Timeout: 10
Environment:
Variables:
TZ: Asia/Tokyo
REMINDER_TABLE_NAME: !Ref ChouseisanReminderTable
INCOMMING_WEBHOOK_URL: !Ref ChouseisanNotifySlackUrl
Policies:
- arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
Events:
NotifySlack:
Type: Schedule
Properties:
Schedule: cron(0 1 * * ? *) # 日本時間AM10時に毎日通知する
ChouseisanNotifyDeadlineFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${ChouseisanNotifyDeadlineFunction}"
Outputs:
SaveDeadlineApi:
Description: "save deadline api"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/reminder/"
Lambdaコード
import boto3
import json
import logging
import os
import requests
from botocore.exceptions import ClientError
from datetime import date, datetime
logger = logging.getLogger()
logger.setLevel(logging.INFO)
INCOMMING_WEBHOOK_URL = os.environ['INCOMMING_WEBHOOK_URL']
dynamodb = boto3.resource('dynamodb')
def lambda_handler(event, context):
today = get_today()
logger.info(f'today: {today}')
remind_data = get_remind_data(today)
logger.info(f'get_remind_data(): {remind_data}')
if remind_data is None:
return
# Slackに通知する
post_slack(remind_data)
def get_today():
today = date.today()
# 今日の0時0分0秒のunixtimeを返す
return int(datetime(today.year, today.month, today.day).timestamp())
def get_remind_data(deadline):
table_name = os.environ['REMINDER_TABLE_NAME']
table = dynamodb.Table(table_name)
try:
res = table.get_item(Key={
'deadline': deadline
}
)
except ClientError as e:
logger.error(e.response['Error']['Message'])
return None
else:
return res.get('Item', None)
def post_slack(remind_data):
# https://api.slack.com/incoming-webhooks
# https://api.slack.com/docs/message-formatting
# https://api.slack.com/docs/messages/builder
payload = {
# https://www.webfx.com/tools/emoji-cheat-sheet/
'icon_emoji': ':bangbang:',
'text': '<!here> 今日が締切です!! 記入お願いします!\n',
'attachments': [
{
'text': remind_data['url']
}
]
}
url = f'https://{INCOMMING_WEBHOOK_URL}'
# http://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/
try:
response = requests.post(url, data=json.dumps(payload))
except requests.exceptions.RequestException as e:
logger.error(e)
else:
logger.info(response.status_code)
ビルド&デプロイ
sam build
sam package \
--output-template-file packaged.yaml \
--s3-bucket cm-fujii.genki-chouseisan-reminder-deploy-bucket
sam deploy \
--template-file packaged.yaml \
--stack-name Chouseisan-Reminder-Stack \
--capabilities CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset \
--parameter-overrides ChouseisanNotifySlackUrl=/Slack/INCOMING_WEBHOOK_URL/channel_name/choseisan_reminder
動作確認(期限の通知)
期日のAM10時に通知が来ました!!
さいごに
Slackのワークフローとサーバーレスな組み合わせで、期日を自動通知する仕組みを作ってみました。 少しでも便利になったはずです!!


































