mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 06:11:27 -07:00
192 lines
6.8 KiB
Markdown
192 lines
6.8 KiB
Markdown
# 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)
|