#やりたいこと
弊チームでは毎週金曜日に担当者が技術事例を発表する試みがあるのですが、日々のタスクに集中しすぎて自分の担当週を忘れることがあり、思い出した誰かが前日に教えてあげたり、当日の明朝にふと思い出すなどで、時間的に余裕がないまま技術記事をまとめるということが度々発生していました。
そこで週次に担当者を通知するだけのSlackbotを作成することとしました。よくよく考えると、週次のアラート担当者へのメンション通知などユースケースは意外とあると思うので、できる限り楽に汎用的に使えるように心がけて作成しました。
#当記事の前提
- AWS環境を使用できること
- Slackを利用していること
CloudWatchEventsで決められたタイミングでLambdaを発火し、担当者をSlackに通知するシンプル構成です。
#作業の流れ
- SlackチャンネルのWebhookURLを取得する。
- 各担当者のSlackのメンバーIDを取得する。
- CFnからスタックを作成する。
##手順
「1.SlackチャンネルのWebhookURLを取得する」については、https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008 を、「2.各担当者のSlackのメンバーIDを取得する。」については、http://help.receptionist.jp/?p=1100#memberid を参考にすると良いでしょう。
CFnテンプレートとLambdaのソースコードについては下記にあげておきますので、ご利用いただければと思います。
CFnに環境に応じたパラメータを設定することでslackbotを作成することが可能です。
パラメータ | 説明 |
---|---|
SlackWebhookUrl | 手順1.で取得したSlackチャンネルのWebhookURLを設定します。 |
MemberId | 手順2. で取得したメンバーIDを一つ設定します。ここで設定した方に対してメンション通知が行われます。 |
CommaSeparatedSlackMemberId | 手順2. で取得した全てのメンバーIDをカンマ区切りで入力します。MemberIdの設定を起点に、左から右、最後まで行ったら最初に戻る、という順番となります。 |
ScheduleExpression | cron式で通知する日時を設定します。 |
###CFnテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
SlackWebhookUrl:
Type: String
Default: 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXX/XXXXXXXXXXXXXXXXXXXXX'
MemberId:
Type: String
Default: 'AAAAAAAA'
CommaSeparatedSlackMemberId:
Type: String
Default: 'AAAAAAAA,BBBBBBBB,CCCCCCCC'
ScheduleExpression:
Type: String
Default: 'cron(0 0 ? * MON *)'
## Create Lambda Functions
Resources:
LambdaFunctionNotifyMessageToMember:
Type: AWS::Lambda::Function
Properties:
Handler: index.lambda_handler
Runtime: python3.7
Code:
ZipFile: |
import boto3
import json
import os
import urllib
import urllib.request
def set_next_member(member_id, slack_webhook_url):
comma_separated_
member_id = os.getenv("COMMA_SEPARATED_SLACK_MEMBER_ID")
lambda_function_name = os.getenv("AWS_LAMBDA_FUNCTION_NAME")
members_list = comma_separated_member_id.split(",")
members_list_index = members_list.index(member_id)
if members_list_index == len(members_list) - 1:
next_member_id = members_list[0]
else:
next_member_id = members_list[members_list_index + 1]
client = boto3.client('lambda')
client.update_function_configuration(
FunctionName=lambda_function_name,
Environment={
'Variables': {
'MEMBER_ID': next_member_id,
'SLACK_WEBHOOK_URL': slack_webhook_url,
'COMMA_SEPARATED_SLACK_MEMBER_ID': comma_separated_member_id
}
}
)
def lambda_handler(event, context):
member_id = os.getenv("MEMBER_ID")
slack_webhook_url = os.getenv("SLACK_WEBHOOK_URL")
message = "今週の担当者は<@" + member_id + ">さんです!\n張り切っていきましょう!!"
send_data = {
"username": "担当者通知ボット",
"icon_emoji": ":rocket:",
"text": message,
}
send_text = "payload=" + json.dumps(send_data)
request = urllib.request.Request(
slack_webhook_url,
data=send_text.encode("utf-8"),
method="POST"
)
with urllib.request.urlopen(request) as response:
response_body = response.read().decode("utf-8")
set_next_member(member_id, slack_webhook_url)
MemorySize: 128
Timeout: 60
Environment:
Variables:
SLACK_WEBHOOK_URL: !Ref SlackWebhookUrl
COMMA_SEPARATED_SLACK_MEMBER_ID: !Ref CommaSeparatedSlackMemberId
MEMBER_ID: !Ref MemberId
Role: !GetAtt IAMRoleNotifyMessageToMember.Arn
IAMRoleNotifyMessageToMember:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2008-10-17'
Statement:
- Effect: 'Allow'
Principal:
Service: 'lambda.amazonaws.com'
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
- !Ref IAMManagedPolicyLamdbaUpdateFunctionConfiguration
IAMManagedPolicyLamdbaUpdateFunctionConfiguration:
Type: AWS::IAM::ManagedPolicy
Properties:
Path: "/"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- lambda:UpdateFunctionConfiguration
Resource: "*"
LambdaPermissionNotifyMessageToMember:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref LambdaFunctionNotifyMessageToMember
Action: "lambda:InvokeFunction"
Principal: "events.amazonaws.com"
SourceArn: !GetAtt EventsRuleNotifyMessageToMemberSchedule.Arn
EventsRuleNotifyMessageToMemberSchedule:
Type: AWS::Events::Rule
Properties:
Description: "Notify message to the member in charge this week"
RoleArn: !GetAtt IAMRoleLambdaExecutionNotifyMessageToMember.Arn
ScheduleExpression: !Ref ScheduleExpression
Targets:
- Arn: !GetAtt LambdaFunctionNotifyMessageToMember.Arn
Id: "Slackbot"
IAMRoleLambdaExecutionNotifyMessageToMember:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action: "sts:AssumeRole"
###ソースコード抜粋
簡単に説明しますと、環境変数に登録したSlackのアカウントに紐づくID(メンバーID)にメンションをつけてメッセージを送り、その後自分自身(Lambda)の環境変数を次の担当者に更新し、次回の実行を待つという流れになります。
import boto3
import json
import os
import urllib
import urllib.request
def set_next_member(member_id, slack_webhook_url):
comma_separated_member_id = os.getenv("COMMA_SEPARATED_SLACK_MEMBER_ID")
lambda_function_name = os.getenv("AWS_LAMBDA_FUNCTION_NAME")
members_list = comma_separated_member_id.split(",")
members_list_index = members_list.index(member_id)
if members_list_index == len(members_list) - 1:
next_member_id = members_list[0]
else:
next_member_id = members_list[members_list_index + 1]
client = boto3.client('lambda')
client.update_function_configuration(
FunctionName=lambda_function_name,
Environment={
'Variables': {
'MEMBER_ID': next_member_id,
'SLACK_WEBHOOK_URL': slack_webhook_url,
'COMMA_SEPARATED_SLACK_MEMBER_ID': comma_separated_member_id
}
}
)
def lambda_handler(event, context):
member_id = os.getenv("MEMBER_ID")
slack_webhook_url = os.getenv("SLACK_WEBHOOK_URL")
message = "今週の担当者は<@" + member_id + ">さんです!\n張り切っていきましょう!!"
send_data = {
"username": "担当者通知ボット",
"icon_emoji": ":rocket:",
"text": message,
}
send_text = "payload=" + json.dumps(send_data)
request = urllib.request.Request(
slack_webhook_url,
data=send_text.encode("utf-8"),
method="POST"
)
with urllib.request.urlopen(request) as response:
response_body = response.read().decode("utf-8")
set_next_member(member_id, slack_webhook_url)
##最後に
何かしらの理由で、その週の担当をスキップして次の週も同じ担当者で、、というケースもあるかと思いますが、その場合はAWS CLIでCFnのスタックを更新してあげれば良いかと思います。
コマンドとしては以下のような感じになるかと。
aws cloudformation update-stack --stack-name [CFnスタック名] \
--use-previous-template --capabilities CAPABILITY_IAM \
--parameters ParameterKey=SlackWebhookUrl,UsePreviousValue=true \
ParameterKey=ScheduleExpression,UsePreviousValue=true \
ParameterKey=CommaSeparatedSlackMemberId,UsePreviousValue=true \
ParameterKey=MemberId,ParameterValue=[担当者のUID]
少しコマンドが長くなりますが、値を変更したくないパラメータは「UsePreviousValue=true」をつけてあげないと、CFnテンプレートで指定しているデフォルト値に設定されてしまうので注意が必要です。