@crossdelta/infrastructure 0.2.27 → 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 +519 -301
- package/bin/generate-env.ts +4 -7
- package/dist/core/compat.d.ts +72 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/runtime-detection.d.ts +15 -0
- package/dist/core/runtime.d.ts +221 -0
- package/dist/core/types.d.ts +419 -0
- package/dist/env.cjs +11 -17
- package/dist/env.d.ts +16 -2
- package/dist/env.js +12 -16
- package/dist/helpers/discover-services.d.ts +62 -5
- package/dist/helpers/docker-hub-image.d.ts +2 -3
- package/dist/helpers/index.d.ts +0 -7
- package/dist/index.cjs +21273 -21029
- package/dist/index.d.ts +28 -1
- package/dist/index.js +21273 -21029
- package/dist/runtimes/doks/types.d.ts +45 -3
- package/dist/runtimes/doks/workloads.d.ts +6 -6
- package/dist/runtimes/local/docker-compose.d.ts +38 -0
- package/dist/runtimes/local/index.d.ts +9 -0
- package/dist/runtimes/local/k3d.d.ts +58 -0
- package/dist/runtimes/local/types.d.ts +118 -0
- package/dist/types/index.d.ts +12 -146
- package/dist/types/service-names.d.ts +1 -1
- package/package.json +3 -2
- package/dist/helpers/config.d.ts +0 -29
- package/dist/helpers/droplet-builder.d.ts +0 -44
- package/dist/helpers/image.d.ts +0 -30
- package/dist/helpers/otel.d.ts +0 -62
- package/dist/helpers/service-builder.d.ts +0 -39
- package/dist/helpers/service-runtime.d.ts +0 -22
- package/dist/helpers/service-urls.d.ts +0 -27
package/README.md
CHANGED
|
@@ -3,56 +3,85 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@crossdelta/infrastructure)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
Infrastructure-as-Code
|
|
6
|
+
Infrastructure-as-Code toolkit for deploying microservices to Kubernetes with Pulumi.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
```
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
54
|
-
- **
|
|
55
|
-
|
|
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.
|
|
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/
|
|
69
|
-
import type
|
|
70
|
-
|
|
71
|
-
const config:
|
|
72
|
-
name: '
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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/
|
|
83
|
-
import {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
###
|
|
138
|
+
### 3. Create your Pulumi infrastructure
|
|
108
139
|
|
|
109
140
|
```typescript
|
|
110
141
|
// infra/index.ts
|
|
111
142
|
import {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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 =
|
|
130
|
-
name:
|
|
154
|
+
// 1. Create VPC for private networking
|
|
155
|
+
const vpc = createVPC({
|
|
156
|
+
name: `${namespace}-vpc`,
|
|
131
157
|
region,
|
|
132
|
-
|
|
158
|
+
description: 'VPC for platform internal communication',
|
|
133
159
|
})
|
|
134
160
|
|
|
135
|
-
//
|
|
136
|
-
const
|
|
137
|
-
|
|
161
|
+
// 2. Create Kubernetes cluster
|
|
162
|
+
const { cluster, provider, kubeconfig } = createDOKSCluster({
|
|
163
|
+
name: `${namespace}-cluster`,
|
|
138
164
|
region,
|
|
139
165
|
vpcUuid: vpc.id,
|
|
140
|
-
|
|
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
|
-
//
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
##
|
|
216
|
+
## K8sServiceConfig API
|
|
166
217
|
|
|
167
|
-
The `
|
|
218
|
+
The `K8sServiceConfig` type is the central configuration for Kubernetes services:
|
|
168
219
|
|
|
169
220
|
```typescript
|
|
170
|
-
|
|
171
|
-
/**
|
|
221
|
+
interface K8sServiceConfig {
|
|
222
|
+
/** Service name (used for deployment, service, ingress names) */
|
|
172
223
|
name: string
|
|
173
224
|
|
|
174
|
-
/**
|
|
175
|
-
|
|
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
|
-
/**
|
|
184
|
-
|
|
228
|
+
/** Number of pod replicas (default: 1) */
|
|
229
|
+
replicas?: number
|
|
185
230
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
231
|
+
/** Resource requests and limits */
|
|
232
|
+
resources?: {
|
|
233
|
+
requests?: { cpu: string; memory: string }
|
|
234
|
+
limits?: { cpu: string; memory: string }
|
|
235
|
+
}
|
|
188
236
|
|
|
189
|
-
/**
|
|
190
|
-
|
|
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
|
-
/**
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
/**
|
|
201
|
-
|
|
250
|
+
/** Environment variables */
|
|
251
|
+
env?: Record<string, pulumi.Input<string>>
|
|
202
252
|
|
|
203
|
-
/**
|
|
204
|
-
|
|
253
|
+
/** Kubernetes secrets */
|
|
254
|
+
secrets?: Record<string, pulumi.Input<string>>
|
|
205
255
|
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
264
|
+
### Fluent Ports API
|
|
234
265
|
|
|
235
|
-
|
|
266
|
+
Build port configurations with a fluent, chainable API:
|
|
236
267
|
|
|
237
268
|
```typescript
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
###
|
|
332
|
+
### Internal Service (Event Consumer)
|
|
257
333
|
|
|
258
334
|
```typescript
|
|
259
|
-
import {
|
|
260
|
-
|
|
261
|
-
const config:
|
|
262
|
-
name: '
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
###
|
|
351
|
+
### Service with Multiple Ports
|
|
281
352
|
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
+
## deployRuntime() - Unified Runtime Components
|
|
389
|
+
|
|
390
|
+
Deploy all runtime components (NATS, Ingress, Cert-Manager) with a single function:
|
|
391
|
+
|
|
314
392
|
```typescript
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
454
|
+
## Environment Generation
|
|
324
455
|
|
|
325
|
-
|
|
456
|
+
Generate `.env.local` for local development from Pulumi config:
|
|
326
457
|
|
|
327
|
-
|
|
458
|
+
```bash
|
|
459
|
+
# In your workspace root
|
|
460
|
+
bun dev # Automatically generates .env.local before starting services
|
|
461
|
+
```
|
|
328
462
|
|
|
329
|
-
|
|
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
|
-
|
|
471
|
+
NATS_URL=nats://localhost:4222
|
|
472
|
+
DATABASE_URL=postgresql://localhost:5432/mydb
|
|
473
|
+
API_KEY=your-secret-key
|
|
474
|
+
```
|
|
332
475
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
487
|
+
## API Reference
|
|
342
488
|
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
531
|
+
### Kubernetes Deployment Options
|
|
365
532
|
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
|
|
555
|
+
### Environment Helpers (`@crossdelta/infrastructure/env`)
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
import { getServicePort } from '@crossdelta/infrastructure/env'
|
|
373
559
|
|
|
374
|
-
//
|
|
375
|
-
const
|
|
560
|
+
// Read SERVICE_NAME_PORT from environment
|
|
561
|
+
const port = getServicePort('orders', 4001) // Fallback to 4001
|
|
376
562
|
```
|
|
377
563
|
|
|
378
|
-
|
|
564
|
+
## Cost Estimation (DOKS)
|
|
379
565
|
|
|
380
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
582
|
+
| Workload | Node Size | Monthly Cost |
|
|
583
|
+
|----------|-----------|--------------|
|
|
584
|
+
| Development | 1× s-2vcpu-4gb | $12 + $18 = $30/mo |
|
|
585
|
+
| Staging | 2× s-2vcpu-4gb | $12 + $36 = $48/mo |
|
|
586
|
+
| Production | 3× s-4vcpu-8gb | $12 + $144 = $156/mo |
|
|
587
|
+
| High Traffic | 5× 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
|
-
|
|
615
|
+
### 2. Follow the Interface Pattern
|
|
616
|
+
|
|
617
|
+
Each component should export a deployment function:
|
|
618
|
+
|
|
408
619
|
```typescript
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
###
|
|
637
|
+
### 3. Update deployRuntime()
|
|
418
638
|
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|