1. はじめに
私は日々のインプットのメモや雑記をSlackを使っています。しかし、多くのワークスペースに入っているとメモも散らばるし、何より後から見返しづらいです。
このプロジェクトでは、SlackのメッセージをMarkdown形式に整え、S3バケットに自動保存するシステムを構築しました。これによってSlackでのメモをひとまとめにしてS3に保存し、Obsidianなどのツールを使って見返すことができるようになります。
2. システム概要
これから説明するシステムは、以下のGitHubリポジトリのものです。お気軽に覗いてみてください!
2.1 アーキテクチャの全体像
このシステムは以下のコンポーネントで構成されています:
- Slack Bot: チャンネルのメッセージを監視し、イベントを検知します
- API Gateway: SlackからのWebhookを受け取り、Lambda関数を起動します
- Lambda関数: メッセージを処理し、Markdown形式に変換します
- S3バケット: 変換されたMarkdownファイルを保存します
- DynamoDB: 処理済みイベントを記録し、重複処理を防止します
処理フローとしては、Slackでメッセージが投稿されると、設定したSlack Botがイベントを検知し、API Gateway経由でLambda関数を呼び出します。Lambda関数はメッセージをMarkdown形式に変換し、S3バケットの指定パスに保存します。その後、ObsidianのS3プラグインなどを通じて、これらのファイルをローカルのObsidianボルトに同期することができます。
2.2 技術スタック
このプロジェクトでは、以下の技術スタックを使用しています:
- バックエンド言語: TypeScript
- サーバーレス環境: AWS Lambda
- API: AWS API Gateway
- ストレージ: AWS S3
- データベース: DynamoDB(重複排除用)
- インフラ管理: Terraform
- イベントソース: Slack API (Events API)
TypeScriptを使用することで、型安全性を確保しつつ、モダンなJavaScript機能を活用した開発が可能になりました。また、AWSのサーバーレスサービスを活用することで、インフラ管理の負担を最小限に抑えながら、拡張性の高いシステムを構築できました。
3. 技術の概要
このプロジェクトでは、以下の技術的なポイントに注目して実装を行いました:
- サーバーレスアーキテクチャ: コスト効率とスケーラビリティの観点から、AWS Lambdaを採用しました。
- TypeScript: 型安全性による開発効率の向上と実行時エラーの減少を図るため、TypeScriptを採用しました。
- インフラのコード化: Terraformを使用してAWSリソースをコードとして管理することで、環境の再現性や変更履歴の追跡が容易になりました。これにより、デプロイプロセスの自動化とドキュメント化も実現しています。
- Slack API連携: Events APIとWeb APIを組み合わせることで、メッセージイベントの取得とユーザー・チャンネル情報の取得を実現しました。セキュリティ面では署名検証を行い、不正なリクエストを排除しています。
これらの技術選定により、保守性が高く、拡張性のあるシステムを実現することができました。
4. 工夫した点・苦労した点
4.1 Slackイベント処理の最適化
Slackイベント処理における主な工夫点は以下の通りです:
- ボットメッセージのフィルタリング: ボット自身のメッセージによる無限ループを防止しました。
// ボットメッセージをスキップ(無限ループ防止)
if (messageEvent.bot_id || messageEvent.subtype === 'bot_message') {
return {
statusCode: 200,
body: JSON.stringify({ success: true, message: 'Bot message ignored' })
};
}
-
エラーハンドリング: 様々なエラーケースに対応し、Slackに適切なレスポンスを返すようにしました。これにより、Slackのイベント再送信を防止し、システムの安定性を高めています。
-
必須フィールドの検証: イベントデータの検証を厳密に行い、不正なデータによる処理エラーを防止しています。
4.2 重複メッセージ防止の仕組み(DynamoDBによる冪等性確保)
Slackイベントは、ネットワークの問題などにより重複して送信されることがあります。そのため、DynamoDBを使用して、すでに処理したイベントを記録し、重複処理を防止する仕組みを実装しました。
export const checkAndMarkProcessed = async (eventId: string, ttlHours: number = 24): Promise<boolean> => {
// TTL(有効期限)を計算
const expiryTime = Math.floor(Date.now() / 1000) + (ttlHours * 60 * 60);
try {
// 条件付き書き込み - event_idが存在しない場合のみ書き込みを行う
await dynamoDB.put({
TableName: TABLE_NAME,
Item: {
event_id: eventId,
expiry_time: expiryTime,
processed_at: new Date().toISOString()
},
ConditionExpression: 'attribute_not_exists(event_id)'
}).promise();
return true; // 新規イベント
} catch (error) {
if (error instanceof Error && error.name === 'ConditionalCheckFailedException') {
return false; // 既に処理済みのイベント
}
// その他のエラー時は安全側に倒して処理を続行
return true;
}
};
DynamoDBのTTL(Time to Live)機能を活用することで、古いレコードを自動的に削除し、データベースのサイズを管理しています。
4.3 S3バケットへの追記処理の実装
S3バケットへのファイル保存において、同じ日のメッセージを一つのファイルに追記していく実装を行いました:
export const saveToS3 = async (channelName: string, messageMarkdown: string, messageDate: Date): Promise<string> => {
// S3のキー(ファイルパス)
const s3Key = `40_slack_memo/${safeChannelName}/${dateString}.md`;
try {
// ファイルが既に存在するか確認
let existingContent = '';
try {
const existingObject = await s3.getObject({
Bucket: bucketName,
Key: s3Key
}).promise();
existingContent = existingObject.Body?.toString('utf-8') || '';
} catch (error) {
// ファイルが存在しない場合は空文字列のまま
if (error instanceof Error && error.name !== 'NoSuchKey') {
throw error;
}
}
// 新しいコンテンツを追加
const updatedContent = existingContent + safeMessageMarkdown;
// S3にファイルを保存
await s3.putObject({
Bucket: bucketName,
Key: s3Key,
Body: updatedContent,
ContentType: 'text/markdown'
}).promise();
return s3Key;
} catch (error) {
throw error;
}
};
この実装では、まず既存ファイルの内容を取得し、新しいメッセージを追加してから再度保存しています。ただし、この方法では同時書き込みによるデータの上書きが発生する可能性があるため、より堅牢な実装方法(S3のバージョニング機能の活用や、追記専用のAPIの使用など)も検討していく必要があります。
5. まとめと今後の展望
5.1 プロジェクトの成果
このプロジェクトにより、以下のような成果が得られました:
- 知識の自動キャプチャ: Slackでの会話内容を自動的にObsidianで使用可能な形式で保存できるようになりました
- サーバーレスアーキテクチャの活用: 管理コストを抑えつつ、スケーラブルなシステムを構築できました
- インフラのコード化: Terraformを使用してインフラをコード化し、再現性と保守性を高めました
- セキュリティの考慮: 署名検証や重複排除など、セキュリティと信頼性を考慮した実装を行いました
5.2 改善ポイントと将来の拡張可能性
今後の改善点や拡張可能性としては、以下のような項目が考えられます:
- Slackのリッチテキスト対応: メンション、リンク、コードブロックなどのSlackの書式をMarkdownに適切に変換する
- ファイル保存に対応させる: 画像やPDFなどをS3に保存できるようにする
- AIによる要約機能: 日々の会話を要約してダイジェストを作成する機能