運用と信頼性/ Reliability & operations
20 年運用に耐える信頼性レイヤとして、多層ウイルススキャン、追記専用の監査ログ、三段階の保持期間運用を組み合わせます。日常運用と長期運用の両方を見据えた設計を本ページで整理します。
多層ウイルススキャンMulti-layer scan
ファイルが利用可能になるまでに 4 層のチェックを通します。クライアントでの形式検証、5 バケット分離によるアップロード隔離、ClamAV による本体スキャン、最後に Google Drive 側の標準スキャンが入る構造です。
クライアントでの形式検証を通過した後、ファイルはまず 未検査バケット に保管されます。Eventarc が object.finalized イベントを docshare-scanner サービスへ Push し、ClamAV が本体をスキャンします。clean なら 検査済みバケット に rewrite され、Drive 同期 ON の Box ならその後 Drive 側でも標準スキャンが走ります。infected の場合は 隔離バケット に隔離され、system_admin・UL者・Box Owner に即時メールが届きます。
クライアント検証
拡張子(許可・拒否リスト)、マジックバイト、ファイルサイズ(200 MB 以下)を Web ブラウザ上で検証します。マクロ Office(.docm / .xlsm / .pptm)と古い Office(.doc / .xls / .ppt)はここで一律拒否します。
5 バケット分離 + Eventarc
アップロード直後のファイルは 未検査バケット に置かれ、ユーザからは開示されません。Eventarc が object.finalized イベントを docshare-scanner に OIDC 認証付きで Push します。
ClamAV 1.4
Go + ClamAV を載せた Cloud Run(min=1 / 大きめのメモリ)が本体スキャンを実行します。シグネチャは freshclam が 6 時間ごとに更新し、月次の EICAR 自動テストでスキャナの常時動作を検証します。
Drive 標準スキャン
Drive 同期 ON の Box では、clean 後のファイルが Drive にもアップロードされ、Google 側で再度マルウェアスキャンが走ります。万一 ClamAV を抜けても、ここで重ねがけ防御になります。
改ざんを防ぐ監査ログAudit log integrity
監査ログは PostgreSQL の RLS(行レベルセキュリティ)で UPDATE と DELETE を拒否します。これに Supabase の自動バックアップと月次パーティションの GCS 退避を組み合わせ、3 重防御で完整性を担保します。
追記専用 RLS
audit_logs テーブルには INSERT と SELECT のポリシーのみを設置し、UPDATE と DELETE は RLS で拒否します。データベース管理者であってもアプリ経路からはレコードを書き換えられません。
Supabase 自動バックアップ + PITR
Supabase Pro の自動スナップショットに加え、PITR(Point-in-Time Recovery、特定時刻への復元)で一定期間内の任意時点に戻せます。バックアップは一定期間保持します。
月次パーティション + GCS 退避
pg_partman で audit_logs を月次パーティションに分割し、25 ヶ月超のパーティションを Cloud Storage の archive class へ CSV で退避します。一定期間の後に完全削除します。
ハッシュチェーンは不採用: 金融・医療レベルの完整性要件であれば prev_hash / row_hash を持つハッシュチェーンも検討対象ですが、一般企業の説明責任要件は上記 3 重防御で十分と判断しました。要件が変われば再評価します。
保持期間と Hold(リーガルホールド)Retention & Legal Hold
保持期間は 3 段階の階層(全社デフォルト → Box デフォルト → 個別 Hold)で運用します。アクセスのたびに期限を延長するローリングウィンドウと、UL 直後の 30 日凍結期間を組み合わせて、「うっかり消える」と「無制限に増える」の両方を防ぎます。
| 項目 | 仕様 |
|---|---|
| 三層階層 | 全社デフォルト(system_admin、初期 180 日) → Box デフォルト(Owner、無制限選択可) → 個別 Hold(Editor / Owner)。優先順位: Hold > Box デフォルト > 全社デフォルト |
| ローリングウィンドウ | DL / プレビュー / 差し替え / コメント / 共有リンク発行のたびに retention_expires_at = now() + 保持期間 に更新。Box 内の他ファイルアクセスでは更新しない |
| 凍結期間 | UL 直後の最低 30 日は削除候補に含めない。retention_expires_at = max(retention_expires_at, created_at + 30d) |
| 個別 Hold | Editor / Owner が任意ファイルに対し 理由テキスト必須で Hold を設定。Hold ファイルは自動削除対象から完全に外れる |
| Hold バッジ | Box 内 Hold 合計容量がしきい値を超えると、Box 一覧・Box 詳細・/admin/boxes の 3 箇所に常時バッジを表示 |
| 削除当日メール通知 | 削除当日にメール送信。宛先は Box Owner(必須) + 最終アクセスユーザ + UL者。配信モード(immediate / digest / off)を問わず immediate 強制 |
| 共有リンク有効ファイル | 共有リンクが有効なファイルは「失効 + 7 日」まで削除を延期 |
| 削除実行ジョブ | retention-deletion-daily(毎日、Cloud Run Jobs)。論理削除 + 当日メール + audit_logs 記録 → Day 30 で trash-cleanup-daily が物理削除 + Drive ゴミ箱送り |
| 影響シミュレーション | 全社・Box デフォルト変更時に「N ファイル / X GB が新デフォルトで自動削除対象になります」を即座に表示。確定で既存 retention_expires_at を一括再計算 + audit_logs |
Drive アーカイブ参照Drive Archive view
アプリ側で削除された後も、Drive に残っているファイルを read-only で参照できる「Drive アーカイブ」タブを Box 詳細に持ちます。Drive 同期 ON の Box でのみ表示されます。
| 項目 | 仕様 |
|---|---|
| 対象 | アプリ側で削除済み(files.deleted_at IS NOT NULL)かつ Drive 側にまだ残っているファイル |
| UI | Box 詳細の /boxes/[id]/drive-archive タブ。アプリ操作は DL と参照のみ可(編集・復元は不可)。Viewer 以上で参照可 |
| データソース | drive_archive_files テーブル(box_id / original_file_id / name / drive_file_id / drive_web_view_link / deleted_at / deletion_reason / exists_in_drive / last_verified_at) |
| 週次バッチ | drive-archive-sync が週次で Drive API を叩き、exists_in_drive を更新。Drive 上で復元・削除されたケースに追従 |
| 監査 | 参照時に drive_archive.viewed / drive_archive.downloaded として audit_logs に記録 |
監視・アラートMonitoring & alerts
通知先は system_admin 全員にメールで統一します。Slack / PagerDuty は採用せず、運用負荷とコストを抑えます。アプリ独自の業務的な信号は app_alerts テーブルにライト版で起票し、/admin/alerts から閲覧できます。
| 条件 | 通知 |
|---|---|
| Cloud Run の 5xx 率 > しきい値 | Cloud Monitoring → メール(system_admin 全員) |
scan_status='infected' > 0 | 即時メール(system_admin + UL者 + Box Owner、Resend) |
| ClamAV シグネチャ更新の連続失敗 | メール(system_admin 全員) |
sync_jobs.status='dead_letter' > 0 | メール(system_admin 全員) |
app_alerts の critical > 0 | メール(即時、system_admin 全員) |
| Cloud Billing 月予算超過 | 段階的な割合でメール |
| 例外・エラー | Cloud Error Reporting が Cloud Logging から自動集約。Sentry SaaS は不採用 |
app_alerts の 7 種: ① 感染検出、② Pub/Sub DLQ のしきい値超過、③ Drive 同期の連続失敗、④ 共有リンク ブルートフォース、⑤ Resend バウンス率のしきい値超過、⑥ ストレージ 80%、⑦ Cloud Billing 超過。/admin/alerts から「対応中 / 対応完了 / 無視(理由付き)」の手動ステータス遷移ができます。
運用作業のサイクルOperations cadence
リアルタイムのアラート対応に加えて、日次・週次・月次・四半期・年次のリズムで運用作業を分散させます。20 年運用に耐えるための仕掛けを定期的に動かします。
| サイクル | 作業内容 |
|---|---|
| リアルタイム | app_alerts の critical / warning への対応。Cloud Run 5xx 率や ClamAV 異常検知のメール対応 |
| 日次 | /admin ヘルスサマリ確認、日次バッチ(保持削除・Drive 補修・admin_metrics 集計)の成否確認 |
| 週次 | 共有リンク監査(残存 7 日以内のハイライト確認)、サマリ通知(digest)の到達確認 |
| 月次 | ストレージ予算レビュー、/admin/boxes / /admin/users の死蔵ハイライト確認、ClamAV 検出テスト(EICAR) |
| 四半期 | バックアップリストア演習(DB を過去の時点へ PITR でリードオンリー復元)、SLO レビュー |
| 年次 | シークレット一括ローテ(JWKS / OAuth client_secret / Resend API Key / BETTER_AUTH_SECRET / Supabase DB パスワード)、災害復旧訓練、SLA / SLO 見直し、無効化ユーザの 5 年保持期限確認 |
長期運用バッチ(18 種)Background jobs
ユーザ操作の裏で動く 18 種類の定期バッチを、単一の Cloud Run Jobs サービス docshare-batch に統合しています。Cloud Scheduler から起動し、--args でジョブ種別を振り分けます。
| ジョブ | 頻度 | 役割 |
|---|---|---|
| outbox-dispatcher | 1 分 | Outbox テーブルの未送信イベントを Pub/Sub に publish |
| dlq-monitor | 5 分 | Dead Letter Queue の件数を監視し、しきい値超で critical アラート起票 |
| share-link-cleanup | 5 分 | 失効期日を過ぎた共有リンクを revoked 化 |
| digest-worker | 30 分 | 該当時刻のユーザの digest 通知をまとめて Resend 送信 |
| retention-deletion-daily | 毎日 | 保持期限切れファイルの論理削除と当日メール通知 |
| trash-cleanup-daily | 毎日 | 論理削除 30 日経過分の物理削除 + Drive ゴミ箱送り |
| drive-repair | 毎日 | Box 単位の並列で Drive 差分補修・orphan 検知・削除検知 |
| admin-metrics-aggregator | 毎日 | storage / user_activity / box_activity の集計と materialized view 更新 |
| retention-warning-aggregator | 毎日 | Box Owner 向け週次「今週削除予定」サマリ生成 |
| expired-invitations-cleanup | 毎日 | 期限を過ぎた招待・参加申請を自動失効 |
| workspace-directory-sync | 毎日 | Google Workspace Directory を読んで在籍状態をユーザに反映 |
| version-cleanup-weekly | 週次 | Box 設定の世代数を超えた旧バージョンを GCS から削除 |
| drive-archive-sync | 週次 | Drive アーカイブの exists_in_drive を週次で更新 |
| audit-log-partition-rotator | 月次 | 25 ヶ月超のパーティションを GCS archive class へ退避し DROP |
| notifications-cleanup-monthly | 月次 | 保存期間を過ぎた通知履歴を単純 DELETE |
| clamav-eicar-test | 月次 | EICAR テストファイルを自動 UL して検出確認の E2E |
| disabled-user-cleanup-5y | 月次 | 5 年経過の無効化ユーザを system_admin 承認のうえ完全消去 |
| jwks-rotator | 年次 | Better Auth の JWT 鍵を年次ローテ、旧鍵の一定期間の並行公開で無停止切替 |
コストの構成Cost structure
費用は、Cloud Run(サービス 4 + Jobs 1)・Cloud Storage(保管と転送)・Cloud Armor / ロードバランサ / 監視の従量課金と、Supabase Pro のプラン料金、Resend(無料枠を超えた分だけ従量)で構成します。Cloud Billing で月予算を設定し、段階的な割合でメール通知します。
長期運用リスクと軽減策Long-term risks & mitigations
20 年運用を見据えたとき、想定すべきリスクと対処をあらかじめ設計に組み込みます。ここでは代表的な 4 件を挙げます。詳細リスト(R-01 〜 R-25)は別資料に整理しています。
監査ログ肥大化
操作のたびに行が増えるため、20 年運用ではテーブルが極めて巨大化します。インデックスや VACUUM の負荷も増えます。
軽減策: pg_partman で月次パーティション、25 ヶ月超を GCS archive class へ退避(CSV / Parquet)、一定期間の後に完全削除。常時オンラインのテーブルサイズを抑えます。
GCS 容量暴走
保持期間設定や Hold の使い方によっては、ストレージ容量が想定外に増加し、月額コストを圧迫します。
軽減策: 全社デフォルト 180 日 + Hold 方式、/admin/storage での予算ライン警告、Object Versioning の noncurrent_version_expiration(一定期間)、Cloud Billing アラート、Box 別 Hold バッジ常時表示(一定容量以上)。
JWT 鍵の長期未ローテ
Better Auth が発行する JWT 鍵を放置すると、鍵漏洩時の影響範囲が拡大します。手動ローテは忘れがちです。
軽減策: jwks-rotator Job が年次ローテを自動実行、旧鍵を一定期間並行公開して無停止切替。鍵の使用が一定日数を超えると warning アラート。
死蔵ユーザ・退職者残存
退職者のアカウントがいつまでも残ったり、未ログインユーザが滞留すると、不要な攻撃面が広がります。
軽減策: 60 日未ログインを /admin/users でハイライト、workspace-directory-sync で Google Workspace の在籍状態を反映し自動無効化を提案、expired-invitations-cleanup で 30 日経過招待を失効、disabled-user-cleanup-5y で 5 年経過後の完全消去。