Cloudflare IaC: Terraform + Wrangler は何をしますか?
核心的な問題
Cloudflareのツールは、2つの世界に分かれています。
- Terraform はインフラストラクチャを管理します:ゾーン、DNSレコード、アクセスポリシー、ファイアウォールルール、アカウントレベルの設定、そしてKV名前空間、D1データベース、R2バケット、キューなどのリソースの存在を管理します。
- Wrangler はアプリケーションデプロイを管理します:Workerコード、Pagesプロジェクト、それらのリソースへのバインディング、そしてローカル開発です。
課題は、WranglerがTerraformが作成するリソースIDを必要とすることです。KV名前空間ID、D1データベースID、キュー名などは、wrangler deployが動作する前にwrangler.toml(またはwrangler.jsonc)に反映されなければならないterraform applyの出力です。
このスキルは、そのギャップを埋めるためのDRYで自動化可能なパターンを体系化します。
責任の分離
Terraformが管理するもの
- ゾーンとDNSレコード
- KV名前空間 (
cloudflare_workers_kv_namespace) - D1データベース (
cloudflare_d1_database) - R2バケット (
cloudflare_r2_bucket) - キュー (
cloudflare_queue) - アクセスポリシー、WAFルール、ページルール
- アカウントレベルの設定
- アプリケーションコードのデプロイより先に存在すべきすべてのリソース
Wranglerが管理するもの
- Workerのソースコードとバンドル
- Pagesプロジェクトのデプロイ
- バインディングの設定(WorkerをTerraformが作成したリソースに接続する)
- ローカル開発 (
wrangler dev) - シークレット (
wrangler secret putによる) - デプロイ (
wrangler deploy)
グレーゾーン
どちらのツールでも管理できるリソースもあります。指針は次の通りです:複数のWorkerやプロジェクトがリソースを共有する場合、Terraformが管理します。単一のWorkerに限定される場合、Wranglerが管理できます。 迷ったらTerraformを優先してください。状態追跡、ドリフト検出、インフラストラクチャの唯一の信頼できる情報源を提供します。
Wranglerの自動プロビジョニング機能(初回デプロイ時にKV/R2/D1リソースを作成する)はプロトタイピングには便利ですが、本番パイプラインでは使用すべきではありません。Terraformの管理外でリソースを作成し、ドリフトや体系的に管理・削除できないリソースを引き起こします。
ブリッジパターン
概要
terraform apply → terraform output -json → ブリッジスクリプト → wrangler.jsonc → wrangler deploy
ブリッジスクリプトはプロジェクト内のNode.jsスクリプトで、package.jsonから呼び出されます。Terraformの出力を読み取り、Wranglerの設定ファイルに書き込みます。同じスクリプトがローカルとCI/CDの両方で実行されます。重複はありません。
ステップ1: Terraform出力
Wranglerが必要とするすべてのリソースIDに対して出力を定義します。
# outputs.tf
output "kv_namespace_id" {
value = cloudflare_workers_kv_namespace.my_kv.id
}
output "d1_database_id" {
value = cloudflare_d1_database.my_db.id
}
output "r2_bucket_name" {
value = cloudflare_r2_bucket.my_bucket.name
}
output "account_id" {
value = var.cloudflare_account_id
}
output "zone_id" {
value = cloudflare_zone.my_zone.id
}
ステップ2: ブリッジスクリプト
scripts/sync-wrangler-config.mjsを作成します。Terraformの出力を読み取り、Wranglerの設定を生成(またはパッチ適用)するNode.jsスクリプトです。
#!/usr/bin/env node
// scripts/sync-wrangler-config.mjs
//
// Terraformの出力を読み取り、正しいリソースIDでwrangler.jsoncを書き込みます。
// ローカルまたはCIで実行 — 同じコマンド、同じ結果。
import { execSync } from "node:child_process";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..");
// --- 設定 ---
// プロジェクトのレイアウトに合わせてパスを調整してください。
const TERRAFORM_DIR = join(ROOT, "terraform");
const WRANGLER_CONFIG_PATH = join(ROOT, "wrangler.jsonc");
const TEMPLATE_PATH = join(ROOT, "wrangler.template.jsonc");
// --- Terraformの出力を読み取る ---
function getTerraformOutputs() {
try {
const raw = execSync("terraform output -json", {
cwd: TERRAFORM_DIR,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
const outputs = JSON.parse(raw);
// terraform output -jsonは各値を { value, type, sensitive } でラップします
const flat = {};
for (const [key, entry] of Object.entries(outputs)) {
flat[key] = entry.value;
}
return flat;
} catch (err) {
console.error("Terraformの出力を読み取れませんでした。'terraform apply'を実行しましたか?");
console.error(err.message);
process.exit(1);
}
}
// --- Wrangler設定を生成する ---
function generateConfig(outputs) {
if (!existsSync(TEMPLATE_PATH)) {
console.error(`テンプレートが見つかりません: ${TEMPLATE_PATH}`);
console.error("__PLACEHOLDER__トークンを含むwrangler.template.jsoncを作成してください。");
process.exit(1);
}
let template = readFileSync(TEMPLATE_PATH, "utf-8");
// __PLACEHOLDER__トークンをTerraformの出力値で置き換えます。
// トークン形式: terraform出力名に対応する__UPPER_SNAKE_CASE__
for (const [key, value] of Object.entries(outputs)) {
const token = `__${key.toUpperCase()}__`;
template = template.replaceAll(token, String(value));
}
// 置き換えられなかったトークンを警告
const remaining = template.match(/__[A-Z_]+__/g);
if (remaining) {
console.warn("警告: テンプレート内で置き換えられなかったトークン:", [...new Set(remaining)]);
}
writeFileSync(WRANGLER_CONFIG_PATH, template, "utf-8");
console.log(`書き込み完了: ${WRANGLER_CONFIG_PATH}`);
}
// --- メイン ---
const outputs = getTerraformOutputs();
generateConfig(outputs);
ステップ3: テンプレート
wrangler.template.jsoncを作成します。Terraformの出力が入るプレースホルダートークンを持つWrangler設定です。
{
// wrangler.template.jsoncからscripts/sync-wrangler-config.mjsによって生成されます
// 直接編集しないでください — 代わりにテンプレートを編集してください。
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"account_id": "__ACCOUNT_ID__",
"kv_namespaces": [
{
"binding": "MY_KV",
"id": "__KV_NAMESPACE_ID__"
}
],
"d1_databases": [
{
"binding": "MY_DB",
"database_id": "__D1_DATABASE_ID__",
"database_name": "my-db"
}
],
"r2_buckets": [
{
"binding": "MY_BUCKET",
"bucket_name": "__R2_BUCKET_NAME__"
}
]
}
ステップ4: package.jsonのスクリプト
{
"scripts": {
"sync": "node scripts/sync-wrangler-config.mjs",
"predeploy": "npm run sync",
"deploy": "wrangler deploy",
"dev": "npm run sync && wrangler dev"
}
}
npm run deployは、predeployフックを介してsyncを自動的に最初に実行します。同じnpm run deployコマンドがローカルとCI/CDの両方で動作します。DRYです。
ステップ5: .gitignore
# 同期スクリプトによって生成されます — コミットしないでください
wrangler.jsonc
# Terraform
terraform/.terraform/
terraform/*.tfstate*
生成されたwrangler.jsoncはコミットすべきではありません。テンプレートと同期スクリプトが信頼できる情報源です。
CI/CD統合
CI/CDパイプラインは、開発者がローカルで実行するのとまったく同じコマンドを実行します。唯一の違いは、Terraformの状態と認証情報がどのように提供されるかです。
汎用パイプライン構造
# 擬似コード — お使いのCI/CDプラットフォームの構文に合わせて調整してください
steps:
# 1. Terraformがインフラストラクチャの変更を適用します
- name: terraform
run: |
cd terraform
terraform init
terraform apply -auto-approve
# 2. ブリッジスクリプトがTerraformの出力をWrangler設定に同期します
# 3. Wranglerがアプリケーションをデプロイします
- name: deploy-worker
run: npm run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
これだけです。npm run deployはpredeploy → sync → wrangler deployを呼び出します。ローカルと同じです。
認証情報
- Terraform: インフラストラクチャリソースを管理するための権限を持つCloudflare APIトークンが必要です。
CLOUDFLARE_API_TOKEN環境変数またはプロバイダー設定で提供します。 - Wrangler: Workerをデプロイするための権限を持つCloudflare APIトークンが必要です。多くの場合同じトークンですが、最小権限のために個別にスコープを設定できます。
- Terraformの状態: CIとローカルが同じ状態を共有するように、リモートバックエンド(S3、GCS、Terraform Cloudなど)を使用します。
環境プロモーション
マルチ環境セットアップ(ステージング、本番)の場合は、Terraformワークスペースまたは別々の状態ファイル、およびWrangler環境を使用します。
// wrangler.template.jsonc内
{
"name": "my-worker",
"env": {
"staging": {
"kv_namespaces": [{ "binding": "MY_KV", "id": "__STAGING_KV_NAMESPACE_ID__" }]
},
"production": {
"kv_namespaces": [{ "binding": "MY_KV", "id": "__PRODUCTION_KV_NAMESPACE_ID__" }]
}
}
}
同期スクリプトはこれを自然に処理します。対応するTerraform出力を追加するだけです。
代替案: Terraform管理のバインディング
Terraformをすべての唯一の信頼できる情報源(デプロイを含む)にしたいチームのために、cloudflare_worker_versionリソースを使用してバインディングを直接管理できます。このモデルでは、Wranglerはコードのバンドルのみを行い、Terraformがアップロードとバインディングの配線を処理します。
resource "cloudflare_worker_version" "my_worker" {
account_id = var.cloudflare_account_id
worker_id = cloudflare_worker.my_worker.id
main_module = "index.js"
compatibility_date = "2025-01-01"
bindings = [
{
type = "kv_namespace"
name = "MY_KV"
namespace_id = cloudflare_workers_kv_namespace.my_kv.id
},
{
type = "d1"
name = "DB"
id = cloudflare_d1_database.my_db.id
},
{
type = "r2_bucket"
name = "MY_BUCKET"
bucket_name = cloudflare_r2_bucket.my_bucket.name
}
]
}
このアプローチでは:
wrangler deploy --dry-run --outdir distがコードをバンドルします- Terraformがバンドルをアップロードし、リソース参照を介してすべてのバインディングを配線します
- ブリッジスクリプトもテンプレートも不要 — TerraformがIDをネイティブに解決します
トレードオフ: バインディング設定に関するWranglerのローカル開発の使いやすさを失います。wrangler devにはwrangler.tomlが依然として必要であり、これはある程度の重複を意味します。ブリッジパターン(上記)は、開発者がwrangler devを積極的に使用する場合に一般的に優れており、Terraformのみのアプローチは、ローカル開発がモックや最小限の設定を使用するCIファーストのワークフローに適しています。
チームがwrangler devを定期的に実行する場合はブリッジパターンを選択してください。Terraformがすでにデプロイツールであり、ローカル開発に実際のバインディングが必要ない場合は、Terraform管理のバインディングを選択してください。
型安全性: wrangler types --check
Wrangler設定を生成または更新した後、wrangler typesを実行してTypeScriptバインディングを再生成します。CIでは、--checkを使用して、型が古い場合にビルドを失敗させます。
{
"scripts": {
"typecheck": "wrangler types --check",
"predeploy": "npm run sync && npm run typecheck",
"deploy": "wrangler deploy"
}
}
これにより、デプロイ前にTerraform管理のリソースとWorkerのTypeScript型の不一致を検出します。
既存プロジェクトへの適応
すでに存在するプロジェクトにこのパターンを適用する場合:
-
現在の状態を監査します。 現在の
wrangler.toml/wrangler.jsoncでハードコードされたリソースIDを確認します。Terraformがすでに使用されているかどうかを確認します。CI/CD設定を確認します。 -
リソースを特定します。 Wrangler設定内のすべてのリソースIDをリストアップします。どのリソースがすでにTerraformにあり、どれをインポートする必要があるかを判断します。
-
既存のリソースをTerraformにインポートします。 再作成するのではなく、
terraform importを使用します。これにより、ダウンタイムとデータ損失を回避できます。 -
テンプレートを作成します。 既存のWrangler設定を
wrangler.template.jsoncにコピーし、ハードコードされたIDを__PLACEHOLDER__トークンに置き換えます。 -
ブリッジスクリプトを作成します。 上記のリファレンススクリプトから始め、パスと出力名を調整します。
-
package.jsonを設定します。
sync、predeploy、deployスクリプトを追加します。 -
CI/CDを更新します。 手動のID受け渡しや設定生成手順を
npm run deployに置き換えます。 -
生成されたファイルが動作することを確認したら、
wrangler.jsoncを.gitignoreに追加します。
一般的なパターン
モノレポ内の複数のWorker
リポジトリに複数のWorkerが含まれる場合、各Workerは独自のテンプレートを取得し、同期スクリプトがそれらすべてを処理します。
project/
├── terraform/
├── workers/
│ ├── api/
│ │ ├── src/
│ │ └── wrangler.template.jsonc
│ └── cron/
│ ├── src/
│ └── wrangler.template.jsonc
├── scripts/
│ └── sync-wrangler-config.mjs
└── package.json
同期スクリプトはワーカーディレクトリを反復処理し、各テンプレートに同じトークン置換ロジックを適用します。
シークレット
シークレットはブリッジスクリプトやテンプレートを経由すべきではありません。wrangler secret put(対話型)またはwrangler secret bulk(JSONファイルから、CIに便利)を使用します。シークレットはCloudflareのAPIでWorkerごとに管理され、設定ファイルでは管理されません。
CIの場合:
echo '{"API_KEY":"...","DB_PASSWORD":"..."}' | wrangler secret bulk
プレビュー環境
PRベースのプレビューデプロイの場合、同期スクリプトは環境引数を受け入れ可能です。
{
"scripts": {
"deploy:preview": "npm run sync -- --env preview && wrangler deploy --env preview"
}
}
トラブルシューティング
| 症状 | 考えられる原因 |
|---|---|
sync が「Terraformの出力を読み取れません」で失敗する | Terraformが適用されていないか、間違ったディレクトリにいます。最初にterraform applyを実行してください。 |
生成された設定に置き換えられていない__TOKENS__がある | Terraformの出力名がトークンと一致しません。terraform output名が大文字のトークンパターンと一致することを確認してください。 |
| Wranglerのデプロイが「リソースが見つかりません」で失敗する | TerraformからのリソースIDが古いか、間違った環境からのものです。terraform applyを再実行し、次にnpm run syncを実行してください。 |
| Terraformの状態とCloudflareの間にドリフトがある | リソースがTerraformの外部で変更されました(おそらくWranglerの自動プロビジョニングによって)。terraform planを実行して検出し、調整してください。 |
リファレンス
scripts/sync-wrangler-config.mjs— ブリッジスクリプト(引数解析とマルチワーカーサポートを備えた本番環境対応バージョンについては、scripts/ディレクトリを参照してください)references/terraform-wrangler-mapping.md— Terra
…[切り詰められました]