diff --git a/cloudflare-ts-cdn-waf/.gitignore b/cloudflare-ts-cdn-waf/.gitignore new file mode 100644 index 000000000..c6958891d --- /dev/null +++ b/cloudflare-ts-cdn-waf/.gitignore @@ -0,0 +1,2 @@ +/bin/ +/node_modules/ diff --git a/cloudflare-ts-cdn-waf/Pulumi.yaml b/cloudflare-ts-cdn-waf/Pulumi.yaml new file mode 100644 index 000000000..d5d51cd76 --- /dev/null +++ b/cloudflare-ts-cdn-waf/Pulumi.yaml @@ -0,0 +1,12 @@ +name: cloudflare-ts-cdn-waf +runtime: nodejs +description: Put Cloudflare's CDN and WAF in front of an origin server +template: + config: + zoneId: + description: The Cloudflare zone ID to configure + origin: + description: The origin hostname to proxy traffic to + hostname: + description: The hostname to serve through Cloudflare + default: www diff --git a/cloudflare-ts-cdn-waf/README.md b/cloudflare-ts-cdn-waf/README.md new file mode 100644 index 000000000..385393c06 --- /dev/null +++ b/cloudflare-ts-cdn-waf/README.md @@ -0,0 +1,56 @@ +[![Deploy this example with Pulumi](https://www.pulumi.com/images/deploy-with-pulumi/dark.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/blob/master/cloudflare-ts-cdn-waf/README.md#gh-light-mode-only) +[![Deploy this example with Pulumi](https://get.pulumi.com/new/button-light.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/blob/master/cloudflare-ts-cdn-waf/README.md#gh-dark-mode-only) + +# Cloudflare CDN and WAF in front of an origin + +Puts Cloudflare in front of an existing origin server: a proxied DNS record routes traffic +through Cloudflare, a [cache ruleset](https://developers.cloudflare.com/cache/how-to/cache-rules/) +caches responses at the edge, and a [rate-limiting ruleset](https://developers.cloudflare.com/waf/rate-limiting-rules/) +protects the origin from abuse. + +## Prerequisites + +1. [Install Pulumi](https://www.pulumi.com/docs/install/) +1. [Install Node.js](https://www.pulumi.com/docs/iac/languages-sdks/javascript/) +1. A domain already added to Cloudflare as a [zone](https://developers.cloudflare.com/dns/zone-setups/). +1. Create a [Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) + with DNS and Zone WAF edit permissions, and export it: + + ```bash + export CLOUDFLARE_API_TOKEN= + ``` + +## Deploying the example + +1. Create a new stack: + + ```bash + pulumi stack init dev + ``` + +1. Configure the zone and origin: + + ```bash + pulumi config set zoneId + pulumi config set origin origin.example.com + ``` + +1. Install dependencies and deploy: + + ```bash + npm install + pulumi up + ``` + +1. The proxied hostname is exported as `url`: + + ```bash + pulumi stack output url + ``` + +## Cleaning up + +```bash +pulumi destroy +pulumi stack rm dev +``` diff --git a/cloudflare-ts-cdn-waf/index.ts b/cloudflare-ts-cdn-waf/index.ts new file mode 100644 index 000000000..d584a730a --- /dev/null +++ b/cloudflare-ts-cdn-waf/index.ts @@ -0,0 +1,67 @@ +// Copyright 2016-2026, Pulumi Corporation. All rights reserved. + +import * as cloudflare from "@pulumi/cloudflare"; +import * as pulumi from "@pulumi/pulumi"; + +// Import the program's configuration settings. +const config = new pulumi.Config(); +const zoneId = config.require("zoneId"); +const origin = config.require("origin"); +const hostname = config.get("hostname") || "www"; + +// Look up the zone so we can build the fully-qualified hostname. +const zone = cloudflare.getZoneOutput({ zoneId: zoneId }); + +// A proxied DNS record, so traffic flows through Cloudflare's CDN and WAF. +const record = new cloudflare.DnsRecord("record", { + zoneId: zoneId, + name: hostname, + type: "CNAME", + content: origin, + ttl: 1, + proxied: true, +}); + +// Cache responses at the edge, overriding the origin's cache headers. +const cache = new cloudflare.Ruleset("cache", { + zoneId: zoneId, + name: "cache-everything", + kind: "zone", + phase: "http_request_cache_settings", + rules: [{ + action: "set_cache_settings", + expression: "true", + enabled: true, + actionParameters: { + cache: true, + edgeTtl: { + mode: "override_origin", + default: 3600, + }, + }, + }], +}); + +// Rate-limit requests per IP to protect the origin from abuse. +const rateLimit = new cloudflare.Ruleset("rate-limit", { + zoneId: zoneId, + name: "rate-limit", + kind: "zone", + phase: "http_ratelimit", + rules: [{ + action: "block", + expression: "true", + enabled: true, + ratelimit: { + characteristics: ["ip.src", "cf.colo.id"], + // period and mitigationTimeout must be 10 on the Free plan; higher + // values require a paid plan. + period: 10, + requestsPerPeriod: 100, + mitigationTimeout: 10, + }, + }], +}); + +// Export the URL served through Cloudflare. +export const url = pulumi.interpolate`https://${hostname}.${zone.name}`; diff --git a/cloudflare-ts-cdn-waf/package.json b/cloudflare-ts-cdn-waf/package.json new file mode 100644 index 000000000..008b31952 --- /dev/null +++ b/cloudflare-ts-cdn-waf/package.json @@ -0,0 +1,13 @@ +{ + "name": "cloudflare-ts-cdn-waf", + "type": "module", + "devDependencies": { + "@types/node": "22.13.14", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + }, + "dependencies": { + "@pulumi/cloudflare": "6.20.0", + "@pulumi/pulumi": "3.228.0" + } +} diff --git a/cloudflare-ts-cdn-waf/tsconfig.json b/cloudflare-ts-cdn-waf/tsconfig.json new file mode 100644 index 000000000..d12a4cdf8 --- /dev/null +++ b/cloudflare-ts-cdn-waf/tsconfig.json @@ -0,0 +1,24 @@ +{ + "ts-node": { + "esm": true + }, + "compilerOptions": { + // Output + "outDir": "bin", + "sourceMap": true, + // Environment + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "moduleDetection": "force", + "types": ["node"], + // Type Checking + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "files": [ + "index.ts" + ] +} diff --git a/cloudflare-ts-dns/.gitignore b/cloudflare-ts-dns/.gitignore new file mode 100644 index 000000000..c6958891d --- /dev/null +++ b/cloudflare-ts-dns/.gitignore @@ -0,0 +1,2 @@ +/bin/ +/node_modules/ diff --git a/cloudflare-ts-dns/Pulumi.yaml b/cloudflare-ts-dns/Pulumi.yaml new file mode 100644 index 000000000..580939659 --- /dev/null +++ b/cloudflare-ts-dns/Pulumi.yaml @@ -0,0 +1,7 @@ +name: cloudflare-ts-dns +runtime: nodejs +description: Manage Cloudflare DNS records on a zone +template: + config: + zoneId: + description: The Cloudflare zone ID to manage records in diff --git a/cloudflare-ts-dns/README.md b/cloudflare-ts-dns/README.md new file mode 100644 index 000000000..1c03b99be --- /dev/null +++ b/cloudflare-ts-dns/README.md @@ -0,0 +1,53 @@ +[![Deploy this example with Pulumi](https://www.pulumi.com/images/deploy-with-pulumi/dark.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/blob/master/cloudflare-ts-dns/README.md#gh-light-mode-only) +[![Deploy this example with Pulumi](https://get.pulumi.com/new/button-light.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/blob/master/cloudflare-ts-dns/README.md#gh-dark-mode-only) + +# Manage Cloudflare DNS records + +Manages a set of [DNS records](https://developers.cloudflare.com/dns/manage-dns-records/) on a +Cloudflare zone — an `A` record, a `CNAME`, and a `TXT` record. + +## Prerequisites + +1. [Install Pulumi](https://www.pulumi.com/docs/install/) +1. [Install Node.js](https://www.pulumi.com/docs/iac/languages-sdks/javascript/) +1. A domain already added to Cloudflare as a [zone](https://developers.cloudflare.com/dns/zone-setups/). +1. Create a [Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) + with DNS edit permissions, and export it: + + ```bash + export CLOUDFLARE_API_TOKEN= + ``` + +## Deploying the example + +1. Create a new stack: + + ```bash + pulumi stack init dev + ``` + +1. Set the zone to manage (find the zone ID on your domain's overview page in the Cloudflare dashboard): + + ```bash + pulumi config set zoneId + ``` + +1. Install dependencies and deploy: + + ```bash + npm install + pulumi up + ``` + +1. Inspect the created records: + + ```bash + pulumi stack output records + ``` + +## Cleaning up + +```bash +pulumi destroy +pulumi stack rm dev +``` diff --git a/cloudflare-ts-dns/index.ts b/cloudflare-ts-dns/index.ts new file mode 100644 index 000000000..032437e18 --- /dev/null +++ b/cloudflare-ts-dns/index.ts @@ -0,0 +1,40 @@ +// Copyright 2016-2026, Pulumi Corporation. All rights reserved. + +import * as cloudflare from "@pulumi/cloudflare"; +import * as pulumi from "@pulumi/pulumi"; + +// Import the program's configuration settings. +const config = new pulumi.Config(); +const zoneId = config.require("zoneId"); + +// An A record pointing "www" at an origin server. +const www = new cloudflare.DnsRecord("www", { + zoneId: zoneId, + name: "www", + type: "A", + content: "192.0.2.1", + ttl: 3600, + proxied: false, +}); + +// A CNAME aliasing "docs" to an externally hosted site. +const docs = new cloudflare.DnsRecord("docs", { + zoneId: zoneId, + name: "docs", + type: "CNAME", + content: "hosting.example.com", + ttl: 3600, + proxied: false, +}); + +// A TXT record, e.g. for domain verification or SPF. +const txt = new cloudflare.DnsRecord("txt", { + zoneId: zoneId, + name: "mail", + type: "TXT", + content: "v=spf1 include:_spf.example.com ~all", + ttl: 3600, +}); + +// Export the fully-qualified names of the records. +export const records = [www.name, docs.name, txt.name]; diff --git a/cloudflare-ts-dns/package.json b/cloudflare-ts-dns/package.json new file mode 100644 index 000000000..22f2ffec4 --- /dev/null +++ b/cloudflare-ts-dns/package.json @@ -0,0 +1,13 @@ +{ + "name": "cloudflare-ts-dns", + "type": "module", + "devDependencies": { + "@types/node": "22.13.14", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + }, + "dependencies": { + "@pulumi/cloudflare": "6.20.0", + "@pulumi/pulumi": "3.228.0" + } +} diff --git a/cloudflare-ts-dns/tsconfig.json b/cloudflare-ts-dns/tsconfig.json new file mode 100644 index 000000000..d12a4cdf8 --- /dev/null +++ b/cloudflare-ts-dns/tsconfig.json @@ -0,0 +1,24 @@ +{ + "ts-node": { + "esm": true + }, + "compilerOptions": { + // Output + "outDir": "bin", + "sourceMap": true, + // Environment + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "moduleDetection": "force", + "types": ["node"], + // Type Checking + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "files": [ + "index.ts" + ] +} diff --git a/cloudflare-ts-serverless-d1/.gitignore b/cloudflare-ts-serverless-d1/.gitignore new file mode 100644 index 000000000..c6958891d --- /dev/null +++ b/cloudflare-ts-serverless-d1/.gitignore @@ -0,0 +1,2 @@ +/bin/ +/node_modules/ diff --git a/cloudflare-ts-serverless-d1/Pulumi.yaml b/cloudflare-ts-serverless-d1/Pulumi.yaml new file mode 100644 index 000000000..18b6fe9aa --- /dev/null +++ b/cloudflare-ts-serverless-d1/Pulumi.yaml @@ -0,0 +1,7 @@ +name: cloudflare-ts-serverless-d1 +runtime: nodejs +description: A serverless Cloudflare Worker backed by a D1 SQL database +template: + config: + accountId: + description: The Cloudflare account ID to deploy into diff --git a/cloudflare-ts-serverless-d1/README.md b/cloudflare-ts-serverless-d1/README.md new file mode 100644 index 000000000..197bcb4c4 --- /dev/null +++ b/cloudflare-ts-serverless-d1/README.md @@ -0,0 +1,60 @@ +[![Deploy this example with Pulumi](https://www.pulumi.com/images/deploy-with-pulumi/dark.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/blob/master/cloudflare-ts-serverless-d1/README.md#gh-light-mode-only) +[![Deploy this example with Pulumi](https://get.pulumi.com/new/button-light.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/blob/master/cloudflare-ts-serverless-d1/README.md#gh-dark-mode-only) + +# Serverless Cloudflare Worker backed by a D1 database + +A [Cloudflare Worker](https://developers.cloudflare.com/workers/) that records each visit in a +[D1](https://developers.cloudflare.com/d1/) serverless SQL database and returns the running +total. The Worker is uploaded as a version and deployed with `cloudflare.WorkersDeployment`, +and is served on its `*.workers.dev` subdomain. + +## Prerequisites + +1. [Install Pulumi](https://www.pulumi.com/docs/install/) +1. [Install Node.js](https://www.pulumi.com/docs/iac/languages-sdks/javascript/) +1. Create a [Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) + with Workers and D1 edit permissions, and export it: + + ```bash + export CLOUDFLARE_API_TOKEN= + ``` + +## Deploying the example + +1. Create a new stack: + + ```bash + pulumi stack init dev + ``` + +1. Set your Cloudflare account ID: + + ```bash + pulumi config set accountId + ``` + +1. Install dependencies: + + ```bash + npm install + ``` + +1. Run `pulumi up` to deploy: + + ```bash + pulumi up + ``` + +1. Visit the Worker's URL, refreshing a few times to watch the counter increment: + + ```bash + curl "$(pulumi stack output url)" + # Hello, world! This page has been visited 1 times. + ``` + +## Cleaning up + +```bash +pulumi destroy +pulumi stack rm dev +``` diff --git a/cloudflare-ts-serverless-d1/index.ts b/cloudflare-ts-serverless-d1/index.ts new file mode 100644 index 000000000..4ff971fbd --- /dev/null +++ b/cloudflare-ts-serverless-d1/index.ts @@ -0,0 +1,55 @@ +// Copyright 2016-2026, Pulumi Corporation. All rights reserved. + +import * as cloudflare from "@pulumi/cloudflare"; +import * as pulumi from "@pulumi/pulumi"; + +// Import the program's configuration settings. +const config = new pulumi.Config(); +const accountId = config.require("accountId"); + +// A D1 (serverless SQLite) database to store visits. +const db = new cloudflare.D1Database("db", { + accountId: accountId, + name: "visits-db", +}); + +// A Cloudflare Worker, exposed on its workers.dev subdomain. +const worker = new cloudflare.Worker("worker", { + accountId: accountId, + name: "d1-app", + subdomain: { + enabled: true, + }, +}); + +// A version of the Worker containing the code (worker.js) and its D1 binding. +const version = new cloudflare.WorkerVersion("version", { + accountId: accountId, + workerId: worker.id, + compatibilityDate: "2025-01-01", + mainModule: "worker.js", + bindings: [{ + name: "DB", + type: "d1", + databaseId: db.uuid, + }], + modules: [{ + name: "worker.js", + contentType: "application/javascript+module", + contentFile: "worker.js", + }], +}); + +// Deploy the version so it serves all of the application's traffic. +const deployment = new cloudflare.WorkersDeployment("deployment", { + accountId: accountId, + scriptName: worker.name, + strategy: "percentage", + versions: [{ + versionId: version.id, + percentage: 100, + }], +}); + +// Export the application's URL. +export const url = worker.subdomain.url; diff --git a/cloudflare-ts-serverless-d1/package.json b/cloudflare-ts-serverless-d1/package.json new file mode 100644 index 000000000..fd857086f --- /dev/null +++ b/cloudflare-ts-serverless-d1/package.json @@ -0,0 +1,13 @@ +{ + "name": "cloudflare-ts-serverless-d1", + "type": "module", + "devDependencies": { + "@types/node": "22.13.14", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + }, + "dependencies": { + "@pulumi/cloudflare": "6.20.0", + "@pulumi/pulumi": "3.228.0" + } +} diff --git a/cloudflare-ts-serverless-d1/tsconfig.json b/cloudflare-ts-serverless-d1/tsconfig.json new file mode 100644 index 000000000..d12a4cdf8 --- /dev/null +++ b/cloudflare-ts-serverless-d1/tsconfig.json @@ -0,0 +1,24 @@ +{ + "ts-node": { + "esm": true + }, + "compilerOptions": { + // Output + "outDir": "bin", + "sourceMap": true, + // Environment + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "moduleDetection": "force", + "types": ["node"], + // Type Checking + "strict": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "files": [ + "index.ts" + ] +} diff --git a/cloudflare-ts-serverless-d1/worker.js b/cloudflare-ts-serverless-d1/worker.js new file mode 100644 index 000000000..842aae877 --- /dev/null +++ b/cloudflare-ts-serverless-d1/worker.js @@ -0,0 +1,12 @@ +export default { + async fetch(request, env) { + await env.DB.prepare( + "CREATE TABLE IF NOT EXISTS visits (id INTEGER PRIMARY KEY AUTOINCREMENT, visited_at TEXT)", + ).run(); + await env.DB.prepare("INSERT INTO visits (visited_at) VALUES (?1)") + .bind(new Date().toISOString()) + .run(); + const row = await env.DB.prepare("SELECT COUNT(*) AS count FROM visits").first(); + return new Response("Hello, world! This page has been visited " + row.count + " times."); + }, +};