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