@crossdelta/infrastructure 0.2.26 → 0.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,56 +3,85 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@crossdelta/infrastructure.svg)](https://www.npmjs.com/package/@crossdelta/infrastructure)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- Infrastructure-as-Code helpers for deploying microservices to DigitalOcean with Pulumi.
6
+ Infrastructure-as-Code toolkit for deploying microservices to Kubernetes with Pulumi.
7
7
 
8
- > **Note**: This is an MVP architecture optimized for simplicity and cost-efficiency.
9
- > For production with SLAs, consider managed services or HA clustering.
8
+ **Current Support:**
9
+ - **DigitalOcean Kubernetes (DOKS)** - Production-ready, fully tested
10
+ - ✅ **Local Kubernetes (k3d)** - Development environment
11
+
12
+ **Planned Support:**
13
+ - 🚧 **AWS EKS** - Planned (contributions welcome)
14
+ - 🚧 **Azure AKS** - Planned (contributions welcome)
15
+ - 🚧 **Google GKE** - Planned (contributions welcome)
16
+
17
+ The architecture is **provider-agnostic** - runtime components (NATS, Ingress, Cert-Manager) use provider-specific implementations, but service configs remain portable.
10
18
 
11
19
  ## Architecture Overview
12
20
 
13
- ```mermaid
14
- graph TB
15
- subgraph DO[DigitalOcean]
16
- subgraph APP[App Platform]
17
- FE[frontend :3000]
18
- API[api :4000]
19
- WRK[worker :4001]
20
- end
21
-
22
- subgraph DROP[Droplet]
23
- NATS[NATS + JetStream]
24
- VOL[(Volume /data)]
25
- NATS --> VOL
26
- end
27
-
28
- APP -->|VPC| DROP
29
- end
30
-
31
- style APP fill:#e1f5fe
32
- style DROP fill:#fff3e0
33
- style VOL fill:#f5f5f5
21
+ ```
22
+ ┌─────────────────────────────────────────┐
23
+ │ DigitalOcean Kubernetes (DOKS) │
24
+ │ │
25
+ ┌───────────────┤ Runtime Components:
26
+ │ │ • NATS + JetStream (messaging) │
27
+ Internet │ • NGINX Ingress (routing) │
28
+ │ │ • Cert-Manager (TLS certs) │
29
+ │ └─────────────────────────────────────────┘
30
+ ▼ │
31
+ ┌──────────┐ ▼
32
+ │ DNS │ ┌──────────────────────────────────────────┐
33
+ └────┬─────┘ │ Microservices: │
34
+ │ │ │
35
+ ▼ │ ┌──────────────┐ ┌──────────────┐ │
36
+ ┌──────────┐ │ │ api-gateway │ │ orders │ │
37
+ │ Load │◄────────┼──┤ :4000 │ │ :4001 │ │
38
+ │ Balancer │ │ └──────┬───────┘ └──────┬───────┘ │
39
+ └──────────┘ │ │ │ │
40
+ │ ▼ ▼ │
41
+ │ ┌──────────────────────────────┐ │
42
+ │ │ NATS (Event Stream) │ │
43
+ │ └──────────────────────────────┘ │
44
+ │ ▲ ▲ │
45
+ │ │ │ │
46
+ │ ┌──────┴───────┐ ┌──────┴───────┐ │
47
+ │ │notifications │ │ storefront │ │
48
+ │ │ :4002 │ │ :3000 │ │
49
+ │ └──────────────┘ └──────────────┘ │
50
+ └──────────────────────────────────────────┘
34
51
  ```
35
52
 
36
- ### Platform Strategy
37
-
38
- | Platform | Use Case | Pros | Cons |
39
- |----------|----------|------|------|
40
- | **App Platform** | Stateless services (APIs, frontends) | Auto-scaling, zero-ops, built-in networking | No persistent volumes |
41
- | **Droplet** | Stateful services (NATS, databases) | Persistent volumes, full control | Manual management |
53
+ ### Why Kubernetes?
42
54
 
43
- **Why this split?** DigitalOcean App Platform doesn't support persistent volumes.
44
- Services like NATS with JetStream need persistent storage for message durability.
55
+ | Feature | Benefit |
56
+ |---------|---------|
57
+ | **Declarative** | Define desired state, K8s maintains it |
58
+ | **Self-healing** | Auto-restart failed pods, reschedule on node failure |
59
+ | **Scaling** | Horizontal pod autoscaling based on metrics |
60
+ | **Rolling updates** | Zero-downtime deployments |
61
+ | **Service discovery** | Built-in DNS for pod-to-pod communication |
62
+ | **Persistent volumes** | StatefulSets for databases and message queues |
45
63
 
46
64
  ## Features
47
65
 
48
- - **Dual Platform Support** - Deploy to App Platform or Droplets from the same config
49
- - **Service Discovery** - Auto-discover service configs from `infra/services/*.ts`
50
- - **Type-safe Config** - Full TypeScript support with `ServiceConfig` type
51
- - **Smart Routing** - Automatic ingress rules for public services
52
- - **VPC Networking** - Secure private communication between platforms
53
- - **Persistent Volumes** - Automatic volume creation and mounting for Droplets
54
- - **Config File Mounting** - Share config files between local dev and production
55
- - **URL Generation** - Auto-generate service URLs and port env vars
66
+ ### 🚀 Core Features
67
+ - **Provider-Agnostic** - One config for DOKS, k3d, EKS, AKS, GKE
68
+ - **Unified Runtime API** - Single `deployRuntime()` for NATS, Ingress, Cert-Manager
69
+ - **Service Discovery** - Auto-discover services from `infra/services/*.ts`
70
+ - **Fluent Port Configuration** - Modern API for HTTP, gRPC, custom ports
71
+ - **Health Checks** - Automatic HTTP health check configuration
72
+ - **Type-Safe** - Full TypeScript support with strict typing
73
+
74
+ ### 📦 Runtime Components
75
+ - **NATS + JetStream** - Event-driven messaging with persistence
76
+ - **NGINX Ingress** - HTTP routing and load balancing
77
+ - **Cert-Manager** - Automatic TLS certificates via Let's Encrypt
78
+ - **OpenTelemetry** - Distributed tracing and metrics (via `@crossdelta/telemetry`)
79
+
80
+ ### 🔧 Developer Experience
81
+ - **Auto-Generated Types** - Service names from directory structure
82
+ - **Environment Generation** - `.env.local` from Pulumi secrets
83
+ - **Image Registry Support** - GHCR, DockerHub, DOCR
84
+ - **Health Check Integration** - Kubernetes liveness/readiness probes
56
85
 
57
86
  ## Installation
58
87
 
@@ -62,381 +91,570 @@ npm install @crossdelta/infrastructure
62
91
 
63
92
  ## Quick Start
64
93
 
65
- ### 1. Create service configs
94
+ ### 1. Install dependencies
95
+
96
+ ```bash
97
+ npm install @crossdelta/infrastructure @pulumi/pulumi @pulumi/kubernetes @pulumi/digitalocean
98
+ ```
99
+
100
+ ### 2. Create service configs
66
101
 
67
102
  ```typescript
68
- // infra/services/api.ts - App Platform (stateless)
69
- import type { ServiceConfig } from '@crossdelta/infrastructure'
70
-
71
- const config: ServiceConfig = {
72
- name: 'api',
73
- httpPort: 4000,
74
- ingressPrefix: '/api',
75
- instanceSizeSlug: 'apps-s-1vcpu-0.5gb',
103
+ // infra/services/orders.ts
104
+ import { ports, type K8sServiceConfig } from '@crossdelta/infrastructure'
105
+
106
+ const config: K8sServiceConfig = {
107
+ name: 'orders',
108
+ ports: ports().http(4001).build(),
109
+ replicas: 1,
110
+ healthCheck: { httpPath: '/health' },
111
+ resources: {
112
+ requests: { cpu: '50m', memory: '64Mi' },
113
+ limits: { cpu: '150m', memory: '128Mi' },
114
+ },
76
115
  }
77
116
 
78
117
  export default config
79
118
  ```
80
119
 
81
120
  ```typescript
82
- // infra/services/nats.ts - Droplet (stateful with persistence)
83
- import { dockerHubImage, type ServiceConfig } from '@crossdelta/infrastructure'
84
- import { readFileSync } from 'node:fs'
85
-
86
- const config: ServiceConfig = {
87
- name: 'nats',
88
- platform: 'droplet',
89
- internalPorts: [4222, 8222],
90
- internalUrl: 'nats://nats:4222',
91
- image: dockerHubImage('nats', '2.10-alpine'),
92
- runCommand: '-c /etc/nats/nats.conf',
93
- droplet: {
94
- size: 's-1vcpu-1gb',
95
- volumes: [{ name: 'nats-data', mountPath: '/data', sizeGb: 10 }],
96
- vpcPorts: [4222, 8222],
97
- configFile: {
98
- containerPath: '/etc/nats/nats.conf',
99
- content: readFileSync('path/to/nats.conf', 'utf-8'),
100
- },
121
+ // infra/services/api-gateway.ts - Public service with ingress
122
+ import { ports, type K8sServiceConfig } from '@crossdelta/infrastructure'
123
+
124
+ const config: K8sServiceConfig = {
125
+ name: 'api-gateway',
126
+ ports: ports().http(4000).public().build(),
127
+ ingress: {
128
+ path: '/api',
129
+ host: 'api.example.com',
101
130
  },
131
+ replicas: 2,
132
+ healthCheck: { httpPath: '/health' },
102
133
  }
103
134
 
104
135
  export default config
105
136
  ```
106
137
 
107
- ### 2. Create your Pulumi entry point
138
+ ### 3. Create your Pulumi infrastructure
108
139
 
109
140
  ```typescript
110
141
  // infra/index.ts
111
142
  import {
112
- buildServices,
113
- buildIngressRules,
114
- buildServiceUrlEnvs,
115
- buildServicePortEnvs,
116
- buildDropletServices,
117
- filterByPlatform,
118
- discoverServices,
143
+ createDOKSCluster,
144
+ createNamespace,
145
+ createVPC,
146
+ deployK8sServices,
147
+ deployRuntime,
148
+ discoverServiceConfigs,
119
149
  } from '@crossdelta/infrastructure'
120
- import { App, Vpc } from '@pulumi/digitalocean'
121
150
 
122
151
  const region = 'fra1'
123
- const serviceConfigs = discoverServices('services')
124
-
125
- // Split services by platform
126
- const { appPlatformServices, dropletServices } = filterByPlatform(serviceConfigs)
152
+ const namespace = 'my-platform'
127
153
 
128
- // Create VPC for private networking
129
- const vpc = new Vpc('platform-vpc', {
130
- name: 'platform-vpc',
154
+ // 1. Create VPC for private networking
155
+ const vpc = createVPC({
156
+ name: `${namespace}-vpc`,
131
157
  region,
132
- ipRange: '10.10.10.0/24',
158
+ description: 'VPC for platform internal communication',
133
159
  })
134
160
 
135
- // Deploy Droplet services (stateful)
136
- const dropletResources = buildDropletServices({
137
- serviceConfigs: dropletServices,
161
+ // 2. Create Kubernetes cluster
162
+ const { cluster, provider, kubeconfig } = createDOKSCluster({
163
+ name: `${namespace}-cluster`,
138
164
  region,
139
165
  vpcUuid: vpc.id,
140
- projectName: 'my-platform',
166
+ nodePool: {
167
+ name: 'default',
168
+ size: 's-2vcpu-4gb',
169
+ nodeCount: 2,
170
+ },
171
+ })
172
+
173
+ // 3. Create namespace
174
+ createNamespace(provider, namespace)
175
+
176
+ // 4. Deploy runtime components (NATS, Ingress, Cert-Manager)
177
+ const runtime = deployRuntime(provider, namespace, {
178
+ nats: {
179
+ enabled: true,
180
+ config: {
181
+ replicas: 1,
182
+ jetstream: {
183
+ enabled: true,
184
+ storageSize: '1Gi',
185
+ },
186
+ },
187
+ },
188
+ ingress: {
189
+ enabled: true,
190
+ config: { replicas: 1 },
191
+ },
192
+ certManager: {
193
+ enabled: true,
194
+ config: { email: 'admin@example.com' },
195
+ },
141
196
  })
142
197
 
143
- // Get env vars for Droplet service URLs
144
- const dropletServiceEnvs = dropletResources.flatMap(r => [
145
- { key: `${r.serviceName.toUpperCase()}_URL`, value: r.internalUrl },
146
- { key: `${r.serviceName.toUpperCase()}_HOST`, value: r.privateIp },
147
- ])
148
-
149
- // Deploy App Platform services (stateless)
150
- const app = new App('my-app', {
151
- spec: {
152
- name: 'my-platform',
153
- region: region.replace(/\d+$/, ''), // 'fra1' -> 'fra'
154
- envs: [
155
- ...buildServiceUrlEnvs(appPlatformServices),
156
- ...buildServicePortEnvs(appPlatformServices),
157
- ...dropletServiceEnvs,
158
- ],
159
- services: buildServices({ serviceConfigs: appPlatformServices }),
160
- ingress: { rules: buildIngressRules(appPlatformServices) },
198
+ // 5. Auto-discover and deploy services
199
+ const serviceConfigs = discoverServiceConfigs('services')
200
+ const deployedServices = deployK8sServices({
201
+ provider,
202
+ namespace,
203
+ serviceConfigs,
204
+ env: {
205
+ NATS_URL: runtime.natsUrl,
161
206
  },
162
207
  })
208
+
209
+ // Export outputs
210
+ export const clusterEndpoint = cluster.endpoint
211
+ export const clusterKubeconfig = kubeconfig
212
+ export const natsInternalUrl = runtime.natsUrl
213
+ export const loadBalancerIP = runtime.loadBalancerIp
163
214
  ```
164
215
 
165
- ## ServiceConfig API
216
+ ## K8sServiceConfig API
166
217
 
167
- The `ServiceConfig` type is the central configuration for all services:
218
+ The `K8sServiceConfig` type is the central configuration for Kubernetes services:
168
219
 
169
220
  ```typescript
170
- type ServiceConfig = {
171
- /** Unique name of the service (required) */
221
+ interface K8sServiceConfig {
222
+ /** Service name (used for deployment, service, ingress names) */
172
223
  name: string
173
224
 
174
- /** Deployment platform: 'app-platform' (default) or 'droplet' */
175
- platform?: 'app-platform' | 'droplet'
176
-
177
- /** Public HTTP port with ingress routing (requires ingressPrefix) */
178
- httpPort?: number
179
-
180
- /** Internal-only ports, not publicly accessible */
181
- internalPorts?: number[]
225
+ /** Port configuration (use fluent ports() builder) */
226
+ ports: PortConfig
182
227
 
183
- /** Ingress path prefix for public routing (e.g., '/api') */
184
- ingressPrefix?: string
228
+ /** Number of pod replicas (default: 1) */
229
+ replicas?: number
185
230
 
186
- /** Override internal URL (e.g., 'nats://nats:4222' for non-HTTP) */
187
- internalUrl?: string
231
+ /** Resource requests and limits */
232
+ resources?: {
233
+ requests?: { cpu: string; memory: string }
234
+ limits?: { cpu: string; memory: string }
235
+ }
188
236
 
189
- /** Exclude from deployment */
190
- skip?: boolean
237
+ /** Health check configuration */
238
+ healthCheck?: {
239
+ httpPath: string // HTTP endpoint (e.g., '/health')
240
+ initialDelaySeconds?: number // Wait before first check (default: 10)
241
+ periodSeconds?: number // Check interval (default: 10)
242
+ }
191
243
 
192
- /** Custom image configuration */
193
- image?: {
194
- registryType: 'DOCKER_HUB' | 'GHCR' | 'DOCR'
195
- registry: string // e.g., 'library', 'myorg'
196
- repository: string // e.g., 'nats', 'platform/api'
197
- tag: string // e.g., '2.10-alpine', 'latest'
244
+ /** Ingress configuration for public services */
245
+ ingress?: {
246
+ path: string // URL path (e.g., '/api')
247
+ host?: string // Domain (e.g., 'api.example.com')
198
248
  }
199
249
 
200
- /** Command to run in the container */
201
- runCommand?: string
250
+ /** Environment variables */
251
+ env?: Record<string, pulumi.Input<string>>
202
252
 
203
- /** App Platform: instance size (e.g., 'apps-s-1vcpu-0.5gb') */
204
- instanceSizeSlug?: string
253
+ /** Kubernetes secrets */
254
+ secrets?: Record<string, pulumi.Input<string>>
205
255
 
206
- /** Droplet-specific configuration */
207
- droplet?: {
208
- /** Droplet size (e.g., 's-1vcpu-1gb') */
209
- size: string
210
-
211
- /** Persistent volumes */
212
- volumes?: Array<{
213
- name: string // Volume name (unique)
214
- mountPath: string // Path in container (e.g., '/data')
215
- sizeGb: number // Size in GB
216
- }>
217
-
218
- /** Ports to open in VPC firewall */
219
- vpcPorts?: number[]
220
-
221
- /** Additional Docker run arguments */
222
- dockerArgs?: string
223
-
224
- /** Config file to mount into container */
225
- configFile?: {
226
- containerPath: string // Path in container
227
- content: string // File content
228
- }
229
- }
256
+ /** Custom image (defaults to GHCR with auto-generated tag) */
257
+ image?: string
258
+
259
+ /** Skip deployment */
260
+ skip?: boolean
230
261
  }
231
262
  ```
232
263
 
233
- ## Service Configuration Examples
264
+ ### Fluent Ports API
234
265
 
235
- ### Public Service (App Platform)
266
+ Build port configurations with a fluent, chainable API:
236
267
 
237
268
  ```typescript
238
- const config: ServiceConfig = {
239
- name: 'frontend',
240
- httpPort: 3000,
241
- ingressPrefix: '/',
242
- instanceSizeSlug: 'apps-s-1vcpu-1gb',
243
- }
269
+ import { ports } from '@crossdelta/infrastructure'
270
+
271
+ // Simple HTTP service
272
+ ports().http(4000).build()
273
+
274
+ // Public HTTP service (exposed via ingress)
275
+ ports().http(4000).public().build()
276
+
277
+ // gRPC service
278
+ ports().grpc(50051).build()
279
+
280
+ // Multiple ports
281
+ ports()
282
+ .http(4000) // Primary port
283
+ .addHttp(8080, 'metrics').public() // Additional public port
284
+ .add(9090, 'admin') // Additional internal port
285
+ .build()
286
+
287
+ // Custom protocol
288
+ ports()
289
+ .primary(4222, 'client')
290
+ .protocol('TCP')
291
+ .add(8222, 'monitoring')
292
+ .build()
244
293
  ```
245
294
 
246
- ### Internal Service (App Platform)
295
+ **Port Builder Methods:**
296
+ - `.http(port)` - HTTP primary port
297
+ - `.https(port)` - HTTPS primary port
298
+ - `.grpc(port)` - gRPC primary port
299
+ - `.primary(port, name?)` - Custom primary port
300
+ - `.add(port, name?, protocol?)` - Add additional port
301
+ - `.addHttp(port, name?)` - Add HTTP additional port
302
+ - `.addGrpc(port, name?)` - Add gRPC additional port
303
+ - `.public()` - Mark last port as public (exposed via ingress)
304
+ - `.protocol(type)` - Set protocol for last port
305
+ - `.build()` - Build final config
306
+
307
+ ## Service Configuration Examples
308
+
309
+ ### Public HTTP Service with Ingress
247
310
 
248
311
  ```typescript
249
- const config: ServiceConfig = {
250
- name: 'worker',
251
- internalPorts: [4001],
252
- instanceSizeSlug: 'apps-s-1vcpu-0.5gb',
312
+ import { ports, type K8sServiceConfig } from '@crossdelta/infrastructure'
313
+
314
+ const config: K8sServiceConfig = {
315
+ name: 'api-gateway',
316
+ ports: ports().http(4000).public().build(),
317
+ replicas: 2,
318
+ healthCheck: { httpPath: '/health' },
319
+ ingress: {
320
+ path: '/api',
321
+ host: 'api.example.com',
322
+ },
323
+ resources: {
324
+ requests: { cpu: '100m', memory: '128Mi' },
325
+ limits: { cpu: '500m', memory: '512Mi' },
326
+ },
253
327
  }
328
+
329
+ export default config
254
330
  ```
255
331
 
256
- ### Stateful Service with Persistence (Droplet)
332
+ ### Internal Service (Event Consumer)
257
333
 
258
334
  ```typescript
259
- import { dockerHubImage, type ServiceConfig } from '@crossdelta/infrastructure'
260
-
261
- const config: ServiceConfig = {
262
- name: 'nats',
263
- platform: 'droplet',
264
- internalPorts: [4222, 8222],
265
- internalUrl: 'nats://nats:4222',
266
- image: dockerHubImage('nats', '2.10-alpine'),
267
- runCommand: '-c /etc/nats/nats.conf',
268
- droplet: {
269
- size: 's-1vcpu-1gb',
270
- volumes: [{ name: 'nats-data', mountPath: '/data', sizeGb: 10 }],
271
- vpcPorts: [4222, 8222],
272
- configFile: {
273
- containerPath: '/etc/nats/nats.conf',
274
- content: readFileSync('services/nats/nats.conf', 'utf-8'),
275
- },
335
+ import { ports, type K8sServiceConfig } from '@crossdelta/infrastructure'
336
+
337
+ const config: K8sServiceConfig = {
338
+ name: 'notifications',
339
+ ports: ports().http(4002).build(),
340
+ replicas: 1,
341
+ healthCheck: { httpPath: '/health' },
342
+ resources: {
343
+ requests: { cpu: '50m', memory: '64Mi' },
344
+ limits: { cpu: '150m', memory: '128Mi' },
276
345
  },
277
346
  }
347
+
348
+ export default config
278
349
  ```
279
350
 
280
- ### Config File Sharing (Local + Production)
351
+ ### Service with Multiple Ports
281
352
 
282
- One of the key features is sharing configuration between local development and production:
353
+ ```typescript
354
+ import { ports, type K8sServiceConfig } from '@crossdelta/infrastructure'
355
+
356
+ const config: K8sServiceConfig = {
357
+ name: 'api-gateway',
358
+ ports: ports()
359
+ .http(4000) // Primary HTTP port
360
+ .addHttp(8080, 'metrics').public() // Metrics endpoint (public)
361
+ .add(9090, 'admin') // Admin endpoint (internal)
362
+ .build(),
363
+ replicas: 2,
364
+ healthCheck: { httpPath: '/health' },
365
+ }
283
366
 
284
- ```
285
- services/nats/
286
- ├── nats.conf # Shared config (used by both)
287
- └── scripts/
288
- └── start-dev.sh # Local dev script
367
+ export default config
289
368
  ```
290
369
 
291
- **nats.conf** (shared):
292
- ```conf
293
- server_name: my-nats-server
294
- listen: 0.0.0.0:4222
295
- http: 0.0.0.0:8222
370
+ ### gRPC Service
296
371
 
297
- jetstream {
298
- store_dir: /data
299
- max_mem: 1G
300
- max_file: 10G
372
+ ```typescript
373
+ import { ports, type K8sServiceConfig } from '@crossdelta/infrastructure'
374
+
375
+ const config: K8sServiceConfig = {
376
+ name: 'grpc-service',
377
+ ports: ports().grpc(50051).build(),
378
+ replicas: 2,
379
+ healthCheck: {
380
+ httpPath: '/grpc.health.v1.Health/Check',
381
+ initialDelaySeconds: 15,
382
+ },
301
383
  }
302
- ```
303
384
 
304
- **start-dev.sh** (local):
305
- ```bash
306
- docker run -d --name nats \
307
- -v "$(pwd)/nats.conf:/etc/nats/nats.conf:ro" \
308
- -v "$(pwd)/.nats-data:/data" \
309
- -p 4222:4222 -p 8222:8222 \
310
- nats:2.10-alpine -c /etc/nats/nats.conf
385
+ export default config
311
386
  ```
312
387
 
313
- **infra/services/nats.ts** (production):
388
+ ## deployRuntime() - Unified Runtime Components
389
+
390
+ Deploy all runtime components (NATS, Ingress, Cert-Manager) with a single function:
391
+
314
392
  ```typescript
315
- droplet: {
316
- configFile: {
317
- containerPath: '/etc/nats/nats.conf',
318
- content: readFileSync('services/nats/nats.conf', 'utf-8'),
393
+ import { deployRuntime } from '@crossdelta/infrastructure'
394
+
395
+ const runtime = deployRuntime(provider, namespace, {
396
+ // NATS with JetStream for event-driven messaging
397
+ nats: {
398
+ enabled: true,
399
+ config: {
400
+ replicas: 1,
401
+ jetstream: {
402
+ enabled: true,
403
+ storageSize: '1Gi',
404
+ storageClass: 'do-block-storage',
405
+ },
406
+ resources: {
407
+ requests: { cpu: '25m', memory: '64Mi' },
408
+ limits: { cpu: '100m', memory: '192Mi' },
409
+ },
410
+ },
411
+ },
412
+
413
+ // NGINX Ingress Controller for HTTP routing
414
+ ingress: {
415
+ enabled: true,
416
+ config: {
417
+ replicas: 1,
418
+ resources: {
419
+ requests: { cpu: '50m', memory: '64Mi' },
420
+ limits: { cpu: '100m', memory: '128Mi' },
421
+ },
422
+ },
319
423
  },
424
+
425
+ // Cert-Manager for automatic TLS certificates
426
+ certManager: {
427
+ enabled: true,
428
+ config: {
429
+ email: 'admin@example.com',
430
+ staging: false, // Use Let's Encrypt production
431
+ resources: {
432
+ requests: { cpu: '10m', memory: '32Mi' },
433
+ limits: { cpu: '100m', memory: '128Mi' },
434
+ },
435
+ },
436
+ },
437
+ })
438
+
439
+ // Use runtime outputs
440
+ console.log('NATS URL:', runtime.natsUrl)
441
+ console.log('LoadBalancer IP:', runtime.loadBalancerIp)
442
+ console.log('Cert-Manager Ready:', runtime.certManagerReady)
443
+ ```
444
+
445
+ **Returns:**
446
+ ```typescript
447
+ interface RuntimeDeploymentResult {
448
+ natsUrl?: pulumi.Output<string> // NATS connection URL
449
+ loadBalancerIp?: pulumi.Output<string> // Ingress LoadBalancer IP
450
+ certManagerReady?: boolean // Cert-Manager installed
320
451
  }
321
452
  ```
322
453
 
323
- Same config file, same behavior, different environments.
454
+ ## Environment Generation
324
455
 
325
- ## CLI: generate-env
456
+ Generate `.env.local` for local development from Pulumi config:
326
457
 
327
- Generate `.env.local` and service-specific `env.ts` files for local development.
458
+ ```bash
459
+ # In your workspace root
460
+ bun dev # Automatically generates .env.local before starting services
461
+ ```
328
462
 
329
- ### Setup
463
+ **Generated `.env.local`:**
464
+ ```bash
465
+ # Auto-generated from Pulumi config (DO NOT EDIT)
466
+ ORDERS_PORT=4001
467
+ NOTIFICATIONS_PORT=4002
468
+ API_GATEWAY_PORT=4000
469
+ STOREFRONT_PORT=3000
330
470
 
331
- Add a script to your workspace `package.json`:
471
+ NATS_URL=nats://localhost:4222
472
+ DATABASE_URL=postgresql://localhost:5432/mydb
473
+ API_KEY=your-secret-key
474
+ ```
332
475
 
333
- ```json
334
- {
335
- "scripts": {
336
- "generate-env": "turbo run generate-env --ui=stream"
337
- }
476
+ **Generated `infra/env.ts`:**
477
+ ```typescript
478
+ // Auto-generated type-safe service names
479
+ export type ServiceName = 'orders' | 'notifications' | 'api-gateway' | 'storefront'
480
+
481
+ export function getServicePort(name: ServiceName, fallback?: number): number {
482
+ const port = process.env[`${name.toUpperCase().replace(/-/g, '_')}_PORT`]
483
+ return port ? Number.parseInt(port, 10) : (fallback ?? 8080)
338
484
  }
339
485
  ```
340
486
 
341
- ### What it does
487
+ ## API Reference
342
488
 
343
- 1. Loads secrets from Pulumi config (dev stack)
344
- 2. Discovers services from `infra/services/*.ts`
345
- 3. Generates `SERVICE_URL` and `SERVICE_PORT` env vars for localhost
346
- 4. Writes `.env.local` to your workspace root
347
- 5. Generates `infra/env.ts` with type-safe `ServiceName` union type
348
- 6. Generates `services/*/src/env.ts` for each service (Docker-compatible)
489
+ ### Core Functions
349
490
 
350
- ### Example output
491
+ | Function | Description |
492
+ |----------|-------------|
493
+ | `deployRuntime(provider, namespace, config)` | Deploy runtime components (NATS, Ingress, Cert-Manager) |
494
+ | `discoverServiceConfigs(dir, options?)` | Auto-discover K8s service configs from directory |
495
+ | `deployK8sServices(options)` | Deploy services to Kubernetes cluster |
496
+ | `createDOKSCluster(config)` | Create DigitalOcean Kubernetes cluster |
497
+ | `createVPC(config)` | Create VPC for private networking |
498
+ | `createNamespace(provider, name, labels?)` | Create Kubernetes namespace |
499
+ | `createImagePullSecret(provider, namespace, name, config)` | Create secret for private registries |
351
500
 
352
- `.env.local`:
353
- ```bash
354
- # Service URLs (localhost for local development)
355
- API_URL=http://localhost:4000
356
- FRONTEND_URL=http://localhost:3000
357
- NATS_URL=nats://localhost:4222
501
+ ### Port Configuration
358
502
 
359
- # Service Ports
360
- API_PORT=4000
361
- FRONTEND_PORT=3000
503
+ | Function | Description |
504
+ |----------|-------------|
505
+ | `ports()` | Start fluent port builder |
506
+ | `.http(port)` | Set HTTP primary port |
507
+ | `.https(port)` | Set HTTPS primary port |
508
+ | `.grpc(port)` | Set gRPC primary port |
509
+ | `.primary(port, name?)` | Set custom primary port |
510
+ | `.add(port, name?, protocol?)` | Add additional port |
511
+ | `.addHttp(port, name?)` | Add HTTP additional port |
512
+ | `.addGrpc(port, name?)` | Add gRPC additional port |
513
+ | `.public()` | Mark last port as public (ingress) |
514
+ | `.protocol(type)` | Set protocol ('TCP', 'UDP', 'SCTP') |
515
+ | `.build()` | Build final port configuration |
516
+
517
+ ### Service Discovery Options
518
+
519
+ ```typescript
520
+ discoverServiceConfigs('services', {
521
+ filter?: RegExp | string, // Filter by service name pattern
522
+ include?: string[], // Whitelist specific services
523
+ exclude?: string[], // Blacklist specific services
524
+ tags?: string[], // Filter by tags
525
+ env?: Record<string, string>, // Inject env vars
526
+ validate?: boolean, // Validate configs (default: true)
527
+ sort?: boolean, // Sort by name (default: true)
528
+ })
362
529
  ```
363
530
 
364
- ## Runtime Helpers
531
+ ### Kubernetes Deployment Options
365
532
 
366
- Use `@crossdelta/infrastructure/env` in your services to read environment configuration:
533
+ ```typescript
534
+ deployK8sServices({
535
+ provider: k8s.Provider, // Kubernetes provider
536
+ namespace: string, // Target namespace
537
+ serviceConfigs: K8sServiceConfig[], // Service configurations
538
+ env?: Record<string, pulumi.Input<string>>, // Global env vars
539
+ secrets?: Record<string, pulumi.Input<string>>, // Global secrets
540
+ imagePullSecrets?: string[], // Image pull secret names
541
+ registry?: string, // Override registry URL
542
+ })
543
+ ```
367
544
 
545
+ Returns a Map:
368
546
  ```typescript
369
- import { getServicePort, getServiceUrl } from '@crossdelta/infrastructure/env'
547
+ Map<string, {
548
+ deployment: k8s.apps.v1.Deployment, // Kubernetes Deployment
549
+ service: k8s.core.v1.Service, // Kubernetes Service
550
+ ingress?: k8s.networking.v1.Ingress, // Ingress (if public)
551
+ internalUrl: pulumi.Output<string>, // Internal service URL
552
+ }>
553
+ ```
370
554
 
371
- // Get port from WORKER_PORT env var (fallback: 8080)
372
- const port = getServicePort('worker', 8080)
555
+ ### Environment Helpers (`@crossdelta/infrastructure/env`)
556
+
557
+ ```typescript
558
+ import { getServicePort } from '@crossdelta/infrastructure/env'
373
559
 
374
- // Get URL from WORKER_URL env var
375
- const url = getServiceUrl('worker')
560
+ // Read SERVICE_NAME_PORT from environment
561
+ const port = getServicePort('orders', 4001) // Fallback to 4001
376
562
  ```
377
563
 
378
- These helpers work both locally (via `.env.local`) and in production (via DO App Platform env injection).
564
+ ## Cost Estimation (DOKS)
379
565
 
380
- ## API Reference
566
+ ### Basic Setup
567
+ - **DOKS Cluster**: $12/month (control plane)
568
+ - **Worker Nodes**: 2× s-2vcpu-4gb = $36/month
569
+ - **Load Balancer**: $12/month (managed by DOKS)
570
+ - **Block Storage**: 10GB for NATS JetStream = $1/month
571
+ - **Total**: ~$61/month
381
572
 
382
- ### Main Export (`@crossdelta/infrastructure`)
573
+ ### Production Setup
574
+ - **DOKS Cluster**: $12/month
575
+ - **Worker Nodes**: 3× s-2vcpu-4gb = $54/month (HA)
576
+ - **Load Balancer**: $12/month
577
+ - **Block Storage**: 50GB = $5/month
578
+ - **Total**: ~$83/month
383
579
 
384
- | Function | Description |
385
- |----------|-------------|
386
- | `discoverServices(dir)` | Auto-discover service configs from directory |
387
- | `buildServices(options)` | Build App Platform service specs |
388
- | `buildIngressRules(configs)` | Generate ingress rules for public services |
389
- | `buildServiceUrlEnvs(configs)` | Create SERVICE_NAME_URL env vars |
390
- | `buildServicePortEnvs(configs)` | Create SERVICE_NAME_PORT env vars |
391
- | `buildLocalUrls(configs)` | Generate localhost URLs for local dev |
392
- | `filterByPlatform(configs)` | Split configs into App Platform and Droplet services |
393
- | `buildDropletServices(options)` | Create Droplet resources with volumes and firewall |
394
- | `dockerHubImage(repo, tag, registry?)` | Create Docker Hub image config (defaults to `library`) |
395
-
396
- ### Droplet Builder Options
580
+ ### Scaling Considerations
397
581
 
398
- ```typescript
399
- buildDropletServices({
400
- serviceConfigs: ServiceConfig[] // Services with platform: 'droplet'
401
- region: string // DO region (e.g., 'fra1')
402
- vpcUuid: pulumi.Output<string> // VPC ID for private networking
403
- projectName: string // Prefix for resource names
404
- })
582
+ | Workload | Node Size | Monthly Cost |
583
+ |----------|-----------|--------------|
584
+ | Development | s-2vcpu-4gb | $12 + $18 = $30/mo |
585
+ | Staging | s-2vcpu-4gb | $12 + $36 = $48/mo |
586
+ | Production | s-4vcpu-8gb | $12 + $144 = $156/mo |
587
+ | High Traffic | s-4vcpu-8gb | $12 + $240 = $252/mo |
588
+
589
+ **Benefits of Kubernetes:**
590
+ - **Auto-scaling**: HPA automatically adjusts pod count
591
+ - **Self-healing**: Failed pods restart automatically
592
+ - **Rolling updates**: Zero-downtime deployments
593
+ - **Resource efficiency**: Better node utilization vs. dedicated VMs
594
+
595
+ ## Adding New Providers
596
+
597
+ The library is designed to be **provider-agnostic**. To add support for a new cloud provider (AWS EKS, Azure AKS, GKE):
598
+
599
+ ### 1. Implement Provider-Specific Runtime Components
600
+
601
+ Create a new directory under `lib/runtimes/`:
602
+
603
+ ```
604
+ lib/runtimes/
605
+ ├── doks/ # DigitalOcean Kubernetes (reference implementation)
606
+ │ ├── nats.ts
607
+ │ ├── ingress.ts
608
+ │ └── cert-manager.ts
609
+ └── eks/ # Your new provider
610
+ ├── nats.ts
611
+ ├── ingress.ts
612
+ └── cert-manager.ts
405
613
  ```
406
614
 
407
- Returns an array of:
615
+ ### 2. Follow the Interface Pattern
616
+
617
+ Each component should export a deployment function:
618
+
408
619
  ```typescript
409
- {
410
- serviceName: string // Service name
411
- droplet: digitalocean.Droplet // Created Droplet resource
412
- privateIp: pulumi.Output<string> // Private IP in VPC
413
- internalUrl: string // URL for service discovery
620
+ // lib/runtimes/eks/nats.ts
621
+ export function deployNats(
622
+ provider: k8s.Provider,
623
+ namespace: string,
624
+ config: NatsConfig
625
+ ): NatsDeploymentResult {
626
+ // Provider-specific NATS deployment
627
+ // Use Helm, manifests, or provider-specific resources
628
+
629
+ return {
630
+ release,
631
+ internalUrl: `nats://nats.${namespace}.svc.cluster.local:4222`,
632
+ serviceDns: `nats.${namespace}.svc.cluster.local`,
633
+ }
414
634
  }
415
635
  ```
416
636
 
417
- ### Env Export (`@crossdelta/infrastructure/env`)
637
+ ### 3. Update deployRuntime()
418
638
 
419
- | Function | Description |
420
- |----------|-------------|
421
- | `getServicePort(name, default)` | Read SERVICE_NAME_PORT from env |
422
- | `getServiceUrl(name)` | Read SERVICE_NAME_URL from env |
423
-
424
- ## MVP Limitations & Future Improvements
639
+ Add your provider to `lib/core/runtime.ts`:
425
640
 
426
- This architecture is designed for MVP/Beta deployments:
641
+ ```typescript
642
+ // Deploy NATS if enabled
643
+ if (config.nats?.enabled) {
644
+ if (providerName === 'doks') {
645
+ const nats = deployNats(provider, namespace, config.nats.config || {})
646
+ result.natsUrl = pulumi.output(nats.internalUrl)
647
+ } else if (providerName === 'eks') {
648
+ const { deployNats } = require('../runtimes/eks/nats')
649
+ const nats = deployNats(provider, namespace, config.nats.config || {})
650
+ result.natsUrl = pulumi.output(nats.internalUrl)
651
+ }
652
+ }
653
+ ```
427
654
 
428
- | Current (MVP) | Future (Production) |
429
- |---------------|---------------------|
430
- | Single NATS Droplet | NATS Cluster (3 nodes) or Synadia Cloud |
431
- | No auto-healing | Kubernetes or managed service |
432
- | Manual scaling | Auto-scaling policies |
433
- | Basic monitoring | Prometheus/Grafana stack |
655
+ ### 4. Service Configs Remain Portable
434
656
 
435
- **Cost estimate (MVP)**:
436
- - App Platform: ~$12-24/month (depending on instance sizes)
437
- - NATS Droplet: ~$6/month (s-1vcpu-1gb)
438
- - Volume: ~$1/month (10GB)
439
- - **Total**: ~$19-31/month
657
+ Service configurations (`K8sServiceConfig`) work across all providers without changes. Only runtime components need provider-specific implementations.
440
658
 
441
659
  ## License
442
660