運用・セキュリティ
納品後の運用で押さえるべき項目: BigQuery allowlist / cost cap / RBAC マトリクス / Sandbox / 監査 / Retention / Observability / Job 状態機械 / 通知。
運用の 6 つの柱
本製品の運用では、資格情報・コスト・監査・ジョブ再開・データ保持・PII 保護を明確に管理します。
BigQuery 接続
本番接続先は allowlist で制限し、生 xhro・xhro_backup・tmp・clns・dataflow_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 復号して rawuidを取得し、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_logにcost_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 は「ask を allow に格下げするモード」であって、ハードガード (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,statsmodelslightgbm,xgboost,torch,lifelinessn.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 |
|---|---|---|
| Bundle | 365 日 (1 年) | bundle.retention_days |
| Bundle (コールド移行) | 90 日後にコールド | bundle.archive_after_days |
| Bundle (1 個のサイズ上限) | 10 GB | bundle.max_size_gb |
| Evidence | 1825 日 (5 年) | evidence.retention_days |
| 監査ログ | 2555 日 (7 年) | audit.retention_days |
| Feature value cache | 30 日 | cache.feature_value_ttl_days |
| Studio preview cache | 600 秒 | cache.preview_ttl_sec |
| Pyramid (Bundle と同寿命) | 0 (Bundle と同じ) | cache.waveform_pyramid_ttl_days |
| Wizard drafts | 24 時間 | ハードコード (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_ready | ✓ | build 起動者 |
| 仮説実行完了 | notify.events.hypothesis_done | ✓ | pre-reg 起動者 |
| evidence stale 化 | notify.events.stale_evidence | ✓ | Project 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_keysecret)。 - ✓
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 リージョンが動いている。