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か

項目AmplifyVercelCloudflare 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_date2024-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 WorkersCloudflare Pages
カスタムドメインCloudflare DNSゾーン必須CNAME設定のみでOK
SSL証明書ゾーン管理下で自動自動発行
デプロイ方法wrangler deploywrangler 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.tsproxy.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が有力な移行先