【環境構築】Next.js × Cloudflare Workers の本番環境を一から構築する

この記事でわかること
- dev と prod で何を分け、何を共通にするかの判断
- Cloudflare API トークンを用途別に 3 種類作る理由と、R2 トークンの 2 種類の違い
- GitHub Environments でブランチごとにシークレットを切り替える設定
- 依存関係を考えた、本番デプロイまでの作業順序(3 フェーズ 15 ステップ)
Next.js(App Router)+ Cloudflare Workers(OpenNext)の構成で、dev 環境がすでに稼働していること。Terraform CLI と GitHub Actions の CI/CD が入っていること。
はじめに
開発環境(dev)で動いている Next.js アプリケーションを本番環境にデプロイするには、単にコードを push するだけでは済みません。データベース、ストレージ、認証、決済など、各サービスのリソースを本番用に新規作成し、適切に分離する必要があります。
この記事では、Cloudflare Workers 上で動く Next.js アプリの本番環境を一から構築した際の手順と設計判断をまとめます。
技術スタック
| サービス | 用途 |
|---|---|
| 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.r2terraform 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://[email protected]: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 で一括登録
# 環境別にシークレットを登録
# 値を引数で渡さない(シェル履歴に残るため)。標準入力かパイプで渡す。
openssl rand -base64 32 | gh secret set AUTH_SECRET --env prod
openssl rand -hex 32 | gh secret set CRON_SECRET --env prod
# 手で入力するものは引数なしで実行し、プロンプトに貼る
gh secret set DATABASE_URL --env prod必要なシークレット一覧
| カテゴリ | シークレット名 | 取得元 |
|---|---|---|
| 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 不要
参考リンク
更新履歴
- コードブロックの言語指定を javascript から text に修正(4箇所)。gh secret set で生成値を引数渡ししていたのをパイプ経由に修正(シェル履歴に残るため)。callout の色指定を他記事と揃えた。参考リンクの節を新設し、内容が重なる関連記事へのリンクを追加。「この記事でわかること」「対象読者」「前提条件」を概要プロパティへ移動。
- 記事内リンクの修正


