Boltを使ってSlackワークフローからDynamoDBにデータを挿入する

Boltを使ってSlackワークフローからDynamoDBにデータを挿入する

2026.04.22

こんにちは👋26新卒の大山です。

Slackワークフローで受け取ったデータをGoogleスプレッドシートに保存している仕組みを、DynamoDBへ移行する計画があり、その一つの方法として「Slackの入口部分はワークフローのままで、保存先だけDynamoDBにする構成」を検証したので、その内容をブログにしようと思います。

Slack ワークフローの種類

Slackでワークフローを作成する方法としてSlackアプリケーション内にあるワークフロービルダーから作成する方法・Custom Functionsを使う方法・Boltを使う3種類あります。

機能 ワークフロービルダー Custom Functions Bolt
プログラミング 不要 必要(Deno + TS) 必要(JS・Python・Java)
インフラ・デプロイ Slackが管理 Slackが管理 AWS Lambdaなど
トリガー メッセージ送信、Webhook、リアクションなど限定的 自由にカスタム可能 自由にカスタム可能
外部連携 コネクターで対応しているもの 自由に実装可能 自由に実装可能
メッセージのカスタマイズ 限定的 Block KitでUIを手軽にカスタマイズ可能 Block KitでUIを手軽にカスタマイズ可能
コスト Proプラン以上で利用可能 Proプラン以上で利用可能 フリープランから無料で利用可能。別途サーバー代
向いている用途 定型的なフロー インフラ管理はしたくない・独自の外部サービス連携が必要 既存のサーバーやAWSなどに統合したい・複雑なロジックが必要

https://docs.slack.dev/workflows/comparing-workflows-apps

今回は統合予定のシステムで使われているAWSサービスがあるので、Boltを使ってLambda経由でDynamoDBに保存する仕組みで作っていきます。

Slackアプリの作成

まずSlack APIにアクセスして、アプリの作成を行います。
スクリーンショット 2026-04-21 14.39.46

アプリの作り方としてUIで作るScratchと、JSON/YAML形式でアプリの基本情報を構築するManifestの2つの方法があります。
今回はManifestの方法で作成していきます。
スクリーンショット 2026-04-21 14.41.03

インストール先のワークスペースの選択

アプリをインストールするワークスペースを選択します。
この選択は後で変更できないため注意してください。
スクリーンショット 2026-04-20 14.17.35

Manifestをアップロード

今回はYAML形式で作成します。
urlrequest_urlの部分は後で書き換えるのでとりあえず仮で入れています。

display_information:
  name: SlackTestApp
  description: フォームからDynamoDBにデータを送信するBot
  background_color: "#2c2d30"
features:
  bot_user:
    display_name: SlackTestApp
    always_online: true
  slash_commands:
    - command: /submit
      url: https://example.com
      description: 申請フォームを開く
      usage_hint: /submit
      should_escape: false
oauth_config:
  scopes:
    bot:
      - commands
      - chat:write
      - users:read
      - users:read.email
  pkce_enabled: false
settings:
  interactivity:
    is_enabled: true
    request_url: https://example.com
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false
  is_mcp_enabled: false

スクリーンショット 2026-04-22 15.21.09

ワークスペースにインストール

作ったアプリを導入するワークスペースにインストールします。

  1. Features > OAuth & Permission > OAuth Tokens内にある、Install to SlackAPP[ワークスペース名]を選択。
    スクリーンショット 2026-04-21 17.45.41

  2. 権限を確認して許可するを選択。
    スクリーンショット 2026-04-21 17.35.26

これでアプリの作成は完了です!

プロジェクトの作成

下記の構成で構築していきます。

  • フレームワーク: Bolt
  • IaC: AWS CDK
  • 言語: TypeScript
  • パッケージマネージャー:pnpm

アーキテクチャはシンプルに、API Gatewayで受けたリクエストをLambdaに渡し、Lambda内でSlackのリクエスト検証を行った上でDynamoDBに保存します。SlackのSigning SecretとBot Tokenは、Parameter Storeにて管理します。
slack-app-aws-architecture.drawio

CDK

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as path from 'path';

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

    // ── DynamoDB ──────────────────────────────────────────────────────────────
    const table = new dynamodb.Table(this, 'SlackAppTest', {
      tableName: 'SlackAppTest',
      partitionKey: {
        name: 'id',
        type: dynamodb.AttributeType.STRING,
      },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // ── Lambda ────────────────────────────────────────────────────────────────
    const handler = new lambdaNode.NodejsFunction(this, 'SlackHandler', {
      functionName: 'slackapp-handler',
      entry: path.join(__dirname, '../../src/index.ts'),
      handler: 'handler',
      runtime: lambda.Runtime.NODEJS_22_X,
      timeout: cdk.Duration.seconds(10),
      memorySize: 128,
      environment: {
        TABLE_NAME: table.tableName,
        SLACK_SIGNING_SECRET_PARAM: '/slackapp/signing-secret',
        SLACK_BOT_TOKEN_PARAM: '/slackapp/bot-token',
      },
    });

    // ── IAM: DynamoDB 書き込み権限 ─────────────────────────────────────────────
    table.grantWriteData(handler);
    // ── IAM: SSM SecureString 読み取り権限 ────────────────────────────────────
    handler.addToRolePolicy(
      new iam.PolicyStatement({
        actions: ['ssm:GetParameter'],
        resources: [
          `arn:aws:ssm:${this.region}:${this.account}:parameter/slackapp/*`,
        ],
      }),
    );

    // ── API Gateway ───────────────────────────────────────────────────────────
    const api = new apigateway.RestApi(this, 'SlackApi', {
      restApiName: 'slackapp-api',
      deployOptions: { stageName: 'dev' },
    });
    const integration = new apigateway.LambdaIntegration(handler, {
      proxy: true,
    });
    const slackResource = api.root.addResource('slack');
    slackResource.addMethod('POST', integration);

    // ── Outputs ───────────────────────────────────────────────────────────────
    new cdk.CfnOutput(this, 'SlackEndpointUrl', {
      value: `${api.url}slack`,
      description: 'Slack のスラッシュコマンド URL・Interactivity Request URL の両方に設定してください',
    });
  }
}

const app = new cdk.App();
new SlackAppStack(app, 'SlackAppStack');

フォームUIの作成

フォームUIの作成にはBlock Kit Builderを使って作成します。

スクリーンショット 2026-04-22 17.55.13
左側のツールバーからブロックを追加したり、ドラッグで並び順を変更したりと、GUIベースで直感的にフォームUIを作成できます。作成できたら、右側の欄に表示されているJSONをコピーし、以下のように views.open の view プロパティに貼り付けます。

await client.views.open({
  view: {
    //ここに入れる
  }
})

Lambda

import { App, AwsLambdaReceiver } from '@slack/bolt';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
import { v4 as uuidv4 } from 'uuid';

// ── AWS クライアント ───────────────────────────────────────────────────────────
const dynamo = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const ssmClient = new SSMClient({});
const TABLE_NAME = process.env.TABLE_NAME!;

async function getParameter(name: string): Promise<string> {
  const result = await ssmClient.send(
    new GetParameterCommand({ Name: name, WithDecryption: true }),
  );
  return result.Parameter!.Value!;
}

// ── Bolt 初期化 ────────────────────
let lambdaHandler: ((...args: any[]) => Promise<unknown>) | undefined;

async function initApp(): Promise<NonNullable<typeof lambdaHandler>> {
  if (lambdaHandler) return lambdaHandler;

  const [signingSecret, botToken] = await Promise.all([
    getParameter(process.env.SLACK_SIGNING_SECRET_PARAM!),
    getParameter(process.env.SLACK_BOT_TOKEN_PARAM!),
  ]);

  const receiver = new AwsLambdaReceiver({ signingSecret });
  const app = new App({ token: botToken, receiver });

  // ── /submit スラッシュコマンド:モーダルを開く ────────────────────────────
  app.command('/submit', async ({ ack, command, client, logger }) => {
    await ack();
    try {
      await client.views.open({
        trigger_id: command.trigger_id,
        view: {
          "callback_id": "submit_modal",
          "type": "modal",
          "title": {
            "type": "plain_text",
            "text": "申請ワークフロー",
            "emoji": true
          },
          "submit": {
            "type": "plain_text",
            "text": "送信",
            "emoji": true
          },
          "close": {
            "type": "plain_text",
            "text": "キャンセル",
            "emoji": true
          },
          "blocks": [
            {
              "type": "input",
              "block_id": "text_block",
              "label": {
                "type": "plain_text",
                "text": "申請内容"
              },
              "element": {
                "type": "plain_text_input",
                "action_id": "text_input",
                "placeholder": {
                  "type": "plain_text",
                  "text": "申請内容を入力してください"
                }
              }
            }
          ]
        }
      });
    } catch (error) {
      logger.error('views.open に失敗しました:', error);
    }
  });

  // ── モーダル送信:DynamoDB に保存 ─────────────────────────────────────────
  app.view('submit_modal', async ({ ack, body, logger }) => {
    await ack();

    const userId = body.user.id;
    const text = body.view.state.values['text_block']['text_input'].value ?? '';

    try {
      await dynamo.send(
        new PutCommand({
          TableName: TABLE_NAME,
          Item: {
            id: uuidv4(),
            userId,
            text,
            submittedAt: new Date().toISOString(),
          },
        }),
      );
      logger.info(`申請を保存しました: userId=${userId}`);
    } catch (error) {
      logger.error('DynamoDB への保存に失敗しました:', error);
    }
  });

  lambdaHandler = await receiver.start();
  return lambdaHandler;
}

// ── Lambda エントリーポイント ──────────────────────────────────────────────────
export const handler = async (...args: any[]) => {
  const fn = await initApp();
  return fn(...args);
};

Slack APIから環境変数を取得

Slack APIのダッシュボードから、Client SecretOAuth Tokensを取得します。

  1. Settings > Basic Information > App CredentialsからClient Secretを取得。
    スクリーンショット 2026-04-21 15.42.51
  2. Features > OAuth & PermissionからOAuth Tokensを取得。
    スクリーンショット 2026-04-21 15.48.56

パラメータストアに環境変数を追加

  1. AWS マネジメントコンソールにログインし、AWS Systems Manager > アプリケーションツール > パラメータストアに遷移します。

  2. パラメーターの作成を選択
    スクリーンショット 2026-04-21 15.38.37

  3. 名前に/slackapp/signing-secret/slackapp/bot-tokenを、それぞれ【利用枠:標準】【タイプ:安全な文字列】【KMSキーソース:現在のアカウント】で該当の値を入力してパラメーターを作成。
    スクリーンショット 2026-04-21 15.53.42

デプロイ

  1. CDKデプロイ
    コマンドはお使いのパッケージマネージャーやAWS CLIのプロファイル名に合わせて変更してください。
pnpm exec cdk deploy --profile work
  1. CloudFormation > スタックにあるステータスがCREATE_COMPLETEになっていればOKです。
    スクリーンショット 2026-04-22 10.53.03

  2. デプロイ後、ターミナルに出力された Outputs の SlackEndpointUrl、もしくは CloudFormation > スタック > (該当スタック) > 出力タブ から SlackEndpointUrl の値をコピーします。

  3. Slack APIFeatures > App Manifestから2箇所に3でコピーしたエンドポイントURLを入力します。

 slash_commands:
    - command: /submit
      url: 👉ここにエンドポイントURLをペースト
      description: 申請フォームを開く
settings:
  interactivity:
    is_enabled: true
    request_url: 👉ここにエンドポイントURLをペースト
  org_deploy_enabled: false

スクリーンショット 2026-04-22 11.24.43

試してみる

  1. アプリをインストールしたSlackワークスペースを開き、テキストボックスから/submitと入力し送信。
    スクリーンショット 2026-04-21 17.49.20

  2. モーダルが開くので、入力して送信ボタンを選択。
    スクリーンショット 2026-04-22 18.12.07

  3. AWSマネジメントコンソールから DynamoDB > 項目を探索 > SlackAppTest を開くと、入力した内容が無事に保存されていることが確認できました🎉
    スクリーンショット 2026-04-22 15.54.14

まとめ

Slackのワークフローには複数の作り方があり、「コネクタ非対応の外部ツールと連携したい」「既存システムと統合したい」など、要件に合わせて柔軟に選択できるのが魅力だと感じました。今回のようにBolt + AWS構成にすることで、Slackを入口にした業務フローをそのままクラウド側の資産として活かせるので、既存システムとの統合を検討している方はぜひ試してみてください!

この記事をシェアする

関連記事