Amplify で Slack アプリつくってみた

# はじめに

この記事は、「 [AWS Amplify Advent Calendar 2021](https://qiita.com/advent-calendar/2021/amplify) 」4日目の記事です。

こんにちは。 Amplify でプロダクトを開発しているスタートアップに参画させていただいて半年ほど経ちました。 Amplify とチームの皆さんのおかげで日々良きサーバレス開発を送れています。

その中で Amplify を使って Slack アプリをつくったので、そのお話を共有させていただきます。

# 前提条件
私の場合、 Slack アプリを開発するにあたり、以下の要素を考慮する必要がありました。

- 開発のメインツールが Amplify のため Amplify 上で Slack アプリを実現したい
- Slack アプリも Amplify の multi env に対応させて環境ごとのリソースを参照したい
- Slack アプリのインフラコストを抑えるために サーバレス (FaaS) で実現したい
- そもそも Slack アプリのようなコミュニケーションツールのアプリ (ボット) をつくるのは初めて

# 検討
まず、 Slack アプリをどのように作成するのか調査しました。
Slack アプリには [Bolt](https://slack.dev/bolt-js/ja-jp/tutorial/getting-started) というフレームワークがあり、こちらを用いることで非常に簡単にアプリをつくることができました。
また、 [Serverless Express](https://github.com/vendia/serverless-express) と組み合わせることで、 Lambda にもデプロイできることがわかりました。

Lambda に載せることができれば、 Amplify で全て管理できると判断しました。
この時点では、1つの Lambda 関数で Amplify リソースの操作もできる Slack アプリがつくれると思っていました。。。

しかし、それは実現できませんでした。
なぜなら、 Slack には Slack API に対して3秒以内に返答しなければならないという制約があります。
(参考: https://api.slack.com/interactivity/handling)

つまり、 Slack API から Slack ワークスペースの操作を受け取った Lambda で Amplify リソース操作 (AppSync, DynamoDB オペレーションなど)を実行していると3秒のタイムアウトが発生してしまいます。

困りました。。。。


# 構成
検討し、最終的に以下の構成で Slack アプリを作りました。
①の赤枠が Slack からの Amplify プロジェクト内の Slack アプリ呼び出し
②の青枠が Amplify プロジェクト起点の Slack アプリ動作
です。

![Untitled Diagram.drawio (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129148/1a75c5b3-7b96-3637-d5e7-36e6b3068b85.png)

## ① Slack からの Amplify プロジェクト内の Slack アプリ呼び出し (赤枠)
Slack API に3秒以内に応答するために Slack API からのリクエスト受信・返答の Lambda 関数、ビジネスロジックを実行する Lambda 関数を分割しました。

Slack ワークスペースのイベント (メッセージの投稿やスラッシュコマンド) を受け取るための API Gateway と受け取ったリクエストを処理するための Lambda 関数を `amplify add api` コマンドで追加しました。

API Gateway に紐づく Lambda 関数は、 Slack Bolt で実装しました。 Slack のイベント毎の処理を記述しやすく、機能別 (例えば Slack アプリのインストールページ) の URL パスも設定済みです。

ビジネスロジックは API Gateway に紐づく Lambda 関数 (Slack Bolt) ではなく、別の Lambda 関数で実装します。
これらのビジネスロジックを含む Lambda 関数は Lambda 関数 (Slack Bolt) から呼び出されます。

Lambda 関数 (Slack Bolt) はビジネスロジックを含む Lambda 関数を呼び出したのち、直ちに Slack API に応答レスポンスを返します。
呼び出された Lambda 関数では Amplify プロジェクトリソースにアクセスした結果をもとに Slack API へリクエストを実施することができます。

```typescript
// API Gateway に紐づく Lambda 関数 Slack Bolt 実装サンプル (TypeScript)
import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";
import { App, ExpressReceiver } from "@slack/bolt";
import serverlessExpress from "@vendia/serverless-express";

const expressReceiver = new ExpressReceiver({
  signingSecret: process.env["SLACK_SIGNING_SECRET"],  // Lambda 環境変数から取得
  clientId: process.env["SLACK_CLIENT_ID"],  // Lambda 環境変数から取得
  clientSecret: process.env["SLACK_CLIENT_SECRET"],  // Lambda 環境変数から取得
  installationStore: {
    // DynamoDB による Slack ワークスペースごとのトークン処理を実施
  },
});

// カスタムレシーバーを用いた Bolt アプリの初期化
const app = new App({
  receiver: expressReceiver,
});

// 別 Lambda 関数を呼び出すために必要な Lambda クライアント
const lambdaClient = new LambdaClient({ region: process.env["REGION"] });

// スラッシュコマンドのリスナー
app.command("/foo", async ({ body, ack }) => {
  const command = new InvokeCommand({
    // amplify cli にて別 Lambda 関数の呼び出し権限を付与済み
    FunctionName: process.env["FUNCTION_FOOCOMMAND_NAME"],
    InvocationType: "Event",
    Payload: new TextEncoder().encode(JSON.stringify(body)),
  });
  await lambdaClient.send(command); // 本処理を実装した Lambda 関数を呼び出す
  await ack(); // Slack API サーバに応答する
});

export const handler = serverlessExpress({
  app: expressReceiver.app,
});

```

## ② Amplify プロジェクト 起点の Slack アプリ動作 (青枠)

AppSync リゾルバによる Lambda 関数の呼び出しや DynamoDB Streams をトリガーとする Lambda 関数内で Slack API へリクエストを投げることで、 Amplify プロジェクトの通知を Slack チャンネルに飛ばすこともできます。
この形式では、もちろん Slack API のタイムアウトを気にする必要はありません。

```typescript
// DynamoDB Streams で起動する Lambda から Slack API へリクエストを投げるサンプル (TypeScript)
import { WebClient } from "@slack/web-api";

export const handler = async (event: Event): Promise<StreamsEventResponse> => {
  // 省略

  // 永続層などから Slack Bot トークンを取得
  const token = getToken();

  // Slack Web API クライアントの初期化
  const slackWebClient = new WebClient(token);

  // Slack へメッセージの投稿
  await slackWebClient.chat.postMessage();

  // 省略
};
```

# 改善したい点

1. Lambda から Lambda 呼び出し問題
 Slack API の3秒制限を回避するために、 Lambda から別の Lambda を呼び出しています。これはアンチパターンの[コード内ではオーケストレーションしない](https://youtu.be/rMG18Fr896U?t=1406)に少なからず、当てはまってそうな気がしています。(SQS などを挟んだ方が良さそうだと感じています...)

2. Identity 問題
 現在、 Slack ユーザと Amplify プロジェクトのユーザ (Cognito) の紐付けは メールアドレスベースで実装しています。できるのであれば OIDC で Slack ユーザと Cognito ユーザの紐付けができると良さそうだと感じています。

知識不足だったりで、よくわかってないところがあるので、上記、間違い指摘・アドバイスいただけたら嬉しいです！

# おわりに
このやり方がベストというわけではないと思いますが、 Amplify のみで Slack アプリをつくれたことはかなりメリットでした。

Slack API の3秒制限は Lambda のコールドスタートという特性がデメリットになります。
これを回避するために、 Fargate ベースの API を Amplify で構築するという手段もありますが、コストの観点からやはり Lambda だけで実現できたのは嬉しいことでした。


Amplify によってクラウドリソースの抽象度が高いおかげで、気軽に色々トライできる環境は非常にありがたいです。 Amplify の進化をキャッチアップし続けていきたいですし、他の AWS サービスにも精通できたらいいなぁなんて思ってます。