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, including cloudflare-typescript ↗ and cloudflare-python ↗. For IaC, you can use tools like HashiCorp's Terraform and the Cloudflare Terraform Provider 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 but another method for uploading, make sure to copy all of your config from wrangler.json
into your Terraform config, API request, or other. 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"}
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 }]}
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 a Worker with a Version and Deployment * * Docs * - https://developers.cloudflare.com/workers/configuration/versions-and-deployments/ * - https://developers.cloudflare.com/workers/platform/infrastructure-as-code/ * * Generate an API token: * https://developers.cloudflare.com/fundamentals/api/get-started/create-token/ * (Not Global API Key!) * * Find your account id: * https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/ * * Find your workers.dev subdomain: * https://developers.cloudflare.com/workers/configuration/routing/workers-dev/ * * Set these environment variables: * - CLOUDFLARE_API_TOKEN * - CLOUDFLARE_ACCOUNT_ID * - CLOUDFLARE_SUBDOMAIN */
import Cloudflare from "cloudflare";
const apiToken = process.env["CLOUDFLARE_API_TOKEN"] ?? "";if (!apiToken) { throw new Error("Please set envar CLOUDFLARE_API_TOKEN");}
const accountID = process.env["CLOUDFLARE_ACCOUNT_ID"] ?? "";if (!accountID) { throw new Error("Please set envar CLOUDFLARE_ACCOUNT_ID");}
const subdomain = process.env["CLOUDFLARE_SUBDOMAIN"] ?? "";
const client = new Cloudflare({ apiToken,});
async function main() { const workerName = "my-hello-world-worker"; const scriptFileName = `${workerName}.mjs`;
// Workers Scripts use ES Module Syntax // https://blog.cloudflare.com/workers-javascript-modules/ const scriptContent = ` export default { async fetch(request, env, ctx) { return new Response(env.MESSAGE, { status: 200 }); } }; `;
/** * Create a Worker and set non-versioned settings like observability */ const worker = await client.workers.create(workerName, { account_id: accountID, subdomain: { enabled: subdomain ? true : false, }, observability: { enabled: true, }, });
/** * Create the first version of the Worker * This is where code and bindings are defined and can be different between versions */ const version = await client.workers.versions.create(worker.id, { account_id: accountID, main_module: scriptFileName, compatibility_date: new Date().toISOString().split("T")[0], bindings: [ { type: "plain_text", name: "MESSAGE", text: "Hello World!", }, ], modules: [ { name: scriptFileName, content_type: "application/javascript+module", content_base64: Buffer.from(scriptContent).toString("base64"), }, ], });
/** * Create a deployment and point all traffic to the version we created * Triggers that hit fetch() in the Worker (ie. HTTP requests from workers.dev, routes, and custom domains) * split requests between one or multiple versions by deployments */ const deployment = await client.workers.scripts.deployments.create( worker.name, { account_id: accountID, strategy: "percentage", versions: [ { percentage: 100, version_id: version.id, }, ], }, );
console.log(JSON.stringify(deployment, null, 2)); if (subdomain) { console.log( `${workerName} is live at: ${workerName}.${subdomain}.workers.dev`, ); } else { console.log( "Setup a route, custom domain, or workers.dev subdomain to access this Worker.", ); }}
main();
#!/usr/bin/env -S npm run tsn -T
/** * Create a Worker with a Version and Deployment * * Docs * - https://developers.cloudflare.com/workers/configuration/versions-and-deployments/ * - https://developers.cloudflare.com/workers/platform/infrastructure-as-code/ * * Generate an API token: * https://developers.cloudflare.com/fundamentals/api/get-started/create-token/ * (Not Global API Key!) * * Find your account id: * https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/ * * Find your workers.dev subdomain: * https://developers.cloudflare.com/workers/configuration/routing/workers-dev/ * * Set these environment variables: * - CLOUDFLARE_API_TOKEN * - CLOUDFLARE_ACCOUNT_ID * - CLOUDFLARE_SUBDOMAIN */
import Cloudflare from 'cloudflare';
const apiToken = process.env['CLOUDFLARE_API_TOKEN'] ?? '';if (!apiToken) { throw new Error('Please set envar CLOUDFLARE_API_TOKEN');}
const accountID = process.env['CLOUDFLARE_ACCOUNT_ID'] ?? '';if (!accountID) { throw new Error('Please set envar CLOUDFLARE_ACCOUNT_ID');}
const subdomain = process.env['CLOUDFLARE_SUBDOMAIN'] ?? '';
const client = new Cloudflare({ apiToken,});
async function main() { const workerName = 'my-hello-world-worker'; const scriptFileName = `${workerName}.mjs`;
// Workers Scripts use ES Module Syntax // https://blog.cloudflare.com/workers-javascript-modules/ const scriptContent = ` export default { async fetch(request, env, ctx) { return new Response(env.MESSAGE, { status: 200 }); } }; `;
/** * Create a Worker and set non-versioned settings like observability */ const worker = await client.workers.create(workerName, { account_id: accountID, subdomain: { enabled: subdomain ? true : false, }, observability: { enabled: true, }, });
/** * Create the first version of the Worker * This is where code and bindings are defined and can be different between versions */ const version = await client.workers.versions.create(worker.id, { account_id: accountID, main_module: scriptFileName, compatibility_date: new Date().toISOString().split('T')[0], bindings: [ { type: 'plain_text', name: 'MESSAGE', text: 'Hello World!', }, ], modules: [ { name: scriptFileName, content_type: 'application/javascript+module', content_base64: Buffer.from(scriptContent).toString('base64'), }, ], });
/** * Create a deployment and point all traffic to the version we created * Triggers that hit fetch() in the Worker (ie. HTTP requests from workers.dev, routes, and custom domains) * split requests between one or multiple versions by deployments */ const deployment = await client.workers.scripts.deployments.create(worker.name, { account_id: accountID, strategy: 'percentage', versions: [ { percentage: 100, version_id: version.id, }, ], });
console.log(JSON.stringify(deployment, null, 2)); if (subdomain) { console.log(`${workerName} is live at: ${workerName}.${subdomain}.workers.dev`); } else { console.log('Setup a route, custom domain, or workers.dev subdomain to access this Worker.'); }}
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
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