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 Cloudflare 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 to create and manage Workers resources.
Below are examples of deploying a Worker using different tools and languages, along with important considerations for managing Workers with IaC. Each example shows how to upload script content and metadata, which may differ depending on the approach. For a detailed definition of script uploads, see the Upload Worker Module API docs here.
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
Then you'd reference the bundled script like build/index.js
.
Make sure to copy all of your config from wrangler.json
into your Terraform config or API request. This is especially important for 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 script_name = "my-worker"}
resource "cloudflare_workers_version" "my_worker_version" { account_id = var.account_id script_id = cloudflare_workers_script.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" content_sha256 = filesha256("my-script.mjs") } ]
# Versions can only be created, never updated or destroyed lifecycle { create_before_destroy = true prevent_destroy = true }}
resource "cloudflare_workers_deployment" "my_worker_deployment" { account_id = var.account_id script_id = cloudflare_workers_script.my_worker.id strategy = "percentage" versions = [{ percentage = 100 version_id = cloudflare_workers_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();
See the same example in Python in the cloudflare-python ↗ SDK.
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)
# Create the Workerworker_id=$(curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers" \ -X POST \ -H "Authorization: Bearer $api_token" \ -H "Content-Type: application/json" \ -d "{ "name": '"$worker_name"' }" \ | jq -r '.result.id')
# Upload the Worker's first versionversion_id=$(curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/$worker_id/versions" \ -X POST \ -H "Authorization: Bearer $api_token" \ -H "Content-Type: application/json" \ -d "{ 'compatibility_date': '$today', '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')
# Create a deployment for the Workercurl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/$worker_id/deployments" \ -X POST \ -H "Authorization: Bearer $api_token" \ -H "Content-Type: application/json" \ -d "{ 'strategy': 'percentage', 'versions': [ { 'percentage': 100, 'version_id': '"$version_id"' } ] }"
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)
# Create the Workerworker_id=$(curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers" \ -X POST \ -H "Authorization: Bearer $api_token" \ -H "Content-Type: application/json" \ -d "{ "name": '"$worker_name"' }" \ | jq -r '.result.id')
# Upload the Worker's first versionversion_id=$(curl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/$worker_id/versions" \ -X POST \ -H "Authorization: Bearer $api_token" \ -H "Content-Type: application/json" \ -d "{ 'compatibility_date': '$today', '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')
# Create a deployment for the Workercurl "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/$worker_id/deployments" \ -X POST \ -H "Authorization: Bearer $api_token" \ -H "Content-Type: application/json" \ -d "{ 'strategy': 'percentage', 'versions': [ { 'percentage': 100, 'version_id': '"$version_id"' } ] }"
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