Next.js製のAPIサーバーをAWS AmplifyからCloudflare Pagesに移行しました。月額約$23(Amplify $15 + WAF $8)のコストが$0になりました。
背景
移行前の構成
- フレームワーク: Next.js 15(App Router)
- ホスティング: AWS Amplify(WEB_COMPUTE / SSR)
- WAF: Amplifyが自動作成したWebACL
- バックエンド: 外部のElasticsearchに接続するAPIルート
- 月額コスト: Amplify $15 + WAF $8 = 約$23/月
無料枠(12ヶ月)が切れた後、1アプリのSSRホスティングとしてはやや割高でした。特にWAFはAmplify作成時に自動的に有効化されており、$5/WebACL/月 + リクエスト課金で$8程度かかっていました。
なぜCloudflare Pagesか
| 項目 | Amplify | Vercel | Cloudflare Pages |
|---|---|---|---|
| SSR対応 | 有料 | 個人無料/チーム$20/人 | 無料 |
| チーム利用 | IAM管理 | 有料 | 無料 |
| 帯域 | 15GB後従量 | 100GB/月 | 無制限 |
| カスタムドメイン | CloudFront経由 | 簡単 | CNAME設定で可能 |
VercelはNext.jsとの相性が最も良いですが、チーム利用は有料($20/ユーザー/月)です。Cloudflare Pagesは無料プランでもチーム・商用利用が可能で、帯域も無制限です。
移行手順
1. @opennextjs/cloudflare のインストール
Cloudflare上でNext.jsを動かすために、@opennextjs/cloudflareを使います。
npm install --save-dev @opennextjs/cloudflare wrangler
2. open-next.config.ts の作成
プロジェクトルートに設定ファイルを作成します。
// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({});
3. next.config.js の修正
output: 'standalone' はCloudflare向けでは不要なので削除します。
/** @type {import('next').NextConfig} */
const nextConfig = {
// output: 'standalone', を削除
outputFileTracingIncludes: {
'/api/**/*': ['./data/**/*', './lib/**/*'],
},
}
module.exports = nextConfig
4. wrangler.jsonc の作成
{
"name": "my-app",
"pages_build_output_dir": ".open-next/assets",
"compatibility_date": "2026-04-06",
"compatibility_flags": ["nodejs_compat"]
}
nodejs_compat フラグが重要です。これにより、Node.jsの組み込みモジュール(fs, path, crypto等)がCloudflare Workers上で利用可能になります。compatibility_date は 2024-09-23 以降を指定する必要があります。
5. package.json にスクリプト追加
{
"scripts": {
"build:cf": "npx @opennextjs/cloudflare build",
"preview": "npx @opennextjs/cloudflare build && wrangler dev",
"deploy": "npx @opennextjs/cloudflare build && wrangler deploy"
}
}
6. Elasticsearchクライアントの書き換え
ここが最大のハマりポイントでした。@elastic/elasticsearch パッケージはNode.jsの https モジュールを使ってTCP接続を行いますが、Cloudflare Workersの nodejs_compat では ALPNProtocols オプションが未実装のためエラーになります。
The options.ALPNProtocols option is not implemented
解決策として、Elasticsearchクライアントを fetch ベースに書き換えました。
// app/lib/elasticsearch.ts(変更後)
const getConfig = () => ({
node: process.env.ES_HOST ? `https://${process.env.ES_HOST}` : '',
auth: {
username: process.env.ES_USER || '',
password: process.env.ES_PASSWORD || ''
}
})
function createClient() {
return {
async search(params: { index: string; body: Record<string, unknown> }) {
const config = getConfig()
const url = `${config.node}/${params.index}/_search`
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (config.auth.username) {
headers['Authorization'] = 'Basic ' +
btoa(`${config.auth.username}:${config.auth.password}`)
}
const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(params.body),
})
if (!res.ok) {
const text = await res.text()
throw new Error(`Elasticsearch error ${res.status}: ${text}`)
}
return await res.json()
}
}
}
let client: ReturnType<typeof createClient> | null = null
export function getClient() {
if (!client) {
client = createClient()
}
return client
}
既存のAPIルートは esClient.search() というインターフェースのみ使っていたため、ルート側の変更は @ts-expect-error の削除程度で済みました。
7. ビルドとローカルテスト
# ビルド
npm run build:cf
# ローカルテスト(.dev.varsに環境変数を設定)
wrangler dev
.dev.vars ファイル(gitignoreに追加):
ES_HOST=your-elasticsearch-host.com
ES_USER=your-username
ES_PASSWORD=your-password
8. Cloudflare Pagesへのデプロイ
# Pagesプロジェクト作成
npx wrangler pages project create my-app --production-branch main
# ビルド出力をデプロイ(_worker.jsを含める)
cp .open-next/worker.js .open-next/_worker.js
npx wrangler pages deploy .open-next --project-name my-app --branch main
# 環境変数(シークレット)設定
npx wrangler pages secret put ES_HOST --project-name my-app
npx wrangler pages secret put ES_USER --project-name my-app
npx wrangler pages secret put ES_PASSWORD --project-name my-app
注意点として、@opennextjs/cloudflare のビルド出力は本来Workers向けですが、Pages にデプロイする場合は .open-next/worker.js を _worker.js としてコピーし、.open-next ディレクトリ全体をデプロイします。
9. カスタムドメイン設定
Cloudflare Pagesのカスタムドメインは、Cloudflare APIでドメインを追加し、DNSのCNAMEを設定するだけです。
# Cloudflare APIでカスタムドメイン追加
curl -X POST \
"https://api.cloudflare.com/client/v4/accounts/{account_id}/pages/projects/my-app/domains" \
-H "Authorization: Bearer {api_token}" \
-H "Content-Type: application/json" \
-d '{"name":"my-app.aws.ldas.jp"}'
DNS側(今回はRoute 53)でCNAMEを設定:
my-app.aws.ldas.jp → my-app.pages.dev
SSL証明書はCloudflareが自動発行します。数分で active になりました。
Route 53でDNSを管理したまま、Cloudflare Pagesのカスタムドメインを使うことができます。Cloudflareにゾーンを移管する必要はありません。
Workers vs Pages — カスタムドメインの違い
移行中に知った重要な違いです。
| 項目 | Cloudflare Workers | Cloudflare Pages |
|---|---|---|
| カスタムドメイン | Cloudflare DNSゾーン必須 | CNAME設定のみでOK |
| SSL証明書 | ゾーン管理下で自動 | 自動発行 |
| デプロイ方法 | wrangler deploy | wrangler pages deploy |
| GitHub連携 | 手動設定 | ダッシュボードから簡単 |
最初Workersにデプロイしましたが、カスタムドメインにCloudflareゾーンが必要と判明し、Pagesに切り替えました。既存のDNS(Route 53、さくら等)をそのまま使いたい場合はPagesを選択してください。
移行後のコスト比較
| 項目 | 移行前(Amplify) | 移行後(Cloudflare Pages) |
|---|---|---|
| ホスティング | $15/月 | $0 |
| WAF | $8/月 | 不要 |
| SSL証明書 | ACM(無料) | Cloudflare(無料) |
| 合計 | $23/月 | $0/月 |
年間で約$276の削減になります。
レスポンス速度の改善
コスト削減だけでなく、体感できるレベルでレスポンスが高速になりました。
移行後のCloudflare Pages上でのTTFB(Time to First Byte)を計測した結果です。
| エンドポイント | TTFB |
|---|---|
/api/health(単純なJSON返却) | 約40ms |
/api/search(Elasticsearch検索) | 約240〜300ms |
/api/pages/:id(単一ドキュメント取得) | 約55〜90ms |
Amplifyは移行前に削除してしまったため直接比較はできませんが、体感として明らかに高速になりました。
理由はSSRの実行場所の違いです。
- Amplify (WEB_COMPUTE): SSRはAWSの特定リージョン(多くの場合us-east-1)で実行される。日本からアクセスすると太平洋を往復するため、+100〜200ms程度のレイテンシが加算される
- Cloudflare Pages (Workers): 東京を含む世界中のEdgeロケーションで実行される。ユーザーに最も近いEdgeでSSRが行われるため、ネットワークレイテンシが最小限
特にAPIサーバーのように頻繁にリクエストが発生する用途では、この差が大きく効いてきます。
移行できないケース — Next.js 16 + proxy.ts
別のNext.js 16プロジェクト(next-intl による多言語対応アプリ)でも同様にCloudflare Pages移行を試みましたが、こちらは失敗しました。
Next.js 16では従来の middleware.ts が proxy.ts にリネームされ、Node.jsランタイム固定になりました。@opennextjs/cloudflare v1.18.0時点ではこの新しい proxy.ts に未対応で、以下のエラーが発生します。
Node.js middleware is not currently supported.
Consider switching to Edge Middleware.
これは opennextjs/opennextjs-cloudflare#962 として報告されている既知の問題です。
移行可否の判断基準
| 条件 | 移行可否 |
|---|---|
| Next.js 15以下 + API Routes のみ | ✅ 移行しやすい |
| Next.js 15以下 + Edge Middleware | ✅ 対応済み |
| Next.js 16 + proxy.ts(旧middleware.ts) | ❌ 現時点では未対応 |
@elastic/elasticsearch 等のTCPライブラリ | ⚠️ fetch への書き換えで対応可 |
sharp 等のネイティブモジュール | ❌ Workers環境では動作しない |
まとめ
@opennextjs/cloudflareでNext.jsアプリをCloudflare上で動かせる@elastic/elasticsearchなどNode.js TCPベースのライブラリはfetchに書き換えが必要- カスタムドメインを外部DNS(Route 53等)で管理したい場合は Workers ではなく Pages を使う
- Next.js 16の
proxy.tsには未対応(2026年4月時点)。移行前にバージョンとMiddleware利用の有無を確認すること - Amplifyの無料枠が切れた個人・小規模プロジェクトには、Cloudflare Pagesが有力な移行先