operations

運用・セキュリティ

納品後の運用で押さえるべき項目: BigQuery allowlist / cost cap / RBAC マトリクス / Sandbox / 監査 / Retention / Observability / Job 状態機械 / 通知。

運用の 6 つの柱

本製品の運用では、資格情報・コスト・監査・ジョブ再開・データ保持・PII 保護を明確に管理します。

BigQuery 接続

本番接続先は allowlist で制限し、生 xhroxhro_backuptmpclnsdataflow_dev は既定 blocklist。BQ クライアントは Studio API / Worker のみ。

Cost 管理

UI preview / window preview / analysis dataset / export / daily user / daily tenant の上限を Settings で分けて持つ。BQ ジョブは dry_run → confirm → submit の 3 段。

監査ログ

設定変更 / Bundle build / Agent tool call / pre-registration / verdict は who / when / input hash / output hash を残す。7 年保持。

Sandbox 隔離

Workspace の任意 Python 実行は read-only Bundle mount / network off / CPU/memory/wall 制限 / stdout JSON envelope に限定。gVisor 等で隔離。

Retention

Bundle 1 年 / Evidence 5 年 / Audit 7 年。Evidence と監査ログは長期保存前提。コールドストレージ移行は 90 日。

Observability

OpenTelemetry endpoint / 構造化ログ / ジョブ progress / Agent stream event / SSE reconnect を運用監視の基本にする。

1. BigQuery セキュリティ

allowlist / blocklist

本番接続先 (sic-poc1-verify) のテーブルアクセスは Settings で制御されます。

bq.source_allowlist (既定)
["xhro_01.*", "xhro_02.*", "xhro_03.*", "xhro_04.*", "xhro_view.*", "xhro_04_modeling_v2.*"]
bq.source_blocklist (既定)
["xhro", "xhro_backup", "tmp", "clns", "dataflow_dev"] — allowlist より強い

xhro を blocklist に入れる理由: requirePartitionFilter=true なのに rangePartitioning の境界が秒スケール、データは ms スケールという不整合があり、全 6.3 G 行が __UNPARTITIONED__ に積まれた状態で、どんな timestamp フィルタを書いてもパーティション削減が効きません。誤って読みに行くと「データなし」が返るか、長時間スキャン後に cost cap 違反でエラーになります。詳細: data § xhro テーブルの罠

サービスアカウント鍵

  • 本番: bq.service_account_key (Settings 経由、KMS 暗号化済 secret)。
  • ローカル開発: .keys/suntory-analytics-bq.json (600 権限、git 管理外)。
  • WIF (Workload Identity Federation): bq.workload_identity_audience を設定すると鍵レス認証に切替可能。
  • ローテーション: SA 鍵を発行し直したら同じ path に上書きするだけ。古い鍵は gcloud iam service-accounts keys delete で取り消し。

SQL の安全性

  • 全クエリは dry_run.estimate(query, purpose) を通す (FR-X2)。`purpose` は ui_preview / preview_window / analysis_dataset / export のいずれか必須。
  • SQL テンプレは packages/bq-access/templates/ 配下に Jinja2 でハードコード。動的 SQL を生成する経路はない。
  • identity 解決: UI / API は public_id のみ。BQ 発火直前に subject_id_map から KMS 復号して raw uid を取得し、WHERE uid = ? に渡す。raw uid はログ / audit payload に出さない。

2. Cost 管理

BQ のコストは purpose 別 cap + daily 累計 cap の 2 段構えで制御します。Settings 値は Tenant scope で変更可能。

Setting key既定値対象超過時の挙動
bq.cost_cap_usd.ui_preview 0.05 USD ui_preview purpose (Step 1-2 のカタログ・availability) ヒット時: HTTP 402 + 「予算外」表示
bq.cost_cap_usd.preview_window 0.50 USD preview_window purpose (Window 抽出) ヒット時: Build ボタン disabled + 黄帯警告
bq.cost_cap_usd.analysis_dataset 5.00 USD analysis_dataset purpose scientist 以上が必要
bq.cost_cap_usd.export 25.00 USD export purpose (大規模エクスポート) admin 承認 + Slack 通知が必要
bq.daily_user_cap_usd 10.00 USD ユーザ 1 日累計 超過で当日 deny
bq.daily_tenant_cap_usd 100.00 USD テナント 1 日累計 超過でテナント全体 deny + admin 通知

cost 監視と通知

  • BQ ジョブ完了時に audit_logcost_usd を記録。日次集計で bq.daily_*_cap_usd 判定。
  • notify.events.cost_threshold = true の user / tenant admin に Slack / メールで通知。
  • LLM コストは copilot.cost_alert_usd_per_day (既定 5.00 USD) を超えると Copilot Pane に黄帯表示。
  • 大規模 export (bq.cost_cap_usd.export 適用) は admin 事前承認画面が出る。

3. RBAC マトリクス (役割 × 操作)

rbac.matrix_json (Tenant scope) で完全に定義可能ですが、組込既定が出荷されます。HUMAN_ONLY マークは「Agent / Copilot から呼んでも常に deny される」操作です。

権限 viewer annotator scientist modeler admin
studio:catalog:read
studio:bundle:read
studio:bundle:build
studio:bundle:archive
studio:bundle:delete ✓ (admin only)
studio:recipe:create
studio:recipe:share:tenant
studio:settings:edit:user
studio:settings:edit:project
studio:settings:edit:tenant
workspace:project:read
workspace:project:create
workspace:annotation:create
workspace:annotation:freeze ✓ (HUMAN_ONLY)
workspace:notebook:apply
workspace:hypothesis:pre_register ✓ (HUMAN_ONLY)
workspace:hypothesis:verdict ✓ (project_owner)
workspace:model:train
workspace:model:pre_register ✓ (HUMAN_ONLY)
workspace:model:promote_to_registry

API での宣言的強制

from fastapi import Depends
from studio_api.auth.rbac import require_perm

@router.post("/v1/bundles", dependencies=[Depends(require_perm("studio:bundle:build"))])
async def create_bundle(req: BuildRequest, ...): ...

# 違反時: 403 Forbidden + audit_log に "rbac.deny" 記録

Agent / Copilot からの呼び出し制御

# canUseTool callback (claude-agent-sdk)
async def can_use_tool(tool_name, tool_input, ctx) -> PermissionResult:
    HUMAN_ONLY = {
        "mcp__studio__studio.build_bundle",
        "mcp__hypothesis__hypothesis.pre_register",
        "mcp__model__model.pre_register",
        "mcp__annotation__annotation.freeze_set",
    }
    if tool_name in HUMAN_ONLY and ctx.invoked_by_ai:
        return { "behavior": "deny",
                 "message": "Human approval required. Proposed but not executed." }

    if not rbac_allows(ctx.user.role, tool_name, tool_input):
        return { "behavior": "deny",
                 "message": f"Role {ctx.user.role} cannot call {tool_name}." }

    return { "behavior": "allow" }

bypassPermissions モード: CI / 定期実行用の permissionMode: bypassPermissions は「askallow に格下げするモード」であって、ハードガード (HUMAN_ONLY) は外れません。Pre-registration / verdict / build_bundle / freeze_set は CI からも deny されます (人間が朝確認する)。

4. Sandbox 隔離 (Workspace Notebook 実行)

Workspace の Notebook セル実行は gVisor 等の sandbox で隔離します。Studio Bundle build パイプラインは sandbox 外 (信頼境界内) で動きますが、Workspace の任意 Python セルは sandbox 必須。

制限

  • ファイルシステム: read-only Bundle mount + scratch tmpfs (`/tmp` 1 GB)。Bundle 以外への書き込み禁止。
  • ネットワーク: sandbox.allow_network = false 既定 (Settings)。外向き通信不可。
  • CPU: sandbox.cpu_cores = 2 (Settings)
  • メモリ: sandbox.memory_mb = 2048
  • Wall time: sandbox.wall_time_sec.feature = 15 / sandbox.wall_time_sec.hypothesis = 120
  • stdout: JSON envelope に限定。生 print は捕捉されない。

許可される import

  • 標準ライブラリ全般
  • numpy, pandas, scipy, scikit-learn, statsmodels
  • lightgbm, xgboost, torch, lifelines
  • sn.ecg.* (built-in 特徴量), sn.bundle (Bundle 読み出しヘルパ), sn.ann (AnnotationSet 読み出し)

禁止 import (deny list)

  • requests, urllib, socket (network)
  • subprocess, os.system (shell)
  • importlib 動的ロード (sandbox 突破防止)

5. 監査ログ

本製品はあらゆる書き込み系操作で監査ログを残します。7 年保持 (audit.retention_days)。誤操作の追跡と pre-registration 改ざん検知が目的です。

監査対象

対象記録項目保持期間
設定変更 who / when / old / new (機密はハッシュのみ) 7 年 (audit.retention_days)
Bundle build actor_id / canonical_hash / cost_usd / duration_sec / source_key 7 年
Bundle archive/delete actor_id / bundle_id / reason 7 年
Agent tool call actor_id / agent_id / subagent / tool / input_hash / output_hash / cost_usd 7 年
Pre-registration actor_id / frozen_hashes / project_id 7 年 (改ざん検知)
Verdict actor_id / hypothesis_id / verdict / 結論テキスト 7 年
AnnotationSet freeze/unfreeze actor_id / set_id / 操作 7 年
権限変更 actor_id / target_user_id / old_role / new_role 7 年

テーブル構造

CREATE TABLE audit_log (
  id          bigserial PRIMARY KEY,
  actor_id    text,
  tenant_id   text,
  action      text NOT NULL,           -- "bundle.build.start", "settings.update", ...
  target_kind text,                    -- "bundle" / "hypothesis" / "settings_key"
  target_id   text,
  payload     jsonb,                   -- 操作のスナップショット (PII 禁止)
  ip          inet,
  user_agent  text,
  occurred_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ON audit_log (tenant_id, occurred_at DESC);
CREATE INDEX ON audit_log (action, occurred_at DESC);

夜次エクスポート

audit.export_destination = gcs / audit.export_bucket = (任意) を設定すると、日次バッチで gs://<bucket>/_audit-export/YYYY-MM-DD/... に JSONL 形式で書き出されます。長期保管・SIEM 連携用。

6. データ保持 (Retention)

対象既定保持期間Setting key
Bundle365 日 (1 年)bundle.retention_days
Bundle (コールド移行)90 日後にコールドbundle.archive_after_days
Bundle (1 個のサイズ上限)10 GBbundle.max_size_gb
Evidence1825 日 (5 年)evidence.retention_days
監査ログ2555 日 (7 年)audit.retention_days
Feature value cache30 日cache.feature_value_ttl_days
Studio preview cache600 秒cache.preview_ttl_sec
Pyramid (Bundle と同寿命)0 (Bundle と同じ)cache.waveform_pyramid_ttl_days
Wizard drafts24 時間ハードコード (FR-X13)
GCS staging (中間 Parquet)7 日 (Object Lifecycle)GCS 側で管理

削除ポリシー

  • Bundle の物理削除は admin のみ。通常は archived フラグで論理削除。
  • 被験者 erasure (GDPR / 個人情報保護法) 時のカスケード規則は別途定義 (`docs/08-data-governance.md` 未作成)。現状は admin が手動で関連 Bundle / AnnotationSet を archive。
  • Evidence / 監査ログは 長期保管前提。retention を短くする場合は法務確認が必要。

7. Observability

OpenTelemetry / 構造化ログ / メトリクスの 3 軸で観測します。

Setting key既定説明
diag.otel_endpoint string OpenTelemetry コレクタ URL (例: http://otel-collector:4318/v1/traces)
diag.log_level enum info 実行時ログレベル (DB 不通時のフォールバックは boot env)
diag.trace_sample_rate float 0.1 トレースサンプリング率 (0.0-1.0)
diag.slow_query_threshold_ms int 1000 スロークエリしきい値 (これ以上のクエリは WARN ログ)

構造化ログのフィールド

# structlog で全 API リクエストに以下を注入
{
  "timestamp":  "2026-05-25T12:34:56.789Z",
  "level":      "info",
  "service":    "studio-api",
  "version":    "0.1.0",
  "trace_id":   "abc123...",          # OpenTelemetry trace ID
  "span_id":    "def456...",
  "tenant_id":  "tnt_01HZMX...",
  "user_id":    "usr_01HZMX...",
  "request_id": "req_01HZMX...",
  "method":     "POST",
  "path":       "/v1/bundles",
  "status":     202,
  "duration_ms": 145,
  "message":    "bundle build enqueued"
}

メトリクス (Prometheus 形式)

  • sn_api_requests_total{method,path,status} — リクエストカウンタ
  • sn_api_request_duration_seconds{method,path} — レイテンシヒストグラム
  • sn_bundle_build_duration_seconds{phase,status} — Build phase レイテンシ
  • sn_bundle_build_cost_usd_total — BQ コスト累計
  • sn_settings_changes_total{scope,key} — 設定変更カウンタ
  • sn_copilot_tokens_used_total{model} — LLM トークン使用量

SLO / アラート (推奨設定例)

  • Studio API 5xx 率 > 1% (5min) → critical
  • Studio API p95 latency > 2s (5min) → warning
  • Worker queue depth > 100 (5min) → warning
  • Bundle build failure rate > 5% (1h) → critical
  • BQ daily cost > 80% of bq.daily_tenant_cap_usd → warning

8. Job 状態機械と障害時運用

Bundle build ジョブ (Arq worker) は phase 境界で pause / resume / cancel 可能です (FR-X9)。詳細は studio § Job 状態機械

各 phase のガード

Phaseガード失敗時の挙動
extract cost_cap (preview_window) / scan_bytes_cap BQ job 起動前に dry-run、超過時は queued のまま failed
clean wall_time (sandbox では無関係。Worker 側 5 分) 無し (実装が決定論的)
detect_rpeaks wall_time 5 分 / 拍数異常 (>240bpm 平均) 異常時は WARN ログ、続行
hrv wall_time 3 分 無し
pyramid GCS 書き込み失敗時に 3 リトライ 失敗 → failed
attach 各 family ごとに extract 同様 個別 cap
manifest integrity hash 計算で異常検知 sha256 不一致 → failed (再実行)
register Postgres トランザクション (1 回のみ) _READY 書き込み失敗 → manual cleanup 要 (Slack 通知)

resume の冪等性

  • resume は 新しい job_id を作らない (同 job の続行)。
  • 同 canonical_hash の Bundle が既に ready なら、resume は再実行せず done を返す (FR-X6)。
  • checkpoint の中間 Parquet (gs://.../_staging/{job_id}/<phase>/) は GCS Object Lifecycle で 7 日後に自動削除。

長時間 stuck の対処

  • Stuck on extract: BQ side の遅延。bq jobs describe <job_id> で確認。タイムアウトは 30 分で自動 cancel。
  • Stuck on register: Postgres トランザクション競合。pg_stat_activity で確認。最大 5 分で release。
  • Worker クラッシュ: Arq は最大 3 回まで自動 retry。失敗時は jobs.error_code = "worker_crashed" で記録、Slack 通知。

手動オペレーション

# 全 stuck ジョブの確認
psql $SN_DB_URL -c "
  SELECT id, status, phase, progress, created_at, NOW() - created_at AS age
  FROM jobs
  WHERE status IN ('queued', 'running', 'paused', 'canceling')
    AND created_at < NOW() - interval '1 hour'
  ORDER BY created_at;"

# 手動キャンセル (admin)
curl -X POST -H "Cookie: ..." http://api.local/v1/jobs/job_01HZMX.../cancel

# checkpoint クリーンアップ (Object Lifecycle が動かない場合)
gsutil -m rm -r "gs://sn-bundles/_staging/job_01HZMX.../"

9. 通知 (Notifications)

運用上のイベントをユーザー / 管理者に通知します。通知チャネルは Email / Slack / アプリ内の 3 種類。

イベント種別

イベントSetting key既定 ON対象
Bundle 完成notify.events.bundle_readybuild 起動者
仮説実行完了notify.events.hypothesis_donepre-reg 起動者
evidence stale 化notify.events.stale_evidenceProject owner
コスト警告notify.events.cost_threshold当該 user + tenant admin
Bundle build 失敗(常時)build 起動者 + 当該 admin
Job 24h 以上 stuck(常時)tenant admin
pre-registration 改ざん試行(常時)tenant admin + security チャネル

Slack 連携

# Settings (Tenant scope)
notify.channel.slack         = true
notify.slack_webhook_url     = <Incoming Webhook URL>   # KMS 暗号化済 secret

# 通知メッセージ例
[Bundle Ready] bdl_01HZMX... (XHRO-04 / XH015) — 12.4 MB, USD 0.0023
[Job Failed] job_01HZMX... bq_cost_cap_exceeded — 詳細を見る: <URL>
[Cost Alert] tenant tnt_01HZMX... reached 80% of daily cap (USD 80 / 100)

日次サマリ

notify.daily_digest_time_local (既定 09:00) に、前日のサマリ (作成 Bundle 数 / 失敗ジョブ / コスト / アクティブ user) を user に送信。Slack の場合は DM ではなくチャンネル投稿可能。

10. バックアップとディザスタリカバリ

Postgres バックアップ

  • GCP Cloud SQL の自動バックアップ (毎日 02:00 JST、7 日保持)。
  • Point-in-time recovery (PITR): 直近 7 日間の任意時点へ復元可能。
  • 本番リカバリ手順は infra/runbooks/postgres-recovery.md (未作成、別途整備)。

GCS バケット

  • Bundle バケット (storage.bucket) は Object Versioning + Object Lifecycle 設定。
  • 削除後 30 日以内なら復元可能。
  • リージョン: us-central1 (本番) / us-east1 (DR 用)。
  • Bundle は immutable なので、Object Versioning は staging のみで実質意味あり。

Settings の外部保存

JSON エクスポート機能で Tenant 設定をまるごと外部保存可能 (機密は除外)。インシデント時の再構成用。

# Tenant 設定のエクスポート
curl -H "Cookie: ..." http://api.local/v1/settings/tenant/tnt_01HZMX.../export \
  > tenant-settings-backup-2026-05-25.json

# 別環境への流し込み
curl -X POST -H "Cookie: ..." -d @tenant-settings-backup-2026-05-25.json \
  http://newapi.local/v1/settings/tenant/tnt_01HZMX.../import

セキュリティチェックリスト (本番デプロイ前)

  • .env にブート env 10 個以外が入っていない (機密は Settings + KMS 経由)。
  • BQ service account JSON は KMS 暗号化済 (bq.service_account_key secret)。
  • bq.source_blocklist に生 xhro が入っている (partition filter 罠回避)。
  • auth.mfa_required = true
  • auth.allowed_ip_cidrs でアクセス元 IP を制限 (必要に応じて)。
  • OIDC issuer / audience / client secret が正しく設定済み。
  • Cookie が HttpOnly + Secure + SameSite=Lax で発行されている。
  • Workspace API の Service Account に BQ 権限が 付いていない
  • Sandbox の allow_network = false
  • Bundle manifest / Parquet / audit_log の PII grep test が通る (CI)。
  • storage.signed_url_ttl_sec が短い (既定 900 秒)。
  • audit.export_destination = gcs で日次エクスポートが流れている。
  • diag.otel_endpoint でトレース / メトリクスが収集されている。
  • SLO アラート (5xx / latency / cost / queue depth) が PagerDuty / Slack に配線済み。
  • Postgres バックアップ + GCS Object Versioning + DR リージョンが動いている。