本文へスキップ

plastic skies /

Cloudflare Workers のアクセス数を NAS に溜める

書いた人 salty_7 #104

  • cloudflare
  • synology
  • analytics

Cloudflare の無料プランは、アクセス統計を7日で捨てるので NAS に保存する仕組みを導入しました。

実施前の状況と課題

Cloudflare Workers サイトのアクセス数は、2つの場所から見られます。

Web Analytics は、ページに埋め込まれた JavaScript でカウントしています。 訪問者のブラウザが実行するので、人が読んだページだけが記録されます。 ただし、広告ブロッカー等でトラッカーがブロックされているとカウントされません。

Security Analytics は、Cloudflare のサーバー側が数えます。 人かどうかに関係なく全リクエストが入っています。

Cloudflare のコンソールやAPIからどちらも確認できますが、 Security Analytics は無料プランでデータ保持が7日、一度に照会できる範囲が24時間しかありません。 先月と比べる、といったことが原理的にできません。

せっかく作った独自ドメインサイトのアクセス数はずっと残しておきたいので、 消える前に自分で溜めるしかありません。

やったこと

Cloudflare の GraphQL API から日次でアクセス数を取得し、NAS に蓄積する仕組みを作りました。

読み取り専用のCloudflare権限を作る

Claude自身にCloudflareの情報を読み取らせるため、最初に確認したのは、 Cloudflare が配布している AI エージェント向けのセットアップ用プロンプトでした。 まずは実行せずにClaudeに内容をチェックさせると、人間に確認を挟まずにスキル群を導入し、5つの MCP サーバーを OAuth 込みで登録させるものでした。 そのまま実行させず、読み取りだけを許可する構成にしました。

Cloudflare上の権限の組み合わせは Claude が提案し、APIトークンの作成は私が行っています。 トークンの値は環境変数 or 設定ファイル経由でClaudeが直接読まないようにしました。 Claude用に用意したCloudflare上の権限は DNS・ゾーン設定・Analytics の読み取りのみ、 対象ゾーンも renidentia991.com だけに限定しました。

Claudeの作業中に insufficient_scope の 403 が返り続けましたが、これは権限不足ではなく、 MCP クライアントがカスタムヘッダーより先に OAuth を試みるためでした。 認証が必要なMCPの利用を諦め、認証不要な cloudflare-docs だけを使うようにしました。 認証が必要なMCPでやろうとしていたことは、直接 REST / GraphQL API をたたいています。

書き込みが実際に 403 で拒否されることは、Claude が存在しないレコードへの削除要求を投げて確かめました。 404 ではなく 403 が返ります。存在確認より先に権限で弾かれています。

実行場所を NAS にする

Claude からは GitHub Actions と NAS の両方の設計が出ましたが、NAS を選びました。

NASは Synology DS215j です。2015年の機種で、CPU は ARMv7 の 800MHz、メモリは 512MB。 Docker も動かない古い箱ですが、1日1回 curl を叩くだけなら十分です。 PC と違って常時起動している点も条件に合っていました。

DSM のタスクスケジューラで毎日 0時30分に実行します。 前日分を取り、CSV に追記するだけの処理です。

raw/2026-08-17.json     生データ(保険)
data/2026-08.csv        date_jst,path,requests
log/2026-08.log

スクリプトは Claude が書き、NAS への配置と実行は私が行いました。 不具合が出るたびにログを渡して直させる、という進め方をしています。 ※後述の DS215j 特有の事象 参照

ポイント

生データを必ず残す

CSV だけでなく、API の応答をそのまま JSON で保存しています。

元データは7日で消えるので、集計の基準を後から変えたくなっても取り直せません。 逆に生データさえ残っていれば、集計は何度でもやり直せます。 時間制約があるのは取得だけで、加工にはありません。

DS215j 特有の事象

どちらも、DS215j の上でしか再現しませんでした。

1. 同梱の curl が古い

curl: (92) HTTP/2 PROTOCOL_ERROR が出て POST だけが全て失敗しました。 原因は、同梱の curl が古く、HTTP/2 でボディ付きの POST を壊していたためでした。--http1.1 オプションをつけることで回避しています。

2. jq のビルドが想定外

CSV にヘッダーしか出力されませんでした。 原因は、DSM の jq が正規表現ライブラリなしでビルドされていて、test() が使えず、CSV作成時のテキスト編集に失敗していたせいでした。 endswith() で書き直しました。

集計したアクセス数にはクローラーが混ざっている

このサイトは WordPress ではないですが、/wp-admin/install.phpへのアクセスがありました。よくある攻撃手段のようです。 こういった攻撃等のアクセス数をカウントしないため、「200 以外の応答」を除外しています。

ただし実在するページを正しく読んでいくクローラーは、200 を返すので集計に残ります。

ある日、複数のページのアクセス数がほぼ同じになっていました。 9, 9, 9, 9, 8, 8, 7, 7, 7, 7 という並びです。 人間のアクセスならこうはならないはずです。

User-Agent を見ると heritrix が14ページ、MJ12bot が12ページ、 Chrome を名乗る何かが11ページを巡回していました。 これらはなんらかのクローラーで、Cloudflare の「認証済みボット」判定には引っかからない種類のものです。

全ページに満遍なくアクセスされている場合、それは読者ではなく巡回とみてよさそうです。 User-Agent で除外もできますが、今のところそのまま集計することにしました。

まとめ

Cloudflare の無料プランではページごとのアクセス数を永続的に保存できませんが、定期的に収集する仕組みを作れました。 今回は収集までなので、ビューワー部分は別途作りたいと思います。

plastic skies の一覧へ RSS