個人開発でApp Storeリリースフローを学ぶ ― UIKit + FastAPI + K8sで作るMinecraftサーバー監視アプリ「MineWatch」
自宅や友人と運営しているMinecraft Java Editionサーバーって、遊びたいときに限って落ちてたりしますよね。気づくのはクライアントを起動してログインしようとした瞬間で、「あ、また落ちてる」で終わる。
この課題を解決するために、iOS(UIKit)+ FastAPI + PostgreSQL + KubernetesでMinecraftサーバー監視アプリ「MineWatch」を個人開発しました。2026-07-19に1.0.0をApp Storeへ提出し、現在審査待ちです。MineWatchの公式サイトはこちらです。
この記事は「アプリの機能紹介」よりも、個人開発でここまでの構成を組んだ理由と、App Storeリリースフローを学ぶために意識した設計判断をまとめたものです。
TL;DR
- 一番の目的はApp Storeのリリースフローを学ぶこと。実装の正しさと同じくらい「なぜそうしたか」を残すことを重視しました。
- 監視はServer List Ping(SLP)――Minecraftクライアントがサーバー一覧画面で使うのと同じ公開プロトコル――で行うため、監視対象のサーバー側は一切変更不要です。
- APIプロセスと監視の実行を分離(
worker/beat)。API/Webが落ちても、既にキューに積まれた監視タスクは実行され続けます。 - プッシュ通知はFCMを経由せずAPNsへ直接送信。iOS側にFirebaseのpush SDKを持たせずに済みます。
- ブランチ運用はgit-flow。PRはGitHub Copilot CLIが自動レビューし、
VERDICTをトリガーにGitHub Actionsが自動マージ/人手レビュー振り分けを行います。
アプリが解決する課題
MineWatchはSLPでサーバーの状態を外側から取得します。SLPはMinecraftクライアントのマルチプレイ画面が使っているのと同じ公開プロトコルなので、監視対象のサーバーにプラグイン導入や設定変更は一切要りません。取得できる情報は以下の通りです。
- オンライン / オフラインの状態
- 現在の接続人数・最大人数
- 応答時間(レイテンシ)・バージョン・MOTD
- 人数推移のグラフ、稼働率
- ダウン時のプッシュ通知(APNs直接送信)
全体構成
ポイントは2つです。
-
監視の実行はAPIプロセスと分離しています。
beatが周期タスクをRedisに積み、workerが実行してダウンを検知したらAPNsへ直接プッシュします。APIが落ちても、既にキューにあるタスクは実行され続けます。 - 通知はFCMを経由しません。APNsへ直接送ることで、iOS側にFirebaseのpush SDKを持たせずに済みます。
技術スタック
| レイヤ | 技術 |
|---|---|
| iOS | UIKit(Storyboard/XIBなし)、MVVM、@Observable、Swift 6 strict concurrency、XcodeGen + SPM |
| API | FastAPI(Python 3.13 / uv)、SQLModel + Alembic |
| 非同期処理 | Celery(worker / beat)+ Redis |
| DB | PostgreSQL 17 |
| 認証 | Firebase Authentication(Apple / Googleサインイン。バックエンドはIDトークンを検証) |
| プッシュ通知 | APNs直接送信(.p8キー。FCM不使用) |
| コンテナ / オーケストレーション | Docker、Kubernetes(F5 NGINX Ingress Controller) |
| GitOps | ArgoCD |
| CI/CD | Jenkins(Macノード + k8s Pod agent)、GitHub Actions |
| エッジ / ネットワーク | Cloudflare(DNS / Tunnel / Access) |
個人開発でここまでのインフラを組んだのは趣味の延長という面もありますが、App Store審査を安定して通すには「動くバックエンドを継続的に維持する仕組み」が要るという気づきが大きいです。審査中にAPIが落ちて再現できない、といった事故を避けたかったので、監視の実行系だけは可用性を切り離しています。
API設計方針
モバイルアプリは全ユーザーが一斉に更新してくれるわけではないので、バックエンドのAPI設計は「後方互換性をどう守るか」を軸に決めました。
-
ディレクトリ=バージョン:
app/api/<version>/<domain>/router.pyという構造にして、起動時にdiscover_routers()が自動登録します。ディレクトリを切ってrouter.pyを置くだけでルートが増える仕組みです。 -
破壊的変更はv1を凍結してv2を新設します。ロジックは
services/に集約し、routerはバージョンごとの薄いアダプタに留めることで、二重実装を避けています。 - DBの型変更はExpand-Contract(新カラム追加 → 移行 → 切替 → 削除)で行います。旧コードと新コードが同じDBを同時に触る時間帯が必ず発生するため、常に両対応できる設計にしています。
-
全レスポンスはオブジェクトで包む方針です。一覧を配列直返ししない(
{"servers": [...], "total": n}の形)ことで、後からメタ情報を足せる余地を残しています。
servers.hostはFernet鍵で暗号化して保存しています。鍵をローテーションする場合はカンマ区切りで新旧を並べ、無停止で切り替えられる設計です。
ブランチ運用とCI/CD
1.0.0のリリースに合わせてgit-flowへ移行しました。
- develop: フィーチャーPRのbase。ArgoCDがstagingへ自動sync。
- release/x.y.z: developから分岐し、リリース資材(安定化・本番overlayのタグ確定)だけを行う。
- main: 本番。release/hotfixのマージだけを受け付け、マージ後にタグを打つ。
PRレビューはGitHub Copilot CLIが自動実行します。JenkinsがPRごとにpytest・buildの検証を回し、CopilotのVERDICTコメントを起点にGitHub Actionsが挙動を振り分けます。
-
VERDICT: APPROVEかつ base ≠ main → squashマージ + ブランチ削除 -
VERDICT: APPROVEかつ base = main → 手動マージ手順を通知(git-flowのリリースは人が行う) -
VERDICT: REQUEST_CHANGES→needs-human-reviewラベル
ここは意図的にClaudeによる自動修正はしない設計にしています。AIレビュー→AI修正→AIレビュー…のループを作らないためです。
セキュリティ・プライバシー
- 認証: Firebaseが発行するIDトークンをバックエンドが検証。
-
暗号化保存: サーバーの接続先
hostはFernetで暗号化。鍵はk8s Secretのみに存在します。 -
内部APIの遮断:
/internal/*はnginxが外部公開経路から遮断し、クラスタ内部からのみ到達可能にしています。 - App Privacy: 収集するのはメールアドレス・ユーザーID(アカウント管理)・利用状況・クラッシュデータのみ。トラッキング(IDFA)は不使用です。
おわりに
MineWatchを作りながら一番学びになったのは、「アプリを作る」ことよりも「アプリを継続的にリリースし続けられる状態を作る」ことの重みでした。git-flow・Expand-Contract・APIバージョニングといった判断は、どれも「破壊的変更を一斉更新できないモバイルアプリでどう安全に出し続けるか」という一点から逆算しています。
審査が通ったら、審査で指摘された点や実際の提出フローの詳細も別記事にまとめる予定です。
後日談:審査を通過し、App Storeで公開されました
その後、リジェクトを経て3回目の提出で無事に審査を通過し、MineWatchはApp Storeで公開されました。
何を指摘されてどう直したかは、続編の記事にまとめています。
本文中の「現在審査待ちです」は、執筆時点の記述のまま残しています。
JQITのエンジニアの95%以上は未経験からの採用です。
よければコーポレートサイトにも遊びに来てください。
未経験から学べます!一緒に挑戦していきましょう![]()
noteやXもやってます↓
