🤖

作ろう 生成 AI Slack 画像 Bot ~ Amazon Nova Canvasと共に ~

2024/12/16に公開

はじめに

本記事では、Amazon Bedrock から生成した画像を Slack に通知する画像 Bot を AWS CDK を使って作成していきます。画像生成に用いる基盤モデルには、せっかくなので AWS re:Invent 2024 で発表された Amazon Nova Canvas を利用していきます。

Amazon Bedrock および Amazon Nova Canvas については下記資料で解説されておりますので、そちらをご確認ください。

方針

下記の記事を参考にして、AWS リソースの作成を AWS CDK で実施します。
参考元の記事では Amazon Bedrock の処理を非同期的に行う仕組みが導入されていますが、本記事では簡単化のために同期的に作っていきます。

https://dev.classmethod.jp/articles/amazon-bedrock-slack-chatbot-generate-image/

全体像

overview

Lambda と API Gateway を組み合わせて画像生成 API を作成します。
API の実処理を担う Lambda では Slack と Amazon Bedrock とやりとりをして画像生成していきます。

事前準備

CDK アプリケーションを作成する前に Slack の設定と Amazon Bedrock の基盤モデル有効化が必要なので、先にそちらを実施していきます。

Slack の設定

新規 Slack App の作成

Slack にサインインした状態で下記の Slack API プラットフォームにアクセスします。

「Create an App」→ 「From scratch」の順に選択します。
遷移後の画面で 下記を指定後に「Create App」を選択して Slack App を作成します。

  • App Name: Slack App の名前
  • Pick a workspace to developt your app in: 開発を行うワークスペース

slack-app-01

Bot の権限設定

左サイドバーから「OAuth & Permissions」を選択します。
slack-app-02

「Scopes」セクションの「Bot Token Scopes」が Slack App に与える権限範囲を設定する項目です。
「Add an OAuth Scope」を選択して下記の権限を追加します。

  • chat:write
  • files:write

slack-app-03

Slack App のインストール

左サイドバーから「Install App」を選択後、「Install to Private」を選択します。
slack-app-04

対象のワークスペースと Slack App に与える権限が意図したものであることを確認して、「許可する」を選択します。
slack-app-05

Slack App のトークン取得

CDK アプリケーションをデプロイする際に下記の情報が必要なので事前に取得しておきます。

  • Singing Secret
  • Bot User OAuth Token

Singing Secret は Basic Information から取得します。
「Singing Secret」項目の「Show」を選択してシークレットを表示して控えておきます。
slack-app-06

Bot User OAuth Token は Install App から取得します。
OAuth Toknes にトークンが表示されるので対象のトークンを控えておきます。
slack-app-07

残り一つ Slack の設定がありますが、CDK アプリケーションのデプロイ後に実施するので、Slack の設定は一旦終わりです。

Amazon Bedrock 基盤モデルの有効化

Amazon Bedrock から基盤モデルにアクセスするためには、事前に基盤モデルへのアクセスを追加する必要があります。
本記事では、Amazon Nova Canvas の基盤モデルへのアクセスを追加していきます。

Amazon Nova Canvas の有効化

バージニア北部リージョンの Amazon Bedrock コンソールで、左サイドバーから「Bedrock Configurations」の「モデルアクセス」-> 「特定のモデルを有効化にする」の順に選択します。

bedrock-access-01

「Nova Canvas」にチェックを入れた後、「次へ」を選択します。
bedrock-access-02

確認画面に遷移するので、モデルが「Nova Canvas」であることを確認後に画面右下の「送信」を選択します。
bedrock-access-03

遷移後の画面で、「Nova Canvas」のアクセスのステータスが「アクセスが付与されました」となっていることを確認します。
bedrock-access-04

これで、Amazon Bedrock の基盤モデル有効化の設定は終わりです。

実装

つくるもの

AWS CDK アプリケーションとして下記のリソースを作成していきます。
コンストラクトを一つだけ含むシンプルな構成です。

  • SlackGenAIImageBotStack
    • Slack からのイベント受信して画像を返す API を作成するスタック
      • API Gateway もこのスタックで作成します。
  • Handler
    • 実際に API ロジックを処理する部分の Lambda を切り出したコンストラクト
      • コンストラクトに付随して Lambda のコードも一緒に作成します。

環境設定

下記の環境で実施しました。

  • Node.js v22.11.0
  • AWS CDK v2.168.0

AWS CDK アプリケーションの作成

プロジェクトの初期化

AWS CDK 用のプロジェクトを作成します。

mkdir slack-gen-ai-image-bot
cd slack-gen-ai-image-bot && cdk init app --language=typescript

Lambda 関数で必要な Slack SDK, AWS SDK および Typescript で開発するためのパッケージ等を追加していきます。
(Node.js v20.6.0 以降は.envが標準サポートなのでdotenvはいらなそうですが、設定に苦戦したので今回は入れてます。。。)

npm install @slack/bolt @slack/web-api @aws-sdk/client-bedrock-runtime
npm install -D @types/aws-lambda esbuild dotenv

Slack App のトークン取得で控えた情報は環境変数として扱いたいので、cdk.jsonを編集して、環境変数を取り扱えるようにします。

cdk.json
{
-  "app": "npx ts-node --prefer-ts-exts bin/slack-gen-ai-image-bot.ts",
+  "app": "npx ts-node -r dotenv/config --prefer-ts-exts bin/slack-gen-ai-image-bot.ts",
  .
  .
}

コンストラクトの作成

API の実処理を担う Handler のコンストラクトを作成していきます

slack-gen-ai-image-bot/lib/construct/handler.ts
import * as cdk from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";

interface HandlerCostructProps extends cdk.StackProps {
  slackSigningSecret: string;
  slackBotToken: string;
}

export class Handler extends Construct {
  public readonly handler: NodejsFunction;

  constructor(scope: Construct, id: string, props: HandlerCostructProps) {
    super(scope, id);

    this.handler = new NodejsFunction(this, "api", {
      environment: {
        SLACK_SIGNING_SECRET: props.slackSigningSecret,
        SLACK_BOT_TOKEN: props.slackBotToken,
      },
      timeout: cdk.Duration.seconds(30),
      bundling: {
        forceDockerBundling: false,
      },
    });

    // bedrock invoke policy
    const bedrockInvokePolicy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ["bedrock:InvokeModel"],
      resources: ["*"],
    });
    const handlerRole = this.handler.role as iam.Role;
    handlerRole.addToPolicy(bedrockInvokePolicy);
  }
}

Slack App とのやりとりを行うので環境変数としてSlack App のトークン取得で取得したトークンを設定しています。

Lambda の既定のタイムアウト時間は 3 秒ですが、画像生成には時間がかかるので、明示的にtimeoutを設定して 30 秒に延長しています。今回 esbuild でビルドしたいので、docker によるビルド設定はfalseとしています。また Lambda は Amazon Bedrock を呼び出して画像を生成するのでモデル呼び出しに必要な権限も付与しています。

Lambda の実処理の作成

Slack および Amazon Bedrock との連携を行う Lambda 関数を作成していきます。

slack-gen-ai-image-bot/lib/construct/handler.api.ts
import { App, AwsLambdaReceiver } from "@slack/bolt";
import { WebClient } from "@slack/web-api";
import type {
  AwsCallback,
  AwsEvent,
} from "@slack/bolt/dist/receivers/AwsLambdaReceiver";
import {
  BedrockRuntimeClient,
  InvokeModelCommand,
  InvokeModelCommandInput,
} from "@aws-sdk/client-bedrock-runtime";
import { Context } from "aws-lambda";

class ImageGenerator {
  constructor(private readonly client: BedrockRuntimeClient) {}

  async generateImage(inputText: string): Promise<Buffer> {
    const request = {
      taskType: "TEXT_IMAGE",
      textToImageParams: {
        text: inputText,
      },
      imageGenerationConfig: {
        cfgScale: 8,
        seed: 0,
        quality: "standard",
        width: 512,
        height: 512,
        numberOfImages: 1,
      },
    };

    const params: InvokeModelCommandInput = {
      modelId: "amazon.nova-canvas-v1:0",
      accept: "application/json",
      contentType: "application/json",
      body: JSON.stringify(request),
    };

    const command = new InvokeModelCommand(params);
    const response = await this.client.send(command);
    const responseBody = JSON.parse(new TextDecoder().decode(response.body));
    return Buffer.from(responseBody.images[0], "base64");
  }
}

const awsLambdaReceiver = new AwsLambdaReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET!,
});

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  receiver: awsLambdaReceiver,
});

const imageGenerator = new ImageGenerator(new BedrockRuntimeClient());
const webClient = new WebClient(process.env.SLACK_BOT_TOKEN);

app.event("app_mention", async ({ event, say }) => {
  const inputText = event.text.replace(/<@.+>/g, "").trim();
  try {
    const generatedImage = await imageGenerator.generateImage(inputText);
    await webClient.filesUploadV2({
      channels: event.channel,
      file: generatedImage,
      filename: "image.png",
      initial_comment: "Generated by Nova Canvas",
    });
  } catch (error) {
    console.error("Error processing image:", error);
    await say("Sorry, couldn't generate the image.");
  }
});

export const handler = async (
  event: AwsEvent,
  context: Context,
  callback: AwsCallback
) => {
  const slackHandler = await awsLambdaReceiver.start();
  return slackHandler(event, context, callback);
};

ImageGeneratorクラスは Amazon Bedrock と通信して画像生成を行います。
request変数で基盤モデルの Amazon Nova Canvas へ渡すパラメータを指定していますが、パラメータは下記のドキュメントを参考にしました。

app.eventから始まる記述は Slack との通信部分です。
この部分は対象の Slack App へのメンションイベントを受け取って所望の処理を行う記述です。
本記事ではメンションされたテキストを基盤モデルに渡して、生成された画像を Slack にアップロードする処理を行っています。
処理には@slack/bolt@slack/web-apiを用いていますが、詳細は下記を参照してください。

スタックの作成

API Gateway の作成と先ほど作成した Handler コンストラクトを取り込んでいきます。

/lib/slack-gen-ai-image-bot-stack.ts
import * as cdk from "aws-cdk-lib";
import * as apigw2 from "aws-cdk-lib/aws-apigatewayv2";
import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
import { Construct } from "constructs";
import { Handler } from "./construct/handler";

export interface SlackGenAiImageBotStackProps extends cdk.StackProps {
  slackSigningSecret: string;
  slackBotToken: string;
}

export class SlackGenAiImageBotStack extends cdk.Stack {
  constructor(
    scope: Construct,
    id: string,
    props: SlackGenAiImageBotStackProps
  ) {
    super(scope, id, props);

    const handler = new Handler(this, "handler", {
      slackSigningSecret: props.slackSigningSecret,
      slackBotToken: props.slackBotToken,
    });

    // API Gateway Integration
    const handlerIntegration = new HttpLambdaIntegration(
      "HandlerIntegration",
      handler.handler
    );
    const slackGenAiImageApi = new apigw2.HttpApi(
      this,
      "slack-genai-image-api",
      {
        defaultIntegration: handlerIntegration,
      }
    );
    slackGenAiImageApi.addRoutes({
      path: "/slack/events",
      methods: [apigw2.HttpMethod.POST],
      integration: handlerIntegration,
    });
  }
}

Lambda と API Gateway の統合は下記の AWS CDK の API リファレンスをもとに実施しました。

エントリーポイントの作成

CDK アプリケーションのエントリーポイントを作成していきます。

/bin/slack-gen-ai-image-bot.ts
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
import { SlackGenAiImageBotStack } from "../lib/slack-gen-ai-image-bot-stack";

if (!process.env.SLACK_SIGNING_SECRET) {
  throw new Error("SLACK_SIGNING_SECRET is not set");
}

if (!process.env.SLACK_BOT_TOKEN) {
  throw new Error("SLACK_BOT_TOKEN is not set");
}

const app = new cdk.App();
new SlackGenAiImageBotStack(app, "Dev-SlackGenAiImageBotStack", {
  description: "Amazon Nova Canvas' Slack Generative AI Image Bot",
  tags: {
    Environment: "Dev",
  },
  slackSigningSecret: process.env.SLACK_SIGNING_SECRET,
  slackBotToken: process.env.SLACK_BOT_TOKEN,
});

デプロイ

まず、CDK プロジェクト配下にデプロイのための環境変数ファイルを作成し、Slack App のトークン取得で取得したトークンを設定します。

.env
SLACK_SIGNING_SECRET="Signing Secret を設定"
SLACK_BOT_TOKEN="Bot User OAuth Token を設定"

CDK のブートストラップスタックをデプロイします。
(※ 対象リージョンで既に他の CDK アプリケーションをデプロイしている場合は不要です。)

cdk bootstrap

下記のように表示されればブートストラップ完了です。
※ 権限エラーとなる場合は AWS CLI のクレデンシャル情報が正しく設定されているかを確認します。

CDKToolkit: creating CloudFormation changeset...
 ✅  Environment aws://000000000000/us-east-1 bootstrapped.

次に CDK アプリケーションをデプロイします。

cdk deploy

権限周りの変更が含まれているが本当にデプロイしてよいかというメッセージが表示されます。
今回アプリケーションの新規作成で問題ないので、y を入力して続行です。

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────────────────────────────────────────────┬────────┬───────────────────────┬────────────────────────────────────────────────────┬────────────────────────────────────────────────────┐
│   │ Resource                                           │ Effect │ Action                │ Principal                                          │ Condition                                          │
├───┼────────────────────────────────────────────────────┼────────┼───────────────────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────┤
│ + │ ${handler/api.Arn}                                 │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                   │ "ArnLike": {                                       │
│   │                                                    │        │                       │                                                    │   "AWS:SourceArn": "arn:${AWS::Partition}:execute- │
│   │                                                    │        │                       │                                                    │ api:${AWS::Region}:${AWS::AccountId}:${apigatewayD │
│   │                                                    │        │                       │                                                    │ B9AED4E}/*/*"                                      │
│   │                                                    │        │                       │                                                    │ }                                                  │
│ + │ ${handler/api.Arn}                                 │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                   │ "ArnLike": {                                       │
│   │                                                    │        │                       │                                                    │   "AWS:SourceArn": "arn:${AWS::Partition}:execute- │
│   │                                                    │        │                       │                                                    │ api:${AWS::Region}:${AWS::AccountId}:${apigatewayD │
│   │                                                    │        │                       │                                                    │ B9AED4E}/*/*/slack/events"                         │
│   │                                                    │        │                       │                                                    │ }                                                  │
├───┼────────────────────────────────────────────────────┼────────┼───────────────────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────┤
│ + │ ${handler/api/ServiceRole.Arn}                     │ Allow  │ sts:AssumeRole        │ Service:lambda.amazonaws.com                       │                                                    │
├───┼────────────────────────────────────────────────────┼────────┼───────────────────────┼────────────────────────────────────────────────────┼────────────────────────────────────────────────────┤
│ + │ *                                                  │ Allow  │ bedrock:InvokeModel   │ AWS:${handler/api/ServiceRole}                     │                                                    │
└───┴────────────────────────────────────────────────────┴────────┴───────────────────────┴────────────────────────────────────────────────────┴────────────────────────────────────────────────────┘
IAM Policy Changes
┌───┬────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                   │ Managed Policy ARN                                                             │
├───┼────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${handler/api/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)?

あとはデプロイが終わるのを待ちます。

Slack AppのEvent Subscription 設定

Slack App へのメンションイベントが発生した場合に呼び出されるように、サブスクリプションの設定を行います。

API エンドポイントの確認

まず、AWS の API Gateway コンソールを開いて対象の API エンドポイントを確認します。
CDK アプリケーションで作成した API を選択します。
slack-event01

次に左サイドバーから作成した API を選択します。
API 詳細にある「デフォルトのエンドポイント」が欲しい API エンドポイントになるので控えておきます。

slack-event02

Event Subscription 設定

Slack API の左サイドバーから「Event Subscriptions」を選択し、「Enable Events」のトグルを On にします。
slack-event03

「Request URL」にAPI エンドポイントの確認を使って URL を入力します。
CDK アプリケーションで/slack/eventsのルートを指定したので、Request URL の形式としては下記のようになります。

  • https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/slack/events

slack-event04

Request URL が「Verified」となれば検証成功です。

「Subscribe to bot events」を選択後、「Add Bot User Event」からイベントを追加します。

slack-event05

今回は「app_mention」を追加します。
slack-event06

「app_mention」のイベントが追加されたことを確認して、右下の「Save Changes」を選択して設定を保存します。
slack-event07

画面上部に Slack App の再インストールを促す表示がされるので、案内に従って再インストールします。
slack-event08

権限確認画面が表示されるので「許可する」を選択します。
slack-event09

Slack へのアプリ追加

対象チャネルの「設定を編集する」を選択します。
add-slackapp01

「インテグレーション」タブ内の App から「アプリを追加」を選択します。
add-slackapp02

作成した Slack App が表示されるので「追加」を選択します。
今回は nova という名前の Slack App を作成したのでこれを追加します。
add-slackapp03

動かしてみる

早速 画像生成 Bot にメンションして、画像を生成してもらいます。

gen-image

画像 Bot にメンションすると赤ちゃんの画像が生成されました。

実はこのままだと同じ画像が3回表示されてしまいます。
これは Slack API の再送仕様と関係してくるのですが、対処方法は下記の記事が参考になります。

さいごに

Slack と Amazon Bedrock を連携させて画像 Bot を作ることができました。
併せて、Amazon Nova Canvas による画像生成も一緒に試すことができました。

作成までの道のりは少し遠かったですが、自分用の Bot を作成するのも面白いなと思いました。

BABY JOB  テックブログ

Discussion