はじめに
お久し振りです。 @Keichan_15 です。
今年は新たに技術好きが集まるOrganizationとしてDevToxicClubを作成し、こちらでAdvent Calendarを執筆することとなりました。
主に私と交流のあるメンバーで構成されていますが、メンバーは随時募集していますので技術がお好きな方で気軽にOrgに属してみたいよ!という方はコメント等にご連絡ください!
さて今年のAdvent Calendar1発目の記事は、Node.jsでSSH接続したお話を書いていこうと思います。
あまりタイトル映えしない内容でいささか恐縮ですが、今回この記事執筆に至った経緯を以下で簡単に…。
経緯
私の過去の記事をご覧になられた方はご存じかと思いますが、年明けに私は興味本位でベアボーンのPCに32GBx2(64GB)のメモリと1TBのNVMe4.0 M.2SSDを搭載したサーバー用ミニPCを購入しました。
該当記事はこちらからでも以下からでもご覧いただけます。
当時大ブームとなっていたPalWorldというゲームのサーバーを運営するために購入を決意。今はほとんど過疎っているのですが…。
そのサーバー運営に関する部分で、私はサーバーの起動・停止、ログ等の監視等を含めDiscord
で一元管理したいと思い、discord.js
を使ったBotによる運用を開始するようになっていくのです。
そうです、僕はPythonがあまり分かりませんもので
作戦を立てる
私は基本放任主義な性格がゆえ、今回はサーバーに参加されている方が、私が不在の際でも以下の操作を行えるようにしようと考えました。
- サーバーの起動
- サーバーの停止
サーバーの起動は、ミニPC内の指定ディレクトリに格納されているシェルスクリプトを実行することで可能です。
ここについては各ゲームで異なるので、それぞれのゲームサーバーを構築する際に確認が必要です。
今回は私の高尚な思想である "自環境は汚さない" をモットーにDockerコンテナ内でBot用のNode.jsを実行することとします。
環境
ちなみに僕のお家のミニPCを取り巻く環境は以下の通りです。
めちゃくちゃ雑なのは許してほしい時間が無かったんや…
その他補足は以下。
"axios": "^1.6.8"
"discord.js": "^14.14.1"
"dotenv": "^16.4.5"
"fs": "^0.0.1-security"
"openai": "^4.36.0"
"ssh2": "^1.15.0"
実装
まずはDocker関連のファイルから。
FROM node:latest
WORKDIR /app
面倒だったのでlatest
で最低限の記載に収めました。
services:
app:
build: .
volumes:
- .:/app
command: node commands/utility/palserver_command.js
ports:
- 3001:3000
tty: true
ここも特段アレンジしている個所はありません。
強いて言えば、もしコンテナを別用途で複数運用する場合はポートの重複使用に気をつけようね。というくらいですかね。
import { Client as DiscordClient, GatewayIntentBits } from "discord.js";
import { exec } from "child_process";
import { REST } from "@discordjs/rest";
import { Routes } from "discord-api-types/v9";
import axios from 'axios';
import { Client as SSHClient} from 'ssh2';
import { readFileSync } from 'fs';
import dotenv from 'dotenv';
dotenv.config()
const client = new DiscordClient({ intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildPresences,
GatewayIntentBits.GuildMembers,
] });
const commands = [
{
name: 'start',
description: 'パルワールドのサーバーを起動するコマンドです'
},
{
name: 'shutdown',
description: 'パルワールドのサーバーをシャットダウンするコマンドです'
}
];
const rest = new REST({ version: '10' }).setToken(process.env.TOKEN);
(async () => {
try {
console.log('(/) スラッシュコマンドのリロードを開始します。');
await rest.put(
Routes.applicationGuildCommands(process.env.CLIENT_ID, process.env.GUILD_ID),
{ body: commands },
);
console.log('(/) スラッシュコマンドのリロードに成功しました。');
} catch (error) {
console.error(error);
}
})();
client.once('ready', () => {
console.log('Ready!');
});
client.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) return;
const { commandName } = interaction;
const getApiInfo = async () => {
try {
const data = JSON.stringify({
"waittime": 2,
"message": "2秒後にシャットダウン!"
});
if (commandName === 'shutdown') {
const response = await axios({
method: 'post',
url: process.env.SERVER_URL + '/v1/api/shutdown',
maxBodyLength: Infinity,
data: data,
headers: {
'Content-Type': 'application/json'
},
auth: {
username: process.env.PAL_USERNAME,
password: process.env.PAL_PASSWORD
}
});
await interaction.reply("シャットダウンが完了しました!");
} else {
const ssh = new SSHClient();
ssh.on('ready', () => {
ssh.exec(process.env.SERVER_EXEC_COMMAND, (err, stream) => {
if (err) throw err;
stream.on('close', (code, signal) => {
ssh.end();
}).on('data', (data) => {
console.log('STDOUT: ' + data);
}).stderr.on('data', (data) => {
console.log('STDERR: ' + data);
});
});
}).connect({
host: process.env.SSH_HOST,
port: process.env.SSH_PORT,
username: process.env.SSH_USERNAME,
privateKey: readFileSync(process.env.SSH_KEY_PATH)
});
await interaction.reply("サーバーの起動が完了しました!");
}
} catch (error) {
console.error(error);
}
};
getApiInfo();
});
client.login(process.env.TOKEN);
今回はSSH2
がメインです。SSH2
はNode.js
からSSH接続を実行するためのライブラリです。
私の場合、基本的に鍵認証によるSSH接続方式にしているので、鍵認証が使えるというのもSSH2
使用を決めた一つの理由とも言えます。
実施している内容はざっくり以下です。
- 指定のDiscordチャンネル内で
/start
と入力すると、BotからミニPC内指定のサーバー起動用ディレクトリのシェルスクリプトをSSH
経由で実行 - 指定のDiscordチャンネル内で
/shutdown
と入力すると、BotからパルワールドのREST API
にあるShutdown URLへPOST
リクエストを実施する
一応これでDiscordチャンネルからPalWorldのサーバー操作ができます。
1つ懸念があるとすれば、2つ目のShutdown
のPOST
リクエストですね。
サーバーが完全に閉じたタイミングを検知しているのではなく、POST
叩いたぜ~って瞬間にレスポンスを受け取ってるみたいなので、シャットダウン後にすぐ起動、はタイミングによってはまずい気がしています。
この辺はsleep
かますなりやり方あるとは思うので、いろいろアレンジしてみるのは良さげかもですね~。
ちなみに環境変数ファイルは一部マスクしますが、こんな感じです。
よしなにご自身の環境に書き換えてくださいね。
TOKEN=MTIzMDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CLIENT_ID=12xxxxxxxxxxxxxx
GUILD_ID=120xxxxxxxxxxxxxx
SERVER_URL=http://192.168.10.188:8212
PAL_USERNAME=admin
PAL_PASSWORD=yourPassword
SERVER_EXEC_COMMAND=~/Steam/steamapps/common/PalServer/PalServer.sh -port=8242 -rcon > nohup.out 2>&1 &
SSH_HOST=192.168.10.188
SSH_PORT=1072
SSH_USERNAME=ssh_your_user_name
SSH_PASSWORD=ssh_your_user_password
SSH_KEY_PATH=./.ssh/id_rsa
おわりに
いかがでしたでしょうか。
今年1発目の記事を前日に執筆し、突貫工事で仕上げる無能ムーブ。Toxic
の名に恥じない行動と自負しております。
明日からは @recodeyo1ko さんが2日連続で執筆くださいます。お楽しみに!
そして皆さん!Node.js
でSSH
接続をしよう!アモーレ!