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。
runtimeCloud Runの実行SA。Syncプロジェクトのroles/datastore.user、Driveプロジェクトでiam.serviceAccounts.getの最小権限。
signerreader-sync-signer@SYNC_PROJECT_ID.iam.gserviceaccount.com。runtimeにこのsignerだけのiam.serviceAccounts.signBlob権限を付与。鍵ファイルは発行しない。
build/deployruntimeと分ける。ビルド方式に応じた権限を付与し、アプリや読者鍵は送信しない。

この構成例ではreaderのemail末尾をDRIVE_PROJECT_IDで制限します。複数のDriveプロジェクトを許可する構成では、プロジェクトごとの許可リストとIAM照会先を実装してください。配布するreaderにFirestoreのIAM権限やトークン署名権限を付けないでください。

3. 構築の順序

  1. 自身のGoogle Cloud環境に必要なプロジェクト・請求設定・APIを用意します。Firebase Authenticationを初期化し、Firestore Nativeの(default)を作成します。カスタム認証にメール/匿名ログインを追加する必要はありません。
  2. reader、runtime、signerを作成し、役割表に沿って権限を設定します。readerのemailとIAM uniqueIdを記録し、読み取り専用のDrive共有を設定します。フォルダーURLの /folders/ の後のIDがrootFolderIdです。
  3. API仕様に従って認証サービスを実装し、コンテナ化します。カスタムトークンの発行にはFirebase Admin SDKを利用できます。Cloud Runが指定するPORTでHTTPリクエストを受け付けます。TLSと入口認証はCloud Runが担当します。
  4. Cloud RunをIAM認証必須で作成し、HTTPSサービスURLを取得します。環境変数のSYNC_AUDIENCEと設定ファイルのendpoint/audienceを、そのURL(末尾 / なし)に一致させます。公開アクセスを付けず、readerだけにサービス単位のInvokerを付与します。
  5. Firestore Rulesとインデックス除外を設定します。Firebaseの公開APIキーを用意し、Identity Toolkit APIでのカスタムログイン・アカウント照会に使える制限を設定します。APIキーを認可の代わりにしません。
  6. 専用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エラー・利用量を監視します。起動遅延や権限反映直後は再試行が必要な場合があります。