#cloudflare (11)
設計判断: Webクロール vs R2同期
Markdownドキュメントサイト(VitePress等)にRAGチャットボット導入時、データソース戦略はR2が正解。
| 観点 | Webクロール | R2 + 同期 |
|---|---|---|
| チャンク品質 | HTMLノイズ混入、低い | Markdownソース直接、高い |
| 横断検索 | ページ単位で弱い | frontmatter活用で強い |
| 導入コスト | URL指定だけ | 同期スクリプト保守が必要 |
判断基準: プロトタイプ以外はR2推奨。20-50ファイルでも品質差が大きく、数百ファイルではWebクロールが不安定化する。
アーキテクチャ
VitePress(チャットUI) → Workers API Proxy → Cloudflare AI Search
↑
R2 Bucket(Markdown)
AI Searchがチャンキング・Embedding・ベクトル検索・LLM回答生成を一気通貫で処理。自前でVectorize + Workers AIを組む必要がない。社内数十人規模なら月$5(Workers有料プラン)で運用可能。
API仕様
検索・回答API
/search- 自然言語検索(関連チャンクの取得)/ai-search- AI回答生成付き検索/ask- NLWeb統合(拡張子からの情報取得も可能)/mcp- MCPサーバー経由のアクセス
内部最適化機能
- 自動インデックス: ファイル変更を検知して再構築(R2ファイル削除時も自動反映)
- クエリ書き換え: ユーザーの曖昧な質問を改善
- リランキング:
@cf/baai/bge-reranker-baseモデルで意味的に再スコア - 類似性キャッシング: 頻出クエリの高速化
- 同期クールダウン: 3分、インデックス速度3-5x高速化
Workers呼び出し
const response = await env.AI.autorag("instance-name").aiSearch({
query: "ユーザーの質問",
model: "external-model-name" // オプション: AI Gateway経由で外部モデル使用可能
});
データソース
- R2: Markdown, docx, odt の自動クローリング
- Website: URLベースの自動クロール
- AI Gateway経由でClaude API等の外部LLMに切り替え可能(設定のみ)
セットアップ
R2同期
# GitHub Actions で定期実行
# docs を R2 にアップロード → AI Search が検索インデックス自動更新
APIトークンの権限
R2操作に「Workers R2 Storage: Edit」だけでは不足。「Edit Cloudflare Workers」テンプレートから作成するのが確実(Account Settings Read等が含まれる)。
wrangler r2の --remote フラグ
- ローカル開発・GitHub Actions ともに
--remoteが必須(デフォルトはlocal R2) Resource location: localと表示されたら--remoteが足りない
Embeddingモデル選定(日本語)
| モデル | 特徴 | コスト |
|---|---|---|
@cf/baai/bge-m3 | 多言語・Dense+Sparse同時対応 | $0.012/MTok |
@cf/preferred-networks/plamo-embedding-1b | 日本語特化 | $0.019/MTok |
小規模ドキュメントならbge-m3のコスト効率が優る。
実装知見・トラブルシューティング
SSEストリーミングの罠
aiSearch({ stream: true }) の戻り値はReadableStreamではなく**Responseオブジェクト**。instanceof Responseでチェックしてbodyを転送する。
if (streamResult instanceof Response) {
const headers = new Headers(streamResult.headers);
return new Response(streamResult.body, { headers });
}
wrangler.tomlの [vars] が secret を上書きする
wrangler secret putで設定した値が、wrangler.tomlの[vars]に同名キーがあると上書きされる。ローカル開発用の値は.dev.varsファイルに分離すること。
CORS設定のセキュアデフォルト
ALLOWED_ORIGINS未設定時は**return false(全拒否)** にする。return true(全許可)だと本番で設定忘れ時に全オリジンからアクセス可能になる。
問題
wrangler r2 object put で R2 にファイルをアップロード → 確認したら何も入っていない。
原因
wrangler は デフォルトでローカル R2(miniflare)にアップロードする
実際の Cloudflare R2 に送るには --remote フラグが必須。
解決方法
# NG:ローカル R2 にのみアップロード
wrangler r2 object put my-bucket/document.md --file ./document.md
# OK:実際の Cloudflare R2 にアップロード
wrangler r2 object put my-bucket/document.md --file ./document.md --remote
確認方法
# ローカル R2 の内容確認(デフォルト)
wrangler r2 object list my-bucket
# リモート(実際)の R2 の内容確認
wrangler r2 object list my-bucket --remote
ベストプラクティス
自動同期スクリプト(GitHub Actions等)では 必ず --remote を指定 する。
# GitHub Actions での R2 アップロード例
- name: Upload to R2
run: |
wrangler r2 object put leango-docs/docs.md \
--file ./dist/docs.md \
--remote
デバッグ Tips
アップロード後の検証:
# リモート確認
wrangler r2 object list my-bucket --remote
# ダウンロード確認
wrangler r2 object get my-bucket/document.md --remote
デフォルトで悩むのは一度で終わらせよう。
料金の核心
静的アセットへのリクエストは完全無料・無制限。課金対象はPages Functions(サーバーレス)のみ。
Freeプラン($0/月)
| 項目 | 制限 |
|---|---|
| 静的リクエスト | 無制限 |
| 帯域幅 | 無制限 |
| サイト数 | 無制限 |
| ビルド | 500回/月 |
| 同時ビルド | 1 |
| Functions | 100,000リクエスト/日(Workers共有) |
Paidプラン($5/月〜)
ビルド5,000回/月、同時ビルド20、Functions 1,000万リクエスト/月含む。
判断基準
output: 'static'の静的サイト → Freeで十分- SSR/Functions使う場合 → Paidプランの$5/月〜
- ビルド頻度が月500回超えるCI/CD → Paid検討
NetlifyやVercelと比べて帯域幅無制限が大きな差別化ポイント。
前提
- Astro 5 で静的サイト生成(
output: 'static') - Cloudflare Pages にデプロイ
- フォーム送信後の webhook 処理が必要
実装パターン
1. Pages Functions の配置
functions/
contact.ts # POST /api/contact
form-utils.ts # 共通ロジック
2. Astro コンポーネント側
---
// src/components/ContactForm.astro
// フォーム validation / UX は astro:islands で React/Svelte
---
<form id="contact-form">
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<button type="submit">送信</button>
</form>
<script>
document.getElementById('contact-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const response = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(formData))
});
// ...
});
</script>
3. Pages Functions で webhook 実行
// functions/contact.ts
export const onRequest: PagesFunction = async (context) => {
try {
const { name, email, message } = await context.request.json();
// Validation
if (!name || !email || !message) {
return new Response('Missing fields', { status: 400 });
}
// Webhook 実行
const slackResponse = await fetch(
context.env.SLACK_WEBHOOK_URL,
{
method: 'POST',
body: JSON.stringify({
text: `新規お問い合わせ\n名前: ${name}\nメール: ${email}\nメッセージ: ${message}`
})
}
);
if (!slackResponse.ok) {
throw new Error('Slack webhook failed');
}
return new Response(
JSON.stringify({ success: true }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
console.error('Form submission error:', error);
return new Response('Internal server error', { status: 500 });
}
};
ポイント
- 環境変数:
wrangler.tomlでenvセクションに Slack Webhook URL を定義 - CORS: Astro 静的ファイルとエッジ関数は同一オリジン(CORS 不要)
- キャッシング: Pages Functions は自動的にキャッシュされない(毎回実行)
メリット
- サーバーレス・自動スケーリング
- Google Firebase/Cloud Run より簡潔
- Cloudflare Analytics Engine で リクエスト監視可能
概要
Next.js App Router + Firebase → Astro + CF Pages への移行時のフォーム実装パターン。reCAPTCHA v3、Firebase Cloud Functions、HubSpot 埋め込みをどう置き換えるか。
アーキテクチャ変更
| 要素 | Next.js | Astro |
|---|---|---|
| CAPTCHA | reCAPTCHA v3 | Cloudflare Turnstile |
| バックエンド | Firebase Cloud Functions | CF Pages Functions (/api/*) |
| フォーム送信 | Server Action + Firebase | CF API ルート |
| HubSpot | 埋め込み(SSR時にスキップ) | React Island化 |
実装フロー
1. Turnstile の統合
<!-- Astro コンポーネント -->
<div id="turnstile-container"></div>
<script>
import Turnstile from '@/components/Turnstile.tsx';
</script>
<Turnstile />
2. API ルートの実装
// src/pages/api/submit-form.ts
export async function POST({ request }) {
const formData = await request.json();
// Turnstile 検証
const verifyResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
body: JSON.stringify({
secret: import.meta.env.TURNSTILE_SECRET_KEY,
response: formData.captchaToken
})
});
if (!verifyResponse.ok) {
return new Response(JSON.stringify({ error: 'Captcha failed' }), { status: 400 });
}
// メール送信等の処理
return new Response(JSON.stringify({ success: true }));
}
3. HubSpot 埋め込みの React Island化
// React コンポーネント
export default function HubSpotForm() {
useEffect(() => {
if (window.hbspt) {
window.hbspt.forms.create({
region: 'na1',
portalId: '...',
formId: '...',
target: '#hubspot-form'
});
}
}, []);
return <div id="hubspot-form" />;
}
Astro での使用:
<HubSpotForm client:load />
メリット
- CF Pages はサーバーレス、メンテナンス不要
- Turnstile は無料+プライバシー優先
- Astro + React Island で必要な部分のみインタラクティブ
検討点
- Firebase から CF Functions への認証情報移行
- メール送信ロジックの Durable Objects 利用の検討
- ログ保存先の決定(D1 / Firestore など)
概要
reCAPTCHA v3 から Cloudflare Turnstile へ移行する際の実装パターン。Turnstile は無料・プライバシーフレンドリーで CF Pages ネイティブ対応。
キー情報
- テストキー
- サイトキー:
1x00000000000000000000AA - シークレット:
1x0000000000000000000AA
- サイトキー:
- 本番環境: Cloudflare Dashboard で生成
クライアント側(React/Svelte)
// CDN 読み込み(head に挿入)
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
// Explicit rendering(推奨)
turnstile.render('#turnstile-container', {
siteKey: import.meta.env.PUBLIC_TURNSTILE_KEY,
theme: 'light',
callback: (token) => {
// トークン取得後の処理(フォーム送信など)
console.log('Turnstile token:', token);
}
});
// リセット(フォーム送信後)
turnstile.reset();
サーバー側(CF Pages Functions / Node.js)
// POST /api/verify-captcha
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: env.TURNSTILE_SECRET_KEY,
response: token
})
});
const data = await response.json();
if (!data.success) {
throw new Error('Captcha verification failed');
}
メリット
- reCAPTCHA より実装が簡単
- ユーザー側のプライバシー尊重(データ共有なし)
- CF Pages との相性が良い
- 無料で使用可能
背景
- Google reCAPTCHA は Google infrastructure 依存
- Cloudflare Pages への移行に伴い、Cloudflare 内部で完結する Turnstile に変更
差異比較
| 項目 | Google reCAPTCHA | Cloudflare Turnstile |
|---|---|---|
| チャレンジ | ロボット判定 | ロボット判定 |
| パフォーマンス | 速い | より高速(CF エッジで実行) |
| プライバシー | Google に送信 | CF 内部で完結 |
| 料金 | 無料 | 無料(CF Pages Free含む) |
| デプロイ | Google Console | wrangler.toml + env var |
| キャプチャUI | Google ブランド | CF ブランド(かつシンプル) |
実装
1. Cloudflare 側
# wrangler.toml
[[env.production.r2_buckets]]
name = "TURNSTILE_SECRET_KEY"
binding = "TURNSTILE_SECRET_KEY"
[env.production.vars]
TURNSTILE_SITE_KEY = "1x..."
2. フロント(Astro コンポーネント)
---
import { TURNSTILE_SITE_KEY } from 'astro:env/client';
---
<form id="contact-form">
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<!-- Turnstile ウィジェット -->
<div class="cf-turnstile" data-sitekey={TURNSTILE_SITE_KEY}></div>
<button type="submit">送信</button>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script>
document.getElementById('contact-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const token = window.turnstile.getResponse();
if (!token) {
alert('Please complete the verification');
return;
}
const formData = new FormData(e.target);
formData.append('cf-turnstile-response', token);
await fetch('/api/contact', {
method: 'POST',
body: new URLSearchParams(formData)
});
});
</script>
3. バック(Pages Functions)
// functions/contact.ts
export const onRequest: PagesFunction = async (context) => {
const formData = await context.request.formData();
const token = formData.get('cf-turnstile-response');
// Turnstile トークン検証
const verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
const verifyResponse = await fetch(verifyUrl, {
method: 'POST',
body: JSON.stringify({
secret: context.env.TURNSTILE_SECRET_KEY,
response: token
})
});
const data = await verifyResponse.json();
if (!data.success) {
return new Response('Verification failed', { status: 400 });
}
// フォーム処理続行...
};
メリット
- プライバシー: Google と通信なし
- 速度: Cloudflare エッジで即座に検証
- 一体性: Pages + Turnstile で CF 内完結
- UI/UX: Turnstile のキャプチャがシンプル
移行時の注意
- Turnstile は reCAPTCHA v3(スコアベース)ではなく v2 相当(チャレンジベース)
- 既存のスコアロジックがあれば別途実装が必要
従来アーキテクチャ(GCP VM)
User Request
↓
GCP Cloud Run / App Engine VM ← コールドスタート / スケーリング遅延
↓
HTML 生成 / SSR
↓
レスポンス
問題
- コールドスタート: 初回実行時 500ms 以上
- スケーリング: 同時アクセス増加時に自動スケール待ち
- 往復レイテンシ: 物理的な距離(地理的に離れたデータセンター)
- 固定コスト: インスタンス維持費
新アーキテクチャ(Astro → CF Pages CDN)
User Request
↓
CDN エッジ(地理的に近い)← 99.99% はキャッシュヒット
↓
静的 HTML / JS / CSS 返却(ミリ秒単位)
↓
レスポンス
メリット
- ゼロレイテンシ: キャッシュは世界 200+ のエッジロケーション
- スケーリング不要: CDN が自動的に分散
- コールドスタート無し: ファイルサーバーなので即座に返却
- 従量課金: 無制限の静的配信(Free)
実測値の例
| 指標 | GCP Cloud Run(SSD ストレージ) | CF Pages CDN |
|---|---|---|
| TTFB(東京から) | 150-300ms | 20-50ms |
| キャッシュヒット率 | N/A(毎回生成) | 99.9% |
| スケール対応 | 数秒待ち | 即座 |
| 月額コスト | $10-50 | $0(Free) |
Astro + CF Pages が強い理由
ビルド時生成
# yarn build
astro build
↓
dist/ に静的 HTML/CSS/JS を生成
↓
Pages が自動的に CF CDN に配置
Edge Functions は必要な時だけ
// Pages Functions(エッジで実行、スピン不要)
export const onRequest: PagesFunction = async (ctx) => {
return fetch('https://api.example.com/data');
};
動的処理はエッジ関数で即座に実行。サーバーVM起動なし。
チューニングのポイント
- キャッシュキー: 静的アセットに
Cache-Control: max-age=31536000(1年) - Stale-While-Revalidate: ISR 相当(バックグラウンド再生成)
- 圧縮: CF が自動的に Brotli 圧縮
// wrangler.toml でのキャッシュ設定
[env.production]
routes = [
{ pattern = "/static/*", cache_control = "public, max-age=31536000" },
{ pattern = "/*", cache_control = "public, max-age=3600, stale-while-revalidate=86400" }
]
移行判断基準
- 静的コンテンツ 80% 以上 → Astro + CDN(この場合)
- ユーザーごとの動的生成 → SSR 維持(Next.js)
- リアルタイム更新 → SPA + API(React)
結論: 静的配信できるなら CDN エッジが最高パフォーマンス。サーバーレス関数は webhook/API にのみ使う。
問題
astro dev では Cloudflare Pages Functions (/api/* 経由) が動作しない。
解決方法
CF Pages 環境をローカルで再現するには wrangler が必要:
# ビルド
astro build
# CF Pages Functions 付きで起動
wrangler pages dev dist
ポイント
1. シークレット管理
.envファイルは wrangler が読まない.dev.varsファイル を使用する- 形式は
.envと同じ(KEY=value)
# .dev.vars の例
TURNSTILE_SECRET_KEY=1x0000000000000000000AA
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
2. 既知の警告(無視してOK)
WebSocket関連のエラー:
TypeError: Web Socket request did not return status 101
→ miniflare の既知問題。ローカル開発に影響なし。
3. 使い分け
| 目的 | コマンド |
|---|---|
| 単純な Astro サイト | astro dev |
| API ルートも含む | wrangler pages dev dist |
ベストプラクティス
開発時は wrangler pages dev dist 一択で、CF Pages 本番環境と同じ条件でテストする。
Cloudflare Pages アプリのアクセス制限
プライベート用アプリにアクセス制限をかける3つの方法:
| 方法 | コスト | 特徴 |
|---|---|---|
| Cloudflare Access (Zero Trust) | 無料50ユーザー | GCP IAP相当。コード変更不要 |
| Basic認証(Honoミドルウェア) | 無料 | UXが悪い |
| OAuth許可リスト | 無料 | 既存OAuth活用 |
Cloudflare Access が最適な理由
- コード変更ゼロ: ダッシュボード設定のみで前段にログインゲートが入る
- 既存のアプリ内認証(Google OAuthなど)と共存可能
- メール、Google、GitHubなど複数のIdPに対応
- デフォルトでワンタイムPIN(メール認証)が使える
設定の流れ
Zero Trust Dashboard > Access > Applications > Self-hosted
→ ドメイン指定 → Policy で許可メールアドレス設定 → 完了
GCP IAP的なことをやりたい場合、Cloudflareならこれ一択。