API Gateway + Lambda で slack app
slack app
slack appとはbotやcommandsなどの集合体で、これを登録すれば、appに含まれているbotやcommandsが使えるようになるものです。
tokenが一意で済むのと、interactive message
を使うことができるようになります。これを使うことで、interactiveに操作を進めることが可能です。
slack appについて詳しくは下記を参照してください。
作成したslack appは全体に公開することもできます。が、今回は完全にprivate目的なので自チームにだけ登録して利用します。
api gateway + Lambda
API Gateway + Lambdaとするのは、常に起動している必要がないからサーバがいらない構成にしたいためです。
さらにslash commandsを使う場合、botと違ってslash commandsを実行した時だけ呼び出されるため、料金的にも安心です。
必要なもの
- apex
lambdaのデプロイに使います。invokeとlogsが便利です。
terraformとも連携できますが、terraformとapi gateway + lambdaの連携がつらいので今回は見送りました。
インストールはこれだけです。
curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh
- aws credentials
aws configure
でアクセスキーとシークレットアクセスキーを設定しておきます。
apexがデフォルトだとiamも配置するので、lambdaとiam関係の権限がなければなりません。
また、tokenなどの保持にkmsを利用するのでその権限も必要です。
複数credentialを持っている場合は、apexの設定からprofile指定もできます。詳しくは公式ページに。
- slack
インストール
apexでlambdaの配置
まずは空のディレクトリで
apex init
をすることで、apexのプロジェクトファイル、apexが利用するIAMの作成が行われます。
project nameはここではcommands
としておきます。
apexに慣れておきたい場合には公式のGetting startedにしたがって、helloを配置してみるといいでしょう。
oauth callbackの作成
slack appの登録にoauthのcallbackが必要です。手でやれないこともないですが、せっかくなのでこれもapi gateway + lambdaで処理してみます。
20170324追記
slack appが開発で使う分にはoauth callbackを使わなくても権限を追加したりtokenをもらったりすることができるようになってました。
lambdaとapi gatewayの配置方法例として以下は残しておきますが、自分たちで使うようなslack appを配置するだけならoauth callbackの配置は不要になってます。
slack appの作成
[Create New App] から新しいAppを作ります。 I plan to submit this app the Slack App Directory
はこのアプリを公開する場合なので今回はチェックは不要です。
作成後に表示されるBasic Infomationにあるclient_id
とclient_secret
を控えておきます。
また、ここで変更できるiconはコマンドを実行した時に表示されるものなので、目的に応じたものに変えるといいでしょう。
KMS
秘密にしなければならないclient_secretなどを載せるようになるので、githubに上げることを考えて、KMSで暗号化します。
- IAMから[暗号化キー]の[キーの作成]
- エイリアス名に適当な名前(今回は
slack
とします)を与えて「次のステップ」 - キー管理アクセス許可に、コマンドラインで使っているcredentialのものを選択して「次のステップ
- キーの使用アクセス許可は一旦飛ばして「次のステップ」
- 「完了」
コンソールがaws credentialsが作成したKMSにアクセスできるものなら、下記のコマンドで暗号化されたテキストが取得できるはずです。
aws kms encrypt --key-id alias/slack --plaintext "暗号化したいテキスト" --query CiphertextBlob --output text
これでclient_id
とclient_secret
の暗号化テキストを作ります。
lambda functionがkmsのキーを読み取れないといけないので、作成されたIAMのポリシーにkms:Decrypt
を追加します。apexで登録したlambdaが使うIAMはproject.json
に記載されています。
functionごとにIAMを変えられるので権限を絞れますが、どうせ全体で使うので手抜きです。
さらに、keyを限定したい場合はResouce
をちゃんと指定しましょう。
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:*"
],
"Effect": "Allow",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": "*"
}
]
}
lambdaの配置
暗号化したテキストは下記のようにproject.jsonの環境にいれてしまいます。これをlambdaのコードのなかでkmsを呼んで復号すればもとのclient_id
とclient_secret
が呼出せます。
{
"name": "commands",
"description": "slack commands",
"memory": 128,
"timeout": 5,
"role": "arn:aws:iam::account-id:role/slack-commands_lambda_function",
"environment": {
"ENCRYPTED_CLIENT_ID": "暗号化したclient_id",
"ENCRYPTED_CLIENT_SECRET": "暗号化したclient_secret"
}
}
lambdaの処理は、javascript苦手でnodeがさっぱりなので全部pythonで行きます。
functions/callback/
を作成して、main.py
を配置します。一応全体公開されるので余計なexceptionを返さないことを目的としてます。
GETされた文字列はマッピングテンプレートに従って、event
のparams
->querystring
内に入っているのでそこから値を取り出します。
slackが送ってくるqueryについては下記を参照してください。
import logging
import json
import os
import boto3
import requests
from base64 import b64decode
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def handle(event, context):
try:
kms = boto3.client('kms')
client_id = kms.decrypt(CiphertextBlob = b64decode(os.environ["ENCRYPTED_CLIENT_ID"]))['Plaintext']
client_secret = kms.decrypt(CiphertextBlob = b64decode(os.environ["ENCRYPTED_CLIENT_SECRET"]))['Plaintext']
except:
logger.error("required environments not found")
return {"text": "error"}
try:
code = event['params']['querystring']['code']
except:
logger.error("code not found")
return {"text": "error"}
try:
response = requests.get(
'https://slack.com/api/oauth.access',
params={'client_id': client_id, 'client_secret': client_secret, 'code': code})
except:
logger.error("oauth request error. response: %s", response.json())
return {"text": "error"}
logger.info(response.json())
return { "text": "ok" }
また、pythonをapexでデプロイする場合には、requirements.txt
で必要なmoduleを記載し、function.json
のhookでインストールしてやります。ついでに.apexignore
で余分なファイルを配置しないようにできます。
boto3
requests
{
"description": "oauth callback",
"hooks":{
"build": "pip install -r requirements.txt -t ."
}
}
*.dist-info/
最後にcallbackをデプロイします。
apex deploy callback
するとlambdaにcommands_callback
が配置されるはずです。
IAM で KMSの権限を追加
lambdaがKMSのキーを読み取る必要があるので権限を追加します。
再度IAMの[暗号化キー]画面からキーユーザの[追加]
lambdaが使っているIAMのロール(今回はcommands_lambda_function)を選択して[追加]
これでlambdaがこのkmsのキーを読み取れます。
今回はプライベートでしか使わないのでやっていませんが、lambdaのfunctionごとに異なるIAMを設定することは可能です。各functionのfunction.json
でrole
を指定できます。
api gateway
- 新規なら[今すぐ始める]、既に使っているなら一覧画面から[APIの作成]
- API名に適当な名前をいれて(ここではcommands)、[APIの作成]
- [アクション] -> [リソースの作成]
- リソース名に
callback
をいれて [リソースの作成]
- [アクション] -> [メソッドの作成] -> GETを選んでチェックをクリック
- セットアップ画面になるので、先ほど作成したlambdaを選んで[保存]
- 権限許可確認画面がでるので[OK]
- メソッドの実行画面で[統合リクエスト]を選択
- [本文マッピングテンプレート]をクリックして詳細を出してから、[マッピングテンプレーとの追加] ->
application/json
を入力してチェックをクリック
- パススルー動作の変更画面がでるので [はい、この統合を保護します]
- GETの中身が欲しいだけなので
テンプレートの生成:
で[メソッドリクエストのパススルー]を選んで[保存]
- API gatewayはデプロイしないと反映されないので[アクション] -> [APIのデプロイ] -> APIのデプロイウィンドウがでるので、
デプロイされるステージ
から[新しいステージ]、ステージ名を適当に(これはAPI gatewayのURLに含まれます。ここではprod
)いれて[デプロイ]
ステージでURLの呼び出し:
で示されるURLを取っておきます。
oauth
まずは、slack appのページの[OAuth & Permissions] 画面で、RedirectURLにAPI Gatewayで作った呼び出しのURLを入れ[Save Changes]します。
その後、ブラウザでいいので下記を開きます。
scopeは必要な権限を入れてください。ここでは、コマンドを実行できるcommands
を許可してます。
oauthのscope一覧は下記です。
クライアントID
はslack app画面のclient_idとして、このURLをブラウザでいいので直接遷移すると下記のような画面に行くはずです。
[Authorize]すると、先ほどのcallback URLに遷移します。うまくいっていれば
{"text":"ok"}
とだけ表示されるはずです。
apex logs callback
を叩いてログ確認します。失敗している場合でもこれでなぜ失敗したかを確認して修正します。
そもそもログでていない場合、API Gatewayからlambdaが呼ばれていないのでAPI Gatewayの設定を見直しましょう。
うまくいっていれば、ログからaccess_token
がもらえているはずです。bot
やincoming-webhook
を有効にしていればそれらも含まれています。
これらも暗号化してproject.json
に放り込みます。
他のslackチームで使うわけでもない場合は、callback
をAPI Gatewayから削除してしまってもいいでしょう。
これでslack appの登録の完了です。
次でslash commandとinteractive messageを登録します。
TIPS
incoming-webhookのkms
incoming-webhookを使う場合、使う文字列は https://
で始まりますが、awsコマンドでfile://
や http://
などで始まる文字列の場合は問答無用で、そのファイルやURLを参照しにいくので、一旦ファイルにテキストを保存して暗号化します。
カレントディレクトリのtmp
にテキストを入れたとして
aws kms encrypt --key-id alias/slack --plaintex file://./tmp
.gitignore
rootに言語の.gitignore、pythonの場合は取得したモジュールがディレクトリに置かれるのでfunctionごとに.gitignoreしてます。
.Python
build/
develop-eggs/
dist/
eggs/
.eggs/
parts/
sdist/
*.egg-info/
*.dist-info/
.installed.cfg
*.egg
boto3/
botocore/
s3transfer/
requests/
jmespath/
docutils/
dateutil/
six.py