こんにちは、tjinjinです。9月からユニファでインフラエンジニアとして働いています。この記事はユニファアドベントカレンダーの2日目です。昨日は渡部さんのJIRAの便利Tipsでしたね、JIRA力を上げていきたい…!
この記事では業務サポート用のslack appを作ったのでご紹介します。
作ったもの
とあるサポート業務で特定の外部の方に対して、ファイルのダウンロードをさせたいという要件がありました。セキュリティ的に厳しくない(誰が使ったかを別の仕組みで検知できる)ため、今回はS3のpresigned URLを使って一時的にダウンロードが可能なリンクを生成することにしました。 また、サポートの方が毎回エンジニアに依頼する方式ではつらそうだったので、弊社だとSlackで業務のやりとりをすることが多いのでSlack Appで実装することにしました
業務フローとしては下記のイメージです。
環境周り
環境構成
SlackのEvent APIを使ってapp_mentionでbotを起動します。あとはEventに応じてAPI Gateway+lambdaが処理を行うような仕組みです。S3のリダイレクトについては別途後述します。
slack bot
slack botを作成の方法はいくつか考えられますが、以下の点からEvent APIを使った実装にしました
- lambdaを使って料金を抑えたい
- 利用頻度はそれほど高くない。必要なときのみ利用する
また、以前使われていたattachmentsは古いとのことだったので、block kitを用いた実装にしました。
Block party - Slack Platform Blog - Medium
block kitの使い方
block kit builderを使ってmockを作ろう
実装するに当たって、block kit builderを使って画面の構成を確認しました。
https://api.slack.com/tools/block-kit-builder
sample templateが用意されているので、どんな風に構成すればいいかがわかり便利です。
サンプルとしてはこんな感じにします。 sample
build kit builderでサンプルができたら、SDKを使ってmodalを起動してみます。
def build_initial_view { "type": "modal", "title": { "type": "plain_text", "text": "My App", "emoji": true }, "submit": { "type": "plain_text", "text": "Submit", "emoji": true }, "close": { "type": "plain_text", "text": "Cancel", "emoji": true }, "blocks": [ { "type": "input", "block_id": "select_user", "element": { "type": "static_select", "action_id": "selected_user", "placeholder": { "type": "plain_text", "text": "Select a user", "emoji": true }, "options": [ { "text": { "type": "plain_text", "text": "AAA", "emoji": true }, "value": "selected_aaa" }, { "text": { "type": "plain_text", "text": "BBB", "emoji": true }, "value": "selected_bbb" } ] }, "label": { "type": "plain_text", "text": "Label", "emoji": true } } ] } end ... @client.views_open( trigger_id: trigger_id, view: build_initial_view, )
今回の場合、mention実行時にtrigger_idが取得できるので、それを使ってviews_openを呼び出しています。これを実行するとmodalが起動します。
modalでSubmitを押したときのinputデータの取得方法
modalでinputされたデータはview_submission payloadとして流れてきます。 https://api.slack.com/reference/interaction-payloads/views
inputデータはview.state.valuesで取得できるのでそれをparseしましょう。
今回のsampleの場合だと、どのユーザをセレクトしたかは、view.state.values.select_user.selected_user.value で取得できます。select_userはblock_id、selected_userはaction_idの値になり、設定されていないとランダムな値になるので設定したほうが楽かと思います。
modalのupdate方法
view_submission イベントが送られてきた後に何らかの処理を行い、modalをupdateしたい場合です。イメージとしては、下記のような画面にアップデートする方法です。
view_submissionイベントが送られてきたときのmodalのupdateは、responseとして返す必要があります。 作成時点ではもともとview_submissionイベント内でview.updateをしてましたが、modal上でエラーとなってしまったためのレスポンスとして返却しています。下記のようなJSONを返せばmodalがupdateされます。
{ "response_action": "update", "view": { "type": "modal", "title": { "type": "plain_text", "text": "My App", "emoji": true }, "close": { "type": "plain_text", "text": "Cancel", "emoji": true }, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "AAAが選ばれました" } } ] } }
response_actionを指定するのがミソですね。view以下はblock kitを返します。
注意点として、responseは3秒いないかつステータスコード200を返さなければいけないため、重い処理を実行する場合はキューを利用するなどすると良いかと思います。
https://api.slack.com/surfaces/modals/using#modifying
S3リダイレクトを利用した短縮URLの発行
こちらのやり方は昔からある手法のようですが、比較的簡単に短縮URLを作れるので採用しました。
簡単に説明すると、S3のオブジェクトにはMetadataを設定することができますが、その中でWebsite Redirect LocationというKeyに対して、リダイレクトさせたいURLなどをValueに設定することで簡易的なリダイレクトが実装できます。
今回の場合、presigned_urlをWebsite Redirect Locationに設定しています。オブジェクトの中身はなくても問題ないため特に設定していません。
resp = @s3.put_object( bucket: s3_shorten_bucket, key: @generate_random, website_redirect_location: presigned_url )
S3オブジェクトのライフサイクル設定も合わせてすることで、古いデータの削除もできるので便利です。
その他はまったこと
補足でハマった部分を記載しておきます。
presigned_urlの有効期間について
presigned_urlの有効期限は最大7日間となっていますが、lambdaに割り当てているIAMロールを使うと7日間経つ前に有効期限が切れる事象が運用テスト中にありました。調べていたところ下記のブログ拝見し、IAMユーザでないと7日間の有効期限のものは作れなさそうということがわかりました。
長い有効期限を設定する場合は注意したいですね。
今後
よりセキュアな情報へのアクセスが求められるケースがあるので、その場合はリンクへのアクセス状況の把握とアクセス後のリンクの破棄などを実装していく予定です。
明日の記事はMatthewさんの記事です、お楽しみに!