はじめに
個人開発の対戦ゲームのAWSインフラをすべてTerraformで構築しました。
この記事ではその構成と設計判断をまとめます。
対象のゲーム「The Suitcase Agents」の特徴は次のとおりです。
・個人開発
・開発期間は3か月
・UE5製
・基本プレイ無料
・対戦TPS
・1部屋は最大10人
・同時接続は最大100人を想定
・カジュアルとランクはGameLiftの専用サーバー
・カスタムはSteamのリッスンサーバー
カジュアル、ランク、カスタムの振り分けは前の記事で解説しています。
筆者のインフラ経験は次のとおりです。
・専門学校時代はAzure PlayFabの管理画面から手作業で構築
・IaCを知ったのは就職後
・AWSは今回が初めて
この記事は対戦ゲームのサーバーを手作業で構築している学生や個人開発者に向けて書いています。
なお今のプレイヤー数では専用サーバーのコストが見合いません。
そのため今後はリッスンサーバーだけの方式へ切り替える予定です。
この記事の構成は切り替え前のものです。
構成図
Terraformで管理しているAWSの範囲を1枚にまとめました。
構成は部屋API、GameLift、監視と予算、キルスイッチの4つに分かれます。
部屋API
部屋APIは部屋の検索と作成を受け持つAPIです。
部屋APIはLambda上で動いています。
クライアントはAPI Gateway経由で部屋APIを呼びます。
呼び出しを受けるとまずSteamの認証チケットで本人を確認します。
次にGameLiftに部屋を作ります。
試合が終わると専用サーバーが結果を送ります。
部屋APIはその結果をSteam Web APIでSteamに書き込みます。
結果ごとのIDをDynamoDBに保存しています。
同じ結果が2回届いても二重に数えないためです。
Steamのキーと専用サーバー用のトークンはSecrets Managerに保存しています。
GameLift
専用サーバーはGameLiftのFleetで動かしています。
Fleetはap-northeast-1とus-east-2に1つずつあります。
プレイヤーはpingが小さいほうのFleetにつながります。
部屋APIはFleetを直接指定せずAliasという別名で指定します。
そのためFleetを作り直しても部屋APIの変更は不要です。
Aliasはカジュアル用とランク用の2つです。
プレイヤーが増えたらランク用だけを別のFleetに切り出せます。
新しいビルドはスクリプトでS3にアップロードしてGameLiftに登録します。
その後TerraformでFleetを作り直します。
監視と予算
予算は月250ドルです。
請求額、APIのエラー、Fleetの台数にはCloudWatch Alarmを設定しています。
監視の詳細は別の記事で解説します。
キルスイッチ
AWSには使った分の請求を途中で止める上限がありません。
そのためどこにいてもサーバーを止められるキルスイッチを作りました。
キルスイッチが動く条件は次のとおりです。
・実際の請求が予算の80%にあたる200ドルを超えたとき
・DDoSなどの緊急時にスマートフォンのブラウザからAmazon SNSにメッセージを送ったとき
どちらの場合もAmazon SNS経由で停止用のLambdaが起動します。
停止用のLambdaの処理は次のとおりです。
- 部屋APIが新しい部屋を作らないようにSSM Parameter Storeのフラグを設定
- 4つのAliasからそれぞれのFleetを特定
- 各FleetのDesired、Min、Maxを0に変更
確実に止めるための工夫は次のとおりです。
・EC2の課金が止まるのはFleetを0台にしたとき
・フラグの設定に失敗してもFleetは必ず停止
・停止対象は2つのRegionのカジュアル用とランク用の計4つ
・フラグが立つと部屋APIは部屋の作成を503エラーで拒否
・部屋APIがフラグを読み直す間隔は60秒
・同じSNSトピックに届くCloudWatch Alarmの通知では起動しない判定
・手動で送るメッセージは予算通知と判定される文面
CloudWatch Alarmの通知を除外する判定は2026年7月にAlarmの通知で誤って停止したことを受けて入れました。
この構成にした理由
Region
Regionはap-northeast-1とus-east-2の2つです。
アジア、北米、EUのプレイヤーをカバーできる想定です。
各Regionを選んだ理由は次のとおりです。
・ap-northeast-1は筆者のいる日本のRegion
・ap-northeast-1はテストのイテレーションが最速
・us-east-2は北米をカバー
・us-east-2はEUからも届く範囲と想定
Lambda
クライアントからGameLiftを直接呼ぶとAWSの認証情報をゲームに入れて配ることになります。
ゲームを解析されると認証情報を抜き取られるおそれがあります。
そのためGameLiftの公式も間にバックエンドを挟む構成を推奨しています。
Lambdaは呼ばれたときだけ料金がかかります。
サーバーの管理も不要です。
そのためバックエンドにはLambdaを選びました。
API GatewayとLambdaの設定は次のとおりです。
| 項目 | 設定 |
|---|---|
| API Gateway | HTTP API |
| レート制限 | 毎秒10リクエスト |
| バースト | 25リクエスト |
| Lambdaのランタイム | Python 3.12 |
| Lambdaのメモリ | 512MB |
| Lambdaのタイムアウト | 30秒 |
| 設定の渡し方 | すべてLambdaの環境変数 |
| SteamのPublisher Web API Keyの保存先 | Secrets Manager |
Spot Instances
Fleetはコストを優先してSpot Instancesで動かしています。
Spot InstancesはAWSの空いているサーバーを安く借りる仕組みです。
ただし途中で止められることがあります。
それでもSpot Instancesを選んだ理由は次のとおりです。
・基本プレイ無料で課金要素なし
・途中で止まってもプレイヤーにある程度は許容してもらえると判断
・リリース前はプレイヤー数が不明
実際には中断はあまり起きていません。
台数の見積もりは次のとおりです。
・c5a.large1台あたり12部屋を想定
・1台あたり最大120人
・同時接続100人なら各Regionに1台で足りる計算
スケールと運用の状況は次のとおりです。
・台数を増やせばそのぶん部屋も増える設計
・スケールは台数の追加で対応する想定
・台数の上限を上げるにはAWSサポートへの申請が必要
・申請はまだ未提出
・プレイヤーの急増はない見込み
・現在は各Regionで1台ずつ稼働
Steam
本人確認、カスタムのマッチング、成績の保存はSteamの機能に任せています。
それぞれに対応するSteamの機能は次のとおりです。
・本人確認はSteamの認証チケット
・カスタムのマッチングはSteamのロビー
・成績の保存先はSteamのStats
Statsの項目はカジュアル、ランク、プレイヤーのレベルの3つに分けています。
ランクポイントの計算はすべてバックエンドの部屋APIで行います。
クライアントはポイントを表示するだけです。
ポイントが反映されるまでの流れは次のとおりです。
- 試合後に専用サーバーが順位とキル数を部屋APIへ送信
- 部屋APIがポイントを計算
- 部屋APIが計算結果をSteamに書き込み
ポイントの増減は次のとおりです。
・初期値は1000
・8人戦の1位は+50
・8人戦の8位は-40
・キル1つにつき+3
・人数が少ない試合ほど順位による増減は縮小
プレイヤーが集まらず出番は少なかったです。
それでもランクの仕組みはここまで設計しました。
EAC
チート対策としてEAC(Easy Anti-Cheat)を導入しています。
EACまわりの運用は次のとおりです。
・専用サーバーのFleetでEACを有効化
・EOSはクライアントIDの登録だけに使用
・BANしたプレイヤーはそのSteamアカウントではアクセス不可
すべてTerraformで作った理由
すべてTerraformで作った理由は次の5つです。
・設定内容をコードで記録
・変更の差分をGitで管理
・.tfファイルはAIにとって画面操作より扱いが容易
・構成図からAIがコードを生成
・AWSの全サービスをAIのチャットから操作可能
ファイル構成
Terraformでは構成を.tfファイルに書きます。
terraform applyを実行するとTerraformがその内容どおりの環境をAWSに作ります。
ファイルは次のように役割ごとに分けています。
_backend/
├── terraform_setup/ # 最初に1回だけ作るもの
│ └── iam_gamelift_build_access.tf
├── terraform_stg/ # 本体
│ ├── api_lambda.tf # API Gateway、Lambda、IAM
│ ├── gamelift_jp.tf # ap-northeast-1のGameLift
│ ├── gamelift_na.tf # us-east-2のGameLift
│ ├── data_store.tf # DynamoDB、Secrets Manager、SSM Parameter
│ ├── monitoring_budget.tf # アラーム、予算
│ ├── budget_guardrail.tf # 予算超過時に止めるLambda
│ └── locals.tf / variables.tf / outputs.tf / providers.tf
├── _lambda/ # Lambdaの中身
└── _build_scripts/ # ビルド、アップロード、applyのシェル
├── awssetup.sh # ビルドをS3に上げてGameLiftに登録
└── terraform_stg_ordered_apply.sh # Regionの順にapply
terraform_setupには最初に1回だけ作るものを置いています。
GameLiftがビルドを読むためのIAM Roleなどが該当します。
普段のデプロイではまずawssetup.shでビルドをS3にアップロードします。
次に本体のterraform_stgをapplyします。
terraform_stg_ordered_apply.shはap-northeast-1から順にapplyします。
途中で失敗した場合はそこで処理を止めます。
Regionを増やすときはgamelift_xx.tfを1つ追加するだけです。
Moduleを使わない理由
TerraformのModuleは使っていません。
Moduleは同じ構成を部品として使い回す仕組みです。
しかしこの構成ではメリットを感じられませんでした。
AIにコードを書かせる今なら似たプロジェクトの構成を持ってきて直すほうが速いです。
環境ごとに完結した素直な書き方なら命名規則もそろいます。
そのほうがAIにとっても書きやすいです。
stgだけで運用している理由
環境はstgしか作っていません。
本来は確認用のstgと本番用のprodを分けます。
しかしGameLiftだけで毎月50〜60ドルかかります。
2環境ぶんの費用は個人開発では負担できません。
そのためstgをそのまま本番環境として運用しています。
おわりに
学生時代の筆者はPlayFabの管理画面を頑張って手で操作してインフラを作っていました。
今はすべてTerraformのようなIaCで記述しています。
AI時代の今はゲームの専門学生であれば誰でもサーバー開発ができるようになっていそうです。
学生時代の筆者のように対戦ゲームの公平なサーバーを外部サービスで運用してみたい方の参考になれば嬉しいです。
この構成で動いているゲームはこちらです。
よろしければ遊んでみてください。





