1. Executive Summary

  • 文脈: 帳票・証憑管理 SaaS の初期検証フェーズで受領した証憑は電子帳簿保存法の施行規則第4条第1項第3号 + 法人税法 保存年限 7 年/10 年に基づき Retention Lock 付きで長期保存する。ADR-0208 で運用実装のフレームワーク (Cloud Run による日次 sweep · Postgres SoT · GCS の retainUntil ビットが一致しているか) は確定したが、実際に動く module (sweep 実装 · 起動 · 再開 · error 処理 · alert) が未確定で、本 ADR がこれを埋める。
  • 課題: Phase 0 の dogfood 開始日から日次で計測する重要な指標の観測値が採取できず、Phase 1 GA 判断 (当該指標継続で 3 ヶ月連続 100% 達成) の実測分母が最悪 3 ヶ月遅延する。
  • 対策: Retention Manager を 5 つのモジュール (役割はそれぞれ sweep 実装 / 起動側の不可分実行 / 中断再開 / error 処理 / alert) に分解し Cloud Run Job + Cloud Scheduler + Cloud Tasks の per-tenant queue で日次 sweep する。
  • 効果: Phase 0 の dogfood 開始日から日次で計測する重要な指標の観測値が採取でき、Phase 1 GA 前に 3 ヶ月連続 100% 達成の実測 (ADR-0208 KPI 5) を確定できる。
  • 代償: Cloud Run + Cloud Tasks で月額 $10/月未満 (Phase 1 GA 50 顧客時点)、Cloud Tasks queue 依存が 1 個増える。

2. 何を解決するか

2.1 直面していること

適合性:

  • 最初の証憑保存 PR (PR-4 相当) の merge 前に Retention Manager runtime を deploy する経路がなく、PR merge 時点で日次 sweep が動かない状態になる。
  • 実装 module 未確定の間は sweep-audit-atomicity.test.ts (integration test) の PASS だけが担保で、実 GCP 環境での動作は billing 有効化 PR 以降まで検証できない。

2.2 放置すると起こること

信頼性:

  • 最初の証憑保存 PR merge 時に Retention Manager が deploy されていないと、日次 KPI 9 (孤立 audit_log 行数 = 0) の観測値採取が翌日以降にずれ、Phase 1 GA 前 3 ヶ月連続の実測分母が最悪 3 ヶ月遅延する。
  • Retention Manager の error 分類 (transient GCS API error / permanent auth error / DB deadlock / etc.) が未定義のまま実装するとリトライ戦略が場当たり的になり、dead-letter queue に落ちない silent failure が発生する。

2.3 潜在リスク (実害未発生 · 発生確率不明)

保守性:

  • Cloud Run Job task-timeout 6h + parallelism 4 の値が Phase 0 dogfood (tenant 1 · 想定 receipts 100-1000 件) 向けで、Phase 1 GA (50 顧客 · 想定 receipts 50-500K 件) では過小になり、複数 task 分割 + Cloud Tasks 経由の並列 fan-out にリファクタが必要になるリスク (行き先: §8 撤退条件 1)。

3. 採用したい方針

3.1 対策方針

Retention Manager を役割の異なる 5 module に分解し、Cloud Run Job を entrypoint に Cloud Scheduler + Cloud Tasks の per-tenant queue で日次 sweep する。1 receipt の削除は Postgres transaction + GCS delete を不可分に扱い、GCS delete 失敗時は Postgres を rollback して整合性を保つ。task 中断からの再開は Postgres の checkpoint 表で per-tenant に管理し、error は分類 tree で transient/permanent/skip/unknown に振り分けて unknown と permanent を dead-letter queue + alert に集約する。sweep-loop-runner の pre-filter で retention_expires_at > now() の receipt を除外し、GCS Object Retention Lock 未到達の証憑を sweep 対象から外して alert 飽和を防ぐ。legal_hold 解除イベントは即時 sweep タスクを enqueue して監査証跡の gap を最小化する。

  • 適合性: ADR-0208 §8 撤退条件 6 の retention_expires_at と GCS retainUntil の bit 一致検証を sweep 前 check として実装する。
  • 信頼性: lock-transaction-boundary で Postgres commit と GCS delete の順序を固定し、GCS delete 失敗時に Postgres 側を rollback して silent partial delete を防ぐ。
  • 運用性: dead-letter queue に message が積まれたら Cloud Monitoring alert 発火 + 代表取締役 ([email protected]) へ email 通知し、backup 通知チャンネル (別メールアドレスまたは Slack webhook) にも同時配送して sweep 失敗の見落としを 0 件にする。

3.2 守るべき設計制約

  • 適合性: ADR-0208 の framework (Cloud Run daily sweep · Postgres SoT · GCS retainUntil bit 一致 · Terraform lifecycle 3 規則) を上書きせず具体化する範囲に留める。
  • 適合性: ADR-0218 の legal_holds 表に active hold が存在する receipt は削除せず deletion_events に理由 = 'legal_hold_active' で INSERT する。
  • 信頼性: 1 receipt の削除は Postgres transaction 開始 → legal_hold gate → GCS delete → deletion_events + audit_log INSERT → receipts DELETE → Postgres commit の順序を固定する。
  • 運用性: tenant 単位の並列度 gate で他 tenant への影響を分離する (Cloud Tasks per-tenant queue)。

3.3 仕様

  • 適合性: 5 module = sweep-loop-runner (Cloud Run Job entrypoint · per-tenant loop · pre-filter で retention_expires_at > now() の receipt を sweep 対象から除外) / lock-transaction-boundary (1 receipt 削除の不可分実行) / checkpoint-resumer (task 再起動時に sweep_checkpoint 表から続きから再開) / error-classifier (transient/permanent/skip/unknown 振り分け) / dead-letter-alerter (dead-letter queue → Cloud Monitoring alert + email + backup チャンネル)。
  • 運用性: Cloud Run Job の task-timeout 6h · parallelism 4 · Cloud Scheduler は日次 UTC 15:00 (JST 00:00) cron 起動 · payload = { tenantIds: null (all tenants) or [specific tenant] }。
  • 運用性: Cloud Tasks per-tenant queue で max_dispatches_per_second = 1 · max_concurrent_dispatches = 4 を設定して並列度 gate を規定する。
  • 運用性: Cloud Monitoring alert policy の通知チャンネルに primary = 代表取締役 email + backup = 別メールアドレスまたは Slack webhook を設定し、単一通知先の受信障害・スパム誤判定・担当者変更で通知が無音になるのを防ぐ。alert 到達確認は §9 Confirmation KPI-7 の月次テストで検証する。
  • 保守性: sweep_checkpoint 表を新設 (tenant_id UUID PRIMARY KEY · last_processed_receipt_id UUID NOT NULL · last_processed_at TIMESTAMPTZ NOT NULL DEFAULT now() · status TEXT NOT NULL CHECK (status IN ('in_progress', 'completed', 'failed')))。
  • 保守性: sweep-loop-runner は per-tenant loop 開始時に INSERT ... ON CONFLICT DO UPDATE で status = 'in_progress' を書き、loop 完了時に 'completed' を書く。task 再起動時に status = 'in_progress' の row を read して last_processed_receipt_id より大きい id から再開。
  • 信頼性: SIGTERM ハンドラで sweep_checkpoint.status を 'failed' に更新する 実装を義務付け、Cloud Run Job の task-timeout 強制終了 (SIGTERM → SIGKILL 10 秒間) で 'in_progress' が残るのを防ぐ。加えて 日次 sweep 開始時に「status = 'in_progress' かつ last_processed_at が 8 時間以上前の row」をタイムアウト孤立として自動検出し alert する ロジックを sweep-loop-runner 起動 hook に実装する。
  • 信頼性: error 分類 tree = HTTP 5xx from GCS API (500/502/503/504) → transient (exponential backoff 3 回) / HTTP 429 from GCS API → transient / Postgres deadlock (SQLSTATE 40P01) → transient / HTTP 403 RETENTION_POLICY_NOT_MET from GCS API → skip (deletion_events に理由 = 'retention_lock_active' で INSERT · alert なし) / HTTP 403 その他 (auth 障害) → permanent (dead-letter + alert) / HTTP 401 from GCS API → permanent (dead-letter + alert) / HTTP 404 from GCS API → skip (既に削除済みの idempotent case · deletion_events に理由 = 'already_deleted' で INSERT · alert なし) / Postgres unique violation (SQLSTATE 23505) → skip (sweep 対象が既に消えている idempotent case) / Postgres foreign key violation (SQLSTATE 23503) → permanent (dead-letter + alert) / legal_hold_placements の active hold 存在 → skip (deletion_events に理由 = 'legal_hold_active' で INSERT · alert なし) / unknown exception → dead-letter + alert (定期的に分類 tree を拡張する)。error-classifier の全分岐は分類結果を structured log に出力し、unknown 枝に到達した件数を Cloud Monitoring カスタムメトリクスとして独立集計する (KPI-5 と KPI-6 の分母)。
  • 信頼性: legal_hold 解除イベントは ADR-0218 の legal_holds 表更新 trigger から Cloud Tasks へ即時 sweep タスクを enqueue し、hold 解除後 1 時間以内の再 sweep を保証する。deletion_events に sweep_triggered_reason カラム (daily_sweep / hold_release_sweep) を追加 して監査証跡の gap をなくす。
  • 保守性: deletion_events の reason 列に retention_lock_active already_deleted legal_hold_active の 3 skip 理由を CHECK 制約で許容し、監査時に skip の原因を区別可能にする。

4. 判断基準

4.1 評価軸

#軸 (Q42)日本語軸名重要度 (係数)案件特有の解釈
1#suitable適合性Must (×2.0)ADR-0208 framework (Cloud Run daily sweep · Postgres SoT · GCS retainUntil bit 一致 · Terraform lifecycle 3 規則) を上書きせず具体化範囲に留める
2#reliable信頼性Must (×2.0)silent partial delete (Postgres commit 済で GCS delete 失敗) と silent failure (dead-letter に落ちない unknown) を防ぐ
3#operable運用性High (×1.0)代表取締役 1 名運用で alert escalation と node 管理負荷が閉じる
4#maintainable保守性High (×1.0)Phase 1 GA (50 顧客 · receipts 50-500K 件) までスケール余地を持つ
5#efficient効率性Medium (×0.5)Phase 1 GA 50 顧客時点で月額 $10/月未満

K.O. criterion: Must 軸 (適合性 · 信頼性) の score < 3 は不採用。

4.2 評価軸 × 案スコア表

係数採択案 (Cloud Run Job)案 B (GKE CronJob)案 C (Cloud Functions)
適合性×2.0523
信頼性×2.0542
運用性×1.0524
保守性×1.0441
効率性×0.5524
加重和 (正規化)0.9360.5180.500
K.O. 通過 (Must ≥3)❌ (適合性 = 2)❌ (信頼性 = 2)

5. 検討した代替案

5.1 案 1 (採用): Cloud Run Job + Cloud Scheduler + Cloud Tasks

採用案。詳細は §3.1 対策方針 参照。

  • 適合性: ADR-0179 の GCP 単一 serverless 前提に沿い、ADR-0208 framework を上書きせず具体化範囲に留まる。
  • 信頼性: lock-transaction-boundary で Postgres transaction と GCS delete の順序を固定でき、silent partial delete を防げる。
  • 運用性: per-tenant queue で tenant 単位の失敗を分離し、node 管理・cluster upgrade が不要。
  • 保守性: task-timeout 6h + parallelism 4 で Phase 0 dogfood を完走でき、Phase 1 GA では複数 task 分割 + fan-out へのリファクタ余地がある (行き先: §8 撤退条件 1)。
  • 効率性: Cloud Run Job 従量課金 + Cloud Tasks + Cloud Scheduler で Phase 1 GA 50 顧客時点 $10/月未満。

5.2 案 2 (不採用): GKE CronJob

  • 効率性: GKE cluster の常時稼働 cost が Cloud Run Job の従量課金を大きく上回り Phase 0 dogfood 規模では過剰。
  • 運用性: GKE の node 管理・upgrade 運用が代表取締役 1 名運用の負荷を増やす。
  • 適合性: ADR-0179 (JTBD-012 SaaS 基盤 GCP 単一) の serverless 前提から外れる。

5.3 案 3 (不採用): Cloud Functions timer trigger

  • 保守性: Cloud Functions の実行時間上限 (2nd gen で 60 分) が dogfood 想定 receipts 1000 件でも余裕がなく Phase 1 GA スケールに耐えない。
  • 信頼性: task 中断からの checkpoint 再開が Cloud Functions では自然に扱えず、独自 state 管理が必要になる。

5.4 案 4 (不採用): shared queue + tenant_id タグによるフィルタリング

  • 効率性: GCP Cloud Tasks の queue 上限 500 件/プロジェクトを回避でき、テナント数が 250 を超えても quota 申請が不要。
  • 信頼性: shared queue では 1 テナントの詰まり (permanent error 連発 · GCS API rate limit) が queue 全体の throughput を落とし、他テナントの sweep 完走率 (KPI-1) が連鎖劣化する。
  • 運用性: tenant_id タグでのフィルタリング実装が sweep-loop-runner の複雑度を上げ、per-tenant queue に比べ debug 難度が高い。
  • 保守性: 現時点 (Phase 1 GA 50 顧客) では queue 上限 500 件の半数まで余裕があり、テナント数 250 到達時点で本方式への切替を検討する余地は残る (行き先: §6.2 負の影響 · 撤退条件 6)。

6. 影響

6.1 正の影響

  • 適合性: Phase 0 dogfood 開始日から日次 KPI 9 (孤立 audit_log 行数 = 0) の観測値が採取でき Phase 1 GA 前 3 ヶ月連続 100% 達成の実測分母が確定する。
  • 信頼性: error 分類 tree + dead-letter queue + alert + backup 通知チャンネルで sweep 失敗の見落としが 0 件になる。
  • 信頼性: legal_hold 解除後 1 時間以内の再 sweep 保証と deletion_events.sweep_triggered_reason カラムで、税務調査時の「改ざん可能期間」指摘リスクを最小化する。
  • 運用性: per-tenant queue で tenant 単位の失敗が他 tenant に波及しない。
  • 保守性: sweep_checkpoint 表と SIGTERM ハンドラで task 中断からの再開経路が明示され spot instance 中断や task-timeout 到達時の recovery が自動化される。

6.2 負の影響 / リスク

  • 効率性: Cloud Tasks queue 依存が 1 個増え APIs & Services の依存本数が増える。
  • 効率性: テナント数が 250 を超えると Cloud Tasks queue 上限 500 件/プロジェクトの半数を Retention Manager が占有し、他用途の queue 追加に GCP quota 申請が必要になる (行き先: §8 撤退条件 6 · §5.4 shared queue 案への切替検討)。
  • 保守性: Phase 1 GA (50 顧客 · receipts 50-500K 件) で task-timeout 6h · parallelism 4 の値が過小になり複数 task 分割 + fan-out へのリファクタが必要になるリスク (行き先: §8 撤退条件 1)。GCS delete の p99 レイテンシが 300-500ms のケースでは 500K 件/テナントで parallelism 4 のまま 10 時間相当となり task-timeout 6h を超える試算のため、Phase 0 dogfood で 1 件あたり p50/p99 レイテンシを実測して撤退条件 1 の定量閾値を前倒し判定する。
  • 保守性: error 分類 tree の unknown 枝が dead-letter に落ちるたびに tree 拡張の運用作業が発生する。

7. コスト試算

7.1 前提条件

項目根拠
Phase 0 dogfood tenant 数1JTBD-012 Phase 0 定義
Phase 0 想定 receipts100-1000 件dogfood 想定利用量
Phase 1 GA 顧客数50ADR-0208 KPI 5 の Phase 1 GA 分母
Phase 1 GA 想定 receipts50-500K 件Phase 1 GA 顧客数 × 平均 receipts/顧客
Cloud Run Job 実行時間 (Phase 0)720 秒/日 × 30 日 = 21,600 秒/月task-timeout 6h × parallelism 4 の実効平均
Cloud Scheduler cron30 dispatches/月日次 UTC 15:00 起動
GCS delete レイテンシ (想定)p50 100ms · p99 300-500msGoogle Cloud Status History 2023-2024 実績。Phase 0 dogfood で実測して §8 撤退条件 1 の定量閾値を確定
Phase 1 GA parallelism 4 完走上限 (試算)約 288K 件/テナント (300ms × 500K ÷ 4 = 37,500 秒 > task-timeout 6h = 21,600 秒 のため)GCS delete p99 300ms 前提 · 実測後に上振れ・下振れを判定
為替150 円/USD起案時レート

7.2 月コスト試算

  • Phase 0 dogfood (tenant 1): Cloud Run Job 実行時間 + Cloud Tasks dispatches + Cloud Scheduler + Cloud Monitoring alert で月額 300 円未満 ($2/月未満) を目安。
  • Phase 1 GA (50 顧客): 同構成で月額 1,500 円未満 ($10/月未満) を目安。
  • 実装工数: 5 module + sweep_checkpoint schema + deletion_events.sweep_triggered_reason カラム追加 + SIGTERM ハンドラ + タイムアウト孤立自動検出 + Terraform lifecycle 追記 で 6 人日相当 (盲点対応で +1 人日)。

8. 撤退条件

  1. Cloud Run Job task-timeout 到達で 3 日連続 checkpoint 未完了、または Phase 0 dogfood 実測 GCS delete p99 × 想定 receipts 数 ÷ parallelism が task-timeout 6h の 80% を超過 → 発火から 14 日以内に 複数 task 分割 + Cloud Tasks 経由の fan-out へのリファクタを扱う後続 ADR を起票する (起票義務)。起票までの暫定運用は parallelism 4 → 8 の増強 (撤退条件 3 と同型) で凌ぐ。
  2. dead-letter queue が 30 日連続で 0 件 → error 分類 tree の網羅性 review (silent failure 疑い) + §9 KPI-6 の月次 chaos drill 結果を確認。
  3. sweep 実行時間の月次中央値が 6h の 80% 超 → parallelism を 4 → 8 へ増強 or task 分割
  4. error 分類 tree の unknown 比率が月次 20% 超 → tree の枝を追加し unknown を permanent/transient/skip に再分類
  5. 月額実コストが想定レンジ (Phase 1 GA 時点 1,500 円/月) を 2 倍超過 → parallelism / cron 頻度 / queue 設定の見直し
  6. テナント数が 250 に到達 → Cloud Tasks queue 上限 500 件/プロジェクトの半数占有時点で §5.4 shared queue + tenant_id タグ方式への切替 ADR を起票する (起票義務)。

9. Confirmation

  1. KPI-1 sweep 完走率 = 100% (検証: sweep_checkpoint.status = 'completed' の割合 · 頻度: 日次 · 違反時: sweep-loop-runner の error log を review し error-classifier に枝追加)
  2. KPI-2 GCS delete 成功率 = 100% (検証: gcs_delete_status = 'success' の割合 · 頻度: 日次 · 違反時: lock-transaction-boundary の rollback が動いたか監査)
  3. KPI-3 dead-letter queue の 30 日累積件数 ≤ 3 件 (検証: Cloud Tasks queue metric · 頻度: 月次 · 違反時: 積まれた message の error 分類を review し tree 拡張)
  4. KPI-4 sweep 実行時間の P95 ≤ task-timeout の 80% (検証: Cloud Run Job execution metric · 頻度: 週次 · 違反時: parallelism 4 → 8 増強 or task 分割検討)
  5. KPI-5 error 分類の transient/permanent/unknown 比率 (検証: error-classifier の structured log 集計 + Cloud Monitoring カスタムメトリクス · 頻度: 月次 · 違反時: unknown が 20% 超なら分類 tree 拡張)
  6. KPI-6 dead-letter 経路の生存確認 (月次 chaos drill) (検証: 意図的にテスト用 unknown exception を inject して dead-letter queue に到達 + alert 発火 + backup 通知チャンネル到達を月次で確認 · 頻度: 月次 · 違反時: dead-letter-alerter または通知チャンネル設定の障害と判断し 7 日以内に修正)
  7. KPI-7 alert 通知チャンネル到達確認 (検証: primary email + backup チャンネル両方に月次テスト alert を送信し受信を確認 · 頻度: 月次 · 違反時: 未達チャンネルの設定を 7 日以内に修正)
  8. KPI-8 legal_hold 解除後の再 sweep 遅延 (検証: legal_hold 解除イベント時刻から次の deletion_events INSERT (sweep_triggered_reason = 'hold_release_sweep') までの経過時間 · 頻度: 月次 · 違反時: 60 分超過事例が 1 件でもあれば legal_holds 表 trigger の enqueue 経路を再点検)

10. 参照

10.1 関連 ADR

  • Amends: ADR-0208 (証憑の保存年限管理と期限後廃棄) — 本 amend は §3.1 対策方針 / §3.3 仕様 / §11 実装配線 を通じて ADR-0208 の運用実装 framework を module 単位に具体化する
  • ADR-0203 (receipts と audit_log の schema 基礎) — 本 ADR が sweep で参照する schema の SoT
  • ADR-0218 (legal_holds 表新設) — 本 ADR の lock-transaction-boundary で legal_hold gate を呼ぶ相手側 schema。legal_holds 表更新 trigger から Cloud Tasks への即時 sweep enqueue 経路を追加する
  • ADR-0217 (税務申告完了関連 schema 追加) — 本 ADR の checkpoint 表新設と同一 amend cycle で扱う schema 変更 (Phase 0 KPI-4 分母への影響を review 要)
  • ADR-0179 (JTBD-012 SaaS 基盤 GCP 単一) — 本 ADR の Cloud Run / Cloud Scheduler / Cloud Tasks が GCP 単一基盤前提

10.2 関連 PR/Issue

  • PR-4 (最初の証憑保存 endpoint · session 24 shipping · #5085) — 本 ADR の Retention Manager runtime deploy を merge 前に済ませる必要がある depends
  • vlt session 24 handover §4 継承残タスク Claude 単独可 (2) — 本 draft prompt 起票の元指示

10.3 外部資料

  • Cloud Run Jobs docs: task-timeout / parallelism / retry policy / SIGTERM グレースフル期間
  • Cloud Scheduler docs: cron 表現 / target = Cloud Run Job invocation
  • Cloud Tasks docs: per-queue rate limit / retry policy / dead-letter / queue 上限 500 件/プロジェクト
  • GCS Object Retention Lock docs: HTTP 403 RETENTION_POLICY_NOT_MET の返却条件
  • Google SRE Book Chapter 6: alert escalation policy · 通知チャンネル冗長化

11. 実装配線

  • lint 配線: scripts/vlt-schema-lint.mjs (= vlt schema の追記専用トリガー等を検査する lint script) に sweep_checkpoint 表の追記専用トリガー存在チェック · lock-transaction-boundary の同一トランザクション性検査 · deletion_events.sweep_triggered_reason カラム CHECK 制約検査を追加 (path-scoped vlt/**/*.sql · WIRING gate) · scripts/adr-body-lint.mjs (= ADR 本文の網羅性 lint script) の error 分類 tree 節に「HTTP status code の網羅性 (401/403/404/429/500/502/503/504) と Postgres SQLSTATE の網羅性 (23505/23503/40P01) と HTTP 403 の RETENTION_POLICY_NOT_MET / auth 障害への分岐存在」を検査項目として追加
  • 起案フロー配線: 本 ADR 受理後、.claude/rules/vlt-schema.md (session 23 で作成済) に「sweep_checkpoint 表は tenant_id PRIMARY KEY で per-tenant checkpoint · in_progress 中の receipt id より大きい id から再開 · SIGTERM ハンドラで 'failed' に更新 · タイムアウト孤立 (in_progress かつ last_processed_at 8 時間超) を日次 sweep 開始時に自動検出」と「deletion_events.sweep_triggered_reason カラム (daily_sweep / hold_release_sweep) と reason skip 3 値 (retention_lock_active / already_deleted / legal_hold_active)」を追記 · ADR-0208 §10.1 起票枠を「本 ADR で消化済」に更新し ADR-0208 の amended_by に本 ADR を追加
  • docs SSoT 同期: docs/architecture/arch_jtbd012_voucher_saas_draft.md (= JTBD-012 の層別 architecture draft) の 層 ④ RetentionManager 節に「5 module (sweep-loop-runner · lock-transaction-boundary · checkpoint-resumer · error-classifier · dead-letter-alerter)」の構造図と「legal_hold 解除 → 即時 sweep enqueue」の flow を追記 · docs/architecture/arch_jtbd012_decision_inventory.md (= JTBD-012 の決定塊 inventory) の D9 塊 (Retention Manager) 実装状態を「実装 module 確定 (ADR-NNNN)」へ更新 · docs/adr/INDEX.md の関係図に本 ADR を追加し amends ADR-0208 の辺を追加