AWS  /  APIGateway  /  Lambda  /  Amazon SQS  /  CDK

前編では、実際に構築したマイクラサーバの使い方をご紹介しました。

後編となる本記事では、CDK・Lambdaを使ったインフラの構築方法とデプロイ手順を詳しく解説します。

前編をまだご覧になっていない方は、先にそちらをご確認ください。

目次

▼ Step 1 CDK・Lambda関数の作成 

・ アーキテクチャの概要 

・ 技術スタックと選定理由 

・ CDKでインフラを定義する 

・ ポイント1:app.pyの引数で柔軟にインフラを構築 

・ ポイント2:user_data.shでEC2をセットアップ 

・ ポイント3:管理Webコンソールの設計 

・ ポイント4:Lambda関数によるシングルファイルWebコンソール 

・ ポイント5:その他設定(EC2ロール・セキュリティグループ) 

▼ Step 2 CDKデプロイ 

▼ まとめ 

実装

アーキテクチャの概要

今回作成してみるシステムの全体像は以下の通りです。

ゲームサーバ(EC2): Amazon Linux 2023(ARM64)上に、定番のDockerイメージである `itzg/minecraft-server` をデプロイして運用。

管理コンソール(API Gateway + Lambda): Web UIを提供。EC2の起動・停止、S3経由でのModファイル管理(アップロード/削除)、ホワイトリスト追加・削除リクエストを受け付けます。

※Lambda関数内に記述しているWeb UIのコードは記載していません。

 いい感じにご自身で作ってみてください。

非同期キュー(SQS): ホワイトリストの更新要求を一時的にバッファするキューです(詳細は後述)。

Mod/Plugin管理用ストレージ(S3): 管理コンソールからのMod/Pluginファイル置き場として使用。EC2起動時にスクリプトで同期を行うように設計しています。

技術スタックと選定理由

  •  EC2 (Docker / Amazon Linux 2023)

コストパフォーマンスを重視し、ARM64(Graviton)インスタンス(t4g.largeなど)を採用。user_data.sh 内で Docker および Docker Compose V2(aarch64)をインストールし、itzg/minecraft-server イメージでコンテナ化しています。

◽️ itzg/minecraft-server

https://hub.docker.com/r/itzg/minecraft-server
  •  API Gateway + Lambda

サーバーの起動状態やコスト(CloudWatchのCPUクレジットや稼働時間から概算)、マイクラからのマルチサーバログインに使用するパブリックIPの確認ができるWeb UIを1ファイルのPython Lambda(boto3)で生成して返却する「Single File Web App」構成です。認証キーによる簡易的なアクセス制限も実装しています。

  •  SQS (ホワイトリスト用の非同期バッファ)

Web UIからの「ホワイトリストへの追加/削除」要求を SQS({customer_id}-whitelist-queue)へキューイングし、EC2側のポーリングスクリプトが処理する仕組みです(詳細は後述)。

  •  S3 (Modファイル管理)

Mod/プラグインサーバ運用時の.jarファイル置き場。

②のWebUIからの操作でMod導入等も完結できるように作りました。

EC2の起動時に aws s3 sync によって自動でModフォルダを最新化する仕組みをLambdaに組んで、ターミナルからの操作の手間を省略したりしています。

CDKでインフラを定義する

ポイント1: app.py からの引数を渡して柔軟なインフラを構築する設計

CDK(Python)側では、設定値(マイクラのバージョン、サーバータイプ、メモリサイズなど)を `CfnParameter` ではなく、`app.py` からの引数として直接コンストラクタへ渡す実装 に倒しています。これにより、環境毎のスタック定義がコードベースで明確になり、CloudFormationのToken起因による型エラーやバリデーションエラーを未然に防いでいます。

app.py

# コマンドライン引数 (-c) から設定を読み込む。指定がない場合はデフォルト値。

arg_title = app.node.try_get_context(“title”) or “EC2サーバ”

arg_type  = app.node.try_get_context(“type”)  or “t4g.large”

arg_key   = app.node.try_get_context(“key”)   or “default-key”

arg_ver   = app.node.try_get_context(“version”) or “latest”

arg_stype = app.node.try_get_context(“stype”) or “VANILLA”

arg_mem   = app.node.try_get_context(“mem”) or “3G”

arg_bucket = app.node.try_get_context(“bucket”)

arg_environment = app.node.try_get_context(“environment”) or “env-001”

# 1. EC2スタックの作成

ec2_stack = CdkEc2Stack(app, “CdkEc2Stack”,

    env=env,

    instance_type=arg_type,

    mc_version=arg_ver,

    server_type=arg_stype,

    memory_size=arg_mem, # JVM Memory size (e.g. 1024M, 2G)

    s3_bucket_name=arg_bucket,

    env_id=arg_environment

)

# 2. 管理コンソールの作成

from cdk_ec2.ec2_controller_stack import Ec2ControllerStack

Ec2ControllerStack(app, “Ec2ControllerStack”,

    env=env,

    target_instance_id=ec2_stack.instance.instance_id,

    target_instance_type=arg_type,

    target_server_title=arg_title,

    target_secret_key=arg_key,

    s3_bucket_name=ec2_stack.bucket.bucket_name, # S3バケット名を渡す

    whitelist_queue=ec2_stack.whitelist_queue, # SQSキューを渡す

    env_id=arg_environment

).add_dependency(ec2_stack)

🌟引数例 title: 管理画面や各種リソースに表示される、サーバーの識別用タイトル type: 構築するマイクラサーバー(EC2インスタンス)のインスタンスタイプ key: 管理画面に実装する、アクセスする際の初期認証パスワード version: 導入するマイクラのゲームバージョン (未指定の場合は最新版) stype: サーバーの実行環境の種類(VANILLA や FORGE、FABRIC など。) mem: サーバー(Java)に割り当てる最大メモリ割り当てサイズ bucket: 資産やModファイルを格納・同期するためのS3バケット名 environment: リソース名やS3の配置パスを識別するための、環境の識別子

ポイント2: ユーザーデータを読み込ませてEC2のセットアップを行う

セットアップ用シェル(user_data.sh)をアセット化し、EC2起動時に流し込むようにしています。

Dockerインストールから、必要なディレクトリ作成、SQSキューのポーリングスクリプト、OS起動時にDockerを実行させるためのcrontabなどをあらかじめ仕込むなどしています。

Dockerインストール、セットアップ、マイクラサーバの起動など、依存関係の少ないベース処理は外部ファイルで綺麗に管理します。

cdk_ec2_stack.py

user_data_path = os.path.join(os.path.dirname(__file__), “..”, “user_data.sh”)

with open(user_data_path, “r”, encoding=”utf-8″) as f:

    user_data_content = f.read()

mapping = {

    “${MC_VERSION}”: mc_version,

    “${SERVER_TYPE}”: server_type,

    # 〜略〜

}

for key, value in mapping.items():

    user_data_content = user_data_content.replace(key, value)

self.instance.user_data.add_commands(user_data_content)

◽️ユーザーデータ

user_data.sh

#!/bin/bash

echo “Starting UserData Script…”

# システムアップデートとDockerインストール (Amazon Linux 2023準拠)

dnf update -y

dnf install -y docker cronie

systemctl start docker

systemctl enable docker

systemctl start crond

systemctl enable crond

usermod -aG docker ec2-user

# Docker Compose V2 インストール

mkdir -p /usr/local/lib/docker/cli-plugins

curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-aarch64 -o /usr/local/lib/docker/cli-plugins/docker-compose

chmod +x /usr/local/lib/docker/cli-plugins/docker-compose

# 実行ディレクトリ作成

mkdir -p /home/ec2-user/minecraft/data

mkdir -p /home/ec2-user/minecraft/data/plugins

mkdir -p /home/ec2-user/minecraft/mods

cd /home/ec2-user/minecraft

# docker-compose.yml 生成

cat <<EOF > docker-compose.yml

services:

  mc:

    image: itzg/minecraft-server:latest

    container_name: minecraft-server

    ports:

      – “25565:25565”

    environment:

      EULA: “TRUE”

      VERSION: “${MC_VERSION}”

      TYPE: “${SERVER_TYPE}”

      MEMORY: “${MEMORY_SIZE}”

      ENABLE_WHITELIST: “TRUE”

    volumes:

      – ./data:/data

      – ./mods:/mods

    restart: unless-stopped

EOF

# — 起動時同期スクリプトの作成 —

cat <<‘STARTUP’ > /home/ec2-user/minecraft/sync_and_start.sh

#!/bin/bash

cd /home/ec2-user/minecraft

# S3からModを同期(差分のみ)

aws s3 sync s3://${S3_BUCKET_NAME}/${CUSTOMER_ID}/mods/ ./mods/ –delete –exclude “*” –include “*.jar”

# 権限調整(itzg/minecraft-server用)

# Mod用

chown -R 1000:1000 /home/ec2-user/minecraft/data /home/ec2-user/minecraft/mods

# Plugin用

chown -R 1000:1000 /home/ec2-user/minecraft/data /home/ec2-user/minecraft/data/plugins

# Mod用、Plugin用Dirはいずれか片方でOK、もしくはバニラであれば不要。

# コンテナの起動(すでに動いている場合は最新化を試みる)

/usr/local/lib/docker/cli-plugins/docker-compose up -d

STARTUP

chmod +x /home/ec2-user/minecraft/sync_and_start.sh

# OS起動時に毎回実行されるようにcrontabに登録

(crontab -l 2>/dev/null; echo “@reboot /home/ec2-user/minecraft/sync_and_start.sh”) | crontab –

# 初回実行

bash /home/ec2-user/minecraft/sync_and_start.sh

# コンテナ内のitzg/minecraft-serverはUID 1000で動作するため、ホスト側のディレクトリ権限を合わせる

別のアプローチとして、CDKのコード内にインラインでユーザーコマンドを置いて流し込むこともできます。

SQSをポーリングするスクリプトは、「このCDKスタックで同時に作られるSQSキュー」のURLを絶対に知っている必要があります。外部ファイル側にこれを連携しようとすると置換ロジックが複雑化しますが、インラインであれば f"{self.whitelist_queue.queue_url}" と書くだけでCDK(CloudFormation)が自動的に依存関係を解決して値を解決してくれます。

◽️ cdk_ec2_stack.py内に直接書いてみるパターン

リソースとの依存が強いものは直接書いてしまうほうがトラブル少ない気がする。

self.instance.user_data.add_commands(

    “dnf install -y jq”,

    f”cat << ‘EOF’ > /usr/local/bin/whitelist_poller.sh”,

    “#!/bin/bash”,

    f”QUEUE_URL=\”{self.whitelist_queue.queue_url}\””,

    f”REGION=\”{self.region}\””,

    “echo \”Starting SQS Poller for $QUEUE_URL\””,

    “while true; do”,

    ”  MESSAGES=$(aws sqs receive-message –queue-url \”$QUEUE_URL\” –max-number-of-messages 10 –wait-time-seconds 20 –region \”$REGION\” –output json)”,

    ”  if [ -z \”$MESSAGES\” ] || [ \”$MESSAGES\” == \”null\” ] || ! echo \”$MESSAGES\” | jq -e ‘.Messages | length > 0’ > /dev/null; then”,

    ”    sleep 5; continue”,

    ”  fi”,

    ”  echo \”$MESSAGES\” | jq -c ‘.Messages[]’ | while read -r msg; do”,

    ”    BODY=$(echo \”$msg\” | jq -r ‘.Body’)”,

    ”    HANDLE=$(echo \”$msg\” | jq -r ‘.ReceiptHandle’)”,

    ”    ACTION=$(echo \”$BODY\” | jq -r ‘.action’)”,

    ”    PLAYER=$(echo \”$BODY\” | jq -r ‘.player_id’)”,

    ”    UUID=$(echo \”$BODY\” | jq -r ‘.uuid’)”,

    ”    echo \”Processing action: $ACTION for player: $PLAYER ($UUID)\””,

    ”    if docker ps | grep -q minecraft-server; then”,

    ”      docker exec minecraft-server rcon-cli whitelist \”$ACTION\” \”$PLAYER\””,

    ”      if [ $? -eq 0 ]; then”,

    ”        aws sqs delete-message –queue-url \”$QUEUE_URL\” –receipt-handle \”$HANDLE\” –region \”$REGION\””,

    ”      fi”,

    ”    else”,

    ”      echo \”Minecraft container not running. Skipping…\””,

    ”    fi”,

    ”  done”,

    “done”,

    “EOF”,

    “chmod +x /usr/local/bin/whitelist_poller.sh”,

    “cat << ‘EOF’ > /etc/systemd/system/whitelist-poller.service”,

    “[Unit]”,

    “Description=Minecraft Whitelist SQS Poller”,

    “After=docker.service”,

    “”,

    “[Service]”,

    “ExecStart=/usr/local/bin/whitelist_poller.sh”,

    “Restart=always”,

    “User=root”,

    “”,

    “[Install]”,

    “WantedBy=multi-user.target”,

    “EOF”,

    “systemctl daemon-reload”,

    “systemctl enable whitelist-poller”,

    “systemctl start whitelist-poller”

🌟使い分けのポイント 「環境依存のない共通処理」は外部ファイル、
          「CDKリソースに密結合な処理」はインラインがおすすめ。

ポイント3: 管理Webコンソール(API Gateway + Lambda)の設計

このスタックでは、API GatewayとLambdaを中心として、管理画面の提供、EC2の操作、S3/SQSへのアクセス権限を一元的に管理しています。

特に注目すべき設計のポイントを4つに分けて解説します。

① 認証キーの「永続化と動的変更」を支えるSSMパラメータストア管理画面へのアクセス制限に使用するアクセスキー(パスワード)の管理には、AWS Systems Manager(SSM)の StringParameter を採用しています。 

パスワードの値は、デプロイ時の引数にて挿入しています。


console_stack.py

# パスワード用SSMパラメータ

secret_param = ssm.StringParameter(

    self, “Ec2ControllerS3UploadSecret”,

    parameter_name=f”/{self.stack_name}/secret_key”,

    string_value=final_secret_key,

    description=”Authentication key for EC2 Controller”

)

② 必要最低限に絞った「最小特権の原則(Least Privilege)」によるIAMポリシーLambda関数には、EC2・CloudWatch・S3・SQSのそれぞれに対して、動作に必要なアクションだけをピンポイントで許可しています。 


console_stack.py

# EC2操作用のIAMポリシーを追加

controller_lambda.add_to_role_policy(iam.PolicyStatement(

    actions=[“ec2:DescribeInstances”], resources=[“*”]

))

controller_lambda.add_to_role_policy(iam.PolicyStatement(

    actions=[“ec2:StartInstances”, “ec2:StopInstances”],

    resources=[f”arn:aws:ec2:{self.region}:{self.account}:instance/{final_instance_id}”]

))

③ スタック間の連携をスムーズにする「クロススタック参照」

このスタックのコンストラクタは引数として whitelist_queue(EC2スタック側で作成されたSQSオブジェクト)を受け取れる設計になっています。


console_stack.py

def __init__(self, scope: Construct, construct_id: str, …,

             whitelist_queue=None, …) -> None:

    …

    # 5. SQSへのメッセージ送信権限を追加

    if whitelist_queue:

        whitelist_queue.grant_send_messages(controller_lambda)

④ API Gatewayの「スロットリング(流量制限)」による防御

API Gatewayの作成時には、デプロイオプションで明示的にスロットリング(レート制限)を設定しています。


console_stack.py

# API Gateway (REST API) の作成

api = apigw.LambdaRestApi(

    self, “Ec2ControllerS3UploadApi”,

    handler=controller_lambda,

    proxy=True,

    deploy_options=apigw.StageOptions(

        throttling_rate_limit=5,   # 1秒あたりの平均リクエスト数

        throttling_burst_limit=10  # 同時バースト許容数

    )

)

ポイント4: Lambda関数でシングルファイルWebコンソールを作成

1ファイルで「簡易認証」「コンソール画面描画」「EC2操作」「S3/SQS連携」などさまざまな機能をこなす関数を作成して実装しています。

・画面内の要素

  1. ダッシュボード: インスタンスの状態(ステータス、IP、CPUクレジット、稼働時間、概算コスト)の可視化。
  2. 操作パネル: EC2の起動/停止ボタン。
  3. ホワイトリスト管理: プレイヤーIDの追加/削除機能。
  4. Modファイル管理: S3と連携したファイル一覧表示、アップロード、削除機能。

遊びたい時にサクッと操作することを想定して、スマホからでも操作のしやすいシンプルなUIを目指してみました。

・Lambda設計・実装のポイント

  •  プラグイン/Modのアップロード時にS3署名付きURLを発行させる

Modファイルのアップロード処理において、Lambdaに直接バイナリデータを送信させるのではなく、S3の署名付きURL(一時的なアップロード専用URL)を発行してブラウザから直接S3へPUTさせています。

console.py

# S3の署名付きURLを生成 (PUT用)

s3_key = f”{ENV_ID}/mods/{file_name}”

upload_url = s3.generate_presigned_url(

    ClientMethod=’put_object’,

    Params={

        ‘Bucket’: S3_BUCKET_NAME,

        ‘Key’: s3_key,

        ‘ContentType’: ‘application/java-archive’

    },

    ExpiresIn=300 # 5分間有効

)

🌟ポイント API Gatewayには「リクエストのペイロードサイズは最大10MBまで」という制限があり、重たいModファイル等がこの制限に引っかかることが予想されるので、その回避も兼ねて実装しています。
  •  SQSを挟んだ非同期ホワイトリスト登録

プレイヤーのホワイトリスト登録(追加・削除)を行う際、EC2へ直接コマンドを叩き込むのではなく、一度Amazon SQS(メッセージキュー)にタスクを投げています。

console.py

# SQSにメッセージを送信 (EC2の状態に関わらずキューイング)

sqs.send_message(

    QueueUrl=WHITELIST_QUEUE_URL,

    MessageBody=json.dumps({

        “action”: cmd_type,

        “player_id”: player_id,

        “uuid”: uuid

    })

)

🌟ポイント EC2が停止中でも、ホワイトリストの登録削除を行えるようにしています。 前述のSQSをポーリングするEC2上のスクリプトをユーザーデータでEC2構築時に流し込んでおき、EC2起動後に定期的にポーリング、~/minecraft/data/whitelist.json に書き込みを行うようにしています。
  •  安全な運用を支える「セキュリティファースト」なバリデーション

外部(Mojang API)からUUIDを引く前やSQSにデータを投げる前に、正規表現を用いた入力値チェック(バリデーション)を行っています。

console.py

# 不正な文字が含まれていないかチェック

import re

if not re.fullmatch(r”^[a-zA-Z0-9_]{3,16}$”, player_id):

    return {“statusCode”: 400, “headers”: SECURITY_HEADERS, “body”: “Invalid Minecraft Player ID format.”}

ポイント5: その他設定部分

EC2用ロール、セキュリティグループ等をよしなに設定しておきます。

◽️EC2用ロール

cdk_ec2_stack.py

instance_role = iam.Role(self, “InstanceRole”,

    assumed_by=iam.ServicePrincipal(“ec2.amazonaws.com”),

    managed_policies=[iam.ManagedPolicy.from_aws_managed_policy_name(“AmazonSSMManagedInstanceCore”)]

)

instance_role.add_to_policy(iam.PolicyStatement(

    actions=[“s3:Get*”, “s3:List*”],

    resources=[

        self.bucket.bucket_arn,

        self.bucket.arn_for_objects(“*”)

    ]

)

◽️セキュリティグループ

cdk_ec2_stack.py

sg = ec2.SecurityGroup(self, “MinecraftSG”,

    vpc=vpc,

    description=”Security group for Minecraft Server”,

    allow_all_outbound=True

)

sg.add_ingress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(25565), “Minecraft Port”)

必要なスタックが作成できたら、実際にcdk deploy してみます。

App.pyから渡す任意の引数を指定、もしくはそのままデフォルトの値のままデプロイを行います。

今回は下記内容のサーバをデプロイするので、下記の引数を渡します。

🌟構成 ・Minecraft Java Edition 26.1.2 ・Paperプラグインサーバ ・インスタンスサイズ:r8g.large (バニラサーバであればt4g.medium程度でもOK) ・最大メモリ割り当て:12GiB ・S3バケット名:mc-plugins-bucket ・管理コンソールPW:hogehoge その他はデフォルトの値

Bash

cdk deploy –all -c version=26.1.2 -c stype=PAPER -c type=r8g.large -c mem=12G -c bucket=mc-plugins-bucket -c key=”hogehoge”

完了時に、デプロイ内容を出力するように cfnOutput を各スタックで記述しておくと便利です。

念の為、各リソースがデプロイされているか確認しておくと良いでしょう。

まとめ

このマイクラサーバについては、実際に上記方法でデプロイ、運用をしています。

元々はシンプルにEC2でマイクラをホストして遊んでいるだけでしたが、「いつでも遊べるようにしてほしい」、「たまに荒らしが湧くのでホワイトリストでアクセス制限して欲しい、でもUUIDとかよくわかんない」、「プラグイン入れたり消したりしたいんだけどサーバ管理者にやってもらうのなんか悪いよなぁ・・・」みたいな要望をちくちく潰しながらあれこれ設計してみた形になります。

なんかこれ使えそうだなぁみたいなものがあれば、参考にしていただければ幸いです。

現在は、別途DockerでPrometheusとGrafanaを導入し、リソース、プロセスの監視、ログイン人数の監視、プレイヤー0人の状態で一定期間経過後に鯖の自動停止Lambdaを発火させるなどの追加機能も導入して更なるコスト最適化なども図ったりしています。

次はプレイ中に寝落ちしたらそれを検知してインスタンス停止とか作れたらなど。

みなさんもぜひマイクラon AWSして遊んでみてください。

楽しいよ!

─── この記事に関するお問い合わせ ───

クラウド事業部 | AWS MSP / SES に関するご相談はお気軽に