DEVELOPER / 管理者向け技術資料
Google Cloudで構成する
同期プロトコル2 · 設定形式v1 · 更新日 2026-10-31
Cloud Run、Firebase Authentication、Firestoreと、サーバー側の操作統合・回復処理を使用する同期プロトコル2の構成例です。例中のプロジェクトID、サービスURL、アカウントIDは、構築する環境の値に置き換えてください。
1. 全体構成
アプリ → Google Drive API → 共有された書籍
アプリ → Google OAuth → 同期入口用IDトークン
アプリ → Cloud Run [IAM認証] → /challenge, /token
Cloud Run → Google公開証明書 + IAM → 読者の署名・利用資格確認
Cloud Run → Firestore → nonceの保存・一回消費
Cloud Run → 専用signer → Firebase custom token
アプリ → Firebase Authentication → Firebase IDトークン
アプリ → Firestore [Security Rules] → 読書位置・しおり
Google発行IDトークンとCloud Run Invokerを組み合わせる入口認証についてはCloud Run公式資料、カスタム認証についてはFirebase公式資料を参照してください。
2. リソースと役割
| 役割 | 構成 |
| Driveプロジェクト | Drive API、配布用読者サービスアカウント。対象フォルダーを閲覧者として共有。 |
| Syncプロジェクト | Firebase Authentication、Firestore Nativeの(default)、Cloud Run、IAM・IAM Credentials API。 |
| reader | 端末へ配布する専用SA。Drive閲覧と対象Cloud Runサービスのroles/run.invoker。 |
| runtime | Cloud Runの実行SA。Syncプロジェクトのroles/datastore.user、Driveプロジェクトでiam.serviceAccounts.getの最小権限。 |
| signer | reader-sync-signer@SYNC_PROJECT_ID.iam.gserviceaccount.com。runtimeにこのsignerだけのiam.serviceAccounts.signBlob権限を付与。鍵ファイルは発行しない。 |
| build/deploy | runtimeと分ける。ビルド方式に応じた権限を付与し、アプリや読者鍵は送信しない。 |
この構成例ではreaderのemail末尾をDRIVE_PROJECT_IDで制限します。複数のDriveプロジェクトを許可する構成では、プロジェクトごとの許可リストとIAM照会先を実装してください。配布するreaderにFirestoreのIAM権限やトークン署名権限を付けないでください。
3. 構築の順序
- 自身のGoogle Cloud環境に必要なプロジェクト・請求設定・APIを用意します。Firebase Authenticationを初期化し、Firestore Nativeの(default)を作成します。カスタム認証にメール/匿名ログインを追加する必要はありません。
- reader、runtime、signerを作成し、役割表に沿って権限を設定します。readerのemailとIAM uniqueIdを記録し、読み取り専用のDrive共有を設定します。フォルダーURLの
/folders/の後のIDがrootFolderIdです。 - API仕様に従って認証サービスを実装し、コンテナ化します。カスタムトークンの発行にはFirebase Admin SDKを利用できます。Cloud Runが指定するPORTでHTTPリクエストを受け付けます。TLSと入口認証はCloud Runが担当します。
- Cloud RunをIAM認証必須で作成し、HTTPSサービスURLを取得します。環境変数のSYNC_AUDIENCEと設定ファイルのendpoint/audienceを、そのURL(末尾 / なし)に一致させます。公開アクセスを付けず、readerだけにサービス単位のInvokerを付与します。
- Firestore Rulesとインデックス除外を設定します。Firebaseの公開APIキーを用意し、Identity Toolkit APIでのカスタムログイン・アカウント照会に使える制限を設定します。APIキーを認可の代わりにしません。
- 専用reader鍵から設定ファイルを発行し、運用対象の各OSでサンプル本を使って受け入れ確認を行います。動作確認前に利用者の本を移行しないでください。
認証サービスの設定例
以下は環境変数で設定を渡す例です。変数名はアプリの通信仕様ではなく、認証サービス側で定義します。
SYNC_PROJECT_ID=example-sync
DRIVE_PROJECT_ID=example-drive
SYNC_SIGNER=reader-sync-signer@example-sync.iam.gserviceaccount.com
SYNC_AUDIENCE=https://YOUR_SERVICE.run.app
SYNC_MIN_CLIENT_PROTOCOL=2
READER_ACCOUNTS={"reader@example-drive.iam.gserviceaccount.com":"123456789012345678901"}
READER_ACCOUNTSはメール→不変IDのJSON objectで、一つ以上の許可が必要です。秘密鍵の置き場ではありません。Cloud RunはruntimeのApplication Default Credentialsを使い、signerへリモート署名を要求します。
4. Firestoreの配置
現行の保存先・操作ID・機能宣言は同期プロトコル2を参照してください。Rulesだけでサーバー正本は構築できません。操作受信箱のイベント処理、旧文書からの統合、投影・差分一覧、再試行・未処理回復を配置し、既存データの照合が完了してからready=trueにします。以下のdevices・recordHeadsは互換用の保存先です。
読書状態は readers/{uid}/books/{bookHash}/devices/{deviceUUID}、既読・履歴の拡張は readers/{uid}/recordHeads/{bookHash}_{deviceUUID}、認証nonceは _readerChallenges/{sha256(email)}。後者はサーバーだけがアクセスし、アプリからは拒否します。各読者につき一つのnonce文書を更新するため、認証のたびに無制限に履歴を追加しません。
以下のSecurity RulesでUID・readerLease・スキーマ・sequence・更新時刻を検証します。インデックス設定ではdevicesとrecordHeadsコレクショングループのpayloadフィールドを除外します。Admin SDKはRulesを迂回するため、runtimeのIAMとサーバー処理も別途保護します。Firestore RESTの認証とRulesも確認してください。
適用するSecurity Rulesの参照定義
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function reader(uid) {
return request.auth != null && request.auth.uid == uid
&& request.auth.token.readerLease is int
&& timestamp.value(request.auth.token.readerLease * 1000) > request.time;
}
function snapshotWrite() {
return request.resource.data.keys().hasOnly(['schemaVersion', 'sequence', 'payload', 'updatedAt'])
&& request.resource.data.schemaVersion == 1
&& request.resource.data.sequence is int && request.resource.data.sequence > 0
&& (resource == null || request.resource.data.sequence > resource.data.sequence)
&& request.resource.data.payload is string && request.resource.data.payload.size() <= 131072
&& request.resource.data.updatedAt == request.time;
}
// One snapshot per installation and book. Old offline devices cannot overwrite
// other devices' heads. Deletion is a bookmark tombstone inside the payload.
match /readers/{uid}/books/{book}/devices/{device} {
allow read: if reader(uid);
allow create, update: if reader(uid)
&& book.matches('^[a-f0-9]{64}$')
&& device.matches('^[a-f0-9-]{36}$')
&& device != '00000000-0000-0000-0000-000000000000'
&& snapshotWrite();
allow delete: if false;
}
// Account-scoped descendant metadata query for the replicated annotation list.
// No write permissions are added; payload/sequence rules above still apply.
match /readers/{uid}/{path=**}/devices/{device} {
allow list: if reader(uid);
}
// Flat, pageable discovery of completed state/history without changing the
// existing book/annotation lanes. Clients merge reversible registers in payload.
match /readers/{uid}/recordHeads/{head} {
allow read: if reader(uid);
allow create, update: if reader(uid)
&& head.matches('^[a-f0-9]{64}_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$')
&& !head.matches('.*_00000000-0000-0000-0000-000000000000$')
&& snapshotWrite();
allow delete: if false;
}
// Flat, server-maintained metadata only. Clients cannot forge change notifications.
match /readers/{uid}/annotationHeads/{head} {
allow get, list: if reader(uid);
allow write: if false;
}
match /syncCapabilities/annotationHeadsV1 {
allow get: if request.auth != null && reader(request.auth.uid);
allow list, write: if false;
}
match /readers/{uid}/canonicalRecords/{head} {
allow read: if reader(uid);
allow write: if false;
}
match /readers/{uid}/books/{book}/canonical/{head} {
allow read: if reader(uid);
allow write: if false;
}
match /readers/{uid}/canonicalAnnotationHeads/{head} {
allow read: if reader(uid);
allow write: if false;
}
match /syncCapabilities/serverCanonicalV1 {
allow get: if request.auth != null && reader(request.auth.uid);
allow list, write: if false;
}
match /readers/{uid}/canonicalOperations/{operation} {
allow read: if reader(uid);
allow create: if reader(uid) && operation.matches('^[a-f0-9]{64}$')
&& get(/databases/$(database)/documents/syncCapabilities/serverCanonicalV1).data.ready == true
&& get(/databases/$(database)/documents/syncCapabilities/serverCanonicalV1).data.protocolVersion == 2
&& request.resource.data.keys().hasOnly(['payload','device','records','processed','updatedAt'])
&& request.resource.data.payload is string && request.resource.data.payload.size() <= 131072
&& request.resource.data.device.matches('^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$')
&& request.resource.data.device != '00000000-0000-0000-0000-000000000000'
&& request.resource.data.records is bool && request.resource.data.processed == false
&& request.resource.data.updatedAt == request.time;
allow update, delete: if false;
}
match /{document=**} { allow read, write: if false; }
}
}
デプロイ対象のプロジェクトを確認し、上記をRulesとして適用します。payloadのインデックス除外は別の設定です。Firestore RESTクライアントのIAM権限だけでは、このRulesの代わりになりません。
5. 稼働確認と運用
最小インスタンス0、少数の最大インスタンスから始め、認証の待ち時間と必要な同時接続数に応じて調整します。上限設定は費用の保証ではないため、予算通知とAPIエラー・利用量を監視します。起動遅延や権限反映直後は再試行が必要な場合があります。
- 426:アプリの対応番号とSYNC_MIN_CLIENT_PROTOCOLを確認。端末の保存済みデータを消さず、対応版へ更新。
- 401/403:入口のInvoker、Google IDトークンのaudience、許可リスト、不変ID、鍵やSAの無効化を確認。
- Firebase交換失敗:Authentication初期化、projectIdとAPIキーの組み合わせ、API制限、signerへのsignBlob権限を確認。
- Firestore拒否:UID、readerLease、Rules、sequenceとupdateTimeを確認。Rulesを全面開放して解決しない。
- Driveに本が出ない:rootFolderId、readerへの共有、対象形式、選択・除外を確認。Drive検索の詳細は公式API資料を参照。
- 廃止:接続停止、権限・鍵の失効、利用者データと認証管理データの削除、保持するログの扱いを管理者が案内。