Communitygithub.com

Cloudflare IaC: Terraform + Wrangler

TerraformとWranglerの間でリソースIDを自動的に橋渡しする再利用可能なエージェントスキルです。

Cloudflare IaC: Terraform + Wrangler とは?

このスキルは、Cloudflareのインフラストラクチャ管理(Terraform)とアプリケーションデプロイ(Wrangler)の間にあるギャップを埋めるための、DRY(Don't Repeat Yourself)で自動化可能なパターンを提供します。TerraformはKV名前空間、D1データベース、R2バケットなどのリソースを作成・管理し、WranglerはWorkerコードのデプロイに必要なバインディング設定を行います。このスキルの中心は、Terraformの出力を読み取り、Wranglerの設定ファイル(wrangler.jsonc)に自動的に反映するブリッジスクリプトです。これにより、リソースIDを手動でコピー&ペーストする必要がなくなり、CI/CDパイプラインでもローカル開発でも同じコマンドを使用できるため、設定の重複や環境ごとの差異を防止します。Wranglerの自動プロビジョニング機能に頼らず、Terraformを唯一の信頼できる情報源とすることで、状態追跡、ドリフト検出、体系的なリソース管理が可能になります。

対応~Claude Code~Codex CLI~Cursor
npx skills add dortort/skills

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

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型の不一致を検出します。

既存プロジェクトへの適応

すでに存在するプロジェクトにこのパターンを適用する場合:

  1. 現在の状態を監査します。 現在のwrangler.toml/wrangler.jsoncでハードコードされたリソースIDを確認します。Terraformがすでに使用されているかどうかを確認します。CI/CD設定を確認します。

  2. リソースを特定します。 Wrangler設定内のすべてのリソースIDをリストアップします。どのリソースがすでにTerraformにあり、どれをインポートする必要があるかを判断します。

  3. 既存のリソースをTerraformにインポートします。 再作成するのではなく、terraform importを使用します。これにより、ダウンタイムとデータ損失を回避できます。

  4. テンプレートを作成します。 既存のWrangler設定をwrangler.template.jsoncにコピーし、ハードコードされたIDを__PLACEHOLDER__トークンに置き換えます。

  5. ブリッジスクリプトを作成します。 上記のリファレンススクリプトから始め、パスと出力名を調整します。

  6. package.jsonを設定します。 sync、predeploy、deployスクリプトを追加します。

  7. CI/CDを更新します。 手動のID受け渡しや設定生成手順をnpm run deployに置き換えます。

  8. 生成されたファイルが動作することを確認したら、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

…[切り詰められました]

関連スキル