@crossdelta/infrastructure 0.2.11 → 0.2.12

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,15 +3,64 @@
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 App Platform with Pulumi.
6
+ Infrastructure-as-Code helpers for deploying microservices to DigitalOcean with Pulumi.
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.
10
+
11
+ ## Architecture Overview
12
+
13
+ ```
14
+ +-----------------------------------------------------------------------------+
15
+ | DigitalOcean |
16
+ | +-----------------------------------------------------------------------+ |
17
+ | | App Platform | |
18
+ | | +-------------+ +-------------+ +-------------+ | |
19
+ | | | frontend | | api | | worker | | |
20
+ | | | (public) | | (public) | | (internal) | | |
21
+ | | | :3000 | | :4000 | | :4001 | | |
22
+ | | +-------------+ +-------------+ +-------------+ | |
23
+ | | | | | | |
24
+ | +---------|----------------|----------------|---------------------------+ |
25
+ | | | | |
26
+ | +----------------+----------------+ |
27
+ | | VPC (private network) |
28
+ | v |
29
+ | +-----------------------------------------------------------------------+ |
30
+ | | Droplet | |
31
+ | | +---------------------------------------------+ | |
32
+ | | | NATS + JetStream | | |
33
+ | | | :4222 (NATS) | | |
34
+ | | | :8222 (HTTP monitoring) | | |
35
+ | | +---------------------------------------------+ | |
36
+ | | | | |
37
+ | | +---------+ | |
38
+ | | | Volume | (persistent storage) | |
39
+ | | | /data | | |
40
+ | | +---------+ | |
41
+ | +-----------------------------------------------------------------------+ |
42
+ +-----------------------------------------------------------------------------+
43
+ ```
44
+
45
+ ### Platform Strategy
46
+
47
+ | Platform | Use Case | Pros | Cons |
48
+ |----------|----------|------|------|
49
+ | **App Platform** | Stateless services (APIs, frontends) | Auto-scaling, zero-ops, built-in networking | No persistent volumes |
50
+ | **Droplet** | Stateful services (NATS, databases) | Persistent volumes, full control | Manual management |
51
+
52
+ **Why this split?** DigitalOcean App Platform doesn't support persistent volumes.
53
+ Services like NATS with JetStream need persistent storage for message durability.
7
54
 
8
55
  ## Features
9
56
 
57
+ - **Dual Platform Support** - Deploy to App Platform or Droplets from the same config
10
58
  - **Service Discovery** - Auto-discover service configs from `infra/services/*.ts`
11
59
  - **Type-safe Config** - Full TypeScript support with `ServiceConfig` type
12
60
  - **Smart Routing** - Automatic ingress rules for public services
13
- - **Internal Services** - Support for internal-only ports (no public exposure)
14
- - **Multi-Registry** - Docker Hub, GHCR, and DOCR image support
61
+ - **VPC Networking** - Secure private communication between platforms
62
+ - **Persistent Volumes** - Automatic volume creation and mounting for Droplets
63
+ - **Config File Mounting** - Share config files between local dev and production
15
64
  - **URL Generation** - Auto-generate service URLs and port env vars
16
65
 
17
66
  ## Installation
@@ -21,17 +70,44 @@ npm install @crossdelta/infrastructure
21
70
  ```
22
71
 
23
72
  ## Quick Start
24
- ### 1. Create a service config
73
+
74
+ ### 1. Create service configs
25
75
 
26
76
  ```typescript
27
- // infra/services/api-gateway.ts
77
+ // infra/services/api.ts - App Platform (stateless)
28
78
  import type { ServiceConfig } from '@crossdelta/infrastructure'
29
79
 
30
80
  const config: ServiceConfig = {
31
- name: 'api-gateway',
32
- instanceSizeSlug: 'apps-s-1vcpu-0.5gb',
81
+ name: 'api',
33
82
  httpPort: 4000,
34
83
  ingressPrefix: '/api',
84
+ instanceSizeSlug: 'apps-s-1vcpu-0.5gb',
85
+ }
86
+
87
+ export default config
88
+ ```
89
+
90
+ ```typescript
91
+ // infra/services/nats.ts - Droplet (stateful with persistence)
92
+ import { dockerHubImage, type ServiceConfig } from '@crossdelta/infrastructure'
93
+ import { readFileSync } from 'node:fs'
94
+
95
+ const config: ServiceConfig = {
96
+ name: 'nats',
97
+ platform: 'droplet',
98
+ internalPorts: [4222, 8222],
99
+ internalUrl: 'nats://nats:4222',
100
+ image: dockerHubImage('nats', '2.10-alpine'),
101
+ runCommand: '-c /etc/nats/nats.conf',
102
+ droplet: {
103
+ size: 's-1vcpu-1gb',
104
+ volumes: [{ name: 'nats-data', mountPath: '/data', sizeGb: 10 }],
105
+ vpcPorts: [4222, 8222],
106
+ configFile: {
107
+ containerPath: '/etc/nats/nats.conf',
108
+ content: readFileSync('path/to/nats.conf', 'utf-8'),
109
+ },
110
+ },
35
111
  }
36
112
 
37
113
  export default config
@@ -46,60 +122,215 @@ import {
46
122
  buildIngressRules,
47
123
  buildServiceUrlEnvs,
48
124
  buildServicePortEnvs,
125
+ buildDropletServices,
126
+ filterByPlatform,
49
127
  discoverServices,
50
128
  } from '@crossdelta/infrastructure'
51
- import { App } from '@pulumi/digitalocean'
129
+ import { App, Vpc } from '@pulumi/digitalocean'
52
130
 
131
+ const region = 'fra1'
53
132
  const serviceConfigs = discoverServices('services')
54
133
 
134
+ // Split services by platform
135
+ const { appPlatformServices, dropletServices } = filterByPlatform(serviceConfigs)
136
+
137
+ // Create VPC for private networking
138
+ const vpc = new Vpc('platform-vpc', {
139
+ name: 'platform-vpc',
140
+ region,
141
+ ipRange: '10.10.10.0/24',
142
+ })
143
+
144
+ // Deploy Droplet services (stateful)
145
+ const dropletResources = buildDropletServices({
146
+ serviceConfigs: dropletServices,
147
+ region,
148
+ vpcUuid: vpc.id,
149
+ projectName: 'my-platform',
150
+ })
151
+
152
+ // Get env vars for Droplet service URLs
153
+ const dropletServiceEnvs = dropletResources.flatMap(r => [
154
+ { key: `${r.serviceName.toUpperCase()}_URL`, value: r.internalUrl },
155
+ { key: `${r.serviceName.toUpperCase()}_HOST`, value: r.privateIp },
156
+ ])
157
+
158
+ // Deploy App Platform services (stateless)
55
159
  const app = new App('my-app', {
56
160
  spec: {
57
161
  name: 'my-platform',
58
- region: 'fra',
162
+ region: region.replace(/\d+$/, ''), // 'fra1' -> 'fra'
59
163
  envs: [
60
- ...buildServiceUrlEnvs(serviceConfigs),
61
- ...buildServicePortEnvs(serviceConfigs),
164
+ ...buildServiceUrlEnvs(appPlatformServices),
165
+ ...buildServicePortEnvs(appPlatformServices),
166
+ ...dropletServiceEnvs,
62
167
  ],
63
- services: buildServices({ serviceConfigs }),
64
- ingress: { rules: buildIngressRules(serviceConfigs) },
168
+ services: buildServices({ serviceConfigs: appPlatformServices }),
169
+ ingress: { rules: buildIngressRules(appPlatformServices) },
65
170
  },
66
171
  })
67
172
  ```
68
173
 
174
+ ## ServiceConfig API
175
+
176
+ The `ServiceConfig` type is the central configuration for all services:
177
+
178
+ ```typescript
179
+ type ServiceConfig = {
180
+ /** Unique name of the service (required) */
181
+ name: string
182
+
183
+ /** Deployment platform: 'app-platform' (default) or 'droplet' */
184
+ platform?: 'app-platform' | 'droplet'
185
+
186
+ /** Public HTTP port with ingress routing (requires ingressPrefix) */
187
+ httpPort?: number
188
+
189
+ /** Internal-only ports, not publicly accessible */
190
+ internalPorts?: number[]
191
+
192
+ /** Ingress path prefix for public routing (e.g., '/api') */
193
+ ingressPrefix?: string
194
+
195
+ /** Override internal URL (e.g., 'nats://nats:4222' for non-HTTP) */
196
+ internalUrl?: string
197
+
198
+ /** Exclude from deployment */
199
+ skip?: boolean
200
+
201
+ /** Custom image configuration */
202
+ image?: {
203
+ registryType: 'DOCKER_HUB' | 'GHCR' | 'DOCR'
204
+ registry: string // e.g., 'library', 'myorg'
205
+ repository: string // e.g., 'nats', 'platform/api'
206
+ tag: string // e.g., '2.10-alpine', 'latest'
207
+ }
208
+
209
+ /** Command to run in the container */
210
+ runCommand?: string
211
+
212
+ /** App Platform: instance size (e.g., 'apps-s-1vcpu-0.5gb') */
213
+ instanceSizeSlug?: string
214
+
215
+ /** Droplet-specific configuration */
216
+ droplet?: {
217
+ /** Droplet size (e.g., 's-1vcpu-1gb') */
218
+ size: string
219
+
220
+ /** Persistent volumes */
221
+ volumes?: Array<{
222
+ name: string // Volume name (unique)
223
+ mountPath: string // Path in container (e.g., '/data')
224
+ sizeGb: number // Size in GB
225
+ }>
226
+
227
+ /** Ports to open in VPC firewall */
228
+ vpcPorts?: number[]
229
+
230
+ /** Additional Docker run arguments */
231
+ dockerArgs?: string
232
+
233
+ /** Config file to mount into container */
234
+ configFile?: {
235
+ containerPath: string // Path in container
236
+ content: string // File content
237
+ }
238
+ }
239
+ }
240
+ ```
241
+
69
242
  ## Service Configuration Examples
70
243
 
71
- ### Public Service
244
+ ### Public Service (App Platform)
72
245
 
73
246
  ```typescript
74
247
  const config: ServiceConfig = {
75
- name: 'storefront',
248
+ name: 'frontend',
76
249
  httpPort: 3000,
77
250
  ingressPrefix: '/',
78
251
  instanceSizeSlug: 'apps-s-1vcpu-1gb',
79
252
  }
80
253
  ```
81
254
 
82
- ### Internal Service
255
+ ### Internal Service (App Platform)
83
256
 
84
257
  ```typescript
85
258
  const config: ServiceConfig = {
86
- name: 'orders',
259
+ name: 'worker',
87
260
  internalPorts: [4001],
88
261
  instanceSizeSlug: 'apps-s-1vcpu-0.5gb',
89
262
  }
90
263
  ```
91
264
 
92
- ### Internal Service with Custom URL (e.g., NATS)
265
+ ### Stateful Service with Persistence (Droplet)
93
266
 
94
267
  ```typescript
268
+ import { dockerHubImage, type ServiceConfig } from '@crossdelta/infrastructure'
269
+
95
270
  const config: ServiceConfig = {
96
271
  name: 'nats',
272
+ platform: 'droplet',
97
273
  internalPorts: [4222, 8222],
98
274
  internalUrl: 'nats://nats:4222',
99
- instanceSizeSlug: 'apps-s-1vcpu-1gb',
275
+ image: dockerHubImage('nats', '2.10-alpine'),
276
+ runCommand: '-c /etc/nats/nats.conf',
277
+ droplet: {
278
+ size: 's-1vcpu-1gb',
279
+ volumes: [{ name: 'nats-data', mountPath: '/data', sizeGb: 10 }],
280
+ vpcPorts: [4222, 8222],
281
+ configFile: {
282
+ containerPath: '/etc/nats/nats.conf',
283
+ content: readFileSync('services/nats/nats.conf', 'utf-8'),
284
+ },
285
+ },
286
+ }
287
+ ```
288
+
289
+ ### Config File Sharing (Local + Production)
290
+
291
+ One of the key features is sharing configuration between local development and production:
292
+
293
+ ```
294
+ services/nats/
295
+ ├── nats.conf # Shared config (used by both)
296
+ └── scripts/
297
+ └── start-dev.sh # Local dev script
298
+ ```
299
+
300
+ **nats.conf** (shared):
301
+ ```conf
302
+ server_name: my-nats-server
303
+ listen: 0.0.0.0:4222
304
+ http: 0.0.0.0:8222
305
+
306
+ jetstream {
307
+ store_dir: /data
308
+ max_mem: 1G
309
+ max_file: 10G
100
310
  }
101
311
  ```
102
312
 
313
+ **start-dev.sh** (local):
314
+ ```bash
315
+ docker run -d --name nats \
316
+ -v "$(pwd)/nats.conf:/etc/nats/nats.conf:ro" \
317
+ -v "$(pwd)/.nats-data:/data" \
318
+ -p 4222:4222 -p 8222:8222 \
319
+ nats:2.10-alpine -c /etc/nats/nats.conf
320
+ ```
321
+
322
+ **infra/services/nats.ts** (production):
323
+ ```typescript
324
+ droplet: {
325
+ configFile: {
326
+ containerPath: '/etc/nats/nats.conf',
327
+ content: readFileSync('services/nats/nats.conf', 'utf-8'),
328
+ },
329
+ }
330
+ ```
331
+
332
+ → Same config file, same behavior, different environments.
333
+
103
334
  ## CLI: generate-env
104
335
 
105
336
  Generate `.env.local` and service-specific `env.ts` files for local development.
@@ -116,8 +347,6 @@ Add a script to your workspace `package.json`:
116
347
  }
117
348
  ```
118
349
 
119
- Or use `pf dev` which runs `generate-env` automatically before starting services.
120
-
121
350
  ### What it does
122
351
 
123
352
  1. Loads secrets from Pulumi config (dev stack)
@@ -132,21 +361,13 @@ Or use `pf dev` which runs `generate-env` automatically before starting services
132
361
  `.env.local`:
133
362
  ```bash
134
363
  # Service URLs (localhost for local development)
135
- API_GATEWAY_URL=http://localhost:4000
136
- STOREFRONT_URL=http://localhost:3000
364
+ API_URL=http://localhost:4000
365
+ FRONTEND_URL=http://localhost:3000
366
+ NATS_URL=nats://localhost:4222
137
367
 
138
368
  # Service Ports
139
- API_GATEWAY_PORT=4000
140
- STOREFRONT_PORT=3000
141
- ```
142
-
143
- `services/orders/src/env.ts`:
144
- ```typescript
145
- import { getServicePort, getServiceUrl } from '@crossdelta/infrastructure/env'
146
-
147
- export const SERVICE_NAME = 'orders' as const
148
- export const getServicePort = (defaultPort = 8080) => _getServicePort(SERVICE_NAME, defaultPort)
149
- export const getServiceUrl = () => _getServiceUrl(SERVICE_NAME)
369
+ API_PORT=4000
370
+ FRONTEND_PORT=3000
150
371
  ```
151
372
 
152
373
  ## Runtime Helpers
@@ -156,27 +377,51 @@ Use `@crossdelta/infrastructure/env` in your services to read environment config
156
377
  ```typescript
157
378
  import { getServicePort, getServiceUrl } from '@crossdelta/infrastructure/env'
158
379
 
159
- // Get port from ORDERS_PORT env var (fallback: 8080)
160
- const port = getServicePort('orders', 8080)
380
+ // Get port from WORKER_PORT env var (fallback: 8080)
381
+ const port = getServicePort('worker', 8080)
161
382
 
162
- // Get URL from ORDERS_URL env var
163
- const url = getServiceUrl('orders')
383
+ // Get URL from WORKER_URL env var
384
+ const url = getServiceUrl('worker')
164
385
  ```
165
386
 
166
387
  These helpers work both locally (via `.env.local`) and in production (via DO App Platform env injection).
167
388
 
168
- ## API
389
+ ## API Reference
169
390
 
170
391
  ### Main Export (`@crossdelta/infrastructure`)
171
392
 
172
393
  | Function | Description |
173
394
  |----------|-------------|
174
- | `discoverServices(dir)` | Auto-discover service configs (relative to cwd or absolute) |
395
+ | `discoverServices(dir)` | Auto-discover service configs from directory |
175
396
  | `buildServices(options)` | Build App Platform service specs |
176
- | `buildIngressRules(configs)` | Generate ingress rules |
397
+ | `buildIngressRules(configs)` | Generate ingress rules for public services |
177
398
  | `buildServiceUrlEnvs(configs)` | Create SERVICE_NAME_URL env vars |
178
399
  | `buildServicePortEnvs(configs)` | Create SERVICE_NAME_PORT env vars |
179
400
  | `buildLocalUrls(configs)` | Generate localhost URLs for local dev |
401
+ | `filterByPlatform(configs)` | Split configs into App Platform and Droplet services |
402
+ | `buildDropletServices(options)` | Create Droplet resources with volumes and firewall |
403
+ | `dockerHubImage(repo, tag, registry?)` | Create Docker Hub image config (defaults to `library`) |
404
+
405
+ ### Droplet Builder Options
406
+
407
+ ```typescript
408
+ buildDropletServices({
409
+ serviceConfigs: ServiceConfig[] // Services with platform: 'droplet'
410
+ region: string // DO region (e.g., 'fra1')
411
+ vpcUuid: pulumi.Output<string> // VPC ID for private networking
412
+ projectName: string // Prefix for resource names
413
+ })
414
+ ```
415
+
416
+ Returns an array of:
417
+ ```typescript
418
+ {
419
+ serviceName: string // Service name
420
+ droplet: digitalocean.Droplet // Created Droplet resource
421
+ privateIp: pulumi.Output<string> // Private IP in VPC
422
+ internalUrl: string // URL for service discovery
423
+ }
424
+ ```
180
425
 
181
426
  ### Env Export (`@crossdelta/infrastructure/env`)
182
427
 
@@ -185,6 +430,23 @@ These helpers work both locally (via `.env.local`) and in production (via DO App
185
430
  | `getServicePort(name, default)` | Read SERVICE_NAME_PORT from env |
186
431
  | `getServiceUrl(name)` | Read SERVICE_NAME_URL from env |
187
432
 
433
+ ## MVP Limitations & Future Improvements
434
+
435
+ This architecture is designed for MVP/Beta deployments:
436
+
437
+ | Current (MVP) | Future (Production) |
438
+ |---------------|---------------------|
439
+ | Single NATS Droplet | NATS Cluster (3 nodes) or Synadia Cloud |
440
+ | No auto-healing | Kubernetes or managed service |
441
+ | Manual scaling | Auto-scaling policies |
442
+ | Basic monitoring | Prometheus/Grafana stack |
443
+
444
+ **Cost estimate (MVP)**:
445
+ - App Platform: ~$12-24/month (depending on instance sizes)
446
+ - NATS Droplet: ~$6/month (s-1vcpu-1gb)
447
+ - Volume: ~$1/month (10GB)
448
+ - **Total**: ~$19-31/month
449
+
188
450
  ## License
189
451
 
190
452
  MIT
@@ -0,0 +1,38 @@
1
+ import * as digitalocean from '@pulumi/digitalocean';
2
+ import * as pulumi from '@pulumi/pulumi';
3
+ import type { ServiceConfig } from '../types';
4
+ export interface DropletServiceResources {
5
+ droplet: digitalocean.Droplet;
6
+ volume?: digitalocean.Volume;
7
+ volumeAttachment?: digitalocean.VolumeAttachment;
8
+ firewall?: digitalocean.Firewall;
9
+ /** Private IP for VPC communication */
10
+ privateIp: pulumi.Output<string>;
11
+ }
12
+ export interface BuildDropletServicesOptions {
13
+ /** Service configs with platform: 'droplet' */
14
+ serviceConfigs: ServiceConfig[];
15
+ /** DigitalOcean region (e.g., 'fra1') */
16
+ region: string;
17
+ /** VPC UUID for private networking */
18
+ vpcUuid: pulumi.Input<string>;
19
+ /** SSH key IDs for Droplet access (optional) */
20
+ sshKeyIds?: pulumi.Input<pulumi.Input<string>[]>;
21
+ /** Registry credentials for private images (optional) */
22
+ registryCredentials?: pulumi.Output<string>;
23
+ }
24
+ /**
25
+ * Builds Droplet resources for services with platform: 'droplet'.
26
+ *
27
+ * Creates:
28
+ * - Droplet with Docker and cloud-init script
29
+ * - Volume(s) for persistent storage (if configured)
30
+ * - Firewall rules for VPC access
31
+ *
32
+ * @returns Map of service name to created resources
33
+ */
34
+ export declare function buildDropletServices(options: BuildDropletServicesOptions): Map<string, DropletServiceResources>;
35
+ /**
36
+ * Filters service configs by platform type.
37
+ */
38
+ export declare function filterByPlatform(configs: ServiceConfig[], platform: 'app-platform' | 'droplet'): ServiceConfig[];
@@ -1,6 +1,7 @@
1
1
  export * from './config';
2
2
  export * from './discover-services';
3
3
  export * from './docker-hub-image';
4
+ export * from './droplet-builder';
4
5
  export * from './image';
5
6
  export * from './service-builder';
6
7
  export * from './service-runtime';