はじめに
AWS EC2上にパルワールドのサーバを立てる機会があったのですが、プレイヤーがいないのにサーバが開きっぱなしになっているのは無駄なので、Discord botと連携してサーバを停止・起動までできるようにしてみました。
備忘録的に書いておきますがもっと良い方法はあるかも。
サーバを準備する
EC2を立てる
まずはサーバ用の箱を作る、具体的にはEC2インスタンスの作成する。
AMIは無料枠のAmazon Linux 2023。
公式ドキュメント( https://docs.palworldgame.com/ja/getting-started/requirements/ )にはCPU4コア、メモリ16GB(安定した動作のために32GB以上を推奨)が必要とあるため、インスタンスタイプはt2.xlargeを選択。
セキュリティグループには以下の設定をしておく。
プロトコル: カスタムUDP、ポート: 8211、送信元: 0.0.0.0/0
プロトコル: SSH、送信元: マイIP
また、サーバを立てたり落としたりする際にIPが変わらないようにElastic IPを付与しておく。
パルワールドサーバの立ち上げ
サーバに入ってDockerを導入
# 1. EC2 に SSH 接続
ssh -i /path/to/your-key.pem ec2-user@<EC2のIPアドレス>
# 2. パッケージの更新と Docker のインストール (Amazon Linux の場合)
sudo dnf update -y
sudo dnf install -y docker
# 3. Docker サービスの起動と自動起動設定
sudo systemctl start docker
sudo systemctl enable docker
# 4. ec2-user に Docker の実行権限を付与(次回ログイン時から sudo なしで実行可能に)
sudo usermod -aG docker ec2-user
公式Dockerコンテナ用いてパルワールドサーバを立てる
mkdir -p ~/palworld_server
# ※操作を簡略化するためパーミッションを全開放
chmod 777 ~/palworld_server
# 公式 SteamCMD コンテナでダウンロード・インストールを実行
# コンテナ内の /data にインストール、Steamに匿名アカウントでログイン、Palworld Dedicated ServerのアプリID(2394010)を指定し、ダウンロード
docker run -it --rm \
-v ~/palworld_server:/data \
steamcmd/steamcmd:latest \
+force_install_dir /data \
+login anonymous \
+app_update 2394010 validate \
+quit
sudo chmod -R 777 ~/palworld_server
# サーバの起動
# -w /palworldで作業ディレクトリを/palworldにセット
docker run -d \
--name palworld-server \
-p 8211:8211/udp \
-v ~/palworld_server:/palworld \
-w /palworld \
cm2network/steamcmd \
./PalServer.sh
# 次回から EC2 起動時に自動で立ち上がるように設定
docker update --restart always palworld-server
# 手動でコンテナを立ち上げる場合
docker start palworld-server
これで:8211にパルワールド上でアクセスすることができる。
サーバにパスワードなどの設定をする場合は、PalWorldSettings.iniを編集すれば良い。詳細な変数は公式ドキュメントに記載されている
https://docs.palworldgame.com/ja/settings-and-operation/configuration/
cd ~/palworld_server/Pal/Saved/Config/LinuxServer/
cp ~/palworld_server/DefaultPalWorldSettings.ini ./PalWorldSettings.ini
nano PalWorldSettings.ini
# ServerPassword="" の部分を編集
Discord連携機能を追加する
これで基本的なサーバとしての機能は完成しました。
続いて「はじめに」で記載したように、このサーバをDiscordのbot経由で立ち上げたり落としたりできる機能を追加します。
IAMロールの追加
LabmdaにEC2の立ち上げ・CloudWatchへのログを書き込む権限を付与するために、ロールを作成します。
- IAMのページから「ロールを作成」を選択
- 信頼されたエンティティタイプ「AWS のサービス」を選択、ユースケース: Lambda を選択して、「次へ」 をクリック
- AmazonEC2FullAccess、AWSLambdaBasicExecutionRole を追加(簡易的に実装するためFullAccessにしていますが、対象に絞ったほうが良いです)
- ロール名: Discord-EC2-Lambda-Role など分かりやすい名前を入力し、一番下の 「ロールを作成」 をクリック
Lambda関数の設定
次に、Discord botから呼び出されたらEC2を立ち上げたり落としたりするようなLambda関数を作成する。
- Lambda関数の作成
「関数を作成」を選択。以下の設定を投入。
- 関数URLの発行
- Lambda画面のタブから「設定」を選択
- 左メニューから「関数 URL」をクリックし、「関数URLの作成」を押す
- 認証タイプ: NONE(認証なし)を選択し、「保存」 をクリック
- 生成された「関数URL」をコピーしておく(これをDiscord botに登録する)
(参考)認証タイプをNONEとしているため、URLを持つユーザーは誰でも関数にアクセスできる状態となっている。そのため今回は認証をコードベースで実装している。
Discord botの作成
- Discord Developer Portal( https://discord.com/developers/home )を開き、アプリを新規作成する
- 作成したBotアプリケーションを選択し、左メニューの「一般情報」を開く
- 名前や概要を入力したら「インタラクション・エンドポイントURL」 の欄に、先ほどコピーしたLambdaの関数URLを貼り付ける。
- 下に記載の「Lambdaにコードを配置」が完了したら、画面下の 「変更を保存」 をクリック
これでDiscord Botが作成された。
- 【参考】Public botをオフにする方法
デフォルトだとPublic Botがオンになっているので以下の手順でオフに設定。
https://qiita.com/nagnag22/items/f5dda441f19f55c40bdd
Lambdaにコードを配置
以下のコードを配置する。
import { crypto } from 'node:crypto';
import { EC2Client, StartInstancesCommand, StopInstancesCommand, DescribeInstancesCommand } from "@aws-sdk/client-ec2";
// ★ Discord Developer Portal の General Information にある PUBLIC KEY
const DISCORD_PUBLIC_KEY = 'Discord botで表示されているPUBLIC_KEYを貼り付け';
// ★ EC2のリージョンとインスタンスID
const REGION = 'リージョンをここに貼り付け';
const INSTANCE_ID = 'EC2インスタンスIDをここに貼り付け';
const ec2 = new EC2Client({ region: REGION });
// Ed25519 署名検証ロジック (Node.js標準機能のみで動作)
function verifySignature(event) {
const headers = Object.fromEntries(
Object.entries(event.headers || {}).map(([k, v]) => [k.toLowerCase(), v])
);
const signature = headers['x-signature-ed25519'];
const timestamp = headers['x-signature-timestamp'];
const body = event.body || '';
if (!signature || !timestamp) return false;
try {
const key = crypto.createPublicKey({
key: Buffer.concat([
Buffer.from('302a300506032b6570032100', 'hex'), // Ed25519 ASN.1 Header
Buffer.from(DISCORD_PUBLIC_KEY, 'hex')
]),
format: 'der',
type: 'spki'
});
return crypto.verify(
null,
Buffer.from(timestamp + body),
key,
Buffer.from(signature, 'hex')
);
} catch (e) {
return false;
}
}
export const handler = async (event) => {
// 1. 署名検証(不正なら 401 を返す:Discord要件)
if (!verifySignature(event)) {
return { statusCode: 401, body: JSON.stringify('invalid request signature') };
}
const body = JSON.parse(event.body || '{}');
// 2. Discord PING テストに応答 (Type 1 -> Type 1)
if (body.type === 1) {
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 1 })
};
}
// 3. スラッシュコマンド処理
const commandName = body.data?.name;
let message = "未知のコマンドです。";
if (commandName === 'start') {
await ec2.send(new StartInstancesCommand({ InstanceIds: [INSTANCE_ID] }));
message = "🚀 **パルワールド サーバーの起動リクエストを送信しました!**\n1〜2分ほどで接続可能になります。";
} else if (commandName === 'stop') {
await ec2.send(new StopInstancesCommand({ InstanceIds: [INSTANCE_ID] }));
message = "🛑 **パルワールド サーバーの停止リクエストを送信しました。**";
} else if (commandName === 'status') {
const res = await ec2.send(new DescribeInstancesCommand({ InstanceIds: [INSTANCE_ID] }));
const state = res.Reservations[0]?.Instances[0]?.State?.Name;
const statusEmoji = state === "running" ? "🟢" : state === "stopped" ? "🔴" : "🟡";
message = `${statusEmoji} **現在のサーバー状態:** \`${state}\``;
}
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 4,
data: { content: message }
})
};
};
仕組みは以下の通り。
- ボットの受信用 URLはインターネット上に公開されるため、悪意のある第三者が勝手にサーバーを起動・停止できないよう、「本当に Discord から届いたリクエストか?」を暗号署名(Ed25519)で厳格にチェック。
// リクエストから署名とタイムスタンプ、リクエスト本文を抽出
// Discordの仕様上、「タイムスタンプ + リクエスト本文」 の文字列を秘密鍵で暗号化したものが 署名(signature) になる
const signature = headers['x-signature-ed25519']; // ① 署名データ
const timestamp = headers['x-signature-timestamp']; // ② タイムスタンプ
const body = event.body || ''; // ③ リクエスト本文
...
// Ed25519のパブリックキーであるという目印となるヘッダを付与
const key = crypto.createPublicKey({
key: Buffer.concat([
Buffer.from('302a300506032b6570032100', 'hex'), // Ed25519 ASN.1 Header
Buffer.from(DISCORD_PUBLIC_KEY, 'hex')
]),
format: 'der',
type: 'spki'
});
...
// verify で検証
return crypto.verify(
null, // Ed25519は暗号化の内部でハッシュ化を行うためアルゴリズム指定は null
Buffer.from(timestamp + body), // 検証対象の元のデータ
key, // 復元したパブリックキー
Buffer.from(signature, 'hex') // Discordから届いた署名
);
- PING テスト応答。Discord Developer Portal で Endpoint URL を設定・保存するときに、Discord から type: 1(PING)が飛んでくるので、それに対して type: 1(PONG)を返さないと、Endpoint の登録に失敗する。
if (body.type === 1) {
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 1 })
};
}
- チャットで打たれたコマンド名に応じて AWS EC2 に命令を渡す。Discord サーバーから Lambda へのリクエストも、Lambda から Discord へのレスポンスも、すべて JSON 形式のデータ構造になっている
/start ➜ StartInstancesCommand(サーバー起動)
/stop ➜ StopInstancesCommand(サーバー停止)
/status ➜ DescribeInstancesCommand(今の状態を取得:running 🟢 や stopped 🔴 など)
const commandName = body.data?.name;
if (commandName === 'start') {
// サーバー起動
await ec2.send(new StartInstancesCommand({ InstanceIds: [INSTANCE_ID] }));
message = "🚀 **パルワールド サーバーの起動リクエストを送信しました!**\n1〜2分ほどで接続可能になります。";
} else if (commandName === 'stop') {
// サーバー停止
await ec2.send(new StopInstancesCommand({ InstanceIds: [INSTANCE_ID] }));
message = "🛑 **パルワールド サーバーの停止リクエストを送信しました。**";
} else if (commandName === 'status') {
// サーバーの状態確認 (running / stopped / pending など)
const res = await ec2.send(new DescribeInstancesCommand({ InstanceIds: [INSTANCE_ID] }));
const state = res.Reservations[0]?.Instances[0]?.State?.Name;
const statusEmoji = state === "running" ? "🟢" : state === "stopped" ? "🔴" : "🟡";
message = `${statusEmoji} **現在のサーバー状態:** \`${state}\``;
}
...
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 4,
data: { content: message }
})
};
handlerの設定
Lambdaの「コード」->「ランタイム設定」から、ハンドラーを「index.handler」に指定する。

DiscordにBot、コマンド登録
Discord サーバーに Bot を招待する
以下の手順で自分の Discord サーバーにbotを招待する。
- Discord Developer Portal の左メニューから 「OAuth2」 ➔ 「OAuth2 URLジェネレーター」 をクリック。
- スコープ には「bot」にチェックを入れる、BOTの権限 では「メッセージを送る」や 「スラッシュコマンドを使用」にチェックを入れる。
- 画面下部に生成された URL をコピー してブラウザで開き、bot導入したいDiscord サーバーを選択すればよい。
スラッシュコマンド(/start など)を登録する
最後にスラッシュコマンドの機能を追加する。
これはローカルから以下のコマンドを叩けばよい。
- ★YOUR_APPLICATION_ID: Developer Portal の General Information にある APPLICATION ID
- ★YOUR_BOT_TOKEN: Developer Portal の Bot タブにある TOKEN(Reset Token で取得できるもの)
curl -X PUT "https://discord.com/api/v10/applications/★YOUR_APPLICATION_ID/commands" \
-H "Authorization: Bot ★YOUR_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '[
{"name": "start", "description": "パルワールドサーバーを起動します"},
{"name": "stop", "description": "パルワールドサーバーを停止します"},
{"name": "status", "description": "パルワールドサーバーの状態を確認します"}
]'
Discordでテストする
- /status と入力して送信
➔ 🟢 現在のサーバー状態: running や 🔴 stopped などが返ってくれば成功 - /start と入力して送信
➔ EC2 インスタンスの起動リクエストが送信される




