概要
個人ブログプロジェクト(Next.js静的エクスポート + Docusaurusドキュメントサイト)をNetlifyからCloudflare Pagesに移行した。サーバーレス関数もNetlify FunctionsからPages Functions(Cloudflare Workers)に書き換え、データストレージもNetlify BlobsからCloudflare KVに移した。そのまとめ。
移行の動機
GitHub Organizationを作って、リポジトリをorgに移したところ、Netlifyでorgのプライベートリポジトリを使うにはTeamプラン(有料)が必要だと言われた。個人のプライベートリポジトリなら無料プランで使えていたのだが、orgになると話が変わる。
Cloudflare Pagesは無料プランでもorgのプライベートリポジトリに対応している。加えて、無料プランでも帯域制限なし、ビルド回数500回/月と太っ腹。KV、R2(オブジェクトストレージ)、D1(SQLite)などエッジストレージの選択肢も豊富で、Netlifyよりもインフラの拡張性がある。
Netlify自体は良いサービスで、使い勝手も悪くない。ただ、自分のケースではCloudflareの方が合っていた。
Cloudflare Pages の概要
Cloudflare Pagesは静的サイトホスティングサービスで、Netlify・Vercelと同カテゴリ。Cloudflareのグローバルエッジネットワーク上で動作する。
デプロイ方法はいくつかあるが、GitHub Actions + wrangler CLIでデプロイする方式がおすすめ。Cloudflare側のビルド機能を使うこともできるが、GitHub Actionsで自分でビルドしてからデプロイする方が柔軟性がある。
Pages Functions
Cloudflare Pagesに統合されたサーバーレス関数で、実体はCloudflare Workers。ファイルベースルーティングで、functions/api/search.tsを置くと/api/searchでアクセスできる。
wrangler pages deploy時に自動的にesbuildでバンドル・コンパイルされる。
Netlify Functionsとの違いとして、関数のシグネチャが異なる。Netlify Functionsはexport default async (req, context) => {...}という形式だが、Pages Functionsは以下のようになる。
interface Env {
KEYWORD_LOGS: KVNamespace;
}
export const onRequestGet: PagesFunction<Env> = async (context) => {
const url = new URL(context.request.url);
const query = url.searchParams.get("q") || "";
// Cloudflare KV へのアクセス
const kv = context.env.KEYWORD_LOGS;
await kv.put("key", JSON.stringify({ data: "value" }));
return new Response(JSON.stringify({ results: [] }), {
headers: { "Content-Type": "application/json" },
});
};EnvインターフェースでKVなどのバインディングを型定義し、context.env経由でアクセスする。onRequestGetはGETリクエスト用のハンドラで、onRequestPostにすればPOSTになる。
Netlify Blobs → Cloudflare KV
データストレージはNetlify BlobsからCloudflare KVに移行した。KVネームスペースはwrangler kv namespace create KEYWORD_LOGSで作成できる。
wrangler.tomlにバインディングを設定する。
name = "my-project"
compatibility_date = "2024-12-01"
[[kv_namespaces]]
binding = "KEYWORD_LOGS"
id = "your-namespace-id"KVのAPIはシンプルで、putとgetだけ覚えれば使える。Netlify Blobsも似たようなAPIだったので、移行はほぼ機械的な書き換えだった。
GitHub Actions からのデプロイ
GitHub Actionsのワークフローでビルドしてからwrangler pages deployでデプロイする。
- name: Install function dependencies
run: npm install minisearch
- name: Deploy to Cloudflare Pages
run: |
wrangler pages deploy deploy \
--project-name=my-project \
--branch=main \
--commit-hash=${GITHUB_SHA}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}CLOUDFLARE_API_TOKENとCLOUDFLARE_ACCOUNT_IDをGitHubのSecretsに登録しておく。
PRプレビューも簡単で、--branch="pr-${PR_NUMBER}"を指定するとhttps://pr-123.my-project.pages.devのようなプレビューURLが生成される。
Netlify との比較
| 項目 | Netlify | Cloudflare Pages |
|---|---|---|
| サーバーレス関数 | Netlify Functions | Pages Functions |
| 関数の形式 | export default async (req, context) => {...} | export const onRequestGet: PagesFunction<Env> = async (context) => {...} |
| データストレージ | Netlify Blobs | Cloudflare KV |
| デプロイCLI | netlify deploy | wrangler pages deploy |
| ルーティング設定 | netlify.tomlのredirects | _redirectsファイル |
概念的には対応関係が明確なので、移行作業自体はそこまで大きくない。関数のシグネチャを書き換えて、ストレージAPIを差し替えて、デプロイ設定を変更する、という流れ。
functions ディレクトリの配置についての注意点
wrangler pages deploy <output-dir>はfunctions/ディレクトリを現在のワーキングディレクトリから探す。<output-dir>/functions/に置いてしまうと、静的ファイルとして扱われて関数がコンパイルされない。
# NG: functions を deploy ディレクトリの中に置く
deploy/
functions/ ← 静的ファイルとして扱われる
index.html
# OK: functions を deploy ディレクトリの兄弟に置く
functions/ ← wrangler が自動検出してコンパイル
deploy/
index.html正しく配置できていれば、wranglerのログにCompiled Worker successfullyと表示される。これが出ていなければ関数が検出されていない。自分はここで配置を間違えて、関数がデプロイされていないのにしばらく気づかなかった。デプロイログを確認するのが確実。
まとめ
移行自体は関数の書き換えとデプロイ設定の変更で完了し、大きな困難はなかった。Cloudflareは無料プランが充実していて、orgのプライベートリポジトリも対応しているので、自分のケースには合っていた。functionsディレクトリの配置だけ注意。
余談
Cloudflareはエッジストレージの選択肢が豊富で、KV以外にもR2(S3互換オブジェクトストレージ、エグレス料金なし)やD1(SQLite)がある。今回はKVしか使っていないが、将来的にもう少し複雑なデータを扱う場面が出てきたらD1を試してみるのも良さそう。