Infrastructure as Code (IaC)
While Wrangler makes it easy to upload and manage Workers, there are times when you need a more programmatic approach. This could involve using Infrastructure as Code (IaC) tools or interacting directly with the Workers API. Examples include build and deploy scripts, CI/CD pipelines, custom developer tools, and automated testing.
To make this easier, Cloudflare provides SDK libraries for popular languages such as cloudflare-typescript ↗ and cloudflare-python ↗. For IaC, you can use tools like HashiCorp's Terraform and the Cloudflare Terraform Provider to manage Workers resources.
Below are examples of deploying a Worker using different tools and languages, along with important considerations for managing Workers with IaC.
All of these examples need an account ID and API token (not Global API key) to work.
None of the examples below do Workers Bundling. This is usually done with Wrangler or a tool like esbuild ↗.
Generally, you'd run this bundling step before applying your Terraform plan or using the API for script upload:
wrangler deploy --dry-run --outdir build
When using Wrangler for building and a different method for uploading, make sure to copy all of your config from wrangler.json
into your Terraform config or API request. This is especially important with compatibility_date
or flags your script relies on.
In this example, you need a local file named my-script.mjs
with script content similar to the below examples. Learn more about the Cloudflare Terraform Provider here, and see an example with all the Workers script resource settings here ↗.
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 = "$today" 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 }]}
Notice how you don't have to manage all of these resources in Terraform. For example, you could just the cloudflare_worker
resource and seamlessly use Wrangler or your own deployment tools for Versions or Deployments.
This example uses the cloudflare-typescript ↗ SDK which provides convenient access to the Cloudflare REST API from server-side JavaScript or TypeScript.
#!/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();
Open a terminal or create a shell script to upload a Worker and manage versions and deployments with curl. Workers scripts are Javascript ES Modules ↗, but we also support Python Workers (open beta) and Rust 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 Workerworker_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 versionversion_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 Workerdeployment_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 have their own special text/x-python
content type and python_workers
compatibility flag.
account_id="replace_me"api_token="replace_me"worker_name="my-hello-world-worker"
worker_script_base64=$(echo 'from workers import Response
def on_fetch(request, env): return Response(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 Workerworker_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 versionversion_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 Workerdeployment_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"
This API uses multipart/form-data ↗ to upload a Worker and will implicitly create a version and deployment. The above API is recommended for direct management of versions and deployments.
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 Workercurl "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_contentEOF
For Workers for Platforms, you can upload a User Worker to a dispatch namespace. Note the API endpoint is on /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 namespacecurl 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 Workercurl "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_contentEOF
Durable Object migrations are applied with deployments. This means you can't bind to a durable object in a Version if a deployment doesn't exist i.e. migrations haven't been applied. For example, running this in Terraform will fail the first time the plan is applied:
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" name = "my_durable_object" class_name = "MyDurableObjectClass" } ] migrations = { new_sqlite_classes = [ "MyDurableObjectClass" ] } # ...version props ommitted for brevity}
resource "cloudflare_workers_deployment" "my_worker_deployment" { # ...deployment props ommitted for brevity}
To make this succeed, you first have to comment out the durable_object
binding block, apply the plan, uncomment it, comment out the migrations
block, then apply again. This time the plan will succeed. This also applies to the API or SDKs. This is an example where it makes sense to just manage the cloudflare_worker
and/or cloudflare_workers_deployment
resources while using Wrangler for build and Version management.
Was this helpful?
- Resources
- API
- New to Cloudflare?
- Products
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark