

【環境構築】Next.js × Cloudflare Workers の本番環境を一から構築する
はじめに
開発環境(dev)で動いている Next.js アプリケーションを本番環境にデプロイするには、単にコードを push するだけでは済みません。データベース、ストレージ、認証、決済など、各サービスのリソースを本番用に新規作成し、適切に分離する必要があります。
この記事では、Cloudflare Workers 上で動く Next.js アプリの本番環境を一から構築した際の手順と設計判断をまとめます。
この記事でわかること
-
dev/prod 環境のリソース分離設計の考え方
-
Cloudflare API トークンの種類と使い分け
-
Terraform で本番 R2 バケットを作成する手順
-
Prisma Postgres の環境別 DB 構成
-
GitHub Secrets を使った環境別シークレット管理
-
本番デプロイまでの正しい作業順序
対象読者
-
Cloudflare Workers + Next.js でアプリを運用している方
-
dev 環境はあるが prod 環境をこれから作る方
-
インフラを Terraform で管理したい方
前提条件
-
Next.js (App Router) + Cloudflare Workers (OpenNext) の構成
-
Terraform CLI がインストール済み
-
GitHub Actions で CI/CD が構築済み
-
dev 環境が稼働中
技術スタック
| サービス | 用途 |
|---|---|
| Cloudflare Workers | サーバーレスランタイム (Next.js) |
| Cloudflare R2 | ファイルストレージ (S3互換) |
| Prisma Postgres | データベース (Accelerate経由) |
| Stripe | 決済・サブスクリプション |
| Resend | トランザクションメール |
| Google OAuth | ソーシャルログイン |
| GitHub Actions | CI/CD |
| Terraform | インフラ管理 |
1. 環境分離の設計方針
リソース分離の原則
本番環境では全リソースを完全分離する方針を取りました。
| リソース | dev | prod |
|---|---|---|
| Cloudflare アカウント | 共通 | 共通 |
| Worker 名 | app-dev | app |
| R2 バケット | app-dev | app-prod |
| データベース | 開発用 DB | 本番用 DB |
| Stripe | テストモード | ライブモード |
| API キー各種 | dev 用 | prod 用 |
API キーも分離する理由
-
レート制限を環境ごとに独立させる
-
本番の障害が dev 環境に波及しない
-
セキュリティ侵害時の影響範囲を限定
2. Cloudflare API トークンの設計
Cloudflare では用途別に3種類のトークンを使い分けます。
トークン構成
| 用途 | トークン名例 | 権限 | 設定先 |
|---|---|---|---|
| Terraform (IaC) | prod-infra | Workers R2 Storage: 編集 | terraform.tfvars |
| wrangler deploy (CI/CD) | ci-deploy | Workers 全般 (Edit Cloudflare Workers テンプレート) | GitHub Secrets |
| R2 S3互換 (アプリ) | prod-r2 | Object Read & Write | GitHub Secrets |
R2 トークンの種類に注意
R2 の API トークン管理画面には2種類あります。
-
アカウント API トークン — 組織に紐づく。組織を離れても有効。本番推奨
-
ユーザー API トークン — 個人に紐づく。組織を離れると無効化
3. Terraform で R2 バケットを作成
ディレクトリ構成
terraform/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
└── modules/
├── r2/ # R2バケット + CORS設定
└── worker/ # Worker Cronトリガー
prod/main.tf の構成
module "r2" {
source = "../../modules/r2"
account_id = var.cloudflare_account_id
bucket_name = "app-prod"
allowed_origins = [
"https://example.com", # 本番ドメインのみ
]
}
module "worker" {
source = "../../modules/worker"
account_id = var.cloudflare_account_id
worker_name = "app"
cron_schedules = ["0 15 * * *"] # JST 0:00
}
R2 だけ先行作成
Worker Cron トリガーは既存の Worker が必要なため、初回は R2 だけ先に作成します。
cd terraform/environments/prod
terraform init
terraform plan -target=module.r2
terraform apply -target=module.r2
terraform apply(target 指定なし)で追加します。4. Prisma Postgres の環境別構成
同一プロジェクト内で DB を分離
Prisma Console では、1つのプロジェクト内に複数のデータベースを作成できます。新しいプロジェクトを作る必要はありません。
プロジェクト: my-app
├── Development ← dev環境用(既存)
└── Production ← prod環境用(新規作成)
作成手順
-
Prisma Console でプロジェクトを開く
-
左サイドバーの DATABASES の
+ボタンをクリック -
名前:
Production、リージョン: Asia Pacific (Tokyo) -
2つの接続 URL を取得
| URL 種類 | 形式 | 用途 |
|---|---|---|
| DATABASE_URL | prisma+postgres://accelerate.prisma-data.net/?api_key=… | アプリからの接続(Accelerate 経由) |
| DIRECT_URL | postgresql://…@db.prisma.io:5432/postgres | マイグレーション実行用(直接接続) |
5. GitHub Secrets の環境別管理
GitHub Environments の活用
GitHub Actions の environment 機能を使い、環境別にシークレットを分離します。
# deploy.yml
jobs:
deploy:
environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
これにより:
-
mainブランチ →prod環境のシークレットを使用 -
developブランチ →dev環境のシークレットを使用
CLI で一括登録
# 環境別にシークレットを登録
gh secret set DATABASE_URL --env prod --body "[DATABASE_URL]"
gh secret set AUTH_SECRET --env prod --body "$(openssl rand -base64 32)"
gh secret set CRON_SECRET --env prod --body "$(openssl rand -hex 32)"
# ... 他のシークレットも同様
必要なシークレット一覧
| カテゴリ | シークレット名 | 取得元 |
|---|---|---|
| Database | DATABASE_URL | Prisma Console |
| Database | DIRECT_URL | Prisma Console |
| Auth | AUTH_SECRET | openssl で生成 |
| Auth | AUTH_GOOGLE_ID | Google Cloud Console |
| Auth | AUTH_GOOGLE_SECRET | Google Cloud Console |
| Auth | NEXTAUTH_URL | 固定値 (本番URL) |
| AI/LLM | OPENAI_API_KEY | OpenAI Dashboard |
| AI/LLM | GEMINI_API_KEY | Google AI Studio |
| AI/TTS | AIVIS_API_KEY | Aivis Cloud |
| RESEND_API_KEY | Resend Dashboard | |
| Cloudflare | CLOUDFLARE_API_TOKEN | Cloudflare API Tokens |
| Cloudflare | CLOUDFLARE_ACCOUNT_ID | Cloudflare Dashboard |
| Storage | R2_ENDPOINT | 固定値 |
| Storage | R2_ACCESS_KEY_ID | R2 API Tokens |
| Storage | R2_SECRET_ACCESS_KEY | R2 API Tokens |
| Storage | R2_BUCKET_NAME | 固定値 |
| Payment | STRIPE_SECRET_KEY | Stripe Dashboard |
| Payment | STRIPE_PUBLISHABLE_KEY | Stripe Dashboard |
| Payment | STRIPE_WEBHOOK_SECRET | Stripe Webhooks |
| Payment | STRIPE_PRICE_ID | Stripe Products |
| Cron | CRON_SECRET | openssl で生成 |
6. Stripe 本番モードの注意点
Stripe Live モードの有効化にはビジネスオンボーディングが必要です。
必要な情報
-
事業形態・個人情報
-
ビジネスのウェブサイト(表示可能で公開されていること)
-
銀行口座情報
-
2段階認証設定
7. 正しい作業順序
各サービス間の依存関係を考慮すると、以下の順序が最適です。
Phase 1: リソース作成
1. Cloudflare API トークン作成 (Terraform用 + wrangler用 + R2用)
2. Terraform apply → R2 バケット作成
3. Prisma Console → 本番 DB 作成
4. Google Cloud Console → OAuth クライアント作成
5. 各サービス → API キー作成 (OpenAI, Gemini, Aivis, Resend)
Phase 2: デプロイ準備
6. AUTH_SECRET, CRON_SECRET を生成
7. GitHub Secrets に全シークレットを登録 (Stripe除く)
8. Worker 名の環境別切り替え対応
Phase 3: デプロイ & 後続設定
9. main ブランチに push → prod デプロイ
10. Cloudflare → カスタムドメイン設定
11. Stripe → ビジネスオンボーディング完了
12. Stripe → Product/Price/Webhook 設定
13. GitHub Secrets に Stripe シークレット追加
14. 再デプロイ
15. Terraform apply → Worker Cron トリガー追加
Tips
terraform.tfvars には機密情報が含まれるため、.gitignore に追加して絶対にコミットしないこと。openssl rand -base64 32 で生成するシークレットは環境ごとに別の値を使うこと。同じ値を使うとセッションハイジャックのリスクがある。8. デプロイ時のトラブルシューティング
初回の本番デプロイでは、開発環境では発生しなかった問題がいくつか発生しました。
pgvector 拡張の未インストール
prisma db push で ERROR: type "vector" does not exist エラーが発生。新規 DB に pgvector 拡張がインストールされていないため。
psql '[DIRECT_URL]' -c 'CREATE EXTENSION IF NOT EXISTS vector;'
prisma db push の --accept-data-loss
新規 DB へのスキーマ push 時に「データ消失の可能性」警告で停止。初回のみローカルから手動実行し、CI のコマンドはそのまま維持。
DIRECT_URL='[接続文字列]' bunx prisma db push --accept-data-loss
--accept-data-loss を CI に常設するのは危険。運用中のデータが消える可能性があるため、初回セットアップ時のみ手動で実行すること。Stripe 環境変数未設定によるビルドエラー
Stripe の初期化コードが環境変数を必須としているため、未設定だとビルドが失敗。ダミー値を設定してビルドを通し、本番 Stripe 設定完了後に差し替える。
9. カスタムドメインの設定
Cloudflare Workers のカスタムドメインは API で設定可能。ダッシュボードに行く必要はありません。
curl -X PUT \
"https://api.cloudflare.com/client/v4/accounts/[ACCOUNT_ID]/workers/domains" \
-H "Authorization: Bearer [API_TOKEN]" \
-H "Content-Type: application/json" \
-d '{"hostname":"example.com","service":"worker-name","environment":"production"}'
-
SSL 証明書は自動発行される
-
DNS が Cloudflare で管理されていれば即時反映
まとめ
-
リソースは完全分離が原則。Cloudflare アカウントだけ共通、それ以外は全て環境別
-
API トークンは用途別に作成。Terraform 用、CI/CD 用、アプリ用で分ける
-
Terraform の部分適用 (
-target) で依存関係のないリソースから作成できる -
GitHub Environments で環境別シークレット管理。ブランチ名で自動切替
-
Stripe はデプロイ後に設定。ウェブサイトの公開が前提条件
-
作業順序が重要。依存関係を把握して効率的に進める
-
初回デプロイは手動ステップが必要。pgvector 拡張のインストール、prisma db push --accept-data-loss
-
Stripe は段階的に有効化。ダミー値でビルドを通し、オンボーディング後に差し替え
-
カスタムドメインは API 設定可能。Cloudflare Dashboard 不要
この記事が役に立ったら共有しよう

Koki
フルスタックエンジニア / React, Next.js, TypeScript

