mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
100
.agents/skills/cloudflare-deploy/references/pulumi/README.md
Normal file
100
.agents/skills/cloudflare-deploy/references/pulumi/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Cloudflare Pulumi Provider
|
||||
|
||||
Expert guidance for Cloudflare Pulumi Provider (@pulumi/cloudflare).
|
||||
|
||||
## Overview
|
||||
|
||||
Programmatic management of Cloudflare resources: Workers, Pages, D1, KV, R2, DNS, Queues, etc.
|
||||
|
||||
**Packages:**
|
||||
- TypeScript/JS: `@pulumi/cloudflare`
|
||||
- Python: `pulumi-cloudflare`
|
||||
- Go: `github.com/pulumi/pulumi-cloudflare/sdk/v6/go/cloudflare`
|
||||
- .NET: `Pulumi.Cloudflare`
|
||||
|
||||
**Version:** v6.x
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. Use API tokens (not legacy API keys)
|
||||
2. Store accountId in stack config
|
||||
3. Match binding names across code/config
|
||||
4. Use `module: true` for ES modules
|
||||
5. Set `compatibilityDate` to lock behavior
|
||||
|
||||
## Authentication
|
||||
|
||||
```typescript
|
||||
import * as cloudflare from "@pulumi/cloudflare";
|
||||
|
||||
// API Token (recommended): CLOUDFLARE_API_TOKEN env
|
||||
const provider = new cloudflare.Provider("cf", { apiToken: process.env.CLOUDFLARE_API_TOKEN });
|
||||
|
||||
// API Key (legacy): CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL env
|
||||
const provider = new cloudflare.Provider("cf", { apiKey: process.env.CLOUDFLARE_API_KEY, email: process.env.CLOUDFLARE_EMAIL });
|
||||
|
||||
// API User Service Key: CLOUDFLARE_API_USER_SERVICE_KEY env
|
||||
const provider = new cloudflare.Provider("cf", { apiUserServiceKey: process.env.CLOUDFLARE_API_USER_SERVICE_KEY });
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
**Pulumi.yaml:**
|
||||
```yaml
|
||||
name: my-cloudflare-app
|
||||
runtime: nodejs
|
||||
config:
|
||||
cloudflare:apiToken:
|
||||
value: ${CLOUDFLARE_API_TOKEN}
|
||||
```
|
||||
|
||||
**Pulumi.<stack>.yaml:**
|
||||
```yaml
|
||||
config:
|
||||
cloudflare:accountId: "abc123..."
|
||||
```
|
||||
|
||||
**index.ts:**
|
||||
```typescript
|
||||
import * as pulumi from "@pulumi/pulumi";
|
||||
import * as cloudflare from "@pulumi/cloudflare";
|
||||
const accountId = new pulumi.Config("cloudflare").require("accountId");
|
||||
```
|
||||
|
||||
## Common Resource Types
|
||||
- `Provider` - Provider config
|
||||
- `WorkerScript` - Worker
|
||||
- `WorkersKvNamespace` - KV
|
||||
- `R2Bucket` - R2
|
||||
- `D1Database` - D1
|
||||
- `Queue` - Queue
|
||||
- `PagesProject` - Pages
|
||||
- `DnsRecord` - DNS
|
||||
- `WorkerRoute` - Worker route
|
||||
- `WorkersDomain` - Custom domain
|
||||
|
||||
## Key Properties
|
||||
- `accountId` - Required for most resources
|
||||
- `zoneId` - Required for DNS/domain
|
||||
- `name`/`title` - Resource identifier
|
||||
- `*Bindings` - Connect resources to Workers
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Order | File | What | When to Read |
|
||||
|-------|------|------|--------------|
|
||||
| 1 | [configuration.md](./configuration.md) | Resource config for Workers/KV/D1/R2/Queues/Pages | First time setup, resource reference |
|
||||
| 2 | [patterns.md](./patterns.md) | Architecture patterns, multi-env, component resources | Building complex apps, best practices |
|
||||
| 3 | [api.md](./api.md) | Outputs, dependencies, imports, dynamic providers | Advanced features, integrations |
|
||||
| 4 | [gotchas.md](./gotchas.md) | Common errors, troubleshooting, limits | Debugging, deployment issues |
|
||||
|
||||
## In This Reference
|
||||
- [configuration.md](./configuration.md) - Provider config, stack setup, Workers/bindings
|
||||
- [api.md](./api.md) - Resource types, Workers script, KV/D1/R2/queues/Pages
|
||||
- [patterns.md](./patterns.md) - Multi-env, secrets, CI/CD, stack management
|
||||
- [gotchas.md](./gotchas.md) - State issues, deployment failures, limits
|
||||
|
||||
## See Also
|
||||
- [terraform](../terraform/) - Alternative IaC for Cloudflare
|
||||
- [wrangler](../wrangler/) - CLI deployment alternative
|
||||
- [workers](../workers/) - Worker runtime documentation
|
||||
200
.agents/skills/cloudflare-deploy/references/pulumi/api.md
Normal file
200
.agents/skills/cloudflare-deploy/references/pulumi/api.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# API & Data Sources
|
||||
|
||||
## Outputs and Exports
|
||||
|
||||
Export resource identifiers:
|
||||
|
||||
```typescript
|
||||
export const kvId = kv.id;
|
||||
export const bucketName = bucket.name;
|
||||
export const workerUrl = worker.subdomain;
|
||||
export const dbId = db.id;
|
||||
```
|
||||
|
||||
## Resource Dependencies
|
||||
|
||||
Implicit dependencies via outputs:
|
||||
|
||||
```typescript
|
||||
const kv = new cloudflare.WorkersKvNamespace("kv", {
|
||||
accountId: accountId,
|
||||
title: "my-kv",
|
||||
});
|
||||
|
||||
// Worker depends on KV (implicit via kv.id)
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
accountId: accountId,
|
||||
name: "my-worker",
|
||||
content: code,
|
||||
kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}], // Creates dependency
|
||||
});
|
||||
```
|
||||
|
||||
Explicit dependencies:
|
||||
|
||||
```typescript
|
||||
const migration = new command.local.Command("migration", {
|
||||
create: pulumi.interpolate`wrangler d1 execute ${db.name} --file ./schema.sql`,
|
||||
}, {dependsOn: [db]});
|
||||
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
accountId: accountId,
|
||||
name: "worker",
|
||||
content: code,
|
||||
d1DatabaseBindings: [{name: "DB", databaseId: db.id}],
|
||||
}, {dependsOn: [migration]}); // Ensure migrations run first
|
||||
```
|
||||
|
||||
## Using Outputs with API Calls
|
||||
|
||||
```typescript
|
||||
const db = new cloudflare.D1Database("db", {accountId, name: "my-db"});
|
||||
|
||||
db.id.apply(async (dbId) => {
|
||||
const response = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${dbId}/query`,
|
||||
{method: "POST", headers: {"Authorization": `Bearer ${apiToken}`, "Content-Type": "application/json"},
|
||||
body: JSON.stringify({sql: "CREATE TABLE users (id INT)"})}
|
||||
);
|
||||
return response.json();
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Dynamic Providers
|
||||
|
||||
For resources not in provider:
|
||||
|
||||
```typescript
|
||||
import * as pulumi from "@pulumi/pulumi";
|
||||
|
||||
class D1MigrationProvider implements pulumi.dynamic.ResourceProvider {
|
||||
async create(inputs: any): Promise<pulumi.dynamic.CreateResult> {
|
||||
const response = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${inputs.accountId}/d1/database/${inputs.databaseId}/query`,
|
||||
{method: "POST", headers: {"Authorization": `Bearer ${inputs.apiToken}`, "Content-Type": "application/json"},
|
||||
body: JSON.stringify({sql: inputs.sql})}
|
||||
);
|
||||
return {id: `${inputs.databaseId}-${Date.now()}`, outs: await response.json()};
|
||||
}
|
||||
async update(id: string, olds: any, news: any): Promise<pulumi.dynamic.UpdateResult> {
|
||||
if (olds.sql !== news.sql) await this.create(news);
|
||||
return {};
|
||||
}
|
||||
async delete(id: string, props: any): Promise<void> {}
|
||||
}
|
||||
|
||||
class D1Migration extends pulumi.dynamic.Resource {
|
||||
constructor(name: string, args: any, opts?: pulumi.CustomResourceOptions) {
|
||||
super(new D1MigrationProvider(), name, args, opts);
|
||||
}
|
||||
}
|
||||
|
||||
const migration = new D1Migration("migration", {
|
||||
accountId, databaseId: db.id, apiToken, sql: "CREATE TABLE users (id INT)",
|
||||
}, {dependsOn: [db]});
|
||||
```
|
||||
|
||||
## Data Sources
|
||||
|
||||
**Get Zone:**
|
||||
```typescript
|
||||
const zone = cloudflare.getZone({name: "example.com"});
|
||||
const zoneId = zone.then(z => z.id);
|
||||
```
|
||||
|
||||
**Get Accounts (via API):**
|
||||
Use Cloudflare API directly or custom dynamic resources.
|
||||
|
||||
## Import Existing Resources
|
||||
|
||||
```bash
|
||||
# Import worker
|
||||
pulumi import cloudflare:index/workerScript:WorkerScript my-worker <account_id>/<worker_name>
|
||||
|
||||
# Import KV namespace
|
||||
pulumi import cloudflare:index/workersKvNamespace:WorkersKvNamespace my-kv <namespace_id>
|
||||
|
||||
# Import R2 bucket
|
||||
pulumi import cloudflare:index/r2Bucket:R2Bucket my-bucket <account_id>/<bucket_name>
|
||||
|
||||
# Import D1 database
|
||||
pulumi import cloudflare:index/d1Database:D1Database my-db <account_id>/<database_id>
|
||||
|
||||
# Import DNS record
|
||||
pulumi import cloudflare:index/dnsRecord:DnsRecord my-record <zone_id>/<record_id>
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
|
||||
```typescript
|
||||
import * as pulumi from "@pulumi/pulumi";
|
||||
|
||||
const config = new pulumi.Config();
|
||||
const apiKey = config.requireSecret("apiKey"); // Encrypted in state
|
||||
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
accountId: accountId,
|
||||
name: "my-worker",
|
||||
content: code,
|
||||
secretTextBindings: [{name: "API_KEY", text: apiKey}],
|
||||
});
|
||||
```
|
||||
|
||||
Store secrets:
|
||||
```bash
|
||||
pulumi config set --secret apiKey "secret-value"
|
||||
```
|
||||
|
||||
## Transform Pattern
|
||||
|
||||
Modify resource args before creation:
|
||||
|
||||
```typescript
|
||||
import {Transform} from "@pulumi/pulumi";
|
||||
|
||||
interface BucketArgs {
|
||||
accountId: pulumi.Input<string>;
|
||||
transform?: {bucket?: Transform<cloudflare.R2BucketArgs>};
|
||||
}
|
||||
|
||||
function createBucket(name: string, args: BucketArgs) {
|
||||
const bucketArgs: cloudflare.R2BucketArgs = {
|
||||
accountId: args.accountId,
|
||||
name: name,
|
||||
location: "auto",
|
||||
};
|
||||
const finalArgs = args.transform?.bucket?.(bucketArgs) ?? bucketArgs;
|
||||
return new cloudflare.R2Bucket(name, finalArgs);
|
||||
}
|
||||
```
|
||||
|
||||
## v6.x Worker Versioning Resources
|
||||
|
||||
**Worker** - Container for versions:
|
||||
```typescript
|
||||
const worker = new cloudflare.Worker("api", {accountId, name: "api-worker"});
|
||||
export const workerId = worker.id;
|
||||
```
|
||||
|
||||
**WorkerVersion** - Immutable code + config:
|
||||
```typescript
|
||||
const version = new cloudflare.WorkerVersion("v1", {
|
||||
accountId, workerId: worker.id,
|
||||
content: fs.readFileSync("./dist/worker.js", "utf8"),
|
||||
compatibilityDate: "2025-01-01",
|
||||
});
|
||||
export const versionId = version.id;
|
||||
```
|
||||
|
||||
**WorkersDeployment** - Active deployment with bindings:
|
||||
```typescript
|
||||
const deployment = new cloudflare.WorkersDeployment("prod", {
|
||||
accountId, workerId: worker.id, versionId: version.id,
|
||||
kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}],
|
||||
});
|
||||
```
|
||||
|
||||
**Use:** Advanced deployments (canary, blue-green). Most apps should use `WorkerScript` (auto-versioning).
|
||||
|
||||
---
|
||||
See: [README.md](./README.md), [configuration.md](./configuration.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md)
|
||||
@@ -0,0 +1,198 @@
|
||||
# Resource Configuration
|
||||
|
||||
## Workers (cloudflare.WorkerScript)
|
||||
|
||||
```typescript
|
||||
import * as cloudflare from "@pulumi/cloudflare";
|
||||
import * as fs from "fs";
|
||||
|
||||
const worker = new cloudflare.WorkerScript("my-worker", {
|
||||
accountId: accountId,
|
||||
name: "my-worker",
|
||||
content: fs.readFileSync("./dist/worker.js", "utf8"),
|
||||
module: true, // ES modules
|
||||
compatibilityDate: "2025-01-01",
|
||||
compatibilityFlags: ["nodejs_compat"],
|
||||
|
||||
// v6.x: Observability
|
||||
logpush: true, // Enable Workers Logpush
|
||||
tailConsumers: [{service: "log-consumer"}], // Stream logs to Worker
|
||||
|
||||
// v6.x: Placement
|
||||
placement: {mode: "smart"}, // Smart placement for latency optimization
|
||||
|
||||
// Bindings
|
||||
kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}],
|
||||
r2BucketBindings: [{name: "MY_BUCKET", bucketName: bucket.name}],
|
||||
d1DatabaseBindings: [{name: "DB", databaseId: db.id}],
|
||||
queueBindings: [{name: "MY_QUEUE", queue: queue.id}],
|
||||
serviceBindings: [{name: "OTHER_SERVICE", service: other.name}],
|
||||
plainTextBindings: [{name: "ENV_VAR", text: "value"}],
|
||||
secretTextBindings: [{name: "API_KEY", text: secret}],
|
||||
|
||||
// v6.x: Advanced bindings
|
||||
analyticsEngineBindings: [{name: "ANALYTICS", dataset: "my-dataset"}],
|
||||
browserBinding: {name: "BROWSER"}, // Browser Rendering
|
||||
aiBinding: {name: "AI"}, // Workers AI
|
||||
hyperdriveBindings: [{name: "HYPERDRIVE", id: hyperdriveConfig.id}],
|
||||
});
|
||||
```
|
||||
|
||||
## Workers KV (cloudflare.WorkersKvNamespace)
|
||||
|
||||
```typescript
|
||||
const kv = new cloudflare.WorkersKvNamespace("my-kv", {
|
||||
accountId: accountId,
|
||||
title: "my-kv-namespace",
|
||||
});
|
||||
|
||||
// Write values
|
||||
const kvValue = new cloudflare.WorkersKvValue("config", {
|
||||
accountId: accountId,
|
||||
namespaceId: kv.id,
|
||||
key: "config",
|
||||
value: JSON.stringify({foo: "bar"}),
|
||||
});
|
||||
```
|
||||
|
||||
## R2 Buckets (cloudflare.R2Bucket)
|
||||
|
||||
```typescript
|
||||
const bucket = new cloudflare.R2Bucket("my-bucket", {
|
||||
accountId: accountId,
|
||||
name: "my-bucket",
|
||||
location: "auto", // or "wnam", etc.
|
||||
});
|
||||
```
|
||||
|
||||
## D1 Databases (cloudflare.D1Database)
|
||||
|
||||
```typescript
|
||||
const db = new cloudflare.D1Database("my-db", {accountId, name: "my-database"});
|
||||
|
||||
// Migrations via wrangler
|
||||
import * as command from "@pulumi/command";
|
||||
const migration = new command.local.Command("d1-migration", {
|
||||
create: pulumi.interpolate`wrangler d1 execute ${db.name} --file ./schema.sql`,
|
||||
}, {dependsOn: [db]});
|
||||
```
|
||||
|
||||
## Queues (cloudflare.Queue)
|
||||
|
||||
```typescript
|
||||
const queue = new cloudflare.Queue("my-queue", {accountId, name: "my-queue"});
|
||||
|
||||
// Producer
|
||||
const producer = new cloudflare.WorkerScript("producer", {
|
||||
accountId, name: "producer", content: code,
|
||||
queueBindings: [{name: "MY_QUEUE", queue: queue.id}],
|
||||
});
|
||||
|
||||
// Consumer
|
||||
const consumer = new cloudflare.WorkerScript("consumer", {
|
||||
accountId, name: "consumer", content: code,
|
||||
queueConsumers: [{queue: queue.name, maxBatchSize: 10, maxRetries: 3}],
|
||||
});
|
||||
```
|
||||
|
||||
## Pages Projects (cloudflare.PagesProject)
|
||||
|
||||
```typescript
|
||||
const pages = new cloudflare.PagesProject("my-site", {
|
||||
accountId, name: "my-site", productionBranch: "main",
|
||||
buildConfig: {buildCommand: "npm run build", destinationDir: "dist"},
|
||||
source: {
|
||||
type: "github",
|
||||
config: {owner: "my-org", repoName: "my-repo", productionBranch: "main"},
|
||||
},
|
||||
deploymentConfigs: {
|
||||
production: {
|
||||
environmentVariables: {NODE_VERSION: "18"},
|
||||
kvNamespaces: {MY_KV: kv.id},
|
||||
d1Databases: {DB: db.id},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## DNS Records (cloudflare.DnsRecord)
|
||||
|
||||
```typescript
|
||||
const zone = cloudflare.getZone({name: "example.com"});
|
||||
const record = new cloudflare.DnsRecord("www", {
|
||||
zoneId: zone.then(z => z.id), name: "www", type: "A",
|
||||
content: "192.0.2.1", ttl: 3600, proxied: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Workers Domains/Routes
|
||||
|
||||
```typescript
|
||||
// Route (pattern-based)
|
||||
const route = new cloudflare.WorkerRoute("my-route", {
|
||||
zoneId: zoneId,
|
||||
pattern: "example.com/api/*",
|
||||
scriptName: worker.name,
|
||||
});
|
||||
|
||||
// Domain (dedicated subdomain)
|
||||
const domain = new cloudflare.WorkersDomain("my-domain", {
|
||||
accountId: accountId,
|
||||
hostname: "api.example.com",
|
||||
service: worker.name,
|
||||
zoneId: zoneId,
|
||||
});
|
||||
```
|
||||
|
||||
## Assets Configuration (v6.x)
|
||||
|
||||
Serve static assets from Workers:
|
||||
|
||||
```typescript
|
||||
const worker = new cloudflare.WorkerScript("app", {
|
||||
accountId: accountId,
|
||||
name: "my-app",
|
||||
content: code,
|
||||
assets: {
|
||||
path: "./public", // Local directory
|
||||
// Assets uploaded and served from Workers
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## v6.x Versioned Deployments (Advanced)
|
||||
|
||||
For gradual rollouts, use 3-resource pattern:
|
||||
|
||||
```typescript
|
||||
// 1. Worker (container for versions)
|
||||
const worker = new cloudflare.Worker("api", {
|
||||
accountId: accountId,
|
||||
name: "api-worker",
|
||||
});
|
||||
|
||||
// 2. Version (immutable code + config)
|
||||
const version = new cloudflare.WorkerVersion("v1", {
|
||||
accountId: accountId,
|
||||
workerId: worker.id,
|
||||
content: fs.readFileSync("./dist/worker.js", "utf8"),
|
||||
compatibilityDate: "2025-01-01",
|
||||
compatibilityFlags: ["nodejs_compat"],
|
||||
// Note: Bindings configured at deployment level
|
||||
});
|
||||
|
||||
// 3. Deployment (version + bindings + traffic split)
|
||||
const deployment = new cloudflare.WorkersDeployment("prod", {
|
||||
accountId: accountId,
|
||||
workerId: worker.id,
|
||||
versionId: version.id,
|
||||
// Bindings applied to deployment
|
||||
kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}],
|
||||
});
|
||||
```
|
||||
|
||||
**When to use:** Blue-green deployments, canary releases, gradual rollouts
|
||||
**When NOT to use:** Simple single-version deployments (use WorkerScript)
|
||||
|
||||
---
|
||||
See: [README.md](./README.md), [api.md](./api.md), [patterns.md](./patterns.md), [gotchas.md](./gotchas.md)
|
||||
181
.agents/skills/cloudflare-deploy/references/pulumi/gotchas.md
Normal file
181
.agents/skills/cloudflare-deploy/references/pulumi/gotchas.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# Troubleshooting & Best Practices
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "No bundler/build step" - Pulumi uploads raw code
|
||||
|
||||
**Problem:** Worker fails with "Cannot use import statement outside a module"
|
||||
**Cause:** Pulumi doesn't bundle Worker code - uploads exactly what you provide
|
||||
**Solution:** Build Worker BEFORE Pulumi deploy
|
||||
|
||||
```typescript
|
||||
// WRONG: Pulumi won't bundle this
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
content: fs.readFileSync("./src/index.ts", "utf8"), // Raw TS file
|
||||
});
|
||||
|
||||
// RIGHT: Build first, then deploy
|
||||
import * as command from "@pulumi/command";
|
||||
const build = new command.local.Command("build", {
|
||||
create: "npm run build",
|
||||
dir: "./worker",
|
||||
});
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
content: build.stdout.apply(() => fs.readFileSync("./worker/dist/index.js", "utf8")),
|
||||
}, {dependsOn: [build]});
|
||||
```
|
||||
|
||||
### "wrangler.toml not consumed" - Config drift
|
||||
|
||||
**Problem:** Local wrangler dev works, Pulumi deploy fails
|
||||
**Cause:** Pulumi ignores wrangler.toml - must duplicate config
|
||||
**Solution:** Generate wrangler.toml from Pulumi or keep synced manually
|
||||
|
||||
```typescript
|
||||
// Pattern: Export Pulumi config to wrangler.toml
|
||||
const workerConfig = {
|
||||
name: "my-worker",
|
||||
compatibilityDate: "2025-01-01",
|
||||
compatibilityFlags: ["nodejs_compat"],
|
||||
};
|
||||
|
||||
new command.local.Command("generate-wrangler", {
|
||||
create: pulumi.interpolate`cat > wrangler.toml <<EOF
|
||||
name = "${workerConfig.name}"
|
||||
compatibility_date = "${workerConfig.compatibilityDate}"
|
||||
compatibility_flags = ${JSON.stringify(workerConfig.compatibilityFlags)}
|
||||
EOF`,
|
||||
});
|
||||
```
|
||||
|
||||
### "False no-changes detection" - Content SHA unchanged
|
||||
|
||||
**Problem:** Worker code updated, Pulumi says "no changes"
|
||||
**Cause:** Content hash identical (whitespace/comment-only change)
|
||||
**Solution:** Add build timestamp or version to force update
|
||||
|
||||
```typescript
|
||||
const version = Date.now().toString();
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
content: code,
|
||||
plainTextBindings: [{name: "VERSION", text: version}], // Forces new deployment
|
||||
});
|
||||
```
|
||||
|
||||
### "D1 migrations don't run on pulumi up"
|
||||
|
||||
**Problem:** Database schema not applied after D1 database created
|
||||
**Cause:** Pulumi creates database but doesn't run migrations
|
||||
**Solution:** Use Command resource with dependsOn
|
||||
|
||||
```typescript
|
||||
const db = new cloudflare.D1Database("db", {accountId, name: "mydb"});
|
||||
|
||||
// Run migrations after DB created
|
||||
const migration = new command.local.Command("migrate", {
|
||||
create: pulumi.interpolate`wrangler d1 execute ${db.name} --file ./schema.sql`,
|
||||
}, {dependsOn: [db]});
|
||||
|
||||
// Worker depends on migrations
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
d1DatabaseBindings: [{name: "DB", databaseId: db.id}],
|
||||
}, {dependsOn: [migration]});
|
||||
```
|
||||
|
||||
### "Missing required property 'accountId'"
|
||||
|
||||
**Problem:** `Error: Missing required property 'accountId'`
|
||||
**Cause:** Account ID not provided in resource configuration
|
||||
**Solution:** Add to stack config
|
||||
|
||||
```yaml
|
||||
# Pulumi.<stack>.yaml
|
||||
config:
|
||||
cloudflare:accountId: "abc123..."
|
||||
```
|
||||
|
||||
### "Binding name mismatch"
|
||||
|
||||
**Problem:** Worker fails with "env.MY_KV is undefined"
|
||||
**Cause:** Binding name in Pulumi != name in Worker code
|
||||
**Solution:** Match exactly (case-sensitive)
|
||||
|
||||
```typescript
|
||||
// Pulumi
|
||||
kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}]
|
||||
|
||||
// Worker code
|
||||
export default { async fetch(request, env) { await env.MY_KV.get("key"); }}
|
||||
```
|
||||
|
||||
### "API token permissions insufficient"
|
||||
|
||||
**Problem:** `Error: authentication error (10000)`
|
||||
**Cause:** Token lacks required permissions
|
||||
**Solution:** Grant token permissions: Account.Workers Scripts:Edit, Account.Account Settings:Read
|
||||
|
||||
### "Resource not found after import"
|
||||
|
||||
**Problem:** Imported resource shows as changed on next `pulumi up`
|
||||
**Cause:** State mismatch between actual resource and Pulumi config
|
||||
**Solution:** Check property names/types match exactly
|
||||
|
||||
```bash
|
||||
pulumi import cloudflare:index/workerScript:WorkerScript my-worker <account_id>/<worker_name>
|
||||
pulumi preview # If shows changes, adjust Pulumi code to match actual resource
|
||||
```
|
||||
|
||||
### "v6.x Worker versioning confusion"
|
||||
|
||||
**Problem:** Worker deployed but not receiving traffic
|
||||
**Cause:** v6.x requires Worker + WorkerVersion + WorkersDeployment (3 resources)
|
||||
**Solution:** Use WorkerScript (auto-versioning) OR full versioning pattern
|
||||
|
||||
```typescript
|
||||
// SIMPLE: WorkerScript auto-versions (default behavior)
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
accountId, name: "my-worker", content: code,
|
||||
});
|
||||
|
||||
// ADVANCED: Manual versioning for gradual rollouts (v6.x)
|
||||
const worker = new cloudflare.Worker("worker", {accountId, name: "my-worker"});
|
||||
const version = new cloudflare.WorkerVersion("v1", {
|
||||
accountId, workerId: worker.id, content: code, compatibilityDate: "2025-01-01",
|
||||
});
|
||||
const deployment = new cloudflare.WorkersDeployment("prod", {
|
||||
accountId, workerId: worker.id, versionId: version.id,
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always set compatibilityDate** - Locks Worker behavior, prevents breaking changes
|
||||
2. **Build before deploy** - Pulumi doesn't bundle; use Command resource or CI build step
|
||||
3. **Match binding names** - Case-sensitive, must match between Pulumi and Worker code
|
||||
4. **Use dependsOn for migrations** - Ensure D1 migrations run before Worker deploys
|
||||
5. **Version Worker content** - Add VERSION binding to force redeployment on content changes
|
||||
6. **Store secrets in stack config** - Use `pulumi config set --secret` for API keys
|
||||
|
||||
## Limits
|
||||
|
||||
| Resource | Limit | Notes |
|
||||
|----------|-------|-------|
|
||||
| Worker script size | 10 MB | Includes all dependencies, after compression |
|
||||
| Worker CPU time | 50ms (free), 30s (paid) | Per request |
|
||||
| KV keys per namespace | Unlimited | 1000 ops/sec write, 100k ops/sec read |
|
||||
| R2 storage | Unlimited | Class A ops: 1M/mo free, Class B: 10M/mo free |
|
||||
| D1 databases | 50,000 per account | Free: 10 per account, 5 GB each |
|
||||
| Queues | 10,000 per account | Free: 1M ops/day |
|
||||
| Pages projects | 500 per account | Free: 100 projects |
|
||||
| API requests | Varies by plan | ~1200 req/5min on free |
|
||||
|
||||
## Resources
|
||||
|
||||
- **Pulumi Registry:** https://www.pulumi.com/registry/packages/cloudflare/
|
||||
- **API Docs:** https://www.pulumi.com/registry/packages/cloudflare/api-docs/
|
||||
- **GitHub:** https://github.com/pulumi/pulumi-cloudflare
|
||||
- **Cloudflare Docs:** https://developers.cloudflare.com/
|
||||
- **Workers Docs:** https://developers.cloudflare.com/workers/
|
||||
|
||||
---
|
||||
See: [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [patterns.md](./patterns.md)
|
||||
191
.agents/skills/cloudflare-deploy/references/pulumi/patterns.md
Normal file
191
.agents/skills/cloudflare-deploy/references/pulumi/patterns.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Architecture Patterns
|
||||
|
||||
## Component Resources
|
||||
|
||||
```typescript
|
||||
class WorkerApp extends pulumi.ComponentResource {
|
||||
constructor(name: string, args: WorkerAppArgs, opts?) {
|
||||
super("custom:cloudflare:WorkerApp", name, {}, opts);
|
||||
const defaultOpts = {parent: this};
|
||||
|
||||
this.kv = new cloudflare.WorkersKvNamespace(`${name}-kv`, {accountId: args.accountId, title: `${name}-kv`}, defaultOpts);
|
||||
this.worker = new cloudflare.WorkerScript(`${name}-worker`, {
|
||||
accountId: args.accountId, name: `${name}-worker`, content: args.workerCode,
|
||||
module: true, kvNamespaceBindings: [{name: "KV", namespaceId: this.kv.id}],
|
||||
}, defaultOpts);
|
||||
this.domain = new cloudflare.WorkersDomain(`${name}-domain`, {
|
||||
accountId: args.accountId, hostname: args.domain, service: this.worker.name,
|
||||
}, defaultOpts);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Full-Stack Worker App
|
||||
|
||||
```typescript
|
||||
const kv = new cloudflare.WorkersKvNamespace("cache", {accountId, title: "api-cache"});
|
||||
const db = new cloudflare.D1Database("db", {accountId, name: "app-database"});
|
||||
const bucket = new cloudflare.R2Bucket("assets", {accountId, name: "app-assets"});
|
||||
|
||||
const apiWorker = new cloudflare.WorkerScript("api", {
|
||||
accountId, name: "api-worker", content: fs.readFileSync("./dist/api.js", "utf8"),
|
||||
module: true, kvNamespaceBindings: [{name: "CACHE", namespaceId: kv.id}],
|
||||
d1DatabaseBindings: [{name: "DB", databaseId: db.id}],
|
||||
r2BucketBindings: [{name: "ASSETS", bucketName: bucket.name}],
|
||||
});
|
||||
```
|
||||
|
||||
## Multi-Environment Setup
|
||||
|
||||
```typescript
|
||||
const stack = pulumi.getStack();
|
||||
const worker = new cloudflare.WorkerScript(`worker-${stack}`, {
|
||||
accountId, name: `my-worker-${stack}`, content: code,
|
||||
plainTextBindings: [{name: "ENVIRONMENT", text: stack}],
|
||||
});
|
||||
```
|
||||
|
||||
## Queue-Based Processing
|
||||
|
||||
```typescript
|
||||
const queue = new cloudflare.Queue("processing-queue", {accountId, name: "image-processing"});
|
||||
|
||||
// Producer: API receives requests
|
||||
const apiWorker = new cloudflare.WorkerScript("api", {
|
||||
accountId, name: "api-worker", content: apiCode,
|
||||
queueBindings: [{name: "PROCESSING_QUEUE", queue: queue.id}],
|
||||
});
|
||||
|
||||
// Consumer: Process async
|
||||
const processorWorker = new cloudflare.WorkerScript("processor", {
|
||||
accountId, name: "processor-worker", content: processorCode,
|
||||
queueConsumers: [{queue: queue.name, maxBatchSize: 10, maxRetries: 3, maxWaitTimeMs: 5000}],
|
||||
r2BucketBindings: [{name: "OUTPUT_BUCKET", bucketName: outputBucket.name}],
|
||||
});
|
||||
```
|
||||
|
||||
## Microservices with Service Bindings
|
||||
|
||||
```typescript
|
||||
const authWorker = new cloudflare.WorkerScript("auth", {accountId, name: "auth-service", content: authCode});
|
||||
const apiWorker = new cloudflare.WorkerScript("api", {
|
||||
accountId, name: "api-service", content: apiCode,
|
||||
serviceBindings: [{name: "AUTH", service: authWorker.name}],
|
||||
});
|
||||
```
|
||||
|
||||
## Event-Driven Architecture
|
||||
|
||||
```typescript
|
||||
const eventQueue = new cloudflare.Queue("events", {accountId, name: "event-bus"});
|
||||
const producer = new cloudflare.WorkerScript("producer", {
|
||||
accountId, name: "api-producer", content: producerCode,
|
||||
queueBindings: [{name: "EVENTS", queue: eventQueue.id}],
|
||||
});
|
||||
const consumer = new cloudflare.WorkerScript("consumer", {
|
||||
accountId, name: "email-consumer", content: consumerCode,
|
||||
queueConsumers: [{queue: eventQueue.name, maxBatchSize: 10}],
|
||||
});
|
||||
```
|
||||
|
||||
## v6.x Versioned Deployments (Blue-Green/Canary)
|
||||
|
||||
```typescript
|
||||
const worker = new cloudflare.Worker("api", {accountId, name: "api-worker"});
|
||||
const v1 = new cloudflare.WorkerVersion("v1", {accountId, workerId: worker.id, content: fs.readFileSync("./dist/v1.js", "utf8"), compatibilityDate: "2025-01-01"});
|
||||
const v2 = new cloudflare.WorkerVersion("v2", {accountId, workerId: worker.id, content: fs.readFileSync("./dist/v2.js", "utf8"), compatibilityDate: "2025-01-01"});
|
||||
|
||||
// Gradual rollout: 10% v2, 90% v1
|
||||
const deployment = new cloudflare.WorkersDeployment("canary", {
|
||||
accountId, workerId: worker.id,
|
||||
versions: [{versionId: v2.id, percentage: 10}, {versionId: v1.id, percentage: 90}],
|
||||
kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}],
|
||||
});
|
||||
```
|
||||
|
||||
**Use:** Canary releases, A/B testing, blue-green. Most apps use `WorkerScript` (auto-versioning).
|
||||
|
||||
## Wrangler.toml Generation (Bridge IaC with Local Dev)
|
||||
|
||||
Generate wrangler.toml from Pulumi config to keep local dev in sync:
|
||||
|
||||
```typescript
|
||||
import * as command from "@pulumi/command";
|
||||
|
||||
const workerConfig = {
|
||||
name: "my-worker",
|
||||
compatibilityDate: "2025-01-01",
|
||||
compatibilityFlags: ["nodejs_compat"],
|
||||
};
|
||||
|
||||
// Create resources
|
||||
const kv = new cloudflare.WorkersKvNamespace("kv", {accountId, title: "my-kv"});
|
||||
const db = new cloudflare.D1Database("db", {accountId, name: "my-db"});
|
||||
const bucket = new cloudflare.R2Bucket("bucket", {accountId, name: "my-bucket"});
|
||||
|
||||
// Generate wrangler.toml after resources created
|
||||
const wranglerGen = new command.local.Command("gen-wrangler", {
|
||||
create: pulumi.interpolate`cat > wrangler.toml <<EOF
|
||||
name = "${workerConfig.name}"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "${workerConfig.compatibilityDate}"
|
||||
compatibility_flags = ${JSON.stringify(workerConfig.compatibilityFlags)}
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "MY_KV"
|
||||
id = "${kv.id}"
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_id = "${db.id}"
|
||||
database_name = "${db.name}"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "MY_BUCKET"
|
||||
bucket_name = "${bucket.name}"
|
||||
EOF`,
|
||||
}, {dependsOn: [kv, db, bucket]});
|
||||
|
||||
// Deploy worker after wrangler.toml generated
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
accountId, name: workerConfig.name, content: code,
|
||||
compatibilityDate: workerConfig.compatibilityDate,
|
||||
compatibilityFlags: workerConfig.compatibilityFlags,
|
||||
kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}],
|
||||
d1DatabaseBindings: [{name: "DB", databaseId: db.id}],
|
||||
r2BucketBindings: [{name: "MY_BUCKET", bucketName: bucket.name}],
|
||||
}, {dependsOn: [wranglerGen]});
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- `wrangler dev` uses same bindings as production
|
||||
- No config drift between Pulumi and local dev
|
||||
- Single source of truth (Pulumi config)
|
||||
|
||||
**Alternative:** Read wrangler.toml in Pulumi (reverse direction) if wrangler is source of truth
|
||||
|
||||
## Build + Deploy Pattern
|
||||
|
||||
```typescript
|
||||
import * as command from "@pulumi/command";
|
||||
const build = new command.local.Command("build", {create: "npm run build", dir: "./worker"});
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
accountId, name: "my-worker",
|
||||
content: build.stdout.apply(() => fs.readFileSync("./worker/dist/index.js", "utf8")),
|
||||
}, {dependsOn: [build]});
|
||||
```
|
||||
|
||||
## Content SHA Pattern (Force Updates)
|
||||
|
||||
Prevent false "no changes" detections:
|
||||
|
||||
```typescript
|
||||
const version = Date.now().toString();
|
||||
const worker = new cloudflare.WorkerScript("worker", {
|
||||
accountId, name: "my-worker", content: code,
|
||||
plainTextBindings: [{name: "VERSION", text: version}], // Forces deployment
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
See: [README.md](./README.md), [configuration.md](./configuration.md), [api.md](./api.md), [gotchas.md](./gotchas.md)
|
||||
Reference in New Issue
Block a user