MA拡張機能 Cloudflare 構築・運用手順書
— AI-LP生成/配信/計測基盤の作り方と回し方 —
| 項目 | 内容 |
|---|---|
| 文書名 | MA拡張機能 Cloudflare 構築・運用手順書 |
| バージョン | v1.0 |
| 作成日 | 2026年8月15日 |
| 対象 | MA_拡張機能仕様書_v2.0.md(F-11〜F-24)の実装・運用担当者 |
| 前提リポジトリ | cf-internal-app-starter(Hono + React + D1 + R2 + KV + Vectorize + Access) |
| 関連文書 | MA_拡張機能仕様書_v2.0.md、MA_機能仕様書.md(v1.2)、cloudflare-feature-catalog-synon.md、cf-internal-app-starter/CLAUDE.md |
目次
- この手順書の使い方
- 構成方針の決定 — 案A / 案B
- Cloudflare サービスの役割分担
- 事前準備
- 構築手順 STEP 1 — リポジトリとリソースの作成
- 構築手順 STEP 2 — バインディングとスキーマ
- 構築手順 STEP 3 — 認証とドメイン
- 構築手順 STEP 4 — LP配信 Worker
- 構築手順 STEP 5 — 計測パイプライン
- 構築手順 STEP 6 — AI生成パイプライン
- 構築手順 STEP 7 — 統計バッチ
- デプロイ運用
- 日次・週次・月次の運用手順
- 監視とアラート
- コスト管理
- トラブルシューティング
- 障害対応・緊急停止
- セキュリティ運用
- 移行手順(案B を選んだ場合)
- チェックリスト
- 付録: 主要コマンド早見表
1. この手順書の使い方
本書は上から順に実行できる手順書である。章ごとに「やること → コマンド → 確認方法 → よくある失敗」の形式で記述する。
| 読者 | 読むべき章 |
|---|---|
| これから構築する人 | 2 → 4 → 5〜11 → 12 → 20 |
| 日々運用する人 | 13 → 14 → 16 → 17 → 21 |
| 判断する立場の人 | 2 → 3 → 15 → 19 |
記法
$で始まる行はシェルコマンド。$は入力しない- 🔒 は秘密情報を扱う手順。ログ・Slack・スクリーンショットに残さない
- ⚠ は失敗すると本番に影響する手順
- ✅ は確認方法
数値の扱い: 本書の料金・上限値は 2026年8月時点の Cloudflare 公式ドキュメントに基づく。変わりうるので、実際の課金判断の前に必ず公式ページを確認すること。 確認が取れなかった項目は「要確認」と明記している。
2. 構成方針の決定 — 案A / 案B
最初にこれを決める。 決めずに手を動かすと、途中で全部やり直しになる。
2.1 二択の中身
| 案A: 部分適用 | 案B: 全面移行 | |
|---|---|---|
| 対象 | 新機能(F-11〜F-24)のみ Cloudflare | MA本体(F-01〜F-10)も含めて Cloudflare |
| 既存MA | そのまま(Node.js/Python + SQLite) | 廃止し Workers + D1 へ移植 |
| Worker数 | 1〜2(LP配信 + 計測) | 1(管理画面SPA + API + LP配信を同梱) |
| 認証 | 既存の認証を継続 + 新規APIはトークン認証 | Cloudflare Access に一本化 |
| 企業マスタ | 既存DBが正。定期同期でD1へコピー | D1 が正 |
| 初期工数の目安 | 2〜3人月 | 5〜8人月(既存の実装量による) |
| 月額インフラ費 | 既存サーバ費 + 約 $5 | 約 $5(サーバ費・DB費が消える) |
| 運用対象 | 2系統(既存 + Cloudflare) | 1系統 |
| 主なリスク | 企業マスタ同期のズレ、認証が2系統 | 移行期間中の二重運用、Workers制約への適合 |
2.2 決定フロー
v1.2(F-01〜F-10)の実装は完了しているか?
│
┌───────────┴───────────┐
YES NO
│ │
本番データが蓄積しているか? │
│ │
┌───────┴───────┐ │
YES NO │
│ │ │
【案A】 【案B】 【案B】
部分適用から 全面移行 最初から一本化
始めて、効果 したほうが したほうが
確認後に案Bを 総工数が小さい 総工数が小さい
検討する
受託案件への横展開を視野に入れるなら、迷わず案B。 構成をそのままテンプレート化して顧客案件に転用できる価値が大きい。
2.3 案Aを選んだ場合の追加考慮
企業マスタの同期方式を決める必要がある。
| 方式 | 内容 | 遅延 | 推奨 |
|---|---|---|---|
| 定期バッチ同期 | 既存MA → D1 へ日次で洗い替え | 最大24時間 | △(ABM-LPの鮮度が落ちる) |
| 差分同期 | 更新分のみを毎時 | 最大1時間 | ○ まずこれ |
| API参照 | LP配信時に既存MAのAPIを都度呼ぶ | なし | ×(LPのTTFBが既存サーバに引きずられる) |
| Webhook | 既存MA側で更新時にD1へPUSH | ほぼなし | ◎(既存MAに手を入れられるなら) |
⚠ API参照方式は選ばないこと。 LPの表示速度が既存サーバの応答速度に律速され、CWVが崩れる。
3. Cloudflare サービスの役割分担
3.1 サービス選定表
| 用途 | 使うもの | 理由 | 無料枠 / 費用(2026年8月時点) |
|---|---|---|---|
| LP配信・API・SPA配信 | Workers (Static Assets) | Cloudflare公式が新規はWorkers推奨。SPAとAPIが1デプロイ単位 | Free 10万req/日 / Paid $5月〜(月1,000万req込み) |
| 業務データの正 | D1 | SQL・結合・即時整合。実験/バリアント/割付 | Free 読取500万行/日・書込10万行/日 / Paid 月25億読取・5,000万書込込み |
| 配信重み・設定のキャッシュ | KV | 読み >>> 書き。結果整合でも実害なし | Free 読取10万/日・書込1,000/日 |
| ファイル実体 | R2 | LP HTML・スクショ・アーカイブ。egress無料 | Free 10GB-月・ClassA 100万・ClassB 1,000万 |
| 生イベント計測 | Workers Analytics Engine | カーディナリティ無制限。バリアント×企業×セグメントを気にせず書ける | 現在無課金。将来 Free 書込10万/日 |
| バンディット状態 | Durable Objects (SQLite backend) | カウンタ更新を直列化。D1へのロック競合を避ける | Free 10万req/日 |
| 生成パイプライン | Workflows | 50件の生成→検査→結合を耐久実行。途中失敗から再開できる | Free ステップ3,000/日 |
| 非同期処理 | Queues | 生成ジョブ・同期ジョブの投入 | Free 1万操作/日(1操作=64KB) |
| AI生成(下書き・分類・埋め込み) | Workers AI | 安価・低レイテンシ | Free 10,000 Neurons/日 |
| AI生成(本番コピー) | AI Gateway 経由の外部LLM | 品質。コスト可視化・キャッシュ・レート制限が付く | コア機能無料 |
| 重複検査 | Vectorize | 埋め込みの類似検索。埋め込みは日本語対応モデルを使う | Free クエリ次元3,000万/月 |
| プレビュー画像 | Browser Run(旧 Browser Rendering) | スクリーンショット・OGP生成 | Free 10分/日・同時3ブラウザ |
| 画像最適化 | Cloudflare Images | LPの画像配信。LCP改善 | Free 月5,000ユニーク変換 |
| ボット対策 | Turnstile | LPフォーム・資料DLフォーム | 無料 |
| 管理画面の認証 | Cloudflare Access | 自前セッションを実装しない | Zero Trust Free枠 |
| タグ・同意管理 | Zaraz | 外部送信規律への対応 | 月100万イベント無料 |
| サイト全体のRUM | Cloudflare Web Analytics | Cookieless。CWVの実測 | 無料 |
| シークレット管理 | Secrets Store(オープンベータ)または wrangler secret |
LLM/媒体APIキー | — |
| 定期処理 | Cron Triggers | 統計バッチ・同期 | — |
| CI/CD | Workers Builds または GitHub Actions | Git連携ビルド | Free 月3,000ビルド分 |
3.2 使ってはいけない組合せ(重要)
| ❌ やってはいけないこと | 理由 | 代わりにすること |
|---|---|---|
| バリアントを Workers の「バージョン」として持つ | Gradual Deployments は直近100バージョンまで、配分変更のたびにデプロイが要る、割付粒度がリクエスト単位 | バリアントはD1のデータとして持ち、単一Workerが描画する |
| 割付結果・計測イベントを KV に書く | KVは結果整合。書いた直後に読むと古い値が返る。計測が壊れる | D1 / Analytics Engine / Durable Objects |
| 生イベントを D1 に1件ずつINSERTする | 書込行数課金 + ロック競合。月100万イベントで詰まる | Analytics Engine に writeDataPoint() |
| バリアントのカウンタを D1 に高頻度UPDATE | 同一行への競合でエラー多発 | Durable Objects で直列化 → 日次でD1へ同期 |
| LP HTML を D1 に入れる | D1は1DB最大10GB(Paid)。行サイズ上限2MB | 実体はR2、D1には r2_key だけ |
| R2 のオブジェクトを公開URLで配る | URLを知れば誰でも取れる | 必ずWorker経由で権限チェック |
| 配信時に優越確率のモンテカルロ10万回を回す | CPU時間を使い切る | 日次バッチで計算しKVにキャッシュ。配信時は1サンプルのみ |
| Free プランでLP配信を本番運用 | CPU時間10ms/呼出。割付+D1+計測+統計で確実に超える | Workers Paid($5/月)は必須と考える |
robots.txt で実験LPディレクトリを Disallow |
他ページからのリンク経由で検索結果に出うる。AdsBotも塞ぐと広告に支障 | <meta name="robots" content="noindex,follow"> |
4. 事前準備
4.1 必要なもの
| # | 項目 | 確認方法 |
|---|---|---|
| 1 | Cloudflare アカウント(Account ID) | ダッシュボード右下 |
| 2 | Workers Paid プラン($5/月) | ダッシュボード > Workers & Pages > Plans |
| 3 | ゾーン(ドメイン)が Cloudflare で管理されていること | DNS > 該当ドメインが Active |
| 4 | LP用のドメイン/サブドメインの決定 | 4.2参照 |
| 5 | Zero Trust の有効化(Access を使う場合) | Zero Trust ダッシュボード |
| 5-b | Zero Trust のシート数。「Free は50ユーザーまで」という情報があるが公式ページで確定できなかった。社員数がこれに近い場合は契約前に必ず確認する(1ユーザーが Access ログインまたは WARP の Gateway 接続をすると1シート消費。アプリ数・ログイン回数によらず1人1シート) | Zero Trust > Users |
| 6 | Node.js 20 以上 | $ node -v |
| 7 | GitHub リポジトリ(CI/CD用) | — |
| 8 | 🔒 LLM APIキー(外部LLMを使う場合) | — |
⚠ Workers Free プランでは本番運用しない。 CPU時間 10ms/呼び出しの制限があり、LP配信処理(文脈構築 + D1参照 + 割付 + 計測書き込み)は確実に超える。$5/月は投資判断が不要なレベル。
4.2 ドメイン設計
LPをコーポレートサイトと同じドメインに置かない。理由は次の3つ。
- 大量生成LPの評価が本体ドメインに影響しうる
- インデックス制御の事故が本体に波及しない
- 障害の影響範囲を分離できる
| 用途 | ドメイン例 | インデックス |
|---|---|---|
| コーポレートサイト | www.synon.co.jp |
index(既存) |
| LP配信 | lp.synon.co.jp |
代表LPのみ index、実験/ABM/広告LPは noindex |
| 管理画面 | ma.synon.co.jp |
全ページ noindex + Access保護 |
| 計測エンドポイント | lp.synon.co.jp/c |
— |
✅ 確認: dig lp.synon.co.jp が Cloudflare のIPを返すこと
4.3 命名規約
後から変えられないので最初に決める。
| リソース | 本番 | ステージング |
|---|---|---|
| Worker | synon-ma-lp |
synon-ma-lp-staging |
| D1 | synon-ma-db |
synon-ma-db-staging |
| R2 | synon-ma-assets |
synon-ma-assets-staging |
| KV | synon-ma-cache |
synon-ma-cache-staging |
| Vectorize | synon-ma-variants |
synon-ma-variants-staging |
| Analytics Engine データセット | ma_lp_events |
ma_lp_events_staging |
| Durable Object クラス | ExperimentCounter |
(同じ) |
| Queue | synon-ma-jobs |
synon-ma-jobs-staging |
5. 構築手順 STEP 1 — リポジトリとリソースの作成
5.1 スターターから開始する
社内には cf-internal-app-starter があり、Hono + React + D1 + R2 + KV + Access の構成が既に組んである。ゼロから作らない。
# 1. スターターを複製
$ cp -r cf-internal-app-starter synon-ma-lp
$ cd synon-ma-lp
$ rm -rf .git && git init
# 2. 名前を置き換える
# package.json の name, wrangler.jsonc の name / database_name / bucket_name
$ npm install
# 3. Cloudflare にログイン
$ npx wrangler login
$ npx wrangler whoami # ✅ Account ID が表示されること
5.2 リソースを作成する
スターターには scripts/bootstrap.sh があり、リソース作成とID埋め込みを自動化している。まずそれを実行する。
$ npm run bootstrap
自動化されていない、または本プロジェクト固有のリソースは手動で作る。
# --- D1 (本番 / ステージング) ---
$ npx wrangler d1 create synon-ma-db
$ npx wrangler d1 create synon-ma-db-staging
# → 出力された database_id を wrangler.jsonc に貼る
# --- R2 ---
$ npx wrangler r2 bucket create synon-ma-assets
$ npx wrangler r2 bucket create synon-ma-assets-staging
# --- KV ---
$ npx wrangler kv namespace create CACHE
$ npx wrangler kv namespace create CACHE --env staging
# --- Vectorize (バリアント重複検査用) ---
# ⚠ 先に埋め込みモデルを決めること。次元数はモデル依存(768次元が一般的)で、
# 作成後に変更できない(インデックス削除→再作成が必要)。
# 日本語コピーの類似度を測るので、必ず日本語(多言語)対応モデルを選ぶ。
$ npx wrangler vectorize create synon-ma-variants \
--dimensions=768 --metric=cosine
# --- Queues ---
$ npx wrangler queues create synon-ma-jobs
$ npx wrangler queues create synon-ma-jobs-dlq # 失敗ジョブの退避先
✅ 確認:
$ npx wrangler d1 list
$ npx wrangler r2 bucket list
$ npx wrangler kv namespace list
$ npx wrangler vectorize list
$ npx wrangler queues list
よくある失敗
| 症状 | 原因 | 対処 |
|---|---|---|
vectorize create が次元数エラー |
使う埋め込みモデルの次元と不一致 | 先にモデルを決めてから作る。作り直しはインデックス削除が必要 |
| ステージングのKV IDを本番に貼ってしまう | 目視ミス | wrangler deploy --dry-run でバインディング一覧を確認する習慣をつける |
6. 構築手順 STEP 2 — バインディングとスキーマ
6.1 バインディングを追加する
⚠ バインディングを足したら、必ず3箇所を同時に更新する。1つ忘れると本番で undefined になる。
wrangler.jsonc— 本番とenv.stagingの両方src/worker/types.tsのEnvインターフェースCLAUDE.mdの表(次にこのリポジトリを触る人・AIが読む)
wrangler.jsonc に追加するブロック(スターターの既存分に足す):
{
"name": "synon-ma-lp",
"main": "src/worker/index.ts",
"compatibility_date": "2026-08-01",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "./dist/client",
"binding": "ASSETS",
"not_found_handling": "single-page-application"
},
// --- 追加: Analytics Engine (生イベント) ---
"analytics_engine_datasets": [
{ "binding": "EVENTS", "dataset": "ma_lp_events" }
],
// --- 追加: Durable Objects (バンディット状態) ---
"durable_objects": {
"bindings": [
{ "name": "EXPERIMENT", "class_name": "ExperimentCounter" }
]
},
"migrations": [
{
"tag": "v1",
// SQLite バックエンドを使う (Cloudflare が新規名前空間で推奨)
"new_sqlite_classes": ["ExperimentCounter"]
}
],
// --- 追加: Queues ---
"queues": {
"producers": [ { "binding": "JOBS", "queue": "synon-ma-jobs" } ],
"consumers": [
{
"queue": "synon-ma-jobs",
"max_batch_size": 10,
"max_retries": 3,
"dead_letter_queue": "synon-ma-jobs-dlq"
}
]
},
// --- 追加: Browser Rendering (プレビュー生成) ---
"browser": { "binding": "BROWSER" },
// --- 追加: Workflows (生成パイプライン) ---
"workflows": [
{
"name": "lp-generation",
"binding": "LP_GEN",
"class_name": "LpGenerationWorkflow"
}
],
// --- Cron: 統計バッチ (すべて UTC。JST は +9h) ---
"triggers": {
"crons": [
"0 * * * *", // 毎時00分 : イベント集計 → D1 日次サマリ
"5 * * * *", // 毎時05分 : GA4 Data API 増分同期 (既存F-09)
"0 17 * * *", // JST 02:00 : 優越確率の再計算
"30 17 * * *", // JST 02:30 : 因子効果の推定
"0 18 * * *", // JST 03:00 : 逐次淘汰の判定
"0 19 * * *", // JST 04:00 : スコア減衰・架電キュー再構築
"0 20 * * *", // JST 05:00 : 広告オーディエンスの同期
"0 21 * * *", // JST 06:00 : noindex/インデックス率の監視
"0 22 * * 0", // JST 月07:00 : 週次レポートのメール配信
"0 15 1 * *" // JST 1日00:00 : 3か月超の生イベントを R2 へアーカイブ
]
}
}
⚠ Cron は UTC で書く。 JST の時刻から9時間を引く。日付をまたぐ場合は曜日指定もずらす。
src/worker/types.ts:
export interface Env {
// 既存(スターター)
ASSETS: Fetcher;
DB: D1Database;
FILES: R2Bucket;
CACHE: KVNamespace;
VECTORIZE: VectorizeIndex;
AI: Ai;
// 追加
EVENTS: AnalyticsEngineDataset;
EXPERIMENT: DurableObjectNamespace;
JOBS: Queue;
BROWSER: Fetcher;
LP_GEN: Workflow;
// vars
ACCESS_TEAM_DOMAIN: string;
ACCESS_AUD: string;
DEV_BYPASS_AUTH: string;
APP_ENV: "production" | "staging" | "development";
LP_DOMAIN: string;
// secrets
LLM_API_KEY: string;
AI_GATEWAY_URL: string;
COOKIE_SIGNING_KEY: string;
}
✅ 確認:
$ npm run cf-typegen # 型定義を再生成
$ npm run typecheck
$ npx wrangler deploy --dry-run --env=""
# → バインディング一覧が表示される。抜けがあればここで気づける
$ npx wrangler deploy --dry-run --env staging
6.2 D1 マイグレーション
⚠ 既存のマイグレーションファイルは絶対に編集しない。 適用済みの環境が壊れる。必ず新しいファイルを足す。
$ npx wrangler d1 migrations create DB lp_core
# → migrations/0002_lp_core.sql が生成される
migrations/0002_lp_core.sql の骨格(仕様書11章に対応):
-- LPライブラリ ---------------------------------------------------------
CREATE TABLE lp_libraries (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
design_tokens TEXT NOT NULL,
factors TEXT NOT NULL,
banned_phrases TEXT,
required_notices TEXT,
is_locked INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 実験 -----------------------------------------------------------------
CREATE TABLE lp_experiments (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft',
library_id TEXT NOT NULL REFERENCES lp_libraries(id),
library_version INTEGER NOT NULL,
target_def TEXT NOT NULL,
service_desc TEXT NOT NULL,
cv_type TEXT NOT NULL,
primary_metric TEXT NOT NULL DEFAULT 'cv',
factors_used TEXT NOT NULL,
min_sessions_per_variant INTEGER NOT NULL DEFAULT 300,
min_days INTEGER NOT NULL DEFAULT 7,
epsilon REAL NOT NULL DEFAULT 0.05,
prior_kappa REAL NOT NULL DEFAULT 100,
current_round INTEGER NOT NULL DEFAULT 0,
winner_variant_id TEXT,
started_at TEXT,
fixed_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- バリアント -----------------------------------------------------------
CREATE TABLE lp_variants (
id TEXT PRIMARY KEY,
experiment_id TEXT NOT NULL REFERENCES lp_experiments(id),
factor_levels TEXT NOT NULL,
content_json TEXT NOT NULL,
html_r2_key TEXT,
screenshot_r2_key TEXT,
embedding_id TEXT,
screen_score REAL,
fact_check_status TEXT NOT NULL DEFAULT 'pending',
fact_check_detail TEXT,
status TEXT NOT NULL DEFAULT 'generated',
eliminated_round INTEGER,
alpha REAL NOT NULL DEFAULT 0,
beta REAL NOT NULL DEFAULT 0,
sessions INTEGER NOT NULL DEFAULT 0,
cta_clicks INTEGER NOT NULL DEFAULT 0,
scroll50 INTEGER NOT NULL DEFAULT 0,
cvs INTEGER NOT NULL DEFAULT 0,
approved_by TEXT,
approved_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_variants_exp ON lp_variants(experiment_id, status);
-- 割付 -----------------------------------------------------------------
CREATE TABLE lp_assignments (
id TEXT PRIMARY KEY,
experiment_id TEXT NOT NULL,
variant_id TEXT NOT NULL,
visitor_id TEXT NOT NULL,
company_id TEXT,
tracking_code TEXT,
context TEXT,
assigned_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX idx_assign_visitor ON lp_assignments(experiment_id, visitor_id);
CREATE UNIQUE INDEX idx_assign_company ON lp_assignments(experiment_id, company_id)
WHERE company_id IS NOT NULL;
-- 日次サマリ -----------------------------------------------------------
CREATE TABLE lp_events (
id TEXT PRIMARY KEY,
date TEXT NOT NULL,
experiment_id TEXT NOT NULL,
variant_id TEXT NOT NULL,
segment_key TEXT NOT NULL DEFAULT '*',
sessions INTEGER NOT NULL DEFAULT 0,
views INTEGER NOT NULL DEFAULT 0,
scroll50 INTEGER NOT NULL DEFAULT 0,
scroll100 INTEGER NOT NULL DEFAULT 0,
cta_clicks INTEGER NOT NULL DEFAULT 0,
form_starts INTEGER NOT NULL DEFAULT 0,
form_submits INTEGER NOT NULL DEFAULT 0,
cvs INTEGER NOT NULL DEFAULT 0,
engaged_ms_sum INTEGER NOT NULL DEFAULT 0,
engaged_ms_count INTEGER NOT NULL DEFAULT 0,
lcp_p75 REAL, inp_p75 REAL, cls_p75 REAL
);
CREATE UNIQUE INDEX idx_events_key ON lp_events(date, variant_id, segment_key);
CREATE INDEX idx_events_exp_date ON lp_events(experiment_id, date);
-- 以降 factor_effects / abm_pages / touchpoints / … を同様に定義
適用:
# ローカル(D1のローカルエミュレーション)
$ npm run db:migrate:local
$ npm run db:seed:local
# ステージング → 本番の順に適用する。逆順は絶対にしない
$ npx wrangler d1 migrations apply DB --remote --env staging
$ npx wrangler d1 migrations apply DB --remote --env=""
✅ 確認:
$ npx wrangler d1 execute DB --local \
--command "SELECT name FROM sqlite_master WHERE type='table'"
SQL実装の鉄則
- 必ずプレースホルダ (
?1,?2) で bind する。文字列連結は禁止。 - 一覧取得で N+1 を作らない。複数クエリは
env.DB.batch()で1往復にする。 - D1 の上限: 1DB最大10GB(Paid)、Worker呼出あたりクエリ1,000、行サイズ2MB、SQL文長10万バイト。
7. 構築手順 STEP 3 — 認証とドメイン
7.1 管理画面 — Cloudflare Access
自前のログイン画面を作らない。Access に寄せる。
7.1.1 目標とする条件
「Google Workspace アカウントで認証」かつ「WARP が有効な会社端末から」。当社は全社員のPCに Cloudflare One Client(WARP)を導入済みなので、この2つを AND で要求する。
社員PC (WARP有効)
│
▼
Cloudflare Access ── Include : Login Method = Google Workspace
│ Emails ending in = @synon.co.jp
│ ── Require : WARP ← 端末条件
│ Gateway ← 併用推奨(7.1.4)
│ Client certificate 等 ← 会社端末限定を厳密にするなら
▼
Worker ── requireAccess で JWT を再検証
⚠ 2026年8月14日に追加された「Worker 単位のワンクリック Access」は、この用途には使えない。 あれは Worker に紐づく workers.dev・カスタムドメイン・ルート・プレビューURLをまとめて保護できる便利な機能だが、身元の条件が「Cloudflareアカウントのメンバー / メールアドレス / メールドメイン」に限られ、WARP などの Device Posture 条件を付けられない。 ステージングやプレビューURLを塞ぐ用途には最適なので、本番の管理画面は下記の通常の Access アプリケーション、ステージング/プレビューはワンクリック Access、と使い分けるのがよい。 (Changelog 2026-08-14)
7.1.2 手順 ① Google Workspace を IdP として登録する
Access の IdP には 「Google」と「Google Workspace」の2種類がある。必ず後者を選ぶ。
| Google Workspace | ||
|---|---|---|
| 対象 | Gmail を含む任意の Google アカウント | 自社 Workspace ドメインに限定 |
| Google グループをポリシー条件に使えるか | 不可 | 可 |
- Google Cloud Console: 新規プロジェクトを作成 → Admin SDK API を有効化
- OAuth 同意画面を Internal で設定
- OAuth クライアントID(ウェブアプリケーション)を作成
- JavaScript origins:
https://<team-name>.cloudflareaccess.com- リダイレクトURI:https://<team-name>.cloudflareaccess.com/cdn-cgi/access/callback - Google 管理コンソール: セキュリティ > アクセスとデータ管理 > API の管理 > 「Trust internal apps」を有効化
- Zero Trust ダッシュボード: Integrations > Identity providers > Google Workspace を追加。Client ID / Secret / Workspace ドメインを入力 → 保存後に出る管理者承認リンクを実行
- Test を実行し、ログインとグループ取得が通ることを確認する
⚠ Google Workspace 統合は SCIM プロビジョニング非対応。また、Access で保護した Google Workspace アカウント自体には使えない。
7.1.3 手順 ② WARP のデバイス登録も Google に寄せる
Zero Trust > Team & Resources > Devices > Device profiles > Management の Device enrollment permissions で、登録時のログイン方法に Google Workspace を指定する。WARP の登録と Access のログインが同じIdPになるため、社員は実質1回のログインで済む。
7.1.4 手順 ③ Access アプリケーションとポリシー
- Zero Trust > Access > Applications > Add an application > Self-hosted
- Application domain:
ma.synon.co.jp - ポリシーを作る
| ルール | セレクタ | 値 |
|---|---|---|
| Include | Login Method | Google Workspace |
| Include | Emails ending in | @synon.co.jp(またはIdP group で特定のGoogleグループ) |
| Require | WARP | Cloudflare One Client が接続されていること |
| Require | Gateway | Gateway に接続されていること(下記の理由で併用推奨) |
なぜ WARP と Gateway を併用するのか: Access のセレクタは Warp(クライアントが接続されている)と Gateway(Gateway に接続されている)が別物で、WARP は Gateway プロキシを使わないモードでも「接続」扱いになりうる。「社内ネットワーク経由であること」まで担保したいなら両方を Require に入れる。挙動はプランと WARP のモードで変わりうるので、必ず実機で確認すること。
- Session duration を設定(既定24時間。15分〜1か月で設定可)
- Application Audience (AUD) Tag をコピー →
wrangler.jsoncのACCESS_AUDに設定
7.1.5 ⚠ 最大の落とし穴 — 「Require WARP」だけでは会社端末に限定できない
公式ドキュメントに次の記載がある。
「This device posture attribute will check for all versions of WARP, including the consumer version.」 (Require WARP)
つまり 私物PCに無料のコンシューマー版WARPを入れただけの端末でも、この条件は満たしうる。 「WARP必須 = 会社端末限定」ではない。
会社支給端末に厳密に限定したい場合は、次のいずれかを Require に追加する。
| 追加する posture check | 内容 | 強度 |
|---|---|---|
| Client certificate (mTLS) | 会社が配布したデバイス証明書を持つ端末のみ | 強(推奨) |
| Device serial numbers | 資産管理台帳のシリアル番号リストと突合 | 強(台帳の維持が要る) |
| Device UUID | 登録済みデバイスのUUID | 中 |
| OS version / Disk encryption / Firewall | 端末の状態要件 | 補助的 |
各 posture check がどのプランで使えるかを明記した公式ページは見つからなかった。契約前にダッシュボードで実際に選択できるか確認すること。
⚠ Access をルートに紐付けていても、Worker 側で必ず JWT を再検証する。 workers.dev などから直接叩かれる経路が残りうる。スターターの src/worker/lib/access.ts がその実装。このファイルは触らない。
ブラウザ
▼
Cloudflare Access ── メールOTP(@synon.co.jp限定) ──▶ 認証済み
│ Cf-Access-Jwt-Assertion ヘッダを付与
▼
Worker ── requireAccess ミドルウェア
│ JWKS (team domain の /cdn-cgi/access/certs) で署名検証
│ aud / iss / email クレームを確認
▼
c.get("user") = { email, sub, groups }
DEV_BYPASS_AUTH はローカル開発専用。APP_ENV=production では効かないよう二重に条件が付いている(テストで担保済み)。
7.2 LP配信 — 認証なし・公開
LPは公開ページなので Access をかけない。同一ホスト内でパスごとに分ける場合は、公開したいパスに decision = Bypass のポリシーを別途作る(Access はパス指定できる)。
| 対象 | 設定 |
|---|---|
/lp/* /r/* /a/* |
公開。Bypass ポリシー(Everyone) |
/api/* |
requireAccess 配下。例外は /api/health のみ |
/c (計測受信) |
公開。ただしレート制限(1 visitor_id 60イベント/分)と Origin 検証 |
/lp/*/submit |
公開。Turnstile 検証必須 |
| 管理画面 SPA | Access(7.1) |
CI や外部システムから /api/* を叩く必要がある場合は Service Token を使う。
- Zero Trust > Access > Service Auth でトークンを発行(Client ID / Secret)
- 該当パスのポリシーに Action = Service Auth の Include ルールを追加(通常の Allow ポリシーとは別立て)
- 呼び出し側は
CF-Access-Client-Id/CF-Access-Client-Secretヘッダーを付与
⚠ ブラウザ用の Allow ポリシーに Service Token を混ぜない。必ず別ポリシーにする。
⚠ /api/* に認証不要のエンドポイントを増やさない。 どうしても必要なら、専用のパス空間(/public/*)を切って明示する。
7.1.6 workers.dev の直接アクセスを塞ぐ
Access はホスト名に対して効くため、*.workers.dev から直接叩かれる経路が残ると素通りになる。
# Settings > Domains & Routes から無効化するだけでは不十分
# wrangler 設定にも書かないと、次回デプロイで復活する
{
"workers_dev": false
}
✅ 確認: curl -I https://synon-ma-lp.<subdomain>.workers.dev/ が到達しないこと
7.1.7 退職者の遮断
Google Workspace 側でアカウントを停止するだけでは、Cloudflare 側で発行済みのセッション(Cookie / JWT)が即座に無効になるとは限らない。 退職手続きに両方を入れる。
| # | 操作 | 場所 |
|---|---|---|
| 1 | Google Workspace アカウントを停止 | Google 管理コンソール |
| 2 | Revoke(セッションを即時終了) | Zero Trust > Team & Resources > Users |
| 3 | Remove(シートを解放) | 同上 |
7.3 ルートの割り当て
"routes": [
{ "pattern": "lp.synon.co.jp/*", "zone_name": "synon.co.jp" },
{ "pattern": "ma.synon.co.jp/*", "zone_name": "synon.co.jp" }
]
✅ 確認: $ curl -I https://lp.synon.co.jp/api/health が 200 を返すこと
7.4 Turnstile
- ダッシュボード > Turnstile > Add site
- Widget mode: Managed(推奨)
- Site Key(公開)→ フロントエンドに埋め込む
- 🔒 Secret Key →
wrangler secret put TURNSTILE_SECRET
サーバ側で https://challenges.cloudflare.com/turnstile/v0/siteverify に POST して検証する。検証をスキップできる分岐を作らない。
8. 構築手順 STEP 4 — LP配信 Worker
8.1 リクエスト処理の流れ
GET /r/{tracking_code} または /lp/{slug}
│
┌──────▼──────────────────────────────────────────┐
│ ① 文脈(context)の構築 │
│ /r/{code} → D1: tracking_code → approach → │
│ company_id, industry, size, pref │
│ /lp/{slug} → UTM / Referer / CF-IPCountry │
│ ※ D1参照は1回にまとめる(JOIN済みのビューを使う) │
└──────┬──────────────────────────────────────────┘
│
┌──────▼──────────────────────────────────────────┐
│ ② 既存割付の確認 │
│ Cookie `ma_v` を HMAC 検証 → 有効なら再利用 │
│ 無ければ D1 の company_id 割付を確認 │
└──────┬──────────────────────────────────────────┘
│ 未割付
┌──────▼──────────────────────────────────────────┐
│ ③ バリアント選択 │
│ KV から配信重み(日次バッチが書いたもの)を取得 │
│ status=exploring → 候補から均等ランダム │
│ status=bandit → Beta分布から1サンプルずつ │
│ argmax。確率εで一様ランダム │
│ status=fixed → 勝者95% / 探索5% │
└──────┬──────────────────────────────────────────┘
│
┌──────▼──────────────────────────────────────────┐
│ ④ 描画 │
│ R2 から HTML 断片を取得(Cache API でキャッシュ) │
│ 差込変数を展開 → HTMLResponse をストリーミング │
│ Set-Cookie: ma_v (署名付き / SameSite=Lax / 90d) │
└──────┬──────────────────────────────────────────┘
│
┌──────▼──────────────────────────────────────────┐
│ ⑤ 記録(レスポンスをブロックしない) │
│ c.executionCtx.waitUntil( │
│ EVENTS.writeDataPoint({...}) ← 生イベント │
│ EXPERIMENT.get(id).increment() ← カウンタ │
│ DB.insert(lp_assignments) ← 初回のみ │
│ ) │
└────────────────────────────────────────────────────┘
⚠ ⑤は必ず waitUntil() に逃がす。 計測処理でレスポンスを待たせると LCP が悪化し、計測のために CVR を下げるという本末転倒になる。
8.2 実装上の必須ルール
| # | ルール | 理由 |
|---|---|---|
| 1 | Worker のグローバルスコープに可変状態を置かない | isolate は再利用される。他ユーザーのデータが漏れる |
| 2 | HTML断片は Cache API でエッジキャッシュする | R2への Class B 操作を減らす。TTFB短縮 |
| 3 | Cookie は HMAC 署名する | 改ざんで任意バリアントを見られると計測が汚れる |
| 4 | D1 参照は 1リクエスト1回にまとめる | Worker呼出あたりのクエリ数と CPU時間の節約 |
| 5 | 未使用ブロックをクライアントに送らない | LCP・INP の悪化を防ぐ |
| 6 | 外部CDNを読み込まない | LCP悪化 + 外部送信規律の対象が増える |
| 7 | console.log に個人情報・APIキーを出さない |
Workers Logs に残る |
| 8 | CPU時間 30秒(Paidデフォルト)を意識する | 重い処理は Queues / Workflows に逃がす |
8.3 バンディット状態(Durable Object)
実験ごとに1インスタンス。SQLite バックエンドを使う。
export class ExperimentCounter {
constructor(private state: DurableObjectState, private env: Env) {
this.state.blockConcurrencyWhile(async () => {
this.state.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS arms (
variant_id TEXT PRIMARY KEY,
n_trial INTEGER NOT NULL DEFAULT 0,
n_success INTEGER NOT NULL DEFAULT 0,
n_cta INTEGER NOT NULL DEFAULT 0,
n_scroll50 INTEGER NOT NULL DEFAULT 0
)`);
});
}
// POST /trial { variant_id }
// POST /success { variant_id, metric }
// GET /snapshot → 全アームの (n_trial, n_success)
}
- 配信のたびに
trialを +1、CV のたびにsuccessを +1 - 日次 Cron が
snapshotを取り、D1 のlp_variantsに同期する - D1 に直接 UPDATE をかけない。 同一行への高頻度更新はロック競合を起こす
8.4 ローカル開発
$ npm run dev # Vite + wrangler dev を同時起動
- D1 / R2 / KV はローカルエミュレーションされる
- Workers AI / Vectorize / Browser Rendering はローカルエミュレーションが無い。
wrangler.jsoncで"remote": trueを付けるとwrangler devが本物のリソースにプロキシする - ローカルD1の中身を見る:
$ npx wrangler d1 execute DB --local \
--command "SELECT id,status,current_round FROM lp_experiments"
9. 構築手順 STEP 5 — 計測パイプライン
9.1 全体像
[ブラウザ] [Worker /c] [保存先]
lp.js (8KB以内) ├ Origin検証 ├ Analytics Engine
├ IntersectionObserver ──┐ ├ レート制限(60/分) │ (生イベント/3か月)
├ Page Visibility API ├──▶├ ボット判定 ──────┤
├ web-vitals │ ├ 正規化 ├ Durable Object
└ sendBeacon ────────────┘ └ waitUntil で書込 │ (カウンタ)
└ (日次Cron)→ D1
9.2 クライアント計測スクリプト
要件
| 項目 | 値 |
|---|---|
| サイズ | gzip後 8KB 以内 |
| 読み込み | defer。LPのレンダリングをブロックしない |
| 送信 | navigator.sendBeacon()。visibilitychange(hidden) と pagehide で発火 |
| スクロール | IntersectionObserver(threshold 0.5)でセクション単位。1セクション1回 |
| 滞在時間 | Page Visibility API で可視状態のみ積算 |
| CWV | web-vitals の onLCP / onINP / onCLS |
| 外部通信 | 自ドメイン /c のみ |
// 最小構成のイメージ
const q = [];
const push = (e) => q.push({ ...e, t: Date.now() });
const flush = () => {
if (!q.length) return;
navigator.sendBeacon('/c', JSON.stringify({ v: VID, s: SID, e: q.splice(0) }));
};
addEventListener('visibilitychange', () => document.visibilityState === 'hidden' && flush());
addEventListener('pagehide', flush);
const seen = new Set();
new IntersectionObserver((es) => {
for (const e of es) {
const d = e.target.dataset.depth;
if (e.isIntersecting && !seen.has(d)) { seen.add(d); push({ n: 'lp_scroll', d: +d }); }
}
}, { threshold: 0.5 }).observe; // 実際は各セクションに .observe()
⚠ unload イベントは使わない。 モバイルで発火しないことがあり、計測が欠ける。
9.3 サーバ側(/c)
app.post('/c', async (c) => {
const body = await c.req.json();
// 1. Origin 検証(自ドメイン以外は破棄。エラーは返さない)
// 2. レート制限: visitor_id あたり 60イベント/分。超過は静かに破棄
// 3. ボット判定 → is_bot フラグを立てる(削除はしない)
// 4. 書き込みは waitUntil に逃がす
c.executionCtx.waitUntil((async () => {
for (const e of body.e) {
c.env.EVENTS.writeDataPoint({
blobs: [e.n, body.v, body.s, e.variant_id ?? '', e.company_id ?? '',
e.channel ?? '', e.segment ?? '', e.page ?? ''],
doubles: [e.d ?? 0, e.ms ?? 0, e.lcp ?? 0, e.inp ?? 0, e.cls ?? 0],
indexes: [e.experiment_id ?? 'none'],
});
}
})());
return c.body(null, 204); // 常に 204。エラーを返さない
});
Analytics Engine の制約(公式値)
| 項目 | 上限 |
|---|---|
| 1 Worker呼出あたりのデータポイント | 250 |
| blob | 20個、合計16KB以内 |
| double | 20個 |
| index | 1個、96バイト以内 |
| 保持期間 | 3か月 |
⚠ indexes は1つしか持てない。 集計軸として最も使うもの(= experiment_id)を入れる。他は blob に入れて SQL API で絞る。
9.4 集計(毎時 Cron)
-- Analytics Engine SQL API
SELECT
blob4 AS variant_id,
blob7 AS segment,
count() AS events,
countIf(blob1 = 'lp_view') AS views,
countIf(blob1 = 'lp_scroll' AND double1 >= 50) AS scroll50,
countIf(blob1 = 'lp_cta_click') AS cta_clicks,
countIf(blob1 = 'lp_cv') AS cvs
FROM ma_lp_events
WHERE index1 = '{experiment_id}'
AND timestamp >= NOW() - INTERVAL '1' HOUR
GROUP BY blob4, blob7
結果を lp_events(D1)に UPSERT する。D1 には日次サマリだけを置く。
10. 構築手順 STEP 6 — AI生成パイプライン
10.1 Workflows で組む理由
50件の生成は「LLM呼び出し → 検査 → 埋め込み → 結合 → スクショ」の5段×50回。途中で1件失敗したときに最初からやり直すのは無駄が大きい。Workflows は各ステップの結果を永続化し、失敗箇所から再開できる。待機中はCPU課金されない。
10.2 パイプライン定義
export class LpGenerationWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event, step) {
const combos = await step.do('expand-factors', async () =>
expandFactorProduct(event.payload.factors)); // → 50組
const generated = [];
for (const [i, combo] of combos.entries()) {
// ステップ名をユニークにする(再開時のキーになる)
const content = await step.do(`gen-${i}`, {
retries: { limit: 3, delay: '10 seconds', backoff: 'exponential' },
}, async () => callLLM(combo, event.payload));
const checked = await step.do(`check-${i}`, async () =>
factCheck(content, event.payload.facts)); // 事実・禁止表現・数値
// ⚠ 埋め込みモデルは必ず日本語(多言語)対応のものを選ぶ。
// 英語特化モデルを使うと日本語コピーの類似度が正しく出ず、
// 重複検知の見落とし / 正常バリアントの誤脱落(閾値0.95)が起きる。
// npx wrangler ai models で現在の多言語埋め込みモデルを確認すること。
const emb = await step.do(`embed-${i}`, async () =>
this.env.AI.run(EMBEDDING_MODEL, { text: [summarize(content)] }));
generated.push({ combo, content, checked, emb });
}
await step.do('dedupe', async () => vectorDedupe(generated, 0.95));
await step.do('render', async () => renderAndStoreToR2(generated));
await step.do('screenshot', async () => captureAll(generated));
await step.do('score', async () => screen0Score(generated));
await step.do('persist', async () => saveVariants(generated));
}
}
10.3 LLM の使い分け
| 用途 | 使うもの | 理由 |
|---|---|---|
| 分類・要約・埋め込み | Workers AI | 無料枠 10,000 Neurons/日 で足りる。低レイテンシ |
| 本番コピー生成 | AI Gateway 経由の外部LLM | 日本語コピーの品質。Gateway でコスト可視化・キャッシュ・レート制限が付く |
| 画像生成(将来) | Workers AI (FLUX系 / SDXL系) | ブランド一貫性の整理が先(仕様書16章) |
🔒 APIキーは Secrets Store または wrangler secret put に置く。vars に平文で書かない。
$ npx wrangler secret put LLM_API_KEY
$ npx wrangler secret put LLM_API_KEY --env staging
⚠ Workers AI のモデルID(@cf/...)は変わりうる。 実装前に必ず現在の一覧を確認する。
$ npx wrangler ai models # 利用可能なモデルの一覧
10.4 生成コストの目安
1バリアント = 入力 約200トークン / 出力 約800トークン。50件で入力1万・出力4万トークン。
| 方式 | 1回(50件)のコスト |
|---|---|
| Workers AI (Llama 3.1 70B 相当) | 約 8,500 Neurons → 無料枠(10,000/日)にほぼ収まる |
| 外部LLM(中位モデル) | 数十円〜数百円/回(モデルによる) |
AI Gateway のキャッシュを有効にする。 同じプロンプトでの再生成が無料になり、開発中の試行錯誤コストが大きく下がる。
10.5 事実チェックの実装
AIの出力を信じない。機械で照合する。
[1] 実績・事例 → content_json.proof.case_ids が事例DBに存在するか
→ 存在しないIDが1つでもあれば FAIL
[2] 数値 → 本文から /\d+(\.\d+)?\s*(%|時間|円|社|名|件|倍)/ を抽出
→ 入力データに無い数値があれば FAIL
[3] 固有名詞 → 顧客名・製品名・技術名を辞書照合 → 未登録語は WARN
[4] 禁止表現 → banned_phrases の正規表現に一致すれば FAIL
[5] 必須表記 → プライバシーポリシー・外部送信公表・会社名のリンクがあるか
FAIL が1件でもあれば fact_check_status='fail' とし、承認APIが403を返す(G-02)。
11. 構築手順 STEP 7 — 統計バッチ
11.1 Cron の割り当て(UTC / JST 併記)
仕様書 12.4「定期実行(Cron)」の10ジョブすべてを実装する。1つでも欠けると、その機能は静かに動かなくなる。
| UTC | JST | 処理 | 対応する機能 |
|---|---|---|---|
0 * * * * |
毎時00分 | Analytics Engine → D1 日次サマリ集計 | F-14 |
5 * * * * |
毎時05分 | GA4 Data API 増分同期 | 既存 F-09 |
0 17 * * * |
02:00 | 優越確率のモンテカルロ(10万回)→ KV へ | F-15 |
30 17 * * * |
02:30 | 因子効果・セグメント効果の階層ベイズ推定 | F-15 |
0 18 * * * |
03:00 | 逐次淘汰の判定・ラウンド進行 | F-15 |
0 19 * * * |
04:00 | スコア減衰(3%)・架電キュー再構築 | F-20 / F-23 |
0 20 * * * |
05:00 | 広告オーディエンスの同期 | F-18 |
0 21 * * * |
06:00 | noindex 欠落監視・インデックス率チェック(Search Console の URL Inspection API。下記) | F-24 |
0 22 * * 0 |
月 07:00 | 週次レポートのメール配信 | F-23 |
0 15 1 * * |
1日 00:00 | 3か月超の生イベントを R2 へアーカイブ | F-14 |
⚠ 曜日・日付指定を含む Cron は、UTC変換で日付がずれる。「JST 月曜07:00」は UTC では日曜22:00(0 22 * * 0)、「JST 毎月1日00:00」は UTC では前月末日15:00 になるため 0 15 1 * * では厳密には JST 1日09:00 になる。時刻の厳密さが要る場合は Worker 内で JST に変換して判定すること。
src/worker/index.ts:
export default {
fetch: app.fetch,
async scheduled(event: ScheduledController, env: Env, ctx: ExecutionContext) {
switch (event.cron) {
case '0 * * * *': ctx.waitUntil(aggregateHourly(env)); break;
case '5 * * * *': ctx.waitUntil(syncGa4(env)); break;
case '0 17 * * *': ctx.waitUntil(recomputePosteriors(env)); break;
case '30 17 * * *': ctx.waitUntil(estimateFactorEffects(env)); break;
case '0 18 * * *': ctx.waitUntil(advanceRounds(env)); break;
case '0 19 * * *': ctx.waitUntil(decayScores(env)); break;
case '0 20 * * *': ctx.waitUntil(syncAdAudiences(env)); break;
case '0 21 * * *': ctx.waitUntil(auditIndexing(env)); break;
case '0 22 * * 0': ctx.waitUntil(sendWeeklyReport(env)); break;
case '0 15 1 * *': ctx.waitUntil(archiveEventsToR2(env)); break;
}
},
};
インデックス監視の実装メモ(G-03 / G-06) 「大量生成LPが誤ってインデックスされていないか」は Search Console でしか確認できない。日次バッチで URL Inspection API を叩き、公開中バリアントの
indexingStateが「インデックス登録を許可 = いいえ」であることを確認する。 クォータはサイトあたり 600 QPM / 2,000 QPD。1回の公開が50ページ以内(G-06)なので、全バリアントを毎日検査しても十分収まる。 🔒 認証は Google サービスアカウント。鍵は Secrets Store に置く(GA4連携と同じ鍵は使わない)。 ⚠robots.txtでブロックしていると Google がnoindexを読めず、検査結果が常に「許可 = はい」になる。robots.txt を使わない理由の一つがこれ(監視が効かなくなる)。
⚠ Cron 実行も Workers の CPU 時間制限を受ける。 10万回×バリアント数のモンテカルロが重い場合は、実験単位で Queues に分割して投げる。
11.2 優越確率の計算
1. D1 から実験ごとの (variant_id, alpha, beta) を取得
2. 経験ベイズ事前分布を計算
p̄ = Σsᵢ / Σnᵢ , κ = 100
α₀ = p̄ × κ , β₀ = (1 - p̄) × κ
3. 各バリアント: αᵢ' = α₀ + sᵢ , βᵢ' = β₀ + (nᵢ - sᵢ)
4. M = 100,000 回サンプリング → P(i が最良) と期待損失 EL(i)
5. KV に書く: key = `exp:{id}:weights`, TTL 3600
{ variants: [{id, p_best, el, post_mean, ci_low, ci_high}], updated_at }
6. 確定条件を判定
max(p_best) >= 0.95 かつ min(EL) <= 0.001
かつ 各バリアントの累計CV >= 20 かつ 経過日数 >= min_days
→ 満たせば status='fixed', winner_variant_id を設定
ベータ分布のサンプリング: JS標準に無いため、ガンマ分布経由(Marsaglia-Tsang法)で実装するか jstat を使う。
X ~ Gamma(α, 1), Y ~ Gamma(β, 1) のとき X / (X + Y) ~ Beta(α, β)
11.3 逐次淘汰(Sequential Halving)
for each experiment where status = 'exploring':
候補 = status='active' のバリアント
if 全候補の sessions >= min_sessions_per_variant:
proxy_score を計算
0.40*z(cta_rate) + 0.30*z(scroll50_rate)
+ 0.20*z(median_engaged_ms) + 0.10*z(form_start_rate)
下位 50% を status='eliminated', eliminated_round=current_round に更新
current_round += 1
if 残り <= 5: status = 'bandit'
早期打ち切り:
P(そのバリアントが最良) < 0.01 のものは min_sessions 未達でも脱落
✅ 確認: ラウンド進行のたびに監査ログを残し、Slack に通知する。気づかないうちにバリアントが消えている状態を作らない。
12. デプロイ運用
12.1 環境
| 環境 | Worker名 | D1 | 用途 |
|---|---|---|---|
| local | — | ローカルエミュレーション | 開発 |
| staging | synon-ma-lp-staging |
synon-ma-db-staging |
検証。本番と同じ手順を必ず先に通す |
| production | synon-ma-lp |
synon-ma-db |
本番 |
12.2 デプロイ手順(標準)
# 0. 前提: main ブランチが最新、テストが通っている
$ npm run typecheck && npm test
# 1. ステージングへ
$ npx wrangler d1 migrations apply DB --remote --env staging
$ npm run deploy:staging
# 2. ステージングで確認(12.4のチェックリスト)
# 3. 本番へ
$ npx wrangler d1 migrations apply DB --remote --env=""
$ npm run deploy
# 4. 監視(5分間)
$ npm run tail
⚠ マイグレーションはデプロイより先に適用する。 逆にすると、新コードが存在しないカラムを参照して本番が落ちる。
⚠ 後方互換のないスキーマ変更は2段階に分ける。 1回目: カラム追加(NULL許容)+ 新旧両対応のコード → 2回目: 旧カラム削除。
12.3 段階デプロイ(大きな変更のとき)
# バージョンをアップロードするだけ(トラフィックは流さない)
$ npx wrangler versions upload --env=""
# 10% だけ新バージョンに流す
$ npx wrangler versions deploy --env=""
# → 対話式で新旧バージョンと配分(%)を指定
# 問題なければ 100% に
⚠ 段階デプロイの割付はリクエスト単位。 同一ユーザーでも毎回別バージョンに当たる。バリアント実験と混同すると計測が汚れるので、実験期間中は段階デプロイを使わないか、Cloudflare-Workers-Version-Key ヘッダで固定する。
12.4 デプロイ後の確認
| # | 確認項目 | 方法 |
|---|---|---|
| 1 | ヘルスチェック | curl -I https://lp.synon.co.jp/api/health → 200 |
| 2 | LP が表示される | 実際にブラウザで /lp/{slug} を開く |
| 3 | バリアントが割り当たる | Cookie ma_v が付き、リロードで同じ面が出る |
| 4 | 計測が飛ぶ | DevTools Network で /c に 204 |
| 5 | Analytics Engine に入る | SQL API で直近5分を SELECT |
| 6 | noindex が付いている | curl -s .../lp/{slug} \| grep -i "robots" |
| 7 | Cron が動く | wrangler tail で次回実行を待つ、または手動トリガ |
| 8 | エラー率 | ダッシュボード > Workers > Metrics |
12.5 ロールバック
# 直前のデプロイに戻す
$ npm run rollback
# 特定バージョンへ戻す
$ npx wrangler deployments list --env=""
$ npx wrangler rollback [version-id] --env=""
⚠ ロールバックしてもDBスキーマは戻らない。 マイグレーションを伴う変更のロールバックは、事前に切り戻し用SQLを用意しておく。
12.6 CI/CD
Workers Builds(Cloudflare のGit連携ビルド。Free 月3,000ビルド分 / Paid 月6,000ビルド分)か、GitHub Actions のどちらかを使う。スターターには .github/workflows/deploy.yml がある。
推奨フロー:
PR 作成 → typecheck + test + dry-run
main マージ → staging へ自動デプロイ
タグ push → production へデプロイ(手動承認を挟む)
🔒 CLOUDFLARE_API_TOKEN は GitHub の Secrets に置く。権限は最小に絞る(Workers Scripts:Edit, D1:Edit, R2:Edit, Workers KV:Edit)。
13. 日次・週次・月次の運用手順
13.1 毎日(所要 5〜10分)
| # | やること | 見る場所 | 異常時 |
|---|---|---|---|
| 1 | Worker のエラー率・実行数 | ダッシュボード > Workers > Metrics | 16章 |
| 2 | Cron の成功可否 | 同 > Cron Events / Slack通知 | 16.4 |
| 3 | 実験の進捗(セッション数・ラウンド) | S-17 実験ダッシュボード | — |
| 4 | バリアントの逐次淘汰ログ | S-17 / Slack | 想定外の脱落は原因を確認 |
| 5 | 架電キューの新規投入 | S-25 | 営業へ引き渡し |
| 6 | 計測イベント数の推移 | Analytics Engine | 前日比 ±30% 超で調査 |
| 7 | LP の CWV | Web Analytics / S-17 | LCP 2.5秒超が続くなら 16.5 |
13.2 毎週(月曜・所要 30分)
| # | やること | 判断基準 |
|---|---|---|
| 1 | 週次レポートの確認(自動配信) | チャネル別CPL・CVR・商談化 |
| 2 | 実験の確定判定 | p_best >= 95% かつ EL <= 0.1% かつ 経過7日以上 |
| 3 | 因子効果のレビュー | どの訴求軸・構成が効いているか。次回生成に反映 |
| 4 | セグメント別最良の確認 | 全体最良と違うセグメントがあれば出し分けを有効化 |
| 5 | 生成キューの消化 | 差戻しになったバリアントの再生成 |
| 6 | コンプライアンス・ダッシュボード | noindex 欠落ゼロ、外部送信公表が最新 |
| 7 | コスト確認 | 15章 |
13.3 毎月
| # | やること |
|---|---|
| 1 | 月次アトリビューションレポート作成(S-26) |
| 2 | 実測CPL/CVRで仕様書4.2のベンチマークを更新(外部の公開値を自社実測で置き換える) |
| 3 | 3か月超の生イベントをR2へアーカイブ |
| 4 | D1 のサイズ確認(上限10GB)。lp_events の古い行を退避 |
| 5 | R2 の容量・オブジェクト数確認 |
| 6 | 依存パッケージの更新(npm outdated → wrangler は特に追随する) |
| 7 | compatibility_date の見直し(半年〜1年に1回、テスト後に更新) |
13.4 四半期
| # | やること |
|---|---|
| 1 | 法令要件のレビュー(改正個人情報保護法の施行スケジュールに追随。仕様書14章) |
| 2 | 外部送信タグ一覧と公表テキストの棚卸し |
| 3 | 法務確認フラグ(G-09/G-10)の再確認 |
| 4 | Cloudflare の料金・上限の変更確認(公式ページ) |
| 5 | Access ポリシーの棚卸し(退職者・権限過剰) |
| 6 | 実験運用の振り返り(仮説と結果の突き合わせ) |
| 7 | バックアップからの復旧訓練(D1 Time Travel の実行確認) |
14. 監視とアラート
14.1 監視項目
| 対象 | 指標 | 閾値 | 通知先 |
|---|---|---|---|
| Worker | エラー率 | 1% 超が5分継続 | Slack #ma-alert |
| Worker | p99 レスポンス時間 | 1秒超 | Slack |
| Worker | CPU時間 | 上限の80%到達 | Slack |
| Cron | 実行失敗 | 1回でも | Slack(即時) |
| D1 | 行書込数 | 無料枠の80% | Slack(日次) |
| D1 | DBサイズ | 8GB(上限10GBの80%) | Slack(日次) |
| R2 | ストレージ | 8GB(無料枠10GBの80%) | Slack(日次) |
| Analytics Engine | 書込数 | 前日比 ±30% | Slack(日次) |
| Workers AI | Neurons 消費 | 無料枠の80% | Slack(日次) |
| LP | LCP p75 | 2.5秒超 | Slack(日次) |
| LP | CVR | 前週比 −30% | Slack(日次) |
| コンプライアンス | noindex 欠落 | 1件でも | Slack(即時)+ メール |
| コンプライアンス | 未承認バリアントの公開試行 | 1件でも | 同上 |
| 課金 | 月額見込み | 予算の80% | Slack + メール |
14.2 設定方法
- Cloudflare Notifications: ダッシュボード > Notifications で、Workers のエラー率・課金アラートを設定(無料)
- Slack 通知:
notify.ts(スターターにある)から Webhook を叩く。🔒 Webhook URL は Secrets Store に置く - 合成監視: Zero Trust の DEX で LP への到達性を常時監視する(無料)
- ログ:
observability.enabled = trueを設定済み。wrangler tailでリアルタイム確認
$ npm run tail # 本番ログ
$ npx wrangler tail --env staging --format json
$ npx wrangler tail --status error # エラーのみ
14.3 通知疲れを避ける
⚠ 通知が多すぎると誰も見なくなる。 以下を守る。
- 即時通知は「人が今すぐ動く必要があるもの」だけ(Cron失敗・コンプライアンス違反・エラー率急増)
- それ以外は日次のサマリにまとめる
- 同じアラートが5回続いたら自動で抑制し、「継続中」の1通に切り替える
15. コスト管理
15.1 月額試算
前提: LP 月間10万PV、CV率5%、生成は週1回50パターン、イベント約20件/セッション。
| サービス | 使用量 | Free プラン | Workers Paid |
|---|---|---|---|
| Workers 基本 | — | $0 | $5.00 |
| Workers リクエスト | 約20万/月 | 無料枠内(10万/日) | 無料枠内(1,000万/月) |
| Workers CPU時間 | — | ⚠ 10ms/呼出で足りない | 無料枠内(3,000万ms/月) |
| Workers AI | 約8,500 Neurons/回 | 無料枠内(1万/日) | 無料枠内 |
| D1 読取・書込 | 数十万件/月 | 無料枠内 | 無料枠内 |
| KV | 数千操作/月 | 無料枠内 | 無料枠内 |
| R2 | 数GB | 無料枠内(10GB) | 無料枠内 |
| Analytics Engine | 約200万件/月 | ⚠ 10万件/日で不足しうる | 無料枠内(1,000万/月) |
| Durable Objects | 約20万req/月 | 無料枠内 | 無料枠内 |
| Browser Rendering | 週1回×50件 | 無料枠内(10分/日) | 無料枠内 |
| Turnstile / Web Analytics / AI Gateway | — | 無料 | 無料 |
| 合計(Cloudflare) | $0 だが運用不可 | 約 $5/月 | |
| 外部LLM(使う場合) | 週1回×50件 | 別途 | 数百〜数千円/月 |
Free プランは数値上は無料枠に収まるが、CPU時間 10ms/呼び出しの制約で実際には動かない。$5/月の Workers Paid を前提にすること。
15.2 スケール時の見通し
| 月間PV | 想定される追加課金 |
|---|---|
| 10万 | なし($5のまま) |
| 100万 | ほぼなし($5のまま。D1書込・AE書込とも無料枠内) |
| 1,000万 | Workers リクエスト超過($0.30/100万)、D1書込超過($1.00/100万行)、AE書込超過($0.25/100万件)が発生し始める |
当面 $5/月で耐えられる設計になっている。 これが Cloudflare を選ぶ最大の理由の一つ。
15.3 コストを膨らませる典型的なミス
| ミス | 影響 | 防ぎ方 |
|---|---|---|
| 生イベントを D1 に1件ずつINSERT | 行書込課金が跳ねる | Analytics Engine を使う |
| 毎リクエストでモンテカルロ10万回 | CPU時間超過 | 日次バッチ + KVキャッシュ |
| 生成を毎日フルで回す | LLM費用 | 週1回。差分生成。AI Gateway のキャッシュを有効に |
| R2 の HTML を毎回取得 | Class B 操作 | Cache API でエッジキャッシュ |
| ボットトラフィックを計測 | AE書込 + 統計汚染 | ボット判定を先に通す |
| LP画像を最適化しない | 帯域(R2はegress無料だが)+ LCP悪化 | Cloudflare Images / format=auto |
15.4 確認方法
# 課金の実績はダッシュボードで確認
# Workers & Pages > Plans / Usage
# R2 / D1 / Workers AI はそれぞれの Metrics タブ
月初に前月分を確認し、予算超過があれば原因を 15.3 の表で切り分ける。
16. トラブルシューティング
16.1 バインディングが undefined
| 症状 | env.EVENTS is undefined のようなエラー |
|---|---|
| 原因 | wrangler.jsonc の本番か staging の片方にしか書いていない |
| 確認 | npx wrangler deploy --dry-run --env="" と --env staging の両方でバインディング一覧を見る |
| 対処 | 3箇所(wrangler.jsonc 両環境 / types.ts / CLAUDE.md)を同時に更新 |
16.2 D1 のエラー
| 症状 | 原因 | 対処 |
|---|---|---|
D1_ERROR: too many SQL variables |
bind パラメータが100超 | バッチを分割する |
database is locked 相当の競合 |
同一行への高頻度UPDATE | Durable Objects で直列化する |
| マイグレーションが適用済み扱いになる | 既存ファイルを編集した | 編集しない。 新しいマイグレーションで修正する |
| クエリが30秒でタイムアウト | 全件スキャン | インデックスを確認。EXPLAIN QUERY PLAN |
# 適用状況の確認
$ npx wrangler d1 migrations list DB --remote --env=""
16.3 計測データが入らない
チェック順:
1. ブラウザ DevTools > Network に /c への POST があるか
→ 無い: クライアントスクリプトが読み込まれていない / JSエラー
2. /c が 204 を返しているか
→ 4xx: Origin検証・レート制限に引っかかっている
3. wrangler tail で writeDataPoint が呼ばれているか
→ 呼ばれていない: waitUntil 内で例外が出ている(try/catchでログ出力を追加)
4. Analytics Engine SQL API で直近5分を SELECT
→ 空: dataset 名の不一致。blob の順序ズレ
5. 集計 Cron のログ
→ D1 の lp_events に反映されない: UPSERT のキー不一致
⚠ waitUntil 内の例外はレスポンスに現れない。 必ず try/catch でログを出す。
16.4 Cron が動かない
| 確認 | 方法 |
|---|---|
| Cron が登録されているか | ダッシュボード > Worker > Settings > Trigger Events |
| UTC で書いたか | JST から9時間引く。日付をまたぐ場合は曜日もずらす |
scheduled ハンドラが export されているか |
export default { fetch, scheduled } |
| CPU時間を超えていないか | ログで実行時間を確認。重い処理は Queues に分割 |
| ローカルでテストしたいとき | wrangler dev --test-scheduled → curl "http://localhost:8787/__scheduled?cron=0+17+*+*+*" |
16.5 LP が遅い(LCP が 2.5秒を超える)
切り分け順:
1. TTFB は速いか(200ms以下)
遅い → Worker 側。D1参照が複数回、同期処理がレスポンスをブロック
2. HTML サイズは 200KB 以下か
大きい → 未使用ブロックが混ざっている / インライン画像
3. LCP要素は何か(DevTools > Performance)
画像 → Cloudflare Images で最適化、fetchpriority="high"、サイズ指定
フォント → 自己ホスト + font-display: swap + サブセット化
4. 外部リクエストがあるか
ある → 削除する(CDN・タグ・埋め込み)
5. 計測スクリプトがブロックしていないか
→ defer が付いているか、8KB以内か
16.6 実験の結果がおかしい
| 症状 | 原因の候補 |
|---|---|
| 1バリアントのCVRが異常に高い | サンプル数が少ない。経験ベイズ事前分布(κ)が効いているか確認。ガードレール(累計100セッション未満は非表示)が働いているか |
| 同じ人に違うバリアントが出る | Cookie が付いていない / 署名検証に失敗 / 段階デプロイと併用している |
| セッション数がバリアント間で偏る | バンディットが効いている(正常)。exploring 中なら割付ロジックのバグ |
| CVが記録されない | CVイベントの発火条件。サンクスページの計測漏れ。フォーム送信後のリダイレクトで visitor_id が切れている |
| いつまでも確定しない | そもそも必要サンプル数に届いていない。 S-15 の試算を見直し、因子を減らすか判定指標をプロキシに変える |
16.7 Workers AI が失敗する
| 症状 | 対処 |
|---|---|
| モデルIDが見つからない | npx wrangler ai models で現在のIDを確認。モデル名は変わりうる |
| レート制限(429) | テキスト生成は既定300req/分。大型モデルはさらに低い。Workflows のリトライ(指数バックオフ)に任せる |
| Neurons を使い切った | 無料枠は1日10,000。UTC 0時にリセット。生成を週1回にする、または外部LLMへ切り替える |
| ローカルで動かない | Workers AI はローカルエミュレーション無し。"remote": true を付ける |
17. 障害対応・緊急停止
17.1 緊急停止の判断基準
| 事象 | 対応 |
|---|---|
| LPに誤った実績・数値が公開されている | 該当バリアントを即時停止(17.2)→ 原因究明 → 事実チェックの穴を塞ぐ |
| 個人情報が意図せず表示されている | LP全体を停止 → 影響範囲の特定 → 法務・経営へ報告 |
| LPが表示されない/エラー多発 | ロールバック(12.5) |
| 計測が完全に停止 | LP配信は継続。計測のみ復旧作業(LP表示を止めない) |
| 広告費が想定を大きく超過 | 媒体側で配信停止 → オーディエンス同期を停止 |
| Cloudflare 側の障害 | Cloudflare Status を確認。復旧を待つ |
17.2 バリアントの緊急停止
# 1. 該当バリアントを無効化(即時反映)
$ npx wrangler kv key put --binding=CACHE \
"exp:{experiment_id}:disabled" '["{variant_id}"]' --env=""
# 2. D1 のステータスも更新
$ npx wrangler d1 execute DB --remote --env="" \
--command "UPDATE lp_variants SET status='eliminated' WHERE id='{variant_id}'"
⚠ Worker 側は毎リクエストで disabled リストを参照し、含まれるバリアントを候補から外す実装にしておくこと。この仕組みを事前に作っておかないと、緊急時にデプロイが必要になる。
17.3 LP 全体の停止
# メンテナンス画面へ切り替え(KVフラグ)
$ npx wrangler kv key put --binding=CACHE "lp:maintenance" "1" --env=""
# 解除
$ npx wrangler kv key delete --binding=CACHE "lp:maintenance" --env=""
KV は結果整合のため、反映まで最大数十秒かかる。即時性が要る場合はロールバックかルート解除を使う。
17.4 データ復旧(D1 Time Travel)
D1 は Time Travel が常時有効。Paid は過去30日、Free は過去7日の任意時点に復元できる。
# 復元ポイントの確認
$ npx wrangler d1 time-travel info DB --env=""
# 特定時刻へ復元(⚠ 破壊的操作。実行前に必ず現状をエクスポート)
$ npx wrangler d1 export DB --remote --env="" --output=backup_$(date +%Y%m%d).sql
$ npx wrangler d1 time-travel restore DB --timestamp=... --env=""
⚠ 復元は破壊的操作。 復元前に必ずエクスポートを取る。四半期に1回、訓練として復元手順を通しておく。
17.5 事後対応
- 発生時刻・影響範囲・原因・対処を記録する
- 再発防止策をガードレール(F-24)として実装できないか検討する。人の注意力に頼る対策は採用しない
- 監査ログを確認し、誰の操作が起点だったかを特定する(責任追及ではなく、UIの改善点を見つけるため)
18. セキュリティ運用
18.1 秘密情報
| 種類 | 置き場所 |
|---|---|
| 複数Workerで共有する秘密(Slack Webhook 等) | Secrets Store(オープンベータ) |
| 1つのWorkerでしか使わない秘密 | wrangler secret put |
| CI/CD 用のAPIトークン | GitHub Secrets(権限は最小に) |
| Cookie署名鍵 | wrangler secret put COOKIE_SIGNING_KEY |
🔒 やってはいけないこと
wrangler.jsoncのvarsに秘密を書く(Gitに載る).dev.vars/.envをコミットする(.gitignore済みだが確認する)console.logに秘密や個人情報を出す(Workers Logs に残る)- スクリーンショットやSlackに貼る
18.2 定期作業
| 頻度 | 作業 |
|---|---|
| 四半期 | Access ポリシーの棚卸し(退職者・権限過剰) |
| 四半期 | APIトークンのローテーション |
| 半期 | LLM APIキー・媒体APIキーのローテーション |
| 随時 | 依存パッケージの脆弱性確認(npm audit) |
18.3 LPのセキュリティ設定
| 項目 | 設定 |
|---|---|
| CSP | default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; 外部を許可しない |
| Cookie | SameSite=Lax、署名付き。個人情報を入れない(識別子のみ) |
| フォーム | Turnstile 必須。サーバ側で必ず検証 |
| R2 | 公開バケット・署名付きURLを使わない。必ず Worker 経由 |
| WAF | 管理画面(ma.synon.co.jp)にレート制限とIP制限 |
| ヘッダ | X-Content-Type-Options: nosniff、Referrer-Policy: strict-origin-when-cross-origin |
19. 移行手順(案B を選んだ場合)
MA本体(F-01〜F-10)を Cloudflare へ移す場合の順序。一度に全部移さない。
19.1 移行の順序
⚠ 重要な前提: Hyperdrive は PostgreSQL / MySQL 専用で、SQLite には接続できない。 v1.2 の DB は SQLite のため、「既存DBを Hyperdrive 経由で参照しつつ画面だけ Workers 化する」という橋渡しは使えない。代わりに Phase 1 で D1 に読み取り用レプリカを作る方式を取る。 (出典: Hyperdrive — 対応DBは PostgreSQL / MySQL / PostgreSQL互換のみ)
Phase 1 参照系の画面から移す 【易】
└ SQLite → D1 へ定期同期した「読み取り用レプリカ」を作る
企業検索・一覧・分析画面だけ Workers で作り直す
既存システムが正。D1は捨てても困らない状態を保つ
▼
Phase 2 書き込みを D1 に切り替える 【中】
└ 企業・アプローチ・テンプレートの更新を D1 正に移す
既存システムは参照のみ。二重書き込み期間を設けて差分突合
▼
Phase 3 送信・計測系を移す 【中】
└ SendGrid送信(F-08) / Event Webhook受信 / GA同期(F-09)
/r/{tracking_code} の計測リダイレクト → Workers + Cron
※ 外部API呼び出しが中心で、Workers と相性が良い
▼
Phase 4 収集系を移す ★最難関 【難】
└ リストアップ(F-01) / 連絡先抽出(F-02) → Workflows + HTMLRewriter
フォーム営業(F-06) → Browser Run (旧 Browser Rendering)
ジョブキュー(BullMQ/Celery) → Queues + Workflows
※ ここだけは「移植」ではなく「作り直し」になる(19.4項)
▼
Phase 5 認証を Access に一本化し、既存システムを停止 【易】
Phase 4 を最後に置く理由: ここが唯一アーキテクチャの再設計を要求する。Phase 1〜3 が終わっていれば、Phase 4 が想定より重いと分かった時点で「収集系だけ既存サーバに残す」という妥協が選べる。逆順にすると引き返せなくなる。
19.2 SQLite → D1 の移行
D1 は SQLite ベースだが、そのままコピーはできない。
# 1. 既存 SQLite からスキーマとデータをエクスポート
$ sqlite3 ma.db .schema > schema.sql
$ sqlite3 ma.db .dump > dump.sql
# 2. D1 非対応の構文を書き換える
# - PRAGMA 文を削除
# - 一部の関数・拡張(FTS5 の設定など)を確認
# - AUTOINCREMENT の扱いを確認
# 3. マイグレーションとして取り込む
$ npx wrangler d1 migrations create DB import_from_sqlite
# 4. データ投入(大きい場合は分割)
$ npx wrangler d1 execute DB --remote --env staging --file=./data_part1.sql
⚠ 注意点
| 項目 | 内容 | 判定 |
|---|---|---|
| FTS5 | D1 は FTS5 を公式にサポートしている(fts5vocab 含む)。v1.2 の全文検索はそのまま移せる。ただし CREATE VIRTUAL TABLE ... USING fts5(...) は小文字で書く(大文字 FTS5 で not authorized になるという報告あり)。インデックス更新の性能は実機で確認する |
○ |
| JSON関数 / Math関数 | 公式にサポート | ○ |
| ビュー / 生成列 | サポート(PRAGMA table_xinfo に生成列が出る) |
○ |
| 外部キー | PRAGMA foreign_keys / defer_foreign_keys をサポート |
○ |
| トリガー / AUTOINCREMENT / 日付関数 | 公式ドキュメントに明記が無い。実機で全DDLを流して確認する | △ |
| 明示的トランザクション | BEGIN / COMMIT は使えない前提で設計する。 D1 の原子性の単位は batch()。v1.2 5.3項の「大量INSERTをトランザクションでまとめる」設計は batch() へ書き換えが必要 |
△ |
| WALモード / busy_timeout | D1 では意識不要(マネージド)。v1.2 5.3項の設計方針はそのまま不要になる | ○ |
| DBサイズ | 1DB最大 10GB(Paid) / 500MB(Free) | ○ |
| インポート | wrangler d1 execute --file は 5GiB まで。SQL文長の上限は10万バイトなので大量INSERTは250行程度に分割する。参照先テーブルを先に投入するか PRAGMA defer_foreign_keys = true を使う |
△ |
公式の「非対応機能の一覧」ページは存在しない。移行前に v1.2 の全 DDL とクエリをステージングの D1 に流して1本ずつ通すこと。 「移してみないと分からない」部分が残る前提で日程を組む。
19.3 移行時のリスクと対策
| リスク | 対策 |
|---|---|
| 移行中のデータ不整合 | 二重書き込み期間を設け、日次で差分を突合する |
| Cron / Queue consumer の wall clock 15分上限 | クロール・一括送信を1ジョブで回さない。Workflows(1ステップの実時間は無制限、ステップ数上限25,000)へ逃がす。19.4項 |
| Workers の CPU時間制限 | I/O待ち(fetch・D1クエリ)は CPU時間に加算されないため、クロール自体は思ったより通る。CPUを食うのは解析・集計側 |
| 既存 Playwright 資産が使えない可能性 | Browser Run の第一級サポートは Puppeteer。移行判断の前に、フォーム営業のPoCを Browser Run で1本通す |
| 認証切り替えでログインできなくなる | Access のポリシーを先に検証環境で通す。切り戻し経路を残す |
| 想定外の機能欠落 | 移行前に F-01〜F-10 の受入テストケースを作り、移行後に全件通す |
19.4 Phase 4(収集系)が「移植」ではなく「作り直し」になる理由
v1.2 の非同期処理は「BullMQ / Celery に数分〜数十分のジョブを投げる」前提で設計されている。Cloudflare にはこの実行モデルが無い。
| v1.2 の前提 | Cloudflare の制約 | 作り直しの内容 |
|---|---|---|
| ジョブキューに長時間ジョブを投げる | Queue consumer / Cron は wall clock 15分(CPU時間ではなく実時間) | 1ジョブ=1企業の粒度に分割し、Workflows のステップとして実装する。Workflows は1ステップの実時間が無制限で、待機中は同時実行数にカウントされない |
| BullMQ (Redis前提) | Redis 前提の npm パッケージは動かない | Queues(配送) + Workflows(実行制御) の2つに分けて再実装 |
| Celery (Python worker) | Workers 上で C拡張を含む Python ライブラリは動かない | TypeScript へ書き換え |
| Playwright でフォーム操作 | Browser Run(2026年4月に Browser Rendering から改称)は Puppeteer が第一級。セッションのアイドルは60秒、keep_alive で最大10分 |
Puppeteer ベースへ書き換え。1フォーム=1セッションに収める設計 |
| 外部プロセス起動 | node:child_process はスタブ実装(import できるが動かない) |
該当処理があれば設計変更 |
| クロールでのHTML解析 | HTMLRewriter に DOM API は無く、JavaScript も実行されない | 静的HTMLの抽出(F-02 のフォームURL・メール抽出)は HTMLRewriter で足りる。JS描画が必要なサイトは Browser Run に回すという2段構えにする |
Browser Run のスループット試算(Workers Paid、1件30秒と仮定)
| 同時ブラウザ | 1時間あたり | 月20営業日 × 8時間 |
|---|---|---|
| 1 | 120件 | 約 19,200件 |
| 10 | 1,200件 | 約 192,000件 |
追加ブラウザ時間の単価は $0.09(2025年8月の課金開始時。現在の単価は料金ページで要確認)。フォーム営業の実務量ならスループットも費用も問題にならない。効いてくるのは既存 Playwright スクリプトの書き換え工数のほう。
20. チェックリスト
20.1 構築完了チェック
- [ ] Workers Paid プランに加入している
- [ ] 本番・ステージングの両方でリソースが作成されている
- [ ]
wrangler deploy --dry-runが両環境で通り、バインディングに欠けがない - [ ]
npm run typecheckとnpm testが通る - [ ] D1 マイグレーションがステージング→本番の順で適用されている
- [ ] Cloudflare Access が管理画面に適用され、Worker 側でもJWT再検証している
- [ ]
/api/*に認証不要のエンドポイントが/api/health以外に無い - [ ] Turnstile が全フォームに適用され、サーバ側で検証している
- [ ] LP に
noindexが付いている(実験・ABM・広告LP) - [ ]
robots.txtで実験LPディレクトリと AdsBot をブロックしていない - [ ] 外部送信公表テキストが生成され、全LPのフッターからリンクされている
- [ ] 計測イベントが Analytics Engine に入り、Cron で D1 に集計されている
- [ ] Cron が UTC で正しく設定され、実際に実行されている
- [ ] 秘密情報が Secrets Store /
wrangler secretにあり、varsに平文が無い - [ ] Slack 通知が届く(テスト送信済み)
- [ ] 緊急停止(バリアント無効化・メンテナンス切替)を実際に試した
- [ ] D1 Time Travel の復元手順を1回通した
- [ ] ロールバックを1回試した
20.2 実験開始前チェック
- [ ] 必要サンプル数と所要日数を確認し、現実的な設定になっている
- [ ] 全バリアントの事実チェックが
pass - [ ] 全バリアントが人の承認を経ている
- [ ] 仮説を文書に書いた(後付けの解釈を防ぐ)
- [ ] 最短実験期間(7日)を設定している
- [ ] 実験期間中に段階デプロイを行わない合意がある
- [ ] ライブラリがロックされている
20.3 実験確定前チェック
- [ ] 最良確率 ≥ 95%
- [ ] 期待損失 ≤ 0.1%
- [ ] 各バリアントの累計CV ≥ 20件
- [ ] 経過日数 ≥ 7日(曜日効果を1周した)
- [ ] 期間中に外部要因(広告予算変更・障害・季節要因)が無かった、または注記した
- [ ] 因子効果とセグメント別効果を確認した
- [ ] 確定後も探索枠(5%)を残す設定になっている
21. 付録: 主要コマンド早見表
21.1 開発
npm run dev # Vite + wrangler dev を同時起動
npm run typecheck # 型検査
npm test # ユニットテスト
npm run cf-typegen # バインディングの型定義を再生成
npx wrangler dev --test-scheduled # Cron のローカルテスト
21.2 D1
npm run db:migrate:local # ローカルへ適用
npm run db:seed:local # テストデータ投入
npx wrangler d1 migrations create DB <説明> # マイグレーション作成
npx wrangler d1 migrations list DB --remote --env="" # 適用状況
npx wrangler d1 migrations apply DB --remote --env staging # ステージングへ適用
npx wrangler d1 migrations apply DB --remote --env="" # 本番へ適用
npx wrangler d1 execute DB --local --command "SELECT ..." # ローカル参照
npx wrangler d1 execute DB --remote --env="" --command "..."# 本番参照(⚠慎重に)
npx wrangler d1 export DB --remote --env="" --output=b.sql # エクスポート
npx wrangler d1 time-travel info DB --env="" # 復元ポイント確認
npx wrangler d1 time-travel restore DB --timestamp=... # 復元(⚠破壊的)
21.3 デプロイ
npx wrangler deploy --dry-run --env="" # バインディング確認(デプロイしない)
npm run deploy:staging # ステージングへ
npm run deploy # 本番へ
npx wrangler versions upload --env="" # バージョンのみ作成
npx wrangler versions deploy --env="" # 段階デプロイ(配分を指定)
npx wrangler deployments list --env="" # デプロイ履歴
npm run rollback # 直前へ戻す
21.4 秘密情報
npx wrangler secret put <NAME> --env="" # 登録・更新
npx wrangler secret list --env="" # 一覧(値は出ない)
npx wrangler secret delete <NAME> --env="" # 削除
21.5 KV / R2 / Queues
npx wrangler kv key put --binding=CACHE "key" "value" --env=""
npx wrangler kv key get --binding=CACHE "key" --env=""
npx wrangler kv key delete --binding=CACHE "key" --env=""
npx wrangler kv key list --binding=CACHE --env=""
npx wrangler r2 object put synon-ma-assets/path/file.html --file=./f.html
npx wrangler r2 object get synon-ma-assets/path/file.html
npx wrangler r2 object delete synon-ma-assets/path/file.html
npx wrangler queues list
npx wrangler queues info synon-ma-jobs
21.6 ログ・監視
npm run tail # 本番ログをストリーム
npx wrangler tail --status error # エラーのみ
npx wrangler tail --env staging --format json
npx wrangler ai models # Workers AI の利用可能モデル
npx wrangler whoami # ログイン中のアカウント
改訂履歴
| 版 | 日付 | 内容 |
|---|---|---|
| v1.0 | 2026-08-15 | 新規作成 |
本手順書の前提と限界
- 料金・上限値は 2026年8月時点の Cloudflare 公式ドキュメントに基づく。変わりうる。 課金判断の前に必ず公式ページで確認すること
- Workers AI のモデルIDは変わりうる。実装前に
wrangler ai modelsで現在の一覧を確認すること- Secrets Store はオープンベータ。GA前に仕様が変わる可能性がある
- Cron Trigger 数の上限について、公式ドキュメント内に「アカウント単位」と「Worker単位」の表記の混在がある。多数の Cron を使う場合は事前に実機で確認すること
- 本書のコマンドは、実行前に必ずステージングで通すこと