
AWS主催フットサル大会の大会管理アプリをサーバーレス構成で作って本番運用してみた!
こんにちは、リテールアプリ共創部の戸田駿太です。
クラスメソッド社内のフットサル部で AWS主催 APN パートナーカップ(AWS の パートナー企業が集まってフットサルで親睦を深めるイベント)に参加させていただきました。

毎年 Google スプレッドシートやExcelなどでタイムテーブルと試合結果を管理していたのですが、今年は「せっかくなら大会専用のWebアプリを作ってみよう」ということで、Kick Summit App という運営・観戦支援アプリを Claude Code で作って当日の運用に使ってみました。
コードはこちらのGitHubリポジトリで公開しています。
この記事では、なぜ作ったのか・どんな構成にしたのか・Claude Code でどう作ったか・実際に当日使ってみてどうだったかをまとめます。
作ったもの
スマホから開く前提のWebアプリです。タイムテーブル・順位表・スコア入力などを1つにまとめました。

| 画面 | 用途 |
|---|---|
| ホーム | 自チームの次の試合・直近結果・進行中の試合 |
| タイムテーブル | 試合一覧 |
| リーグ表 | グループ別の順位・勝点・得失点差 |
| スコア入力 | 出場チーム / 審判チーム / 管理者のみ入力可 |
特徴: 最初に「自分のチーム」を選ぶことでUIがパーソナライズされる
このアプリで一番こだわったのが、初回アクセス時に自分のチームを選んでもらう仕様です。一度選んでおくとその端末では選択が保持されて、以降は 自分のチーム視点に最適化されたUI が表示されます。
例えばホーム画面では、
- 自チームの次の試合が何時から・どのコートで・誰と対戦するか
- 自チームが審判担当の試合がいつあるか
- 自チームの現在の順位・勝点・得失点差
がひと目で分かります。スプレッドシートで全試合一覧を眺めていたときは、自分のチームに関係する試合だけ抜き出すのが地味に大変で、「次の試合いつだっけ?」「次の審判いつだっけ?」が毎回発生していました。チーム選択を起点に表示を絞ることで、その手間がほぼゼロになりました。
特徴: チーム選択をスコア入力の権限管理にも活用
さらに、選んだチーム情報を スコア入力の権限管理 にも流用しています。簡易的な仕組みではありますが、以下のいずれかに該当する場合だけスコアを入力できる仕様にしました。
- 試合に出場している当該チームのメンバー
- その試合の審判担当チームのメンバー
- 管理者(パスワードログイン)
従来は運営が1人で打ち込んでいたところを「気づいた人がどんどん入力する」運用にしたかった、というのが狙いです。チーム選択をそのまま権限判定に流用することで、ログイン機構を作り込まずに権限制御が成り立っています。
画面ごとの機能
各画面でできることを順に紹介します。
チーム選択画面

- 初回アクセス時に表示される、自分のチームを選ぶ画面です
- 選んだチームは端末(localStorage)に保存され、次回以降は自動でこのチーム視点のUIが表示されます
- 後からヘッダーのチーム名をタップして、いつでも選び直せます
ホーム画面

選択中のチームに合わせて、自分のチームに関係する情報だけをひとまとめに表示します。
- 次の試合: 何時から・どのコートで・誰と対戦するか
- 次の審判担当: いつ・どの試合の審判か
- 進行中の試合: 今コートで行われている試合のリアルタイムスコア
- 直近の結果: 自チームの直近の試合結果
- 現在の順位: グループ内での順位・勝点・得失点差
30秒ごとに自動更新されるので、スマホを開きっぱなしにしておけば最新状態が見られます。
タイムテーブル画面

- 全試合を時間順に一覧表示します
- 試合ごとに 対戦カード / コート / 開始時刻 / スコア / ステータス(予定 / 進行中 / 完了)が並びます
- 自分のチームが関係する試合(出場 or 審判)はハイライトされるので、スクロール中に見つけやすいです
- 試合カードをタップするとスコア入力ダイアログが開きます(権限がある場合のみ)
リーグ表画面

- グループごとの順位表をタブ切り替えで表示します
- 表示項目: 順位 / チーム名 / 試合数 / 勝 / 分 / 負 / 得点 / 失点 / 得失点差 / 勝点
- 試合結果が確定(試合完了トグルON)されたタイミングで自動的に順位が再計算されます
- 自分のチームの行はハイライト表示
スコア入力ダイアログ

タイムテーブルやホームの試合カードをタップすると開きます。
- 前半 / 後半それぞれのスコアを入力
- 試合完了トグル をONにすると、順位計算とトーナメント反映が走ります
- 試合中の途中経過を更新する場合はOFFのままにしておきます
- 入力できるのは以下の人だけ(権限がない人がタップしてもダイアログ自体が開きません)
- 出場している当該チームのメンバー
- その試合の審判担当チームのメンバー
- 管理者(パスワードログイン)
大会説明画面

- 当日の運用ルール・タイムスケジュール・スコア入力の手順を Markdown でまとめた静的ページです
- 初参加の人や、当日になって運用を思い出したい人のためのリファレンス置き場として用意しました
その他 / 管理者ログイン画面

- 大会説明ページへのリンク
- 管理者ログイン(パスワード入力)
- 管理者でログインすると、全ての試合でスコア入力が可能になります(運営用)
管理者画面の機能
管理者ログイン後にアクセスできる、運営用の機能群です。画像は割愛してテキストで紹介します。
- 大会基本情報の編集 — 大会名 / 開催日 / 管理者パスワードの変更
- チーム管理 — チームの追加・編集・削除、チーム名・チームカラー(HEX)の設定
- グループ管理 — 予選リーグのグループ作成・編集、各グループへのチーム割り当て
- 試合管理 — 試合の追加・編集・削除、開始時刻・コート・対戦カード・審判担当チームの設定
- 全試合スコア入力 — 出場チームや審判チームでなくても、全ての試合のスコアを入力・修正できる
- 試合ステータスの強制操作 — 進行中 / 完了 の切り替えを手動で行える(誤入力の巻き戻しなど)
- シードデータ投入 — 開発・検証用にサンプルデータを一括投入
当日の運用では、大会開始前のデータ仕込み(チーム・グループ・試合の登録)と、参加者の入力ミスを修正するときくらいしか使わない想定ですが、「困ったときの最後の砦」として一通りの管理機能を揃えています。
なぜ作ったのか
毎年フットサル大会のたびに、運営と参加者の両方で以下のような課題を感じていました。
- タイムテーブルが紙とスプレッドシートの二重管理になり、当日の進行状況がリアルタイムにわからない
- 試合結果の集計・順位計算・トーナメント反映が運営の手作業
- 観戦している人が「次の自分の試合いつ?」を対戦表の画像やスプレッドシートで確認するのが見づらい
これらをまとめて解決できるWebアプリがあれば、運営の負荷も下がるし、参加者の体験も良くなりそうだなと考えたのが出発点です。
加えて、技術的なモチベーションとして以下も試したいと思っていました。
- Next.js 16 (App Router) + Server Actions の本格活用
- AWS Lambda 上で Next.js を SSR で動かす構成(Lambda Web Adapter) の検証
- Lambda + DynamoDB の組み合わせで低料金に運用する構成 の検証
業務でいきなり使う前に、身近なドメインで素振りしておきたい、という気持ちもありました。
アーキテクチャ
技術スタック
| レイヤー | 採用技術 |
|---|---|
| フレームワーク | Next.js 16 (App Router, Turbopack) |
| ランタイム | React 19, TypeScript 5.7 |
| スタイリング | Tailwind CSS v4, shadcn/ui |
| バリデーション | Zod |
| インフラ | AWS CDK v2 (TypeScript) |
| DB | Amazon DynamoDB(マルチテーブル + GSI) |
| コンピュート | AWS Lambda (Docker / ARM64) + Lambda Web Adapter |
| API | Amazon API Gateway (HTTP API) |
| CDN | Amazon CloudFront |
| パッケージ管理 | pnpm 10 + Turborepo 2.8 |
| ローカルDB | DynamoDB Local (Docker Compose) |
モノレポ構成
kick-summit-app/
├── packages/
│ ├── web/ # Next.js アプリケーション
│ ├── infra/ # AWS CDK インフラ定義
│ └── shared/ # 共有型定義
├── scripts/ # シード・セットアップスクリプト
└── docker-compose.yml
pnpm workspace + Turborepo の最小構成です。shared には web と infra の両方から参照する型だけを置いて、相互依存を最小化しました。
全体構成
Next.js を Docker イメージで Lambda に乗せ、Lambda Web Adapter で HTTP リクエストをそのまま Node サーバに流す構成です。ECS や App Runner を使わずに済むので、イベントの数日・数時間しか動かさない用途にはかなり相性が良かったです。
フロントエンド — Server Component 中心
App Router の Server Component を全面採用しています。
ページ (Server Component)
├── サーバーでデータ取得 (async/await)
├── 静的な表示部分はサーバーでレンダリング
└── インタラクティブな部分のみ Client Component に分離
Refresher (Client Component)
└── 30秒ごとに router.refresh() で再レンダリング
WebSocket / SSE は使わず、30秒ポーリング + router.refresh() でサーバーコンポーネントを再実行するだけのシンプルな実装にしました。試合の更新頻度を考えれば十分ですし、Lambda の請求もポーリング頻度でコントロールしやすいです。
バックエンド — DDD + Server Actions
依存関係はクリーンアーキテクチャに倣って 内向き に揃えています。
各層の実体は以下の通りです。
| 層 | パス | 役割 |
|---|---|---|
| 外殻(入口) | src/lib/actions/<feature>.ts |
Server Actions。ブラウザからの入口 |
| 外殻(出口) | src/server/infrastructure/repositories/ |
DynamoDB へのアクセス |
| Usecase | src/server/usecase/<feature>/ |
業務ロジック |
| Domain | src/server/domain/ |
エンティティ / ドメインサービス / リポジトリIF |
Server Actions と Infrastructure は同じ「外殻」に属していて、片方はブラウザとの接点(入口)、もう片方は DynamoDB との接点(出口)です。どちらも内側(Usecase / Domain)に依存することはあっても、内側から外殻が参照されることはありません。
この同心円構造のメリットは以下の通りです。
-
DynamoDB を業務ロジックから隠す — Usecase はリポジトリ IF しか触らないので、後で DB を差し替えても Usecase は書き換え不要
-
テストで In-Memory リポジトリに差し替えられる — DI コンテナ (
src/server/container.ts) でリポジトリ実装を差し込んでいるので、テスト時は In-Memory 版を差し込めば DynamoDB Local なしで Usecase を単体テストできる -
ブラウザ側のプロトコル(Server Actions / REST / GraphQL …)も差し替え可能 — 入口側も外殻なので、Usecase に手を入れずに置き換えられる
-
API Routes は使わず、すべて Server Actions 経由
-
Zod バリデーション を「ドメイン層スキーマ」「DB応答 parse」「Server Actions 入力検証」の3箇所で実施
DynamoDB はマルチテーブル + Composite Key GSI
DynamoDB は マルチテーブル + Composite Key の GSI で設計しました。テーブルは tournaments / groups / teams / matches の4つです。
シングルテーブル設計は採用しませんでした。今回のように
- アクセスパターンが多様(時間順 / グループ別 / ステータス別 / 試合単体 / 全チーム…)
- 個人開発で長期的にメンテする
という条件だと、エンティティごとにテーブルを分けたほうが認知負荷が低く、新しいアクセスパターンが追加されたときの設計変更も局所化できる、と判断しました。
matches テーブルには以下の GSI を置いて、ほとんどの画面が 1クエリでデータを取れる ようにしています。
| GSI | PK | SK | 用途 |
|---|---|---|---|
schedule-index |
tournamentId |
scheduledTime |
タイムテーブル |
status-index |
[tournamentId, status] |
scheduledTime |
進行中の試合 |
group-index |
groupId |
scheduledTime |
グループ内試合 |
Composite Key を [A, B] の形にすることで「大会×ステータス」のような複合条件のクエリも素直に書けます。
Claude Code でどう作ったか
このアプリの コード自体はほぼ全て Claude Code が書いています。一方で、アーキテクチャ・ディレクトリ構成・技術選定といった「設計の骨組み」は自分で決めています。役割分担を明確にすると以下のような形です。
| 担当 | 内容 |
|---|---|
| 自分 | 技術スタック選定、インフラ構成、レイヤー設計、ディレクトリ構成、DBスキーマ設計、運用ルール |
| Claude Code | 上記方針に沿った実装、テスト、リファクタ、ドキュメント生成 |
自分で決めた「骨組み」の部分
具体的には、以下のような判断は自分で先に決めてから Claude Code に渡しました。
インフラ構成
CloudFront → API Gateway (HTTP API) → Lambda (Lambda Web Adapter) → DynamoDB
ECS / App Runner ではなく Lambda + Lambda Web Adapter にしたのは、イベントの数日間しか動かさない用途で scale-to-zero できる構成にしたかった からです。Lambda Web Adapter にすることで Next.js のコードを Lambda 用に書き換える必要がなく、ローカルと同じコードがそのまま動きます。
バックエンド / フロントエンドの構成
- クリーンアーキテクチャを意識した4層構成(Server Actions / Application / Domain / Infrastructure)
- REST API は作らず、Server Actions ベース で全ての書き込みを集約
- Zod でドメイン層・インフラ層・アプリ層の3箇所で検証
- App Router の Server Component を全面採用 し、Client Component は必要箇所だけ
- UI は shadcn/ui (new-york) をベースにして、Tailwind v4 でカスタマイズ
「Server Actions だけにする」「リポジトリパターンで DynamoDB を切り離す」「shadcn/ui を使う」のような アーキテクチャの選択 は、Claude Code に任せると標準的すぎる構成(API Routes + fetch + 普通の Tailwind)が出てくるので、ここは自分で決めてしまったほうが早かったです。
ディレクトリ構成
packages/web/src/features/<feature>/ 配下に domain / application / infrastructure / components を置く feature-based 構成も先に決めました。これも放っておくと components/ lib/ utils/ のような type-based 構成になりがちなので、最初に固めておくのが重要です。
Claude Code に「方針」を渡す仕組み
決めた骨組みは、Claude Code が自動で読んでくれるファイルに書き出しておきます。
docs/architecture.md— 技術スタック・ページ構成・レイヤー構成DB設計.md— テーブル / GSI / アクセスパターンの一覧- 各ディレクトリの
CLAUDE.md— そのディレクトリで守ってほしい方針
CLAUDE.md は Claude Code が会話冒頭で自動的に読み込んでくれるファイルで、ここに「Server Actions 経由でしか書き込みしない」「Zod を domain / infra / application の3層で使う」のような方針を書いておくと、後の実装依頼で毎回プロンプトに書き直さなくて済みます。
ハマりどころは Claude Code に「再構築させる」
開発中に「ビジネスロジックを全部カスタム Hooks に集約する」設計を試してみたのですが、React 公式のガイドラインに照らすと過剰な抽象化になっていました。
このとき、自分で1ファイルずつ書き直すのではなく、Claude Code に 「React 公式のカスタムフック作成基準を調べて、現状を再評価して、hooks/ ディレクトリを削除する形で再構築して」 と依頼しました。Web 検索 → 既存コードの全体把握 → 差分作成までを1セッションでやってくれて、半日で再構築が完了しました。
人間がやると「途中で諦めて部分修正で済ませる」になりがちな大きめのリファクタも、エージェントなら淡々と最後までやり切ってくれます。
体感
ざっくりですが、
- コードの 9割以上は Claude Code が書いた
- 一方で アーキテクチャ・ディレクトリ構成・技術選定は自分で決めた
- 自分の作業は「設計の方向付け」「PRレビュー相当のチェック」「動かない部分のデバッグ補助」が中心
- 1人で書いていたら週末3〜4回はかかったボリュームが、週末1〜2回で完成した
という感覚です。「設計は人間、実装は Claude Code」という分担にすると、設計の意図がブレずに最後まで通せる のが一番のメリットでした。逆に設計まで Claude Code に丸投げすると、無難ではあるけど面白みのない構成になりがちです。
当日使ってみてどうだったか
良かったこと
スコア入力の分担運用がきれいにハマった
スコア入力を出場チーム・審判チームに開放したことで、運営は全体の進行を見ているだけで良くなりました。例年だと運営が結果集計でずっとスマホを握っていたのですが、今年は普通に観戦・参加できました。
「次の試合いつ?」が画面で完結
ホーム画面に「自チームの次の試合」「直近の結果」「進行中の試合」を出すようにしたところ、参加者から「○○の試合何時から?」のような口頭の確認がほぼ来なくなりました。スプレッドシートを開かなくていいだけで体感がだいぶ違います。
Lambda Web Adapter は意外と素直
心配していたコールドスタートも、ARM64 / 1024MB の組み合わせで体感1〜2秒程度でした。常時起動が不要な用途なら十分使えます。
コストも 1日 0.2 ドルで収まった
大会当日の AWS 利用料は 1日あたり約 0.2 ドル(おおよそ 30 円)で済みました。Lambda + DynamoDB のオンデマンド課金 + CloudFront の組み合わせで、リクエストがあった分しか課金されないので、こういう「数日だけ動かすイベント用アプリ」にはまさに最適な構成だなと再認識しました。
反省点
30秒ポーリングは「ちょうど良い」けど少しラグい
得点直後の盛り上がりで「もう更新された?」と画面をスワイプする人が一定数いました。次回は SSE か API Gateway WebSocket API を検討したいです。ただ Lambda 上で常時接続を維持する設計はそれはそれで重いので、用途次第かなとも思います。
アナウンス機能があるといいと思った
大会中に試合の試合進行がちょっとずつ遅れていき最大2分ほどのズレが発生していました。
その場合に大会運営側からのアナウンスを表示できるとユーザーが試合の進行を把握しやすくなると思いました。

大変だったこと
大会本番の進行を背負うプレッシャー
技術的な大変さよりも、「このアプリが動かないと大会の試合進行や点数計算ができなくなる」 という責任感がいちばん重かったです。スプレッドシート併用ではなく完全にアプリへ寄せる前提だったので、当日アプリが落ちたら大会自体が止まる、という状況でした。
結果的にはアプリが動かなくなることもなく、最後の結果発表まで無事に終えられたのでとても良かったです。
まとめ
身近な「困りごと」を Next.js 16 + AWS Lambda + DynamoDB で素直に作ってみました。
実際に作って当日運用してみて感じたのは、Lambda Web Adapter を使った Lambda + App Router の Server Component + Server Actions が思った以上に書き味が良いということでした。この大会のように「短期間だけ動かす内製ツール」との相性は良く、開発環境の構築が簡単で料金が抑えられているので、このような用途にはまさに最適な構成だなと再認識しました。
そして何より、「アーキテクチャ・ディレクトリ構成・技術選定は自分で決めて、コードは Claude Code に書いてもらう」 という分担がうまくハマりました。設計の意図を自分で握ったまま、実装スピードだけ底上げできる感覚で、個人開発でここまで作り込めたのは Claude Code がいなければ難しかったと思います。
「次のフットサル大会、自前アプリで運営してみたい」という方の参考になれば幸いです。
参考







