Boltで実装したアプリをCDKでAPI GatewayとLambdaにデプロイしてみた

2020.11.26

こんにちは。坂井です。

最近Slackアプリを調査する機会があり、色々と調査を進めている中で、Boltに触ってみたいなぁと思ったので、Bolt 入門ガイドを見ながら、アプリを作ってみました。

アプリを実装している段階ではローカルで起動して、動作確認を実施したのですが、どこかの環境にデプロイしたかったので、CDKを使って、サクッとデプロイできないかなぁと思って調べてみたのですが、シンプルなCDKのデプロイサンプルを見つけることができなかったので、実際にやってみました。

実装イメージ

開発環境

  • Node.js
    • 12.16.1
  • Bolt:
    • 2.4.1
  • CDK
    • 1.74.0
  • TypeScript
    • 3.9.7

Slackアプリの作成

まずはSlackアプリを作成します。こちらの入門ガイドの通り、Slackアプリを作成します。

主に設定した部分は以下となります。

  • OAuth & Permissionsで、Bot Token Scopeschat:writeを追加
  • Event SubscriptionEnable Events有効に設定
    • Request URLは、ローカル実行時はngrokでフォワードするURL、AWSで実行する場合は、API Gatewayのエンドポイントを設定する
  • Interactivity & ShortcutsInteractivity有効に設定

Boltアプリを実装

続いてBoltでアプリのコードを以下のように実装します。このソースの中で、

  • メッセージのリスニングと応答をする
  • アクションの送信と応答
  • アプリをローカル起動する
  • Lambdaで動作するようにaws-serverless-expressを利用してプロキシする

といった処理を実装しています。

基本的には、Boltの入門ガイドと同等のコードとなりますが、Lambdaで動かすために、aws-serverless-expressを利用してプロキシする処理を追加しています。

app.ts

import { App, ExpressReceiver } from "@slack/bolt";
import * as awsServerlessExpress from "aws-serverless-express";
import { APIGatewayProxyEvent, Context } from "aws-lambda";

const processBeforeResponse = true;

const expressReceiver = new ExpressReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET ?? "", // Lambdaの環境変数から取得
  processBeforeResponse,
});
const app = new App({
  token: process.env.SLACK_BOT_TOKEN, // Lambdaの環境変数から取得
  receiver: expressReceiver,
  processBeforeResponse,
});

const server = awsServerlessExpress.createServer(expressReceiver.app);
export const handler = (
  event: APIGatewayProxyEvent,
  context: Context
): void => {
  awsServerlessExpress.proxy(server, event, context);
};

// メッセージに"hello"が含まれていたら実行する処理
app.message("hello", async ({ message, say }) => {
  await say({
    blocks: [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `Hey there <@${message.user}>!`,
        },
        accessory: {
          type: "button",
          text: {
            type: "plain_text",
            text: "Click Me",
          },
          action_id: "button_click",
        },
      },
    ],
    text: `Hey there <@${message.user}>!`,
  });
});

// action_idが"button_click"のアクションが実行された際に実行する処理
app.action("button_click", async ({ body, ack, say }) => {
  // Acknowledge the action
  await ack();
  await say(`<@${body.user.id}> clicked the button`);
});

// ローカル起動時に実行するコード
if (process.env.IS_LOCAL === "true") {
  (async () => {
    // Start your app
    await app.start(process.env.PORT || 3000);

    console.log("⚡️ Bolt app is running!");
  })();
}

package.json

・・・
  "devDependencies": {
    "@aws-cdk/assert": "1.74.0",
    "@aws-cdk/aws-apigateway": "^1.74.0",
    "@aws-cdk/aws-lambda-nodejs": "^1.74.0",
    "@types/aws-lambda": "^8.10.64",
    "@types/aws-serverless-express": "^3.3.3",
    "@types/jest": "^26.0.10",
    "@types/node": "10.17.27",
    "aws-cdk": "1.74.0",
    "jest": "^26.4.2",
    "prettier": "^2.1.2",
    "ts-jest": "^26.2.0",
    "ts-node": "^8.1.0",
    "typescript": "~3.9.7"
  },
  "dependencies": {
    "@aws-cdk/core": "1.74.0",
    "@slack/bolt": "^2.4.1",
    "aws-serverless-express": "^3.3.8",
    "source-map-support": "^0.5.16"
  }
・・・

デプロイ

Boltアプリはできたので、このアプリをLambdaにデプロイするためにCDKで必要なリソースを定義して、デプロイします。

リソース定義としては、最低限の必要なリソースである、上記で実装したBoltアプリのLambdaとAPI Gatewayのみとなります。 Lambdaの依存関係については、@aws-cdk/aws-lambda-nodejsNodejsFunctionを利用して、デプロイ時にバンドルされるように実装しています。

slack-bolt-cdk-sample-stack.ts

import * as cdk from "@aws-cdk/core";
import { NodejsFunction } from "@aws-cdk/aws-lambda-nodejs";
import * as apigateway from "@aws-cdk/aws-apigateway";

export class SlackBoltCdkSampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const appLambda = new NodejsFunction(this, "appLambda", {
      entry: "src/lambda/handlers/app.ts",
      handler: "handler",
      environment: {
        SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN || "",
        SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET || "",
      },
    });

    new apigateway.LambdaRestApi(this, "slackApi", {
      handler: appLambda,
    });
  }
}

package.json

・・・
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "bootstrap": "cdk bootstrap",
    "deploy": "cdk deploy  --require-approval never",
    "dev": "IS_LOCAL=true ts-node src/lambda/handlers/app.ts"
  },
・・・

以下のコマンドを実行してデプロイします。
※初回デプロイ時は、cdk bootstrapが必要な場合があります。

npm run deploy

なお、ローカルで起動する際は以下のコマンドを実行して起動します。(環境変数指定しているだけですが。。。)

npm run dev

動かしてみる

デプロイできたら、実際に動かしてみます。動かす前にSlackアプリのEvent SubscriptionsRequest URLInteractivity & ShortcutsRequest URLがデプロイしたエンドポイントに設定する必要があります。

例えば、API Gatewayのエンドポイントが、https://XXX.execute-api.ap-northeast-1.amazonaws.com/prodの場合、https://XXX.execute-api.ap-northeast-1.amazonaws.com/prod/slack/eventsを設定します。
※エンドポイントの末尾に/slack/eventsを追加しています。

では、実際に動かしてみます。Slackアプリの設定でダイレクトメッセージをリッスンするようにSubscribe to bot eventsmessage.imを追加してありますので、アプリにhelloというダイレクトメッセージを送ってみます。

helloというメッセージに対して、Hey there @[ユーザー名]!というメッセージとボタンが表示されました。ボタンをクリックすると、@[ユーザー名] clicked the buttonというメッセージが表示されることを確認できました。 期待したとおりの動きとなりました。

さいごに

今回は、Bolt入門ガイドに沿ってシンプルなアプリをLambdaで動かしたくてやってみましたが、少し工夫すれば特に問題なくデプロイできることを確認できました。もう少し複雑なアプリになると実装上の工夫が必要になるかと思います。

まずはシンプルなBoltアプリをCDKでLambdaにデプロイしてみたいという方の参考になれば幸いです。

今回サンプルで作成したソース一式は以下に配置してありますがので、興味を持たれた方は実際に動かしてみてください。

参考