Week:02|1人開発でFirebase prod/devを1日で立てる。ドキュメント駆動×AI協業
結論:ドキュメントを先に書けば、インフラは1日で立つ
Firebase の本番・開発2環境(Firestore / Storage / Cloud Functions / Hosting / Authentication)を1日で構築した。ゼロからだ。
なぜ1日で終わったか。先に22セクションの手順書を書いたからだ。設定値、作業順序、判断基準を全て文書化してから構築に入ったので、作業中に「次は何をやるんだっけ」「この値は何を入れるんだっけ」と迷う時間がほぼなかった。
さらに Claude Code(AI エージェント)と協業することで、セキュリティルールの変換、初期データ投入スクリプトの作成、環境分岐コードの生成を並行して進められた。AI が詰まっても自己解決してくれるので、人間は Console 操作と判断に集中できる。
この記事では、実際に構築した環境の全体像と、構築中に得た実践的な知見(PITR の即時有効化、gcloud CLI なしの REST API フォールバック、セキュリティルールの設計書変換、シークレット管理体制)を紹介する。
構築した環境の全体像
プロジェクト構成
Firebase
├── myapp(本番) Blaze プラン
└── myapp-dev(開発) Blaze プラン
リージョン: asia-northeast1(東京)— 全サービス統一
有効化したサービス
| サービス | 用途 | 備考 |
|---|---|---|
| Authentication | メール/Google/Apple/X ログイン | Apple・X は後日設定 |
| Cloud Firestore | メインDB | Standard Edition |
| Cloud Storage | 画像ファイル | |
| Cloud Functions | サーバーサイドロジック(37トリガー) | TypeScript |
| Firebase Hosting | 利用規約・プライバシーポリシー | 静的ページ |
| Crashlytics | クラッシュ監視 | SDK 組込後に有効化 |
| Analytics | 利用分析 |
全サービスを prod / dev 両方に同一構成で構築した。 開発環境だけ構成が違う、という状態は避ける。本番と同じ環境でテストしないと意味がない。
アプローチ:手順書を先に書く
Firebase Console を開く前に、22セクションの手順書(infrastructure-guide.md)を作った。
目次:
1. 前提条件
2. Firebase プロジェクト作成
3. Firebase サービス有効化
4. 認証プロバイダ設定
5. Firestore 設定
6. Cloud Storage 設定
7. Cloud Functions 設定
8. Firebase Cloud Messaging 設定
9. Crashlytics 設定
10. Firebase Analytics 設定
11. Firebase Hosting 設定
12. App Check 設定
13. RevenueCat 設定
14. Flutter プロジェクト作成
15. FlutterFire CLI 接続
16. 環境分離の設定
17. デプロイスクリプト作成
18. シークレット・機密情報管理
19. Google Spreadsheet 連携
20. データバックアップ
21. App Store / Google Play 準備
22. 構築完了チェックリスト
各セクションには設定値をテーブルで記載し、コマンドもコピペできる状態にしてある。これにより:
- 判断に迷わない: 値が全て決まっているので、Console で入力するだけ
- 中断しても復帰できる: チェックリストで「どこまでやったか」が明確
- 人間と AI の役割分担が明確: 人間は Console 操作、AI はスクリプト生成とファイル変換
手順書は AI に書かせた。ER 図・仕様書・画面定義書が先週の時点で完成していたので、「これらの設計書をもとにインフラ構築手順書を書いて」と指示するだけで、必要なサービス、設定値、作業順序が自動的に導出された。
環境分離:prod / dev の構成
.firebaserc
{
"projects": {
"prod": "myapp",
"dev": "myapp-dev"
}
}
firebase use dev / firebase use prod でプロジェクトを切り替える。デプロイは常にどちらの環境かを明示する。
firebase.json
{
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"storage": {
"rules": "storage.rules"
},
"hosting": {
"public": "hosting",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
},
"functions": [
{
"source": "functions",
"codebase": "default",
"predeploy": ["npm --prefix \"$RESOURCE_DIR\" run build"]
}
]
}
ルールファイル、Hosting ディレクトリ、Functions ソースを一箇所で管理する。firebase deploy はこの設定に従って動く。
落とし穴:firebase.json がないと firebase use が動かない
firebase use --add を実行した際、「Firebase project directory ではない」と拒否された。AI は .firebaserc だけで動くと想定していたが、firebase.json がプロジェクトルートに存在しないと Firebase CLI はそのディレクトリを Firebase プロジェクトとして認識しない。
解決: 先に firebase.json を手動作成(最小構成でいい)してから firebase use --add を実行する。
デプロイスクリプト
# scripts/deploy_dev.sh
#!/bin/bash
set -e
echo "開発環境(myapp-dev)にデプロイします"
firebase use dev
firebase deploy --only firestore:rules,storage,hosting,functions
echo "デプロイ完了: myapp-dev"
# scripts/deploy_prod.sh
#!/bin/bash
set -e
echo "本番環境(myapp)にデプロイします"
echo "続行しますか? (yes/no)"
read CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "デプロイを中止しました"
exit 1
fi
firebase use prod
firebase deploy --only firestore:rules,storage,hosting,functions
echo "デプロイ完了: myapp"
本番デプロイには確認プロンプトを入れている。set -e で途中のエラーは即停止。
セキュリティルール:設計書から実ファイルへの変換
Firestore Rules(約200行)
ER 図に定義した全コレクション・フィールドの CRUD 権限を、Firestore セキュリティルールに変換した。変換は AI に任せたが、設計書の権限定義と1:1で対応するように指示した。
設計:
users コレクション:
- read: 認証済みユーザー
- create: 自分自身のみ、必須フィールドチェック、カウント初期値
- update: 自分自身のみ、stats・entitlement は CF 経由のため変更不可
- delete: 自分自身のみ
変換後のルール:
match /users/{userId} {
allow read: if isAuthenticated();
allow create: if isAuthenticated()
&& isOwner(userId)
&& request.resource.data.keys().hasAll([
'profile', 'auth', 'primaryCategories',
'timestamps', 'entitlement', 'stats'
])
&& request.resource.data.entitlement == 'free'
&& request.resource.data.stats.itemCount == 0
&& request.resource.data.stats.followerCount == 0
&& request.resource.data.stats.followingCount == 0;
allow update: if isAuthenticated()
&& isOwner(userId)
&& !request.resource.data.diff(resource.data)
.affectedKeys()
.hasAny(['stats', 'entitlement', 'subscription']);
allow delete: if isAuthenticated() && isOwner(userId);
}
ポイント:
-
ヘルパー関数を先に定義:
isAuthenticated(),isOwner(),isBlockedBy(),isItemOwner()等。ルール本体の可読性が上がる -
diff().affectedKeys().hasAny()で CF 管理フィールドの書き換えを防止。クライアントから stats や entitlement を直接書き換えられない -
keys().hasAll()で必須フィールドの存在を保証。不完全なドキュメントの作成を防ぐ -
カウント初期値チェック:
itemCount == 0,followerCount == 0を create 時に強制。クライアントが不正な初期値を送れない
Storage Rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
function isAuthenticated() {
return request.auth != null;
}
match /users/{userId}/{allPaths=**} {
allow read: if isAuthenticated();
allow write: if isAuthenticated()
&& request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
match /chapters/{chapterId}/{allPaths=**} {
allow read: if isAuthenticated();
allow write: if isAuthenticated()
&& request.resource.size < 10 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
match /posts/{postId}/{allPaths=**} {
allow read: if isAuthenticated();
allow write: if isAuthenticated()
&& request.resource.size < 10 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
}
}
- ファイルサイズ制限: アバター 5MB、その他 10MB
-
Content-Type 制限:
image/.*のみ。動画や実行ファイルのアップロードを防止 -
アバターは本人のみ書き込み可:
request.auth.uid == userId
デプロイ
firebase use dev && firebase deploy --only firestore:rules,storage
firebase use prod && firebase deploy --only firestore:rules,storage
ルールファイルは prod / dev 共通。同じファイルを両環境にデプロイする。
PITR:初日に有効化すべき理由
PITR(Point-in-Time Recovery)は Firestore の組み込み機能で、過去7日間の任意の時点にデータを復元できる。
有効化方法
Firebase Console → Firestore Database → 設定 → Point-in-time Recovery → 「有効にする」
以上。スクリプト不要、ダウンタイムなし、稼働中のデータベースに即時有効化できる。
なぜ初日に有効化するのか
| 理由 | 詳細 |
|---|---|
| 追加コストが微小 | ストレージ量に応じた課金。開発初期はデータが少ないため月額数円〜数十円 |
| 有効化前のデータは復元不可 | PITR は有効化後のデータのみ対象。後から有効化しても、有効化前に消えたデータは戻せない |
| 開発中こそ事故が起きる | テスト中の誤操作、バグによるデータ破壊は開発フェーズで頻発する |
「リリース前に有効化すればいい」は間違い。 開発中のテストデータが消えても、PITR があれば復元できる。
復元方法
gcloud firestore databases restore \
--source-database='(default)' \
--destination-database='restore-db' \
--snapshot-time='2026-02-09T10:00:00Z' \
--project=myapp
復元は既存データベースを上書きしない。新しいデータベースにリストアされるため安全。必要なドキュメントだけ本番にコピーすればいい。
補足:長期バックアップ
PITR は 7 日間が上限。それを超える長期バックアップには、Cloud Functions のスケジュール実行で GCS に定期エクスポートする。
export const scheduledFirestoreExport = onSchedule(
{
schedule: "0 4 * * *", // 毎日 AM 4:00 JST
timeZone: "Asia/Tokyo",
region: "asia-northeast1",
},
async () => {
const client = new (await import("@google-cloud/firestore"))
.v1.FirestoreAdminClient();
const projectId = process.env.GCLOUD_PROJECT;
const databaseName = client.databasePath(projectId!, "(default)");
await client.exportDocuments({
name: databaseName,
outputUriPrefix: "gs://myapp-firestore-backups",
collectionIds: [], // 空 = 全コレクション
});
}
);
GCS バケットは Nearline ストレージクラス(月1回アクセス向け、低コスト)で作成し、ライフサイクルルールで自動削除(prod: 90日、dev: 30日)を設定。
REST API フォールバック:gcloud CLI なしで Firebase を操作する
問題
初期データ投入スクリプト(seed-app-config.js)で firebase-admin の applicationDefault() が失敗した。
Error: Could not load the default credentials.
原因は gcloud CLI が未インストール だったこと。applicationDefault() は gcloud auth application-default login で取得した認証情報を前提にしている。AI はローカル環境に gcloud CLI があると想定してスクリプトを書いたが、実際にはインストールしていなかった。
解決策:Firebase CLI の refresh token で REST API を叩く
Firebase CLI はログイン済みの状態で refresh token を持っている。この token を使って OAuth2 で access token を取得し、Firestore REST API を直接叩けば gcloud CLI は不要。
# Firebase CLI の refresh token を取得
TOKEN_FILE="$HOME/.config/firebase/tokens.json"
REFRESH_TOKEN=$(cat "$TOKEN_FILE" | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(data['tokens']['refresh_token'])
")
# refresh token → access token に変換
ACCESS_TOKEN=$(curl -s -X POST \
"https://oauth2.googleapis.com/token" \
-d "client_id=<FIREBASE_CLI_CLIENT_ID>" \
-d "client_secret=<FIREBASE_CLI_CLIENT_SECRET>" \
-d "refresh_token=$REFRESH_TOKEN" \
-d "grant_type=refresh_token" \
| python3 -c "import sys, json; print(json.load(sys.stdin)['access_token'])")
# Firestore REST API でドキュメントを作成
curl -X PATCH \
"https://firestore.googleapis.com/v1/projects/myapp-dev/databases/(default)/documents/app_config/version" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fields": {
"termsVersion": {"stringValue": "1.0"},
"privacyVersion": {"stringValue": "1.0"}
}
}'
ポイント:
-
client_id/client_secretは Firebase CLI に埋め込まれている公開値。実際の値は Firebase CLI のソースコード、または各自の環境から取得してください - refresh token → access token の変換は標準的な OAuth2 フロー
- Firestore REST API は
https://firestore.googleapis.com/v1/projects/{PROJECT_ID}/databases/(default)/documents/{COLLECTION}/{DOC_ID}でドキュメントを CRUD できる
この方法は以下のケースで有用:
- gcloud CLI をインストールしたくない: Firebase CLI だけで開発したい場合
- CI/CD 環境: Firebase CLI のサービスアカウントで認証している場合
- Firestore のドキュメント操作をスクリプト化したい: Admin SDK を使わずに REST で直接操作
注意点
Firebase CLI の認証情報は ~/.config/firebase/tokens.json(macOS / Linux)に保存されている。このファイルは git に入れない。
また、REST API で Firestore を操作する場合、セキュリティルールは 適用されない(Admin SDK と同じ動作)。本番データの操作には注意が必要。
シークレット管理:API キーは秘密ではない
Firebase のインフラを構築すると、大量の設定ファイルと認証情報が生成される。これらの管理方針を先に決めた。
方針
.secrets/ # .gitignore で除外
├── figma-token # Figma PAT
└── env-config.md # 環境情報一覧
firebase/ # 環境別 Firebase config
├── dev/
│ ├── GoogleService-Info.plist
│ └── google-services.json
└── prod/
├── GoogleService-Info.plist
└── google-services.json
Firebase API キーの扱い
Firebase の API キーはクライアント埋め込み前提の公開情報であり、秘密ではない。 これは Firebase の設計思想だ。
セキュリティは API キーではなく、以下の3層で担保する:
- Firestore / Storage セキュリティルール: 誰が何をできるかを制御
- App Check: 正規のアプリからのリクエストのみ許可
- Authentication: ユーザーの身元確認
では何を git から除外すべきか:
| ファイル | git に入れるか | 理由 |
|---|---|---|
firebase.json |
Yes | プロジェクト設定。秘密情報なし |
firestore.rules |
Yes | ルール定義。秘密情報なし |
.firebaserc |
Yes | プロジェクトエイリアス |
google-services.json |
No | API キー自体は公開前提だが、将来の秘密鍵と管理ルールを統一するため除外 |
GoogleService-Info.plist |
No | 同上 |
firebase_options.dart |
No | 両環境の設定が含まれる |
| APNs .p8 | No | 再ダウンロード不可。紛失したら鍵を revoke して再作成 |
| Android Keystore | No | 紛失したら Play Store でアプリ更新不可能 |
再取得不可のファイルは即座にバックアップ
APNs .p8 と Android Keystore は取得した瞬間に外部バックアップを取る。これは「後でやる」では遅い。
- 1Password(パスワードマネージャー)に格納するのが最も安全
- なければ暗号化 zip にして iCloud Drive / Google Drive に保存
- 最低でも USB / 外部 SSD にコピー
FlutterFire CLI:手動ダウンロードは過去のもの
環境構築で一番効率的だったのが FlutterFire CLI による Firebase 接続だ。
従来の方法(もう使わない)
- Firebase Console でアプリを登録
-
google-services.jsonをダウンロード -
android/app/に手動配置 -
GoogleService-Info.plistをダウンロード - Xcode で手動配置
-
firebase_options.dartを手動作成
FlutterFire CLI
# 開発環境
flutterfire configure --project=myapp-dev
# 本番環境
flutterfire configure --project=myapp
1コマンドで:
- Firebase Console へのアプリ登録
-
google-services.json/GoogleService-Info.plistの自動生成・配置 -
firebase_options.dartの自動生成 -
firebase_app_id_file.jsonの自動生成
が完了する。2環境分やっても5分かからない。
環境分岐
firebase_options.dart を編集し、ビルドモードで設定を自動切り替えする:
static bool get isProduction =>
const bool.fromEnvironment('dart.vm.product');
static FirebaseOptions get currentPlatform {
if (isProduction) {
return _prodOptions;
} else {
return _devOptions;
}
}
Debug ビルドは自動的に dev 環境、Release ビルドは prod 環境に接続する。開発者が意識する必要がない。
実際に1日でやったことのタイムライン
| 順番 | やったこと | 所要時間の体感 |
|---|---|---|
| 1 | Firebase Console でプロジェクト2つ作成、Blaze プラン有効化 | 短 |
| 2 | 認証プロバイダ有効化(メール / Google) | 短 |
| 3 | Firestore / Storage 作成(asia-northeast1) | 短 |
| 4 | セキュリティルール変換(設計書 → 実ファイル) + デプロイ | 中 |
| 5 | Cloud Functions 初期化(TypeScript) | 短 |
| 6 | 初期データ投入(app_config)— REST API workaround | 中 |
| 7 | Hosting デプロイ(利用規約・プライバシーポリシー雛形) | 短 |
| 8 | Flutter プロジェクト作成 + FlutterFire CLI 接続 | 短 |
| 9 | 環境分岐コード生成(firebase_options.dart) | 短 |
| 10 | デプロイスクリプト作成 | 短 |
| 11 | Spreadsheet 作成 + SA 権限付与 | 短 |
| 12 | シークレット管理体制整備 | 短 |
| 13 | PITR 有効化(両環境 + 別プロジェクトも追加で有効化) | 短 |
「中」がついているのは、AI が詰まって自己解決した工程。それでも長くはかからなかった。
AI との協業で見えた役割分担
| 役割 | 人間 | AI |
|---|---|---|
| Console 操作 | Firebase Console / GCP Console でボタンを押す | 不可能(GUI は操作できない) |
| 判断 | Standard vs Enterprise、SA の権限範囲、バックアップ方針 | 選択肢と比較材料を提示 |
| スクリプト作成 | レビュー・実行 | 生成(デプロイスクリプト、シードスクリプト等) |
| ルール変換 | 設計書の正しさを保証 | 設計書 → ルールファイルの変換(約200行) |
| トラブル対応 | エラーメッセージを共有 | 原因特定・代替案提示・自力で書き換え |
「指示する → 実行させる」ではなく、「一緒に進めて、詰まったら互いに補う」。 AI が試行錯誤して自己解決してくれるので、人間は意思決定に集中できる。
gcloud CLI がない状況を AI 自身が検知し、Firebase CLI の refresh token を使う REST API 方式に自力で切り替えたのは象徴的だった。エラーの原因を特定し、別のアプローチに切り替え、動くスクリプトを書き直すまで人間の介入はゼロだった。
落とし穴と教訓
Firestore のロケーションは変更不可
Firestore のロケーション(リージョン)はデータベース作成時に決定され、後から変更できない。Storage も同様。Firestore と Storage は同じリージョンに揃える。 レイテンシとコストの両面で有利。
Firebase Console の UI バグ
Firestore 作成時にロケーション選択のドロップダウンが反応しないことがあった。一度戻って再度「作成」に進んだら選択できた。Console の操作は時折こういう罠がある。焦らず再操作する。
firebase_options.dart の環境分岐は手動
FlutterFire CLI は1環境分の設定しか生成しない。2環境を1ファイルにまとめる環境分岐コードは手動で書く必要がある。ここは AI に生成させるのが効率的。
Private リポジトリでも秘密鍵は git に入れない
「Private リポジトリだから大丈夫」は危険。事故で Public 化した瞬間に git 履歴から機密情報を消すのは極めて困難。最初から入れない。
まとめ
| やったこと | 得られたもの |
|---|---|
| 22セクションの手順書を先に作成 | 判断に迷わない環境構築。中断しても復帰可能 |
| prod / dev 2環境を同一構成で構築 | 本番と同じ環境でテスト可能 |
| セキュリティルールを設計書から変換 | 約200行のルールを正確にデプロイ |
| PITR を初日に有効化 | 開発中の誤操作からの復元手段を確保 |
| REST API フォールバック | gcloud CLI なしでも Firestore を操作可能 |
| シークレット管理体制を初日に整備 | 機密情報の管理ルールが明確化 |
| FlutterFire CLI で接続 | 手動ダウンロード・手動配置を排除 |
| デプロイスクリプト | 本番デプロイの安全弁(確認プロンプト) |
1人開発で Firebase 環境を構築する場合、ドキュメント駆動が最も効率的だ。先に手順書を書き、AI と協業しながら手順通りに進める。判断はドキュメントに従い、詰まったら AI が代替策を出す。
「とりあえず Console を触りながら考える」アプローチでは、2環境の構成がばらつく、設定値を忘れる、後から何を設定したか分からなくなる。手順書を書く時間は構築時間の短縮で回収できる。
Discussion