任意のメッセージを Slack の特定のチャネルに通知する仕組みを AWS SNS + Lambda で作ります。
- アプリケーションから任意のメッセージを通知したい
- 複数の通知先がある(Slackとメーリングリストなど)
- しかし、通知先の設定などはなるべくアプリケーションで管理したくない
こういうときに役に立つと思います
Slack APP の登録とチャネルの Webhook URL の生成(手動)
https://api.slack.com/apps?new_app=1 にアクセスして、 Workspace に Slack APP を作成します
Incoming Webhooks
を選択します
初期状態で Incoming Webhooks は有効になってないので、 ON にします
ON にすると Webhook を追加できるようになります
Add New Webhook to Workspace
を選択します
OAuth で権限をリクエストする画面に遷移します
Webhook でメッセージを投稿するさきのチャンネルを選択して許可します
Webhook URL が生成されました1
これは後ほど、 Serverless Framework で利用します
Sample curl request も生成されています
指定したチャネルへ Webhook でメッセージ送信できるか、事前にテストしておくこともできます
$ curl -X POST -H 'Content-type: application/json' --data '{"text":"Hello, World!"}' https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX
ok%
Serverless Framework で SNS + Lambda を作成する
実際にこの手順どおりに実装したリポジトリを参考に置いておきます
Serverless Framework のインストールとプロジェクト作成
Get started や CLI reference を見てもらうのが一番良いですが、必要なステップだけ抜粋しておきます
環境は MacOS/Linux で、言語は python3 にします
他の環境へのインストール方法や他の言語のテンプレートはドキュメントを参照してください
$ curl -o- -L https://slss.io/install | bash
$ sls create --template aws-python3
Serverless: Generating boilerplate...
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v2.6.0
-------'
Serverless: Successfully generated boilerplate for template: "aws-python3"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name
以下のファイルが生成されます
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: .gitignore
new file: handler.py
new file: serverless.yml
handler.py
の中身を見てもらえればわかりますが、固定の文字列と Lambda event を返す関数が定義されています
import json
def hello(event, context):
body = {
"message": "Go Serverless v1.0! Your function executed successfully!",
"input": event
}
response = {
"statusCode": 200,
"body": json.dumps(body)
}
return response
# Use this code if you don't use the http event with the LAMBDA-PROXY
# integration
"""
return {
"message": "Go Serverless v1.0! Your function executed successfully!",
"event": event
}
"""
このままデプロイしてみましょう
$ sls deploy -v
Serverless: Packaging service...
...(snip)...
Serverless: Stack update finished...
Service Information
service: sls-sns-lambda-to-slack
stage: dev
region: us-east-1
stack: sls-sns-lambda-to-slack-dev
resources: 6
api keys:
None
endpoints:
None
functions:
hello: sls-sns-lambda-to-slack-dev-hello
layers:
None
Stack Outputs
HelloLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:000123456789:function:sls-sns-lambda-to-slack-dev-hello:1
ServerlessDeploymentBucketName: sls-sns-lambda-to-slack-serverlessdeploymentbuck-1drc5f5y43luq
**************************************************************************************************************************************
Serverless: Announcing Metrics, CI/CD, Secrets and more built into Serverless Framework. Run "serverless login" to activate for free..
**************************************************************************************************************************************
動作確認もしてみます
$ sls invoke -f hello -d "{\"message\": \"This is input message.\"}"
{
"statusCode": 200,
"body": "{\"message\": \"Go Serverless v1.0! Your function executed successfully!\", \"input\": {\"message\": \"This is input message.\"}}"
}
Slack 通知する Lambda の実装
handler.py
の実装
- 受け取ったメッセージを Slack API の chat.postMessage payload に変換します
- payload を先程作成した Webhook に POST すれば良いです
import json
import urllib3
import os
http = urllib3.PoolManager()
def invoke(event, context):
message = event['message']
payload = json.dumps({'text': message})
url = os.environ["WEBHOOK_URL"]
resp = http.request('POST', url, body=payload)
print({
"url": url,
"payload": payload,
"status_code": resp.status,
"response": resp.data
})
return payload
-
serverless.yml
の修正も必要です-
handler.py
のエントリポイントの関数名をhello
からinvoke
に変更したので
-
- ついでに Lambda の関数名もいい感じの名前 (
post-to-slack
) に変えておきましょう
- hello:
- handler: handler.hello
+ post-to-slack:
+ handler: handler.invoke
環境変数の設定
- Webhook を環境変数として渡しますが、リポジトリには commit したくありません 1
-
.gitignore
したファイルで読ませることにします
- # environment:
- # variable1: value1
+ environment:
+ WEBHOOK_URL: ${file(./config.json):WEBHOOK_URL}
{
"WEBHOOK_URL": "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX"
}
デプロイと動作確認
- ここまでの内容をデプロイして動作確認してみましょう
- Lambda へ渡す payload は
{"message": "Slack に投稿されるメッセージ"}
という形式です- Lambda が
event['message']
で Slack に投稿するメッセージを受け取っています
- Lambda が
$ sls deploy -v
$ sls invoke -f post-to-slack -d "{\"message\": \"Slack に投稿されるメッセージ\"}"
"{\"text\": \"Slack \\u306b\\u6295\\u7a3f\\u3055\\u308c\\u308b\\u30e1\\u30c3\\u30bb\\u30fc\\u30b8\"}"
SNS Topic 作成と Lambda Subscription の設定
serverless.yml
を少し修正するだけで、 SNS Topic と Lambda Subscription を設定できます
SNS Topic のリソースを定義
resources:
Resources:
snsTopic:
Type: AWS::SNS::Topic
Outputs:
snsTopicArn:
Description: "ARN of SNS Topic"
Value: !Ref snsTopic
Lambda Subscription の設定
関数にイベントとして SNS Topic を紐付けます
functions:
post-to-slack:
handler: handler.invoke
+ events:
+ - sns:
+ arn: !Ref snsTopic
+ topicName: snsTopic
たったこれだけです
デプロイして SNS Topic と Lambda Subscription が作成されたことを確認しましょう
SNS Topic からのメッセージを処理するよう、 Lambda 関数を修正
- SNS Topic はできましたが、まだ Lambda 関数が SNS Topic から送られてくる payload を解釈できていません
- AWS のコンソールなどから SNS Topic にメッセージを発行できますが、これまで使ってきた payload の形式で送ってもうまくいきません
[ERROR] KeyError: 'message'
Traceback (most recent call last):
File "/var/task/handler.py", line 9, in invoke
message = event['message']
- SNS から送られてくる payload がどういう形で
event
に格納されているか、知る必要があります - とりあえず、関数の冒頭で
print(event)
してログを見るとわかりやすいです
def invoke(event, context):
+ print(event)
message = event['message']
Records.0.Sns.Message
に SNS Topic 経由で送信されたメッセージの内容が格納されていることがわかりました
Lambda 関数のコードを修正してデプロイしましょう
def invoke(event, context):
- message = event['message']
+ message = event['Records'][0]['Sns']['Message']
payload = json.dumps({'text': message})
url = os.environ["WEBHOOK_URL"]
resp = http.request('POST', url, body=payload)
- AWS コンソールから SNS 経由でメッセージを送ってみます
- JSON payload にする必要がないので、メッセージ本文をそのまま submit します
届きましたね
アプリケーションから SNS Topic にメッセージを publish する
- あとは、アプリケーションから AWS SDK で SNS Topic にメッセージを publish するだけです
- 例として、 PHP/Laravel 2 で実装するサンプルはこんな感じです
$client = App::make('aws')->createClient('sns');
$client->publish([
'Message' => 'アプリケーションから送信するメッセージ', // REQUIRED
'TopicArn' => 'arn:aws:sns:us-east-1:000123456789:sls-sns-lambda-to-slack-dev-snsTopic-1OUEKFS94QV16'
]);
応用編: 通知の環境分離
- 環境ごとに通知先を分けたいニーズはよくあると思います
- Serverless Framework は環境の概念を持っていて、環境ごとに独立したリソースをデプロイできます
通知先チャネルごとの Webhook URL の作成
冒頭で実施した Webhook URL 生成と同じ手順で、Slack App に Webhook URL を追加しましょう
serverless.yml
と config.json
の修正
環境ごとに読み込む config.json
を分けることで、環境変数の Webhook URL を切り替えられるようにします
environment:
- WEBHOOK_URL: ${file(./config.json):WEBHOOK_URL}
+ WEBHOOK_URL: ${file(./config.${opt:stage, self:provider.stage, 'dev'}.json):WEBHOOK_URL}
$ ls -l config*
-rw-r--r-- 1 m_kanno AD\Domain Users 103 Dec 4 19:58 config.dev.json
-rw-r--r-- 1 m_kanno AD\Domain Users 103 Dec 4 19:58 config.stg.json
commit したくないファイルの名前が変わるので、 .gitignore
の修正も忘れずに
# config
-config.json
+config.*.json
環境ごとにデプロイする
-
--stage
オプションでデプロイ先の環境を指定できます -
--stage
オプションを指定しなかった場合、デフォルトでdev
環境にデプロイされます- これまでは
dev
環境にデプロイさていました - デフォルトの環境は
serverless.yml
で上書きすることも可能です
- これまでは
# you can overwrite defaults here
# stage: dev
# region: us-east-1
$ sls deploy -v --stage stg
stg 環境の SNS Topic などが作成されました
メッセージを送ってみます
別の Webhook URL で指定したチャネルにメッセージが届きました
リソースの削除
- 必要なくなったら、
sls remove
でリソースを削除できます - これも
--stage
オプションで環境ごとに削除できます
$ sls remove
$ sls remove --stage stg
Slack App は手動で削除してください
まとめ
- Serverless Framework を使って、Slack 通知する SNS Topic + Lambda を簡単に作れます
- 慣れればリポジトリ作成から30分くらいでできるようになります
- Seerverless Framework は関連するリソースや IAM ポリシーもざっと作ってくれるのでアプリケーション開発に集中できます
参考
- Serverless Getting Started Guid
- Serverless - AWS Lambda - CLI Reference
- chat.postMessage method | Slack
-
Webhook URL を公開してしまわないように注意しましょう。 Webhook URL を知っていると、その Webhook に紐付いているチャンネルにメッセージを投稿し放題になってしまいます。 ↩ ↩2
-
サンプルコードは
aws/aws-sdk-php-laravel
を利用する前提です ↩