DiscorbボットをGAE(Google App Engine)上で構築する時のtips。
Discordボットは基本的にスラッシュコマンド(Interaction)を駆使して運用するのがベターですが、今回は監視が必要だったのでWebSocketで常時リッスンします。
使用技術
- Node.js v20
- discord.js: ^14
- GCP
require("dotenv").config();
require("./deploy-commands.js").deployCommands();
const { Client, Events, GatewayIntentBits, Partials } = require("discord.js");
const tallyFile = require("./commands/tally.js");
const http = require('http');
const server = http.createServer((_req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Bot実行中です');
});
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
console.log(`サーバーが起動しました PORT: ${PORT}`);
});
const client = new Client({
intents: [
GatewayIntentBits.DirectMessages,
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
partials: [Partials.Message, Partials.Channel],
});
client.once(Events.ClientReady, (c) => {
console.log(`準備OKです! ${c.user.tag}がログインします。`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === tallyFile.data.name) {
try {
await tallyFile.execute(interaction);
} catch (error) {
console.error(error);
if (interaction.replied || interaction.deferred) {
await interaction.followUp({
content: "コマンド実行時にエラーになりました。",
ephemeral: true,
});
} else {
await interaction.reply({
content: "コマンド実行時にエラーになりました。",
ephemeral: true,
});
}
}
} else {
console.error(
`${interaction.commandName}というコマンドには対応していません。`
);
}
});
client.on("messageCreate", async (message) => {
// add
});
client.login("TOKEN");
const { REST, Routes } = require("discord.js");
const hoge = require("./commands/hoge.js");
const commands = [hoge.data.toJSON()];
const rest = new REST({ version: "10" }).setToken("token");
const deployCommands = async () => {
try {
await rest.put(Routes.applicationGuildCommands("applicationId", "guildId"), {
body: commands,
});
console.log("サーバー固有のコマンドが登録されました!");
} catch (error) {
console.error("コマンドの登録中にエラーが発生しました:", error);
}
};
module.exports = { deployCommands };
runtime: nodejs20
env: standard
instance_class: F1
handlers:
- url: .*
script: auto
env_variables:
# 環境変数
tips1: WEBページが必須
GAEの本来の用途はWebのホスティングです。
そのため、HTTPリクエストに対してレスポンス200のお返事ができないとエラーを吐かれてシャットダウンします。
これについては、
const http = require('http');
const server = http.createServer((_req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Bot実行中です');
});
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
console.log(`サーバーが起動しました PORT: ${PORT}`);
});
サーバーを立ててしまえばOK。
tips2: アクセスがないと10分で落ちる
公式ドキュメントを見ると、
アプリで自動スケーリングを使用する場合は、アイドル状態のインスタンスが約 15 分間非アクティブになり、シャットダウンが開始されます。1 つ以上のアイドル インスタンスを実行し続けるには、min_idle_instances の値を 1 以上に設定します。
とのことで、今回はコスパ重視でF1インスタンスを使っているのですがF1は「自動スケーリングのみ」という制約があるため、cron.yamlを使って定時httpリクエストを行います。
cron:
- url: /
schedule: every 10 minutes
retry_parameters:
min_backoff_seconds: 5
こんな感じ。
自動スケーリングによる処理の重複
自動スケーリングによってインスタンスの入れ替わりが自動で行われる都合だと思いますが、たまに処理が重複します。
これについては実際のプログラム上で排他制御か同期を取って回避するのが良いと思います。
また追記していきます