AWS Lambda が HTTPS エンドポイントから実行可能となったので、Slack Bot を作ってみる
初めに
vercel
で運用しているサービスを slack
からデプロイしたくて、どうしようかと悶々と考えておりました。vercel
のデプロイには WebHook
が用意されているので、そのURLをキックするだけなのですが、、
一番簡単なのは slack
のスラッシュコマンドを使うことです。ただ、スラッシュコマンドは誰でも実行でき、かつ、誰が実行したのか分からないため避けたいです。
そこで slack bot
を作るしかないかなと考えていました。ただ、このためだけに、heroku
とかにサーバーを建てるのも、嫌だな〜 Lambda かな〜。でも Lambda だと、API Gateway + Lambda
の構成か、腰が重いな〜と悶々と考えていました。
そんな時に!!
「AWS Lambda が HTTPS エンドポイントから実行可能になった」というニュースが飛び込んできました。
よし! Lambdaでやろう!!そう思いました。
vercel
のデプロイは副次的な話で、主たる話は AWS Lambda
で slack bot
を動かすこととなりますので、slack bot
を作成したい方や、 AWS Lambda
を触ってみたいみたい方は是非、挑戦してみて下さい。
(slack bot は 以後 bot として記載します)
作成する bot は以下のイメージです。
実装は GitHub にあります。
HTTPS エンドポイントから実行可能な Lambda を作成する
兎にも角にも、まずは Lambda
を用意します。
AWS のコンソールに入って、 Lambda
を作成しましょう。作成時に、詳細設定にある「関数 URL を有効化」にチェックを入れて、NONEを選択しましょう。
作成されたら、index.js
を index.mjs
にリネームします。これにより、Lambda上で、ESモジュールが使えるようになります。
そして、index.mjs の内容を書き換えて、デプロイしましょう。
export async function handler(event) {
const response = {
statusCode: 200,
body: JSON.stringify("Hello from Lambda!"),
};
return response;
}
デプロイ完了したら、「関数の概要」欄の右下にある関数URLのリンクを押下してみて下さい。"Hello from Lambda!"
と表示されていれば、OKです。
bot を用意する
では、次に bot
を用意致しましょう。api slack にアクセスして、App(bot)を新規作成します。
作成したら、「Event Subscriptions」からEvent
を有効にして、 Request URL
に Lambda
のHTTPS エンドポイントURLを入れます。
すると
Your request URL didn’t respond with the correct challenge value. Update your URL to receive a new request and value.
というメッセージが表示されるかと思います。
これは、slack
が実施しているチェックをパスしなかったためです。slack
は入力された URLに対して、challenge 値を含めてリクエストを投げます。受け取った方は、 この challenge 値をそのまま、レスポンスに返す必要があり、これにより slack は正しいURLと検証できます。
それでは、 index.mjs
を challenge 値を返すように変更致しましょう。
export async function handler(event) {
const body = JSON.parse(event.body);
let responseBody = "Hello from Lambda!";
// bodyに challenge がある場合は、それをそのまま返す
if (body.challenge) {
responseBody = {
challenge: body.challenge,
};
}
const response = {
statusCode: 200,
body: JSON.stringify(responseBody),
};
return response;
}
デプロイしてから、Event Subscriptions
のURL検証をリトライしてみて下さい。今度は上手くいくはずです。
それでは bot の設定を実施していきましょう。
まず、 bot
にメンションした時に反応して欲しいので、 Subscribe to bot events
に app_mentions
を追加して、保存します。
次に「App Home」にて、bot
の Display Name
を設定します。
次に「OAuth & Permissions」にて、Scopes
に chat:write
の権限を付与します。
また、「OAuth & Permissions」に「Bot User OAuth Token」がありますので、値を控えておきます。
これで作成出来ました!!
「Install App to Your Team」より作成した bot
を install
しましょう。
ただ、この時点では slack
上で bot
に話しかけても何も反応がありません。Lambda からレスポンスを返すように致しましょう。
Bot を動かす
bot
を反応させるためには、 http の post 処理が必要となりますので、 先に http.mjs
を作成しておきます。
import https from "https";
export const postRequest = async (url, headers, message) => {
const options = { method: "POST", headers: headers };
return new Promise((resolve, reject) => {
let req = https
.request(url, options, (res) => {
res.on("end", () => {
console.log("completed postRequest");
resolve(res.statusCode);
});
})
.on("error", (e) => {
console.log("error postRequest:" + e.message);
reject(e);
});
req.write(message);
req.end();
});
};
では、bot
が呼ばれたら反応するように index.mjs
を変更致しましょう。
import { postRequest } from "./http.mjs";
export async function handler(event) {
const body = JSON.parse(event.body);
let responseBody = "Hello from Lambda!";
// bodyに challenge がある場合は、それをそのまま返す
if (body.challenge) {
responseBody = {
challenge: body.challenge,
};
}
if (body.event.type == "app_mention") {
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + process.env["SLACK_BOT_USER_ACCESS_TOKEN"],
};
const data = {
channel: body.event.channel,
text: responseBody,
};
await postRequest(
process.env["SLACK_POST_MESSAGE_URL"],
headers,
JSON.stringify(data)
);
}
const response = {
statusCode: 200,
body: JSON.stringify(responseBody),
};
return response;
}
お気づきかもしれませんが、一部の情報を環境変数に保持しています。Lambdaの設定 → 環境変数から以下を設定下さい。
SLACK_BOT_USER_ACCESS_TOKEN: Bot User OAuth Token の値
SLACK_POST_MESSAGE_URL: https://slack.com/api/chat.postMessage
こちらで、デプロイして、slack
から再度 bot
話しかけてみましょう。slack
上に Hello from Lambda!
が返ってきたら成功です。
Interactive Message
bot
が返答できるようになったので、会話をしていくことにしましょう。
slack
の bot
設定画面から 「Interactivity & Shortcuts」を選択し、Interactivity を 有効にします。URL入力欄が表示されるので、 Lambda のエンドポイントURLを入れましょう。
次に bot
が返信する内容も短文ではなく、ボタンなどのアクションを含んだメッセージに変更します。今回は vercel
のデプロイ用なので、以下のように設定します。
独自のメッセージを作成したい人は slackの Block Kit Builder を使うのをおすすめします。
それでは、ブロックで構築したメッセージを返すように変更します。
import { blocks } from "./slack.mjs";
...
const data = {
channel: body.event.channel,
// ここを text から blocks に変更
blocks: blocks,
};
...
これで、ボタン付きのメッセージを bot
が返してくれます。ただ、現状ではボタンを押下しても何も起こらないので、ボタン押下時の受け口を作っていきます。
ボタン押下時のリクエストは content-type
が x-www-form-urlencoded
となります。(今までは json
でした)
ここらへんから実装が複雑になってくるので、処理を分けた方が良さそうです。上述した通り、content-type
は json
と x-www-form-urlencoded
の2種類あるので、 handleJson
handleUrlEncoded
追加します。
import { postRequest } from "./http.mjs";
import { blocks } from "./slack.mjs";
export async function handler(event) {
const contentType = event.headers["content-type"];
let responseBody = "Hello from Lambda!";
//contentType により処理を分ける
if (contentType == "application/json") {
responseBody = await handleJSON(event.body);
} else if (contentType == "application/x-www-form-urlencoded") {
responseBody = await handleUrlEncoded(event.body);
}
const response = {
statusCode: 200,
body: JSON.stringify(responseBody),
};
return response;
}
const handleUrlEncoded = async (requestBody) => {
// 後で実装する
console.log(requestBody);
};
const handleJSON = async (requestBody) => {
const body = JSON.parse(requestBody);
if (body.challenge) {
const responseBody = {
challenge: body.challenge,
};
return responseBody;
}
if (body.event.type == "app_mention") {
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + process.env["SLACK_BOT_USER_ACCESS_TOKEN"],
};
const data = {
channel: body.event.channel,
blocks: blocks,
};
await postRequest(
process.env["SLACK_POST_MESSAGE_URL"],
headers,
JSON.stringify(data)
);
return "messageを送信しました";
}
};
それでは handleUrlEncoded
を実装していきます。実際のデータは base64
エンコードされているので、まず、デコードして、その後 querystring
を使って、URLクエリパラメーター形式をパースします。データが取り出せたら handleAction
で押されたボタンに紐づくメッセージを返すようにします。
import querystring from "querystring";
...
const handleUrlEncoded = async (requestBody) => {
const queryParameter = Buffer.from(requestBody, "base64").toString();
const body = querystring.parse(queryParameter);
const payload = JSON.parse(body.payload);
console.log(JSON.stringify(payload));
const headers = {
"Content-Type": "application/json",
};
const result = handleAction(payload);
const data = {
text: result,
};
await postRequest(payload.response_url, headers, JSON.stringify(data));
return result;
};
...
const handleAction = (payload) => {
const user = payload.user.username;
switch (payload.actions[0].action_id) {
case "main":
return `<@${user}> が本番環境をデプロイしました。`;
case "staging":
return `<@${user}> がステージング環境をデプロイしました。`;
case "cancel":
return "キャンセルしました。";
default:
return "よう分からんわ。";
}
};
これで、ボタンを押下したら、bot
が反応してくれるようになります。ここまできたら、もう、ほぼ完成で、あとは、 handleAction
で実施した処理を書くだけです。
今回実施したい処理は、 vercel
のデプロイ(webhook の kick) だったので、そちらを追加します。
const handleAction = (payload) => {
// WebHook を Kick するだけなので、設定不要
const headers = {};
const data = "";
const user = payload.user.username;
switch (payload.actions[0].action_id) {
case "main":
postRequest(process.env["MAIN_WEBHOOK_URL"], headers, data);
return `<@${user}> が本番環境をデプロイしました。`;
case "staging":
postRequest(process.env["STAGING_WEBHOOK_URL"], headers, data);
return `<@${user}> がステージング環境をデプロイしました。`;
case "cancel":
return "キャンセルしました。";
default:
return "よう分からんわ。";
}
};
こちらで完成です!!
お疲れ様でした。AWS Lambda
が HTTPS
エンドポイントをサポートしてくれたおかげで、bot
作成が簡単にできるようになりました。 (ただ、API Gateway が不要になっただけ)
皆様も色々な bot
を作成して、楽していきましょう!!
Discussion