Cloudflare Workers でサイト監視 + Discord通知を作った

外形監視をどこに任せるか迷った。ラズパイでやるか、Cloudflareに任せるか。 外から見えるサービスの監視なら せっかくなのでCloudflare Workersにする。 自宅NWそこそこ落ちたりするので・・・。 ラズパイと比較した Cloudflare Workers ラズパイ 向いてる監視 外形監視(外からの死活確認) 内部NW監視 コスト 無料 電気代のみ メンテ ほぼゼロ たまに落ちる ローカルNW確認 ❌ ✅ 最小間隔 1分 自由 今回は wasutech.dev と blog.wasutech.dev、techblog.wasutech.dev の3つを監視したい。 Cloudflare Workers 無料枠で十分な理由 公式ドキュメント - Limits によると、無料プランは以下のとおり。 Cron Triggers: 5個まで リクエスト: 1日10万回まで CPU時間: 10ms/invocation 今回のユースケースは「HTTPリクエスト投げてステータスコード確認して Discord Webhook 叩く」だけなので、CPU時間は 2〜3ms で収まる。5分間隔で3ドメイン監視しても 1日864リクエストなので余裕。 コード全文 // src/index.ts const TARGETS = [ { name: "wasutech.dev", url: "https://wasutech.dev" }, { name: "blog.wasutech.dev", url: "https://blog.wasutech.dev" }, { name: "techblog.wasutech.dev", url: "https://techblog.wasutech.dev" }, ]; export default { async scheduled(_event: ScheduledEvent, env: Env, _ctx: ExecutionContext) { const results = await Promise.allSettled( TARGETS.map((t) => check(t.name, t.url)) ); const failures = results .map((r, i) => ({ result: r, target: TARGETS[i] })) .filter(({ result }) => result.status === "rejected" || (result.status === "fulfilled" && !result.value.ok) ); if (failures.length > 0) { const lines = failures.map(({ result, target }) => { const detail = result.status === "rejected" ? (result.reason as Error).message : `HTTP ${(result.value as Response).status}`; return `🔴 ${target.name} | ${detail}`; }); await notify(env.DISCORD_WEBHOOK, lines.join("\n")); } }, }; async function check(name: string, url: string): Promise<Response> { const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(10000), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res; } async function notify(webhookUrl: string, msg: string) { await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: msg }), }); } interface Env { DISCORD_WEBHOOK: string; } # wrangler.toml name = "wasutech-monitor" main = "src/index.ts" compatibility_date = "2025-01-01" [triggers] crons = ["*/5 * * * *"] Promise.allSettled を使った理由 Promise.all だと1つが失敗した時点で残りを待たずに throw する。 Promise.allSettled なら3つ並列でチェックして、全部の結果を集めてからまとめて通知できる。 1メッセージにまとめることでDiscordがスパムにならない。 ...

April 13, 2026 · 2 min

CloudflareアラートをWorker + Gemini APIでDiscordに要約通知する

Cloudflareの通知をDiscord Webhookに流していたら、通知が大量に届くようになってしまった。重要なアラートが埋もれるので、Cloudflare Workers + Gemini APIで要約してから投稿するようにした。 方針 Cloudflareの通知設定で追加できるアラートはとりあえず全部有効にしている。各アラートが実際に役立つかは様子を見ながら判断する予定。 ただしセキュリティアラートはだいたい30分に数回届いたため、現時点では通知をスキップするようにした。それ以外のアラートは引き続き通知して様子見中。 構成 Cloudflare Alert → Worker受信 → フィルタリング(スキップ対象なら早期リターン) → Gemini API で日本語要約 → Discord Webhook に送信 前提 Cloudflare WorkersにDiscord Webhook通知のWorkerが既にある Google AI StudioのAPIキーを持っている モデル選定 Geminiのモデルは料金帯がいくつかある。 gemini-2.5-pro > gemini-2.5-flash > gemini-2.5-flash-lite Cloudflareアラートの要約程度であれば gemini-2.5-flash-lite で十分。一番安い。 最新のモデル名は公式ドキュメントで確認すること。 https://ai.google.dev/gemini-api/docs/models 実装 フィルタリングの考え方 頻度の高いアラートはWorker側でスキップできるようにしている。SKIP_ALERT_TYPES に列挙したタイプが一致した場合、Gemini APIを呼ばずに早期リターンする。不要なAPI呼び出しも減るのでコスト面でも良い。 どのアラートがどの alert_type を持つかはCloudflareの公式ドキュメントで確認できる。 https://developers.cloudflare.com/notifications/notification-available/ Worker コード // スキップしたいアラートタイプ(頻度が高くて邪魔なものを列挙) const SKIP_ALERT_TYPES = [ "security_alerts", // セキュリティアラート:30分に数回来るので無効化中 ]; async function summarizeWithGemini(apiKey, input) { const prompt = `以下のCloudflareアラートを日本語で3行以内に要約してください。重要度(🔴高/🟡中/🟢低)も判定してください。\n\n${input}`; const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite-preview-06-17:generateContent?key=${apiKey}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }) } ); const data = await res.json(); console.log("Gemini response:", JSON.stringify(data)); return data.candidates?.[0]?.content?.parts?.[0]?.text || "要約失敗"; } export default { async fetch(request, env) { if (request.method !== "POST") { return new Response("ok"); } let body; try { body = await request.json(); } catch { return new Response("invalid json", { status: 400 }); } // アラートタイプによるフィルタリング const alertType = body.text?.alert_type || body.alert_type || ""; if (SKIP_ALERT_TYPES.some(t => alertType.includes(t))) { console.log(`Skipped alert type: ${alertType}`); return new Response("skipped"); } // Geminiには常にbody全体を渡す const input = JSON.stringify(body).slice(0, 500); const summary = await summarizeWithGemini(env.GEMINI_API_KEY, input); const message = { username: "Cloudflare", embeds: [{ title: body.text?.title || body.text?.description || "Cloudflare Notification", description: summary, color: 0xF6821F, timestamp: new Date().toISOString() }] }; await fetch(env.DISCORD_WEBHOOK, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) }); return new Response("ok"); } } シークレット登録 wrangler secret put GEMINI_API_KEY wrangler secret put DISCORD_WEBHOOK 動作確認 curl -X POST https://<your-worker>.workers.dev \ -H "Content-Type: application/json" \ -d '{"text": {"title": "テスト通知", "description": "これはテストです"}}' スキップ確認: ...

April 12, 2026 · 2 min

Cloudflareの全通知をDiscord Webhookに飛ばす

CloudflareのNotificationsはUIから1個ずつ設定するのがとにかくだるい。 種別も多いし、手動でDiscord Webhookを登録していくのは非現実的。 Cloudflare APIとWorkersを組み合わせて全通知を一括登録する。 構成 Cloudflare Notifications ↓ Cloudflare Worker (受け口) ↓ Discord Webhook Cloudflare NotificationsはWebhook送信に対応しているので、Workerを受け口にしてDiscordに転送する。 1. Discord Webhookを作成 通知を飛ばしたいDiscordチャンネルの設定から作成する。 チャンネル設定 → Integrations → Webhooks → New Webhook → Copy Webhook URL 2. Cloudflare Workerを作成 Cloudflareダッシュボードから Workers & Pages → Create → Hello World で作成する。 名前は cf-notify-discord など適当につけてDeploy。 エディタ画面で以下のコードに置き換える。 export default { async fetch(request, env) { if (request.method !== "POST") { return new Response("ok"); } let body; try { body = await request.json(); } catch { return new Response("invalid json", { status: 400 }); } const message = { username: "Cloudflare", embeds: [{ title: body.text?.title || "Cloudflare Notification", description: body.text?.description || JSON.stringify(body, null, 2), color: 0xF6821F, timestamp: new Date().toISOString() }] }; await fetch(env.DISCORD_WEBHOOK, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message) }); return new Response("ok"); } } Discord WebhookのURLはWorkerのSettings → Variables and Secretsでシークレットとして登録する。 ...

April 12, 2026 · 3 min