Slack+Chalice(API Gateway+Lambda)を使ってHerokuのコマンドを実行してみた #slack #aws #chalice #lambda #heroku
chalice(Python Serverless Microframework for AWS)を使って Content-Type: application/x-www-form-urlencoded
のPOSTリクエストを受けるところまでは確認済みなので、今回はこれを使ってやりたかったことにチャレンジしてみました。
書いてる間にバージョンが0.3.0
に上がっていました・・・試したのは0.2.0
になりますので差異があったらごめんなさい
きっかけ
仕事で Heroku
を使う機会が増えている今日この頃です。今までこんなに触る機会がなかったのでなかなか楽しく作業しております。
Heroku は多くのAdd-ons
があり、無償で利用できる物もあります。
当然今のプロジェクトでもいろいろと利用しているのですが、その中でHubotの無償運用なんかでおなじみのHeroku Scheduler
を利用しています
Heroku Schedulerの説明を確認すると
このような一文が・・・
Scheduler is a “best effort” service
ですってよ!
Scheduler is a “best effort” service, meaning that execution is expected but not guaranteed. Scheduler is known to occasionally (but rarely) miss the execution of scheduled jobs. If scheduled jobs are a critical component of your application, it is recommended to run a custom clock process instead for more reliability, control, and visibility.
実際に約2ヶ月間で1回だけ実行されないことがあり、そういう場合に何かゆるい感じで対応できるといいなぁーと思ったのが作業のきっかけです。上記の文面をみればわかりますが、ちゃんと対応するならrun a custom clock process
にしろと書いてありますしねー
ちなみに、今回実行されていないことを通知したのも無償で使えるadd−onのDead Man's Snitch
になります
チームメンバーに教えてもらったのですが、今まで全然知りませんでした。結構前からあるサービスみたいですねーいやー便利。単純に払い出されたURLにGETリクエストするとチェックイン状態になり、そのチェックインを毎時/毎日みたいなタイミングでチェックして、チェックインがなければ通知してくれるだけのシンプルなサービスです。
Herokuのadd−on経由だと1つのチェックインが無償で、チェックのインターバルと通知先に制限がある状態です。通常は有償なので制限ありでも無償で使えるのはありがたいし試しやすいですよね。
インターバルの制限は今回は問題にならないのですが、通知先がmailのみとのことなので、slackのEmail integration
を利用してチームメンバーと共有可能な状態にしました
通知はされるが
Email integrationを利用したので、何かあればslackに通知がされるようになりました。Email integrationでの通知はemailを添付ファイル扱い
で通知する仕様のようです。
最初に思いついたのは
みたいな流れなのですが・・・Outgoing WebHooks
が添付ファイル部分には反応しない仕様のようです。
email文中の特定のキーワードに反応してくれたら十分なんですが、残念ながら無理っぽいようです
結局
色々試した結果Outgoing WebHooksのTrigger Word(s)
にemail
を指定してすることでemailの受信時にWebHookがかかるようになりました。これはemail受信時の投稿がemail uploaded a file:<添付ファイルのURL>
のような形式となっているので、この文中のemail
に反応してWebHookが発行されているようです。
注意が必要なのは今回は動作確認なのでemailという単語でWebHookを発行しています。Outgoing WebHooksをintegrationしたslackチャンネルで日常的にemailという単語が使われている場合はTrigger Word(s)を別のものにした方が良いかと思います。
Outgoing WebHooks の設定画面のドキュメントには以下のようなデータがPOSTされると書かれています。
token=blaNqr3fL7b8AqnwCNFW86oZ team_id=T0001 team_domain=example channel_id=C2147483705 channel_name=test timestamp=1355517523.000005 user_id=U2147483697 user_name=Steve text=googlebot: What is the air-speed velocity of an unladen swallow? trigger_word=googlebot:
実際にはこのデータがapplication/x-www-form-urlencoded
で送信されるので
token=XXXXXXXXXXXXXXXXXXXXXXXX&team_id=T0295086H&team_domain=serverworks&service_id=77430880359&channel_id=C1X85EYNR&channel_name=times-uchida×tamp=1476076555.001561&user_id=USLACKBOT&user_name=slackbot&text=email+uploaded+a+file%3A+%3Chttps%3A%2F%2Fserverworks.slack.com%2Ffiles%2Fslackbot%2XXXXXXXXXX%2F_missing__import_data_monitoring_hasn_t_checked_in%7C%5BMISSING%5D+import+data+monitoring+hasn%27t+checked+in%3E&trigger_word=email
のような形式で送信されてきます。また、今回のように添付ファイルがある場合でもPOSTデータに添付ファイルのデータが入るようなことはないようです。残念。
Please note that the content of message attachments will not be included in the outgoing POST data.
chaliceを利用する
WebHookのPOST先を作らないといけないわけですが、あまり手間をかけずに簡単に作りたいわけですね。Heroku使ってるならそれでいいじゃん!という話もありますが、chaliceでapplication/x-www-form-urlencodedが受けられるようになったのを確認したばかりなのでchaliceを使います
chaliceそのものの使い方は簡単&他に詳しく書かれているものがたくさんあるので割愛しますが、chalice (0.2.0)
以上を利用しないと上手く行かないと思うのでその点だけ確認してください
$ chalice new-project
で新規にプロジェクトを作成し、application/x-www-form-urlencodedのPOSTが受けられるよう設定します
from chalice import Chalice app = Chalice(app_name='slack_receive') @app.route('/slack', methods=['POST'], content_types=['application/x-www-form-urlencoded']) def slack(): body = app.current_request.raw_body
app.current_request.raw_body
には前述の&
で連結された情報が入っているので、この中から必要なものを取り出してあげます。
Pythonのデフォルトライブラリを利用してパースを行い、後で扱いやすいように辞書型にしておきます。
from urlparse import parse_qsl body = app.current_request.raw_body parsed = dict(parse_qsl(body))
パースした結果は以下のような状態になっていると思います
{'user_id': 'USLACKBOT', 'channel_name': 'times-uchida', 'timestamp': '1476076555.001561', 'team_id': 'T0295086H', 'trigger_word': 'email', 'channel_id': 'C1X85EYNR', 'token': 'XXXXXXXXXXXXXXXXXXXXXXXX', 'text': "email uploaded a file: <https://serverworks.slack.com/files/slackbot/XXXXXXXXX/_missing__import_data_monitoring_hasn_t_checked_in|[MISSING] import data monitoring hasn't checked in>", 'service_id': '77430880359', 'team_domain': 'serverworks', 'user_name': 'slackbot'}
必要に応じて値のチェックを行うわけですが、今回はtoken
とtext
をチェックしています。tokenに関してはLambdaのblueprintでslack関連のものを見ると、以下のようにAWS Key Management Service (KMS)
を使った例がありますのでそれをそのまま利用します。
import boto3 from base64 import b64decode ENCRYPTED_EXPECTED_TOKEN = '<kmsEncryptedToken>' # Enter the base-64 encoded, encrypted Slack command token (CiphertextBlob) kms = boto3.client('kms') expected_token = kms.decrypt(CiphertextBlob=b64decode(ENCRYPTED_EXPECTED_TOKEN))['Plaintext']
KMSを追加してchaliceにてデプロイを行うと以下のようなメッセージが表示されるかと思います
$ chalice deploy Updating IAM policy. Unsupported service: kms
開発中ということもありすべてのAWSサービスには対応してないようです。ポリシーを自動生成してくれるのは非常に便利なので、今後のサービス対応に期待しましょう
じゃーどうすんの?って話なんですが、ドキュメントを見ると手動で作ったポリシーを使う方法があります。chaliceのプロジェクトディレクトにある.chalice/polict.json
を編集してkmsの復号の権限を付加します
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Stmt1443036478000", "Effect": "Allow", "Action": [ "kms:Decrypt" ], "Resource": [ "<your KMS key ARN>" ] } ] }
これでポリシーは問題なくなりましたが、今までと同様にデプロイを実行すると以下のような確認が行われます。この確認でYes
を選択すると上記で手動追加したポリシーが削除され、chaliceが自動生成するものに置き換わりますので注意が必要です。no
を選択するとデプロイ自体がキャンセルされます。
$ chalice deploy Updating IAM policy. Unsupported service: kms The following action will be removed from the execution policy: kms:Decrypt Would you like to continue? [Y/n]: n Error: Error when deploying:
手動で編集したポリシーを利用するためにはドキュメントにあるように--no-autogen-policy
をつけてデプロイを実行します
The automatic policy generation is still in the early stages, it should be considered experimental. You can always disable policy generation with --no-autogen-policy for complete control.
これでKMSが利用できるようになったので、slackのtokenが意図したものかどうかを確認することができます。同時にtext中に意図した文字列があるかどうかも確認を行います。
Heroku APIを利用する
ここまでで、slackからのWebHookを受けて内容を確認するところまで終了しました。次はHeroku APIを利用してHerokuのコマンドを実行してみます。
APIを利用するためには認証キーを取得する必要があります。Herokuのアカウントページで認証キーを確認しておいてください
Authentication is passed in the Authorization header with a value set to :{token}. You can find a token to use on the “Account” page (in the “API Key” section) on your dashboard or by running this command:
このAPI認証キーもKMSを利用して暗号化を行って利用します。コマンドを実行するためにはHeroku Platform API
のDyno
関連を利用します。
Herokuはドキュメントが非常に丁寧に書かれているので、ドキュメントをみれば問題ないかと思います。ドキュメントではcurl
の例が示されていますが、Pythonではrequests
ライブラリを利用しました。
Requests: 人間のためのHTTP — requests-docs-ja 1.0.4 documentation
http://requests-docs-ja.readthedocs.io/en/latest/requests-docs-ja.readthedocs.io
Heroku APIで指定されているHTTPヘッダー項目を付けるのを忘れなければ特別なことをする必要がなくDyno Create
でコマンドが実行できます
このAPIでコマンドを実行した場合、レスポンスについてはHTTPステータスコード201
が返却されます。
HTTP/1.1 201 Created ETag: "0123456789abcdef0123456789abcdef" Last-Modified: Sun, 01 Jan 2012 12:00:00 GMT RateLimit-Remaining: 2400
HTTPステータスコード - Wikipedia
https://ja.wikipedia.org/wiki/HTTP%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89ja.wikipedia.org
このレスポンスからだと実行したコマンドが正常に終了したかどうか判断することができません。APIで結果を確認するにはLog Session
関連を利用するのが良さそうです
Log Session Create
APIのレスポンスで得られたlogplex_url
に対してGET
を行うと実際にLogを取得することができます。
heroku[router]: at=info method=HEAD path="/login" host=hoge.herokuapp.com request_id=8184c319-ffb0-43ab-9ea7-861991176b3a fwd="XX.XXX.XXX.XX9X" dyno=web.1 connect=1ms service=7ms status=401 bytes=354\n2016-10-10T05:15:58.327710+00:00 heroku[api]: Starting process with command `python manage.py purgerequests 4 months --noinput` by uchida@serverworks.co.jp\n']
実際にはコマンド実行後に非同期でLogを確認する必要があるので、1つのLambda関数だけでなくSQSなどを利用して他のLambda関数で確認する必要があります。また、Herokuに関して言えばAdd−onで解決してしまうのもアリだと思います。
Papertrail
のようなLogを管理できるサービスを利用すればサービス上で特定のLogを検知してslackに通知することが可能です。これを利用すればコマンド実行後の状態を検知してslackに通知してしまえば結果を確認することが可能となるため、わざわざプログラムを組む必要はなくなります。楽できる方がいいですしね。
Slackにレスポンスを戻す
ここまでで、slackからのWebHookを受けて内容を確認してHeroku APIでコマンドを実行するところまで終了しました。最後にslackにレスポンスを戻して終了です。
Outgoing Webhooks | Slack
https://api.slack.com/outgoing-webhooksapi.slack.com
ドキュメントを確認するとJSON形式
でtext
プロパティの値を設定すれば良いので、requests
のレスポンスを.json()
で設定してslackに戻しています。ただし、このレスポンス中にWebHookのTrigger Word(s)が含まれていないことが前提になります。
import requests import json resp = requests.post('<URL>', data=json.dumps(data), headers=headers) result = {'text': resp.json()} return result
まとめ
ということで、これでSlack+Chalice(API Gateway+Lambda)を使ってHerokuのコマンドを実行する
ことができました。まー何に使うの?って話はありますが・・・
chaliceを使うと非常に簡単にAPI GatewayとLambdaを使うことができます。自動で生成されるポリシーも便利ですし今後も楽しみです。一方で複雑になったり大規模になったりした場合は大変かなぁーという印象もあります。他のLambdaを利用できるフレームワークもたくさんあるので使いやすいものを選択するのが良さそうです。
また、今後リリースされるというFlourish
も楽しみですね。もうすぐre:Invent 2016
が開催されますし期待しながら待っています!
Appendix
利用したchaliceのコード例
import boto3 import requests import json import time from chalice import Chalice from urlparse import parse_qsl from base64 import b64decode app = Chalice(app_name='slack_receive') kms_client = boto3.client('kms') ENCRYPTED_SLACK_TOKEN = '<your own value>' SLACK_TOKEN = kms_client.decrypt(CiphertextBlob=b64decode(ENCRYPTED_SLACK_TOKEN))['Plaintext'] ENCRYPTED_HEROKU_API = '<your own value>' HEROKU_API = kms_client.decrypt(CiphertextBlob=b64decode(ENCRYPTED_HEROKU_API))['Plaintext'] SLACK_KEYWORDS = "<your own value>" @app.route('/slack', methods=['POST'], content_types=['application/x-www-form-urlencoded']) def slack(): body = app.current_request.raw_body print('body={}'.format(body)) result = {"text": "post OK !"} slack_token = parsed_l.get('token') if slack_token != SLACK_TOKEN: result = {"text": "slack token not valid!"} return result slack_text = parsed_l.get('text') if slack_text: print('slack_text={}'.format(slack_text)) index = slack_text.find(SLACK_KEYWORDS) print('index={}'.format(index)) if index != -1: start = slack_text.find('|') end = slack_text.find('>') sub_text = slack_text[start+1:end] print('sub_text={}'.format(sub_text)) if SLACK_KEYWORDS == sub_text: print("--- same words!! ---") # call heroku API auth_str = 'Bearer {}'.format(HEROKU_API) headers = {'Accept':'application/vnd.heroku+json; version=3', 'Authorization':auth_str, 'Content-Type': 'application/json'} data = {'command':'<your own value>', "attach": 'false', "type": "run", "time_to_live": 1800} print('headers={}'.format(headers)) print('data={}'.format(json.dumps(data))) resp = requests.post('https://api.heroku.com/apps/<your own value>/dynos', data=json.dumps(data), headers=headers) cont = {'status_code': resp.status_code, 'body': resp.json()} result = {'text': json.dumps(cont)} # log time.sleep(5) data2 = {} resp2 = requests.post('https://api.heroku.com/apps/<your own value>/log-sessions', data=json.dumps(data2), headers=headers) print{'resp2={}'.format(resp2.text)} log_url = resp2.json().get('logplex_url') resp3 = requests.get(log_url, headers=headers) print{'resp3={}'.format(resp3.text)} return result
こんなエラーメッセージが出たら
$ chalice deploy --no-autogen-policy Updating IAM policy. Updating lambda function... Creating deployment package. Sending changes to lambda. Lambda deploy done. API Gateway rest API already found. Deleting root resource id Done deleting existing resources. Error: Error when deploying: An error occurred (PolicyLengthExceededException) when calling the AddPermission operation: The final policy size (20575) is bigger than the limit (20480).
issue にもありますがLambda関数自体を削除して再度デプロイを行えば問題ありませんでした。
と思ったら、冒頭にも書いた通りにバージョンが0.3.0
になり、このissueの対応が行われています。0.2.0
以下の人はバージョンを上げましょう
地味に困る
エラーが起きるとChaliceのエラー
としてraise
されるので、何が原因なのか特定しにくい。単純にimport
を忘れた場合もchaliceのエラーとなるので他の場所かと思ってしまうと言うね・・・この辺は何か改善してほしいなぁーすでにあるなら教えてください!
ChaliceViewError: An internal server error occurred.: ChaliceViewError Traceback (most recent call last): File "/var/task/chalice/__init__.py", line 207, in __call__ raise ChaliceViewError("An internal server error occurred.") ChaliceViewError: ChaliceViewError: An internal server error occurred.
以上になります