Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Infrastructure as Code (IaC)

最終更新 Markdown で表示Agent セットアップ

Wrangler は Workers のアップロードと管理を簡単にしますが、よりプログラム的な方法が必要なこともあります。Infrastructure as Code (IaC) ツールの利用や、Workers API との直接のやり取りです。ビルドとデプロイのスクリプト、CI/CD パイプライン、独自の開発者ツール、自動テストなどが該当します。

これを簡単にするため、Cloudflare は cloudflare-typescriptcloudflare-python など、よく使われる言語向けの SDK ライブラリを提供しています。IaC では、HashiCorp の Terraform と Cloudflare Terraform Provider を使い、Workers リソースを管理できます。

以下は、異なるツールと言語で Worker をデプロイする例と、IaC で Workers を管理するときの重要な注意点です。

これらの例はすべて、動作に アカウント IDAPI トークン(Global API key ではありません)が必要です。

Workers のバンドル

以下の例はいずれも Workers Bundling を行いません。通常は Wrangler または esbuild のようなツールで行います。

一般的には、Terraform プランの適用や API でのスクリプトアップロードの前に、このバンドル手順を実行します。

wrangler deploy --dry-run --outdir build

Wrangler でビルドし、別の方法でアップロードする場合は、wrangler.json の設定をすべて Terraform 設定または API リクエストへコピーします。スクリプトが依存する compatibility_date やフラグでは特に重要です。

Terraform

この例では、以下の例に近いスクリプト内容を持つ my-script.mjs というローカルファイルが必要です。Cloudflare Terraform Provider の詳細と、利用できるすべてのリソース設定は Workers script リソースの例 を参照してください。

variable "account_id" {
  default = "replace_me"
}

resource "cloudflare_worker" "my_worker" {
  account_id = var.account_id
  name = "my-worker"
  observability = {
    enabled = true
  }
}

resource "cloudflare_worker_version" "my_worker_version" {
  account_id = var.account_id
  worker_id = cloudflare_worker.my_worker.id
  compatibility_date = "2025-02-21" # Set this to today's date
  main_module = "my-script.mjs"
  modules = [
    {
      name = "my-script.mjs"
      content_type = "application/javascript+module"
      # Replacement (version creation) is triggered whenever this file changes
      content_file = "my-script.mjs"
    }
  ]
}

resource "cloudflare_workers_deployment" "my_worker_deployment" {
  account_id = var.account_id
  script_name = cloudflare_worker.my_worker.name
  strategy = "percentage"
  versions = [{
    percentage = 100
    version_id = cloudflare_worker_version.my_worker_version.id
  }]
}

これらのリソースをすべて Terraform で管理する必要はありません。たとえば cloudflare_worker リソースだけを使い、Versions や Deployments は Wrangler や独自のデプロイツールでシームレスに扱えます。

Terraform でのバインディング

バインディング は、Worker が Cloudflare Developer Platform 上のリソースとやり取りできるようにします。Terraform では、バインディングの設定方法が Wrangler と異なります。バインディング種類ごとのトップレベルプロパティ(kv_namespacesr2_buckets など)ではなく、Terraform は 1 つの bindings 配列を使い、各バインディングは type プロパティと種類固有のプロパティを持ちます。

各バインディング種類と必須プロパティの例は次のとおりです。

KV Namespace バインディング

キーバリューストレージ向けに KV 名前空間 へバインドします。

bindings = [{
  type = "kv_namespace"
  name = "MY_KV"
  namespace_id = "your-kv-namespace-id"
}]

プロパティ:

  • type: "kv_namespace"
  • name: バインディングの変数名です。env.MY_KV からアクセスできます
  • namespace_id: KV 名前空間の ID です

R2 Bucket バインディング

オブジェクトストレージ向けに R2 バケット へバインドします。

bindings = [{
  type = "r2_bucket"
  name = "MY_BUCKET"
  bucket_name = "my-bucket-name"
}]

プロパティ:

  • type: "r2_bucket"
  • name: env.MY_BUCKET からアクセスするバインディング名です
  • bucket_name: R2 バケットの名前です

D1 Database バインディング

SQL ストレージ向けに D1 データベース へバインドします。

bindings = [{
  type = "d1"
  name = "DB"
  id = "your-database-id"
}]

プロパティ:

  • type: "d1"
  • name: env.DB からアクセスするバインディング名です
  • id: D1 データベースの ID です

Durable Object バインディング

Durable Object クラスへバインドします。

bindings = [{
  type = "durable_object_namespace"
  name = "MY_DURABLE_OBJECT"
  class_name = "MyDurableObjectClass"
}]

プロパティ:

  • type: "durable_object_namespace"
  • name: env.MY_DURABLE_OBJECT からアクセスするバインディング名です
  • class_name: Durable Object のエクスポートされたクラス名です
  • script_name: (任意)この Durable Object クラスをエクスポートする Worker スクリプトです。同じ Worker でクラスが定義されている場合は省略します。

Service バインディング

Worker 間通信向けに、別の Worker へバインドします。

bindings = [{
  type = "service"
  name = "MY_SERVICE"
  service = "other-worker-name"
}]

プロパティ:

  • type: "service"
  • name: env.MY_SERVICE からアクセスするバインディング名です
  • service: 対象 Worker の名前です
  • entrypoint: (任意)バインドする名前付き エントリポイント です

Queue バインディング

メッセージ受け渡し向けに Queue へバインドします。

メッセージの送信:

bindings = [{
  type = "queue"
  name = "MY_QUEUE"
  queue_name = "my-queue"
}]

プロパティ:

  • type: "queue"
  • name: env.MY_QUEUE からアクセスするバインディング名です
  • queue_name: Queue の名前です

メッセージの消費では、バインディングではなく、queue リソース自体で Worker をコンシューマーとして設定します。

Vectorize バインディング

ベクトル検索向けに Vectorize インデックス へバインドします。

bindings = [{
  type = "vectorize"
  name = "VECTORIZE_INDEX"
  index_name = "my-index"
}]

プロパティ:

  • type: "vectorize"
  • name: env.VECTORIZE_INDEX からアクセスするバインディング名です
  • index_name: Vectorize インデックスの名前です

Workers AI バインディング

AI 推論向けに Workers AI へバインドします。

bindings = [{
  type = "ai"
  name = "AI"
}]

プロパティ:

  • type: "ai"
  • name: env.AI からアクセスするバインディング名です

Hyperdrive バインディング

データベース接続プーリング向けに Hyperdrive 設定へバインドします。

bindings = [{
  type = "hyperdrive"
  name = "HYPERDRIVE"
  id = "your-hyperdrive-config-id"
}]

プロパティ:

  • type: "hyperdrive"
  • name: env.HYPERDRIVE からアクセスするバインディング名です
  • id: Hyperdrive 設定の ID です

VPC Service バインディング

プライベートネットワーク内のリソースへアクセスするため、VPC Service へバインドします。

bindings = [{
  type = "vpc_service"
  name = "PRIVATE_API"
  service_id = "your-vpc-service-id"
}]

プロパティ:

  • type: "vpc_service"
  • name: env.PRIVATE_API からアクセスするバインディング名です
  • service_id: VPC Service の ID です(cloudflare_connectivity_directory_service またはダッシュボードから)

VPC Service は cloudflare_connectivity_directory_service リソースで Terraform から作成できます。全体の手順は Terraform で VPC Services を設定する を参照してください。

Analytics Engine バインディング

Analytics Engine データセットへバインドします。

bindings = [{
  type = "analytics_engine"
  name = "ANALYTICS"
  dataset = "my_dataset"
}]

プロパティ:

  • type: "analytics_engine"
  • name: env.ANALYTICS からアクセスするバインディング名です
  • dataset: Analytics Engine データセットの名前です

環境変数

プレーンテキストの環境変数には、plain_text バインディング種類を使います。

bindings = [{
  type = "plain_text"
  name = "MY_VARIABLE"
  text = "my-value"
}]

プロパティ:

  • type: "plain_text"
  • name: env.MY_VARIABLE からアクセスするバインディング名です
  • text: 環境変数の値です

Secret Text バインディング

暗号化されたシークレットには、secret_text バインディング種類を使います。

bindings = [{
  type = "secret_text"
  name = "API_KEY"
  text = var.api_key
}]

プロパティ:

  • type: "secret_text"
  • name: env.API_KEY からアクセスするバインディング名です
  • text: シークレットの値です(暗号化されます)

完全な例

複数のバインディング種類を組み合わせた例です。

resource "cloudflare_worker_version" "my_worker_version" {
  account_id = var.account_id
  worker_id = cloudflare_worker.my_worker.id
  compatibility_date = "2025-08-06"
  main_module = "worker.js"

  modules = [{
    name = "worker.js"
    content_type = "application/javascript+module"
    content_file = "worker.js"
  }]

  bindings = [
    {
      type = "kv_namespace"
      name = "MY_KV"
      namespace_id = var.kv_namespace_id
    },
    {
      type = "r2_bucket"
      name = "MY_BUCKET"
      bucket_name = "my-bucket"
    },
    {
      type = "d1"
      name = "DB"
      id = var.d1_database_id
    },
    {
      type = "service"
      name = "AUTH_SERVICE"
      service = "auth-worker"
    },
    {
      type = "plain_text"
      name = "ENVIRONMENT"
      text = "production"
    },
    {
      type = "secret_text"
      name = "API_KEY"
      text = var.api_key
    },
    {
      type = "vpc_service"
      name = "PRIVATE_API"
      service_id = var.vpc_service_id
    }
  ]
}

Cloudflare API ライブラリ

この例は cloudflare-typescript SDK を使います。サーバーサイドの JavaScript または TypeScript から Cloudflare REST API へ便利にアクセスできます。

#!/usr/bin/env -S npm run tsn -T

/**
 * Create and deploy a Worker
 *
 * Docs:
 * - https://developers.cloudflare.com/workers/configuration/versions-and-deployments/
 * - https://developers.cloudflare.com/workers/platform/infrastructure-as-code/
 *
 * Prerequisites:
 * 1. Generate an API token: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
 * 2. Find your account ID: https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/
 * 3. Find your workers.dev subdomain: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/
 *
 * Environment variables:
 *   - CLOUDFLARE_API_TOKEN (required)
 *   - CLOUDFLARE_ACCOUNT_ID (required)
 *   - CLOUDFLARE_SUBDOMAIN (optional)
 *
 * Usage:
 *   Run this script to deploy a simple "Hello World" Worker.
 *   Access it at: my-hello-world-worker.$subdomain.workers.dev
 */

import { exit } from "node:process";

import Cloudflare from "cloudflare";

const WORKER_NAME = "my-hello-world-worker";
const SCRIPT_FILENAME = `${WORKER_NAME}.mjs`;

function loadConfig() {
	const apiToken = process.env["CLOUDFLARE_API_TOKEN"];
	if (!apiToken) {
		throw new Error(
			"Missing required environment variable: CLOUDFLARE_API_TOKEN",
		);
	}

	const accountId = process.env["CLOUDFLARE_ACCOUNT_ID"];
	if (!accountId) {
		throw new Error(
			"Missing required environment variable: CLOUDFLARE_ACCOUNT_ID",
		);
	}

	const subdomain = process.env["CLOUDFLARE_SUBDOMAIN"];

	return {
		apiToken,
		accountId,
		subdomain: subdomain || undefined,
		workerName: WORKER_NAME,
	};
}

const config = loadConfig();
const client = new Cloudflare({
	apiToken: config.apiToken,
});

async function main() {
	try {
		console.log("🚀 Starting Worker creation and deployment...");

		const scriptContent = `
      export default {
        async fetch(request, env, ctx) {
          return new Response(env.MESSAGE, { status: 200 });
        },
      }`.trim();

		let worker;
		try {
			worker = await client.workers.beta.workers.get(config.workerName, {
				account_id: config.accountId,
			});
			console.log(`♻️  Worker ${config.workerName} already exists. Using it.`);
		} catch (error) {
			if (!(error instanceof Cloudflare.NotFoundError)) {
				throw error;
			}
			console.log(`✏️  Creating Worker ${config.workerName}...`);
			worker = await client.workers.beta.workers.create({
				account_id: config.accountId,
				name: config.workerName,
				subdomain: {
					enabled: config.subdomain !== undefined,
				},
				observability: {
					enabled: true,
				},
			});
		}

		console.log(`⚙️  Worker id: ${worker.id}`);
		console.log("✏️  Creating Worker version...");

		// Create the first version of the Worker
		const version = await client.workers.beta.workers.versions.create(
			worker.id,
			{
				account_id: config.accountId,
				main_module: SCRIPT_FILENAME,
				compatibility_date: new Date().toISOString().split("T")[0],
				bindings: [
					{
						type: "plain_text",
						name: "MESSAGE",
						text: "Hello World!",
					},
				],
				modules: [
					{
						name: SCRIPT_FILENAME,
						content_type: "application/javascript+module",
						content_base64: Buffer.from(scriptContent).toString("base64"),
					},
				],
			},
		);

		console.log(`⚙️  Version id: ${version.id}`);
		console.log("🚚 Creating Worker deployment...");

		// Create a deployment and point all traffic to the version we created
		await client.workers.scripts.deployments.create(config.workerName, {
			account_id: config.accountId,
			strategy: "percentage",
			versions: [
				{
					percentage: 100,
					version_id: version.id,
				},
			],
		});

		console.log("✅ Deployment successful!");

		if (config.subdomain) {
			console.log(`
🌍 Your Worker is live!
📍 URL: https://${config.workerName}.${config.subdomain}.workers.dev/
`);
		} else {
			console.log(`
⚠️  Set up a route, custom domain, or workers.dev subdomain to access your Worker.
Add CLOUDFLARE_SUBDOMAIN to your environment variables to set one up automatically.
`);
		}
	} catch (error) {
		console.error("❌ Deployment failed:", error);
		exit(1);
	}
}

main();
#!/usr/bin/env -S npm run tsn -T

/**
 * Create and deploy a Worker
 *
 * Docs:
 * - https://developers.cloudflare.com/workers/configuration/versions-and-deployments/
 * - https://developers.cloudflare.com/workers/platform/infrastructure-as-code/
 *
 * Prerequisites:
 * 1. Generate an API token: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
 * 2. Find your account ID: https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/
 * 3. Find your workers.dev subdomain: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/
 *
 * Environment variables:
 *   - CLOUDFLARE_API_TOKEN (required)
 *   - CLOUDFLARE_ACCOUNT_ID (required)
 *   - CLOUDFLARE_SUBDOMAIN (optional)
 *
 * Usage:
 *   Run this script to deploy a simple "Hello World" Worker.
 *   Access it at: my-hello-world-worker.$subdomain.workers.dev
 */

import { exit } from 'node:process';

import Cloudflare from 'cloudflare';

interface Config {
  apiToken: string;
  accountId: string;
  subdomain: string | undefined;
  workerName: string;
}

const WORKER_NAME = 'my-hello-world-worker';
const SCRIPT_FILENAME = `${WORKER_NAME}.mjs`;

function loadConfig(): Config {
  const apiToken = process.env['CLOUDFLARE_API_TOKEN'];
  if (!apiToken) {
    throw new Error('Missing required environment variable: CLOUDFLARE_API_TOKEN');
  }

  const accountId = process.env['CLOUDFLARE_ACCOUNT_ID'];
  if (!accountId) {
    throw new Error('Missing required environment variable: CLOUDFLARE_ACCOUNT_ID');
  }

  const subdomain = process.env['CLOUDFLARE_SUBDOMAIN'];

  return {
    apiToken,
    accountId,
    subdomain: subdomain || undefined,
    workerName: WORKER_NAME,
  };
}

const config = loadConfig();
const client = new Cloudflare({
  apiToken: config.apiToken,
});

async function main(): Promise<void> {
  try {
    console.log('🚀 Starting Worker creation and deployment...');

    const scriptContent = `
      export default {
        async fetch(request, env, ctx) {
          return new Response(env.MESSAGE, { status: 200 });
        },
      }`.trim();

    let worker;
    try {
      worker = await client.workers.beta.workers.get(config.workerName, {
        account_id: config.accountId,
      });
      console.log(`♻️  Worker ${config.workerName} already exists. Using it.`);
    } catch (error) {
      if (!(error instanceof Cloudflare.NotFoundError)) { throw error; }
      console.log(`✏️  Creating Worker ${config.workerName}...`);
      worker = await client.workers.beta.workers.create({
        account_id: config.accountId,
        name: config.workerName,
        subdomain: {
          enabled: config.subdomain !== undefined,
        },
        observability: {
          enabled: true,
        },
      });
    }

    console.log(`⚙️  Worker id: ${worker.id}`);
    console.log('✏️  Creating Worker version...');

    // Create the first version of the Worker
    const version = await client.workers.beta.workers.versions.create(worker.id, {
      account_id: config.accountId,
      main_module: SCRIPT_FILENAME,
      compatibility_date: new Date().toISOString().split('T')[0]!,
      bindings: [
        {
          type: 'plain_text',
          name: 'MESSAGE',
          text: 'Hello World!',
        },
      ],
      modules: [
        {
          name: SCRIPT_FILENAME,
          content_type: 'application/javascript+module',
          content_base64: Buffer.from(scriptContent).toString('base64'),
        },
      ],
    });

    console.log(`⚙️  Version id: ${version.id}`);
    console.log('🚚 Creating Worker deployment...');

    // Create a deployment and point all traffic to the version we created
    await client.workers.scripts.deployments.create(config.workerName, {
      account_id: config.accountId,
      strategy: 'percentage',
      versions: [
        {
            percentage: 100,
            version_id: version.id,
          },
        ],
    });

    console.log('✅ Deployment successful!');

    if (config.subdomain) {
      console.log(`
🌍 Your Worker is live!
📍 URL: https://${config.workerName}.${config.subdomain}.workers.dev/
`);
    } else {
      console.log(`
⚠️  Set up a route, custom domain, or workers.dev subdomain to access your Worker.
Add CLOUDFLARE_SUBDOMAIN to your environment variables to set one up automatically.
`);
    }
  } catch (error) {
    console.error('❌ Deployment failed:', error);
    exit(1);
  }
}

main();

Cloudflare REST API

ターミナルを開くかシェルスクリプトを作成し、curl で Worker をアップロードし、バージョンとデプロイを管理します。Workers スクリプトは JavaScript ES Modules ですが、Python WorkersRust Workers にも対応しています。

account_id="replace_me"
api_token="replace_me"
worker_name="my-hello-world-worker"

worker_script_base64=$(echo '
export default {
  async fetch(request, env, ctx) {
    return new Response(env.MESSAGE, { status: 200 });
  }
};
' | base64)

# Note the below will fail if the worker already exists!
# Here's how to delete the Worker
#
# worker_id="replace-me"
# curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/workers/$worker_id" \
#   -X DELETE \
#   -H "Authorization: Bearer $api_token"

# Create the Worker
worker_id=$(curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/workers" \
  -X POST \
  -H "Authorization: Bearer $api_token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "'$worker_name'"
  }' \
  | jq -r '.result.id')

echo "\nWorker ID: $worker_id\n"

# Upload the Worker's first version
version_id=$(curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/workers/$worker_id/versions" \
  -X POST \
  -H "Authorization: Bearer $api_token" \
  -H "Content-Type: application/json" \
  -d '{
    "compatibility_date": "2025-08-06",
    "main_module": "'$worker_name'.mjs",
    "modules": [
      {
        "name": "'$worker_name'.mjs",
        "content_type": "application/javascript+module",
        "content_base64": "'$worker_script_base64'"
      }
    ],
    "bindings": [
      {
        "type": "plain_text",
        "name": "MESSAGE",
        "text": "Hello World!"
      }
    ]
  }' \
  | jq -r '.result.id')

echo "\nVersion ID: $version_id\n"

# Create a deployment for the Worker
deployment_id=$(curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/scripts/$worker_name/deployments" \
  -X POST \
  -H "Authorization: Bearer $api_token" \
  -H "Content-Type: application/json" \
  -d '{
    "strategy": "percentage",
    "versions": [
      {
        "percentage": 100,
        "version_id": "'$version_id'"
      }
    ]
  }' \
  | jq -r '.result.id')

echo "\nDeployment ID: $deployment_id\n"

Python Workers には、独自の text/x-python コンテンツタイプと python_workers 互換フラグがあります。

account_id="replace_me"
api_token="replace_me"
worker_name="my-hello-world-worker"

worker_script_base64=$(echo '
from workers import WorkerEntrypoint, Response

class Default(WorkerEntrypoint):
    async def fetch(self, request):
        return Response(self.env.MESSAGE)
' | base64)

# Note the below will fail if the worker already exists!
# Here's how to delete the Worker
#
# worker_id="replace-me"
# curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/workers/$worker_id" \
#   -X DELETE \
#   -H "Authorization: Bearer $api_token"

# Create the Worker
worker_id=$(curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/workers" \
  -X POST \
  -H "Authorization: Bearer $api_token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "'$worker_name'"
  }' \
  | jq -r '.result.id')

echo "\nWorker ID: $worker_id\n"

# Upload the Worker's first version
version_id=$(curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/workers/$worker_id/versions" \
  -X POST \
  -H "Authorization: Bearer $api_token" \
  -H "Content-Type: application/json" \
  -d '{
    "compatibility_date": "2025-08-06",
    "compatibility_flags": [
      "python_workers"
    ],
    "main_module": "'$worker_name'.py",
    "modules": [
      {
        "name": "'$worker_name'.py",
        "content_type": "text/x-python",
        "content_base64": "'$worker_script_base64'"
      }
    ],
    "bindings": [
      {
        "type": "plain_text",
        "name": "MESSAGE",
        "text": "Hello World!"
      }
    ]
  }' \
  | jq -r '.result.id')

echo "\nVersion ID: $version_id\n"

# Create a deployment for the Worker
deployment_id=$(curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/scripts/$worker_name/deployments" \
  -X POST \
  -H "Authorization: Bearer $api_token" \
  -H "Content-Type: application/json" \
  -d '{
    "strategy": "percentage",
    "versions": [
      {
        "percentage": 100,
        "version_id": "'$version_id'"
      }
    ]
  }' \
  | jq -r '.result.id')

echo "\nDeployment ID: $deployment_id\n"

multipart/form-data アップロード API

この API は multipart/form-data で Worker をアップロードし、バージョンとデプロイを暗黙的に作成します。バージョンとデプロイを直接管理するには、上記の API を推奨します。

account_id="replace_me"
api_token="replace_me"
worker_name="my-hello-world-script"

script_content='export default {
  async fetch(request, env, ctx) {
    return new Response(env.MESSAGE, { status: 200 });
  }
};'

# Upload the Worker
curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/scripts/$worker_name" \
  -X PUT \
  -H "Authorization: Bearer $api_token" \
  -F "metadata={
    'main_module': '"$worker_name".mjs',
    'bindings': [
      {
        'type': 'plain_text',
        'name': 'MESSAGE',
        'text': 'Hello World!'
      }
    ],
    'compatibility_date': '$today'
  };type=application/json" \
  -F "$worker_name.mjs=@-;filename=$worker_name.mjs;type=application/javascript+module" <<EOF
$script_content
EOF

Workers for Platforms では、dispatch namespaceUser Worker をアップロードできます。API エンドポイント/workers/dispatch/namespaces/$DISPATCH_NAMESPACE/scripts/$SCRIPT_NAME です。

account_id="replace_me"
api_token="replace_me"
dispatch_namespace="replace_me"
worker_name="my-hello-world-script"

script_content='export default {
  async fetch(request, env, ctx) {
    return new Response(env.MESSAGE, { status: 200 });
  }
};'

# Create a dispatch namespace
curl https://api.cloudflare.com/client/v4/accounts/$account_id/workers/dispatch/namespaces \
  -X POST \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $api_token" \
  -d '{
    "name": "'$dispatch_namespace'"
  }'

# Upload the Worker
curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/dispatch/namespaces/$dispatch_namespace/scripts/$worker_name" \
  -X PUT \
  -H "Authorization: Bearer $api_token" \
  -F "metadata={
    'main_module': '"$worker_name".mjs',
    'bindings': [
      {
        'type': 'plain_text',
        'name': 'MESSAGE',
        'text': 'Hello World!'
      }
    ],
    'compatibility_date': '$today'
  };type=application/json" \
  -F "$worker_name.mjs=@-;filename=$worker_name.mjs;type=application/javascript+module" <<EOF
$script_content
EOF

Python Workers

Python Workers には、multipart/form-data API でアップロードするための独自の text/x-python コンテンツタイプと python_workers 互換フラグがあります。

curl https://api.cloudflare.com/client/v4/accounts/<account_id>/workers/scripts/my-hello-world-script \
  -X PUT \
  -H 'Authorization: Bearer <api_token>' \
  -F 'metadata={
        "main_module": "my-hello-world-script.py",
        "bindings": [
          {
            "type": "plain_text",
            "name": "MESSAGE",
            "text": "Hello World!"
          }
        ],
        "compatibility_date": "$today",
        "compatibility_flags": [
          "python_workers"
        ]
      };type=application/json' \
  -F 'my-hello-world-script.py=@-;filename=my-hello-world-script.py;type=text/x-python' <<EOF
from workers import WorkerEntrypoint, Response

class Default(WorkerEntrypoint):
    async def fetch(self, request):
        return Response(self.env.MESSAGE)
EOF

Durable Objects に関する注意

Durable Object のマイグレーションはデプロイ時に適用されます。つまり、デプロイが存在しない(マイグレーションが適用されていない)場合、Version で Durable Object にバインドできません。たとえば、この Terraform を最初に適用すると失敗します。

resource "cloudflare_worker" "my_worker" {
  account_id = var.account_id
  name = "my-worker"
}

resource "cloudflare_worker_version" "my_worker_version" {
  account_id = var.account_id
  worker_id = cloudflare_worker.my_worker.id
  bindings = [
    {
      type = "durable_object_namespace"
      name = "my_durable_object"
      class_name = "MyDurableObjectClass"
    }
  ]
  migrations = {
    new_sqlite_classes = [
      "MyDurableObjectClass"
    ]
  }
  # ...version props omitted for brevity
}

resource "cloudflare_workers_deployment" "my_worker_deployment" {
  # ...deployment props omitted for brevity
}

成功させるには、まず durable_object バインディングブロックをコメントアウトしてプランを適用し、コメントを外してから migrations ブロックをコメントアウトし、もう一度適用します。この 2 回目でプランは成功します。これは API や SDK でも同じです。cloudflare_workercloudflare_workers_deployment リソースだけを管理し、ビルドと Version 管理は Wrangler に任せるのが妥当な例です。

Worker Versions に関する注意

リソースの不変性

Worker のバージョンは API レベルで不変です。作成後に更新できず、希望する変更で再作成するだけです。そのため、cloudflare_worker_version Terraform リソースへの意味のある変更は常に置換をトリガーします。cloudflare_worker_version リソースが置換されると、希望する変更を持つ新しいバージョンが作成されますが、前のバージョンは削除されません。Terraform で管理するとき、Worker が完全なバージョン履歴を持つようにするためです。つまり、バージョンは不変であり、追記専用です。親の cloudflare_worker リソースが削除されると、その Worker に関連する既存バージョンもすべて削除されます。

モジュールのコンテンツ

Worker バージョンのモジュールは、コンテンツを渡す互いに排他的な 2 つの方法に対応します。

  • content_file - ローカルファイルを指します
  • content_base64 - インラインの base64 エンコード済みコンテンツです

どちらの場合も、基になるコンテンツの変更は計算済みの content_sha256 属性で追跡されます。ほぼすべての場合、content_file 属性でコンテンツを指定することを推奨します。コンテンツ自体を state に保存しないためです。モジュールのコンテンツはかなり大きくなることがあり(数十メガバイトまで)、state に保存すると state ファイルが肥大化し、Terraform 操作の性能が落ちます。content_base64 属性の主な用途は、下記で説明する、API からの cloudflare_worker_version Terraform リソースのインポートです。

インポートの挙動

インポート時、Terraform は設定で使った属性に関係なく、常に state の content_base64 属性を埋めます。

terraform import cloudflare_worker_version.my_worker_version <account_id>/<worker_id>/<version_id>

設定が content_file を使う場合、インポート後に不一致が出ます(state は content_base64、設定は content_file)。これは想定どおりです。

content_file が参照するローカルファイルのコンテンツがインポートしたコンテンツと一致し、content_sha256 値が同じなら、cloudflare_worker_version Terraform リソースの in-place 更新になります。基になるコンテンツは変わらない(どちらの場合も content_sha256 属性は同じ)ため、置換ではなく in-place 更新になるはずです。API レベルでリソースを更新する必要はありません。更新が必要なのは Terraform state だけで、更新後は content_base64 から content_file へ切り替わります。

Terraform が計算済み content_sha256 値の差を理由にリソースの置換を求める場合、content_file が参照するローカルファイルのコンテンツはインポートしたコンテンツと一致せず、ローカルファイルを期待される API 値に合わせて更新しないと、きれいにインポートできません。

content_file を使う場合:

resource "cloudflare_worker_version" "content_file_example" {
  account_id  = var.account_id
  worker_id   = cloudflare_worker.example.id
  main_module = "worker.js"
  modules = [{
    name         = "worker.js"
    content_type = "application/javascript+module"
    content_file = "build/worker.js"
  }]
}

content_base64 を使う場合:

resource "cloudflare_worker_version" "content_base64_example" {
  account_id  = var.account_id
  worker_id   = cloudflare_worker.example.id
  main_module = "worker.js"
  modules = [{
    name           = "worker.js"
    content_type   = "application/javascript+module"
    content_base64 = base64encode("export default { async fetch() { return new Response('Hello world!') } }")
  }]
}

役に立ちましたか?