はじめに
この記事では、AWSで低コストなMod環境のMinecraftサーバー構築する手順を、前編・後編の 2 回に分けて紹介します。
特に Minecraft の Mod 環境(ATM 系や大規模 Modpack など)は、 メモリ・CPU をしっかり確保したい一方で、常時稼働させるほど遊ばない というケースが多いと思います。
そこで今回は、
という方に向けて、 Discord Bot からサーバーの起動/停止を操作し、遊び終わったら自動で止める構成を作っていきます。
- はじめに
- 概要
- 本記事について
- プロジェクト準備
- CDK で Lambda を定義する
- Lambda の処理を作成する
- Discord 側の設定
- Bot をサーバーに追加する
- Slash Command を登録する
- 動作確認
- おわりに
概要
この構成では、以下のように Discord から Minecraft サーバーを操作できる仕組みを作ります。
- Discord Bot を AWS Lambda 上で動かす
- Slash Command(
/start,/stop)でサーバーを制御 - Minecraft サーバーは スポットインスタンスを使ってコスト削減
- EBS スナップショットでワールドデータを永続化
- Spot 中断通知が来たら、自動で安全停止+再起動する復旧フローも実装
想定しているユースケース
- 友達と Mod 環境の Minecraft を遊びたい
- でも常時サーバーを稼働させるほどではない
- そこそこ高いスペックが必要なので、できる限り安く済ませたい
- AWS の勉強としても、触りながら構築してみたい
「遊ぶときだけ /start」
「終わったら /stop」
で快適に運用できる仕組みを目指します。
構成図

本記事について
この記事は 前編 で、主に以下を扱います。
- Discord Bot を Lambda 上で動かす構成
- Slash Command の受信・署名検証
/hello/echoの実装- Discord → Lambda → Discord の基本的な往復
ここまでできれば、後編の 「Lambda からスポットインスタンスの Minecraft サーバーを起動する」 にスムーズに進むことができます。
プロジェクト準備
今回の構築では、以下の環境で動作確認しています。
使用バージョン
- Node.js 22.21.0
- npm 10.9.4
- AWS CDK CLI 2.1014.0
- TypeScript 5.6.3
- ts-node 10.9.2
- aws-cdk-lib 2.194.0
- constructs 10.0.0
- discord-interactions 4.0.0
- node-fetch 3.3.2
- dotenv 17.2.3
作業ディレクトリを作成し、CDK プロジェクトを初期化します。
mkdir blog-discord-bot cd blog-discord-bot cdk init --language=typescript
CDK で Lambda を定義する
lib/blog-discord-bot-stack.ts に、Discord リクエストを受ける Lambda を作成します。
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs'; export class BlogDiscordBotStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const fn = new lambdaNode.NodejsFunction(this, 'BlogDiscordBot', { entry: 'lambda/index.ts', handler: 'handler', runtime: lambda.Runtime.NODEJS_20_X, architecture: lambda.Architecture.ARM_64, memorySize: 512, timeout: cdk.Duration.seconds(60), bundling: { externalModules: [], minify: true, sourceMap: true, format: lambdaNode.OutputFormat.CJS, target: 'node20', }, environment: { DISCORD_PUBLIC_KEY: process.env.DISCORD_PUBLIC_KEY ?? '', DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID ?? '', }, }); // Discord の Interactions Endpoint 用に Function URL を作成 const url = fn.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.NONE, cors: { allowedOrigins: ['*'], allowedMethods: [lambda.HttpMethod.ALL], }, }); new cdk.CfnOutput(this, 'FunctionUrl', { value: url.url }); } }
stack の書き方は、以下の API Reference を参照してください。
準備ができたら、cdk deploy で Lambda が作成されます。
コンソールに表示される Function URL を後で Discord 側に設定します。

Lambda の処理を作成する
lambda/index.ts を作成します。
Discord の署名検証と、Slash Command の応答を行うシンプルなハンドラです。
import { APIGatewayProxyStructuredResultV2 } from 'aws-lambda'; import { verifyKey } from 'discord-interactions'; const { DISCORD_PUBLIC_KEY } = process.env; const CORS_HEADERS = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST,OPTIONS', 'access-control-allow-headers': 'content-type,x-signature-ed25519,x-signature-timestamp', }; function json(statusCode: number, body?: unknown): APIGatewayProxyStructuredResultV2 { return { statusCode, headers: { ...CORS_HEADERS, 'content-type': 'application/json' }, body: body !== undefined ? (typeof body === 'string' ? body : JSON.stringify(body)) : '', }; } function hget(headers: Record<string, string | undefined> | undefined, name: string) { if (!headers) return undefined; const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase()); return key ? headers[key] : undefined; } // Discord 署名検証 async function verifyDiscordRequest(event: any) { const signature = hget(event.headers, 'x-signature-ed25519'); const timestamp = hget(event.headers, 'x-signature-timestamp'); if (!DISCORD_PUBLIC_KEY || !signature || !timestamp) { return { ok: false, code: 401, msg: 'missing headers or PUBLIC_KEY' }; } const rawBody = event.isBase64Encoded && event.body ? Buffer.from(event.body, 'base64').toString('utf8') : event.body ?? ''; try { const ok = await verifyKey(rawBody, signature, timestamp, DISCORD_PUBLIC_KEY); if (!ok) return { ok: false, code: 401, msg: 'invalid request signature' }; return { ok: true, rawBody }; } catch { return { ok: false, code: 401, msg: 'verification error' }; } } export const handler = async (event: any) => { if (event?.requestContext?.http?.method === 'OPTIONS') return json(204, ''); const verified = await verifyDiscordRequest(event); if (!verified.ok) return json(verified.code, { error: verified.msg }); let interaction; try { interaction = JSON.parse(verified.rawBody); } catch { return json(400, { error: 'invalid json' }); } // Ping if (interaction.type === 1) { return json(200, { type: 1 }); } const cmd = interaction.data?.name; // /hello if (cmd === 'hello') { return json(200, { type: 4, data: { content: 'こんにちは 👋' }, }); } // /echo if (cmd === 'echo') { const text = interaction.data?.options?.find((o: any) => o.name === 'text')?.value ?? '(テキストがありません)'; return json(200, { type: 4, data: { content: `echo: ${text}` }, }); } return json(200, { type: 5 }); };
署名検証が通らないと Discord に拒否されるため、
verifyDiscordRequest が正しく動作していることが重要です。
interactions については以下のドキュメントを参照してください。
Discord 側の設定
まず、Discord のアプリケーションを作成します。
https://discord.com/developers/applications

アプリケーション作成後の画面から、以下の情報を取得します。

- Application ID
- Public Key
これらを .env に保存します。
DISCORD_APPLICATION_ID=xxxxxxxxxxxx DISCORD_PUBLIC_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
次に、アプリケーション画面内の「Interactions Endpoint URL」に、 CDK デプロイ時に出力された Function URL を入力します。

認証が成功すればそのまま保存できます。
Bot をサーバーに追加する
続いて、Bot を Discord サーバーに追加します。
アプリケーションの Bot ページを開き、「Reset Token」でトークンを発行しておきます。

権限の追加は不要です。
画面下の Generated URL をコピーします。

新しいタブに URL を貼り付けて開き、 管理権限を持つ任意のサーバーへ Bot を追加します。

.env への追記
コマンド登録時に必要となる以下の情報を .env に追加します。
- Bot Token

DISCORD_BOT_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxx
- Guild ID(サーバーID)
Discord の「開発者モード」を有効化し、 Bot を使用するサーバーを右クリック → ID をコピー
DISCORD_GUILD_ID=xxxxxxxxxxxxxxxxxxxx
ここまでで、Discord 側の準備は完了です。
Slash Command を登録する
script/register-commands.ts を作成します。
Bot Token を使って Discord API にコマンドを PUT します。
import fetch from 'node-fetch'; import 'dotenv/config'; const { DISCORD_APPLICATION_ID, DISCORD_BOT_TOKEN, DISCORD_GUILD_ID, } = process.env; if (!DISCORD_APPLICATION_ID) throw new Error('DISCORD_APPLICATION_ID is missing'); if (!DISCORD_BOT_TOKEN) throw new Error('DISCORD_BOT_TOKEN is missing'); const apiBase = 'https://discord.com/api/v10'; const commands = [ { name: 'hello', description: '挨拶を返します' }, { name: 'echo', description: '入力したテキストをそのまま返します', options: [{ type: 3, name: 'text', description: '返す内容', required: true }], }, ]; async function registerCommands() { const url = DISCORD_GUILD_ID ? `${apiBase}/applications/${DISCORD_APPLICATION_ID}/guilds/${DISCORD_GUILD_ID}/commands` : `${apiBase}/applications/${DISCORD_APPLICATION_ID}/commands`; console.log('Registering commands to', url); const res = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bot ${DISCORD_BOT_TOKEN}`, }, body: JSON.stringify(commands), }); if (!res.ok) { console.error(await res.text()); process.exit(1); } console.log('Registered successfully'); } registerCommands().catch((e) => { console.error(e); process.exit(1); });
実行します。
npx ts-node script/register-commands.ts
成功すると Discord サーバー側で /hello /echo が利用できるようになります。
動作確認
Discord サーバーで /hello を実行すると Lambda が応答し、


と返ってくれば成功です。
/echo text:xxx も同様に応答します。
おわりに
ここまでで、Discord からのリクエストを Lambda で受け取り、
簡単なコマンド(/hello /echo)に応答できるところまで構築できました。
次の記事(後編)では、今回の Bot を拡張し、 スポットインスタンスを用いた Minecraft サーバーの起動/停止、自動復旧フローまでを解説します。
サーバー側のインフラ構築や Lambda との連携が本格的に始まるので、ぜひ続けてご覧ください。




