Discord Bot のアーキテクチャを見直した話
Discord Bot の構成メモ コマンド/イベントをプラグイン的に追加
こんにちは!パン君です。
この記事では、個人で運用している Discord Bot のアーキテクチャをどう組み直したか をまとめます。
やりたかったことはざっくりいうと、
- コマンドやイベントを「 ファイルを追加するだけ 」で拡張できる
- サーバー上では
pm2で常駐管理 git pull+シェルスクリプトで 半自動デプロイ- 将来的に「管理用 Web ページ」や「API」とも連携しやすくしておく
という構成です。
目標と前提
目標
- Bot 本体をコア/プラグインに分離
- コマンドやイベントは「後から足せるモジュール」として扱う。
- 運用を楽にする
- Ubuntu サーバー上で
pm2にお任せして、SSH を切っても動き続ける。
- Ubuntu サーバー上で
- デプロイをシンプルにする
- 管理用リポジトリに push → サーバー側で pull +再起動するだけ。
前提
- 言語:Node.js(TypeScript も見据えつつ、現在は主に JS)
- ライブラリ:discord.js
- 実行環境:Ubuntu Server + pm2
- 外部アクセス:playit.gg でトンネルを張って、外部から接続可能
ディレクトリ構成
最終的に落ち着いた構成はこんな感じです。
Text
bot-root/
├── Command/ # コマンドモジュール
├── Event/ # イベントハンドラ
├── Data/ # JSON 等の設定・永続データ
├── Class/ # 共通クラス・ヘルパ
├── Shell/ # デプロイ用シェルスクリプト
├── types/ # JSDoc など型関連
├── logs/ # pm2 のログ出力先
├── config.json # トークンや設定(dotenv でも可)
└── index.js # エントリポイント(コア)ポイントは Command / Event / Data を完全に分離 したことです。
- index.js(コア)は「モジュールを読み込んでWiringするだけ」
- コマンドの中身は Command/ 下のJSファイルが担当
- イベントは Event/ 下のJSファイルが担当 という形にすることで、 Bot本体を触らずに機能追加 ができるようになりました。
コマンドのプラグイン構造
コマンドモジュールの形
各コマンドは大体こんな形のJSファイルを1ファイルで持ちます。
JavaScript
// Command/ping.js
/** @type {import('../types').BotCommand} */
module.exports = {
name: "ping",
description: "疎通確認用コマンド",
options: [],
async execute(interaction, context) {
const start = Date.now();
await interaction.reply("Pong!");
const ms = Date.now() - start;
console.log(`[ping] latency: ${ms}ms`);
},
};- name / description / options はそのまま slash command の定義に流用
- execute には interaction と共通の context(設定・ロガーなど)を渡す
動的読み込み
コア側では、起動時に Command/ ディレクトリを走査して全部読み込みます。
やっていることはシンプルで、
- fs.readdirSync("Command") で .js ファイル一覧を取得
- require して name をキーに Map に登録
- slash command の登録用に JSON を組み立てる という流れです。
このおかげで、新しいコマンドを追加するときは
- Command/ に JS ファイルを作る
- Git に commit + push
- サーバーで pull して再起動 だけで済むようになりました。
イベントハンドラの構造
イベントもコマンドと同じノリでファイルベースにしています。
JavaScript
// Event/ready.js
/** @type {import('../types').BotEvent} */
module.exports = {
name: "ready",
once: true,
execute(client, context) {
console.log(`[ready] Logged in as ${client.user.tag}`);
},
};
JavaScript
// Event/interactionCreate.js
module.exports = {
name: "interactionCreate",
once: false,
async execute(interaction, context) {
if (!interaction.isChatInputCommand()) return;
const command = context.commands.get(interaction.commandName);
if (!command) return;
try {
await command.execute(interaction, context);
} catch (err) {
console.error(err);
if (!interaction.replied) {
await interaction.reply({
content: "エラーが発生しました。",
ephemeral: true,
});
}
}
},
};コア側では、Event/ を走査してclient.on(event.name, handler) or client.once(...)
を自動で登録するだけにしています。
pm2とデプロイフロー
pm2での常駐
Ubuntu Server 上では pm2 を使って常駐させています。
Bash
pm2 start index.js --name bread-bot
pm2 save
pm2 startup- SSH を切っても Bot は動き続ける
- pm2 logs bread-bot でログ確認
- pm2 restart bread-bot で反映
という基本の運用です。
デプロイ用スクリプト
デプロイはシンプルに Shell/ に 1 本スクリプトを置きました。
Bash
#!/usr/bin/env bash
set -eu
cd /opt/bread-bot
echo "[deploy] git pull"
git pull origin main
echo "[deploy] npm install"
npm install --production
echo "[deploy] restart pm2"
pm2 restart bread-bot
- リポジトリを更新 → サーバー側でこのスクリプト実行
- 将来的には Webhook や CI から叩いてもよい
という形にしています。
これからやりたい拡張
現状でも「コマンドを JS 1 ファイルで追加できる Bot」になりましたが、さらにやりたいのはこのあたりです。
- 型まわりの強化
- TypeScript 化 or JSDoc で interaction / context に型をつける
- ApplicationCommandOptionData を定義専用のヘルパにまとめる
- 管理用 Web ページとの連携
- Express / Fastify などで API を立てて
- 「今登録されているコマンド一覧」や「Guild 状態」を Web から確認
3.ログとメトリクス
- エラーや特定イベントを別チャンネルに送る
- Cloud 上のログ基盤に送るところまで視野に入れる
まとめ
- コマンド/イベントを ディレクトリ単位で分割して、Bot 本体は「読み込んで繋ぐだけ」にする
- pm2 と簡単なシェルスクリプトで、運用とデプロイをシンプルにする
- 将来の拡張(Web 管理画面や API)を見据えて、context という共通オブジェクトをコマンドに渡す
こういう形にしたことで、 「1つの巨大な index.js に全部書いてある Discord Bot」から、だいぶ卒業できた気がします。
同じように Bot を長期運用したい人の参考になれば嬉しいです。
コメントを読み込み中...