@onivoro/server-aws-ecs 22.11.0 → 24.0.0

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.
Files changed (38) hide show
  1. package/README.md +684 -0
  2. package/jest.config.ts +11 -0
  3. package/package.json +10 -45
  4. package/project.json +23 -0
  5. package/{dist/cjs/index.d.ts → src/index.ts} +2 -0
  6. package/src/lib/classes/server-aws-ecs-config.class.ts +4 -0
  7. package/src/lib/functions/parse-csv-string.function.ts +3 -0
  8. package/src/lib/server-aws-ecs.module.ts +33 -0
  9. package/src/lib/services/__snapshots__/ecs.service.spec.ts.snap +21 -0
  10. package/src/lib/services/ecs.service.spec.ts +13 -0
  11. package/src/lib/services/ecs.service.ts +41 -0
  12. package/tsconfig.json +16 -0
  13. package/tsconfig.lib.json +8 -0
  14. package/tsconfig.spec.json +21 -0
  15. package/dist/cjs/index.js +0 -19
  16. package/dist/cjs/lib/classes/server-aws-ecs-config.class.d.ts +0 -6
  17. package/dist/cjs/lib/classes/server-aws-ecs-config.class.js +0 -10
  18. package/dist/cjs/lib/functions/parse-csv-string.function.d.ts +0 -1
  19. package/dist/cjs/lib/functions/parse-csv-string.function.js +0 -6
  20. package/dist/cjs/lib/server-aws-ecs.module.d.ts +0 -4
  21. package/dist/cjs/lib/server-aws-ecs.module.js +0 -46
  22. package/dist/cjs/lib/services/ecs.service.d.ts +0 -12
  23. package/dist/cjs/lib/services/ecs.service.js +0 -52
  24. package/dist/esm/index.d.ts +0 -3
  25. package/dist/esm/index.js +0 -19
  26. package/dist/esm/lib/classes/server-aws-ecs-config.class.d.ts +0 -6
  27. package/dist/esm/lib/classes/server-aws-ecs-config.class.js +0 -10
  28. package/dist/esm/lib/functions/parse-csv-string.function.d.ts +0 -1
  29. package/dist/esm/lib/functions/parse-csv-string.function.js +0 -6
  30. package/dist/esm/lib/server-aws-ecs.module.d.ts +0 -4
  31. package/dist/esm/lib/server-aws-ecs.module.js +0 -46
  32. package/dist/esm/lib/services/ecs.service.d.ts +0 -12
  33. package/dist/esm/lib/services/ecs.service.js +0 -52
  34. package/dist/types/index.d.ts +0 -3
  35. package/dist/types/lib/classes/server-aws-ecs-config.class.d.ts +0 -6
  36. package/dist/types/lib/functions/parse-csv-string.function.d.ts +0 -1
  37. package/dist/types/lib/server-aws-ecs.module.d.ts +0 -4
  38. package/dist/types/lib/services/ecs.service.d.ts +0 -12
package/README.md ADDED
@@ -0,0 +1,684 @@
1
+ # @onivoro/server-aws-ecs
2
+
3
+ A NestJS module for integrating with AWS ECS (Elastic Container Service), providing task execution, container management, and deployment orchestration capabilities for your server applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @onivoro/server-aws-ecs
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Task Execution**: Run ECS tasks with Fargate launch type
14
+ - **Network Configuration**: Configure VPC, subnets, and security groups
15
+ - **Environment Variable Mapping**: Convert objects to ECS environment variable format
16
+ - **Batch Task Running**: Execute multiple tasks concurrently
17
+ - **Task Overrides**: Dynamic container and task definition overrides
18
+ - **CSV String Parsing**: Utility for parsing comma-separated configuration values
19
+ - **Error Handling**: Comprehensive error handling for ECS operations
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Module Configuration
24
+
25
+ ```typescript
26
+ import { ServerAwsEcsModule } from '@onivoro/server-aws-ecs';
27
+
28
+ @Module({
29
+ imports: [
30
+ ServerAwsEcsModule.configure({
31
+ AWS_REGION: 'us-east-1',
32
+ CLUSTER_NAME: process.env.ECS_CLUSTER_NAME,
33
+ TASK_DEFINITION: process.env.ECS_TASK_DEFINITION,
34
+ SUBNETS: process.env.ECS_SUBNETS,
35
+ SECURITY_GROUPS: process.env.ECS_SECURITY_GROUPS,
36
+ AWS_PROFILE: process.env.AWS_PROFILE || 'default',
37
+ }),
38
+ ],
39
+ })
40
+ export class AppModule {}
41
+ ```
42
+
43
+ ### 2. Basic Usage
44
+
45
+ ```typescript
46
+ import { EcsService } from '@onivoro/server-aws-ecs';
47
+
48
+ @Injectable()
49
+ export class TaskRunnerService {
50
+ constructor(private ecsService: EcsService) {}
51
+
52
+ async runDataProcessingTask(jobData: any) {
53
+ const taskParams = {
54
+ taskDefinition: 'data-processor:1',
55
+ cluster: 'my-cluster',
56
+ subnets: 'subnet-12345,subnet-67890',
57
+ securityGroups: 'sg-12345',
58
+ taskCount: 1,
59
+ overrides: {
60
+ containerOverrides: [{
61
+ name: 'data-processor',
62
+ environment: EcsService.mapObjectToEcsEnvironmentArray({
63
+ JOB_ID: jobData.id,
64
+ JOB_TYPE: jobData.type,
65
+ DATA_SOURCE: jobData.source
66
+ })
67
+ }]
68
+ }
69
+ };
70
+
71
+ return this.ecsService.runTasks(taskParams);
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ ### ServerAwsEcsConfig
79
+
80
+ ```typescript
81
+ import { ServerAwsEcsConfig } from '@onivoro/server-aws-ecs';
82
+
83
+ export class AppEcsConfig extends ServerAwsEcsConfig {
84
+ AWS_REGION = process.env.AWS_REGION || 'us-east-1';
85
+ CLUSTER_NAME = process.env.ECS_CLUSTER_NAME || 'default-cluster';
86
+ TASK_DEFINITION = process.env.ECS_TASK_DEFINITION || 'my-task-definition';
87
+ SUBNETS = process.env.ECS_SUBNETS || 'subnet-12345,subnet-67890';
88
+ SECURITY_GROUPS = process.env.ECS_SECURITY_GROUPS || 'sg-12345';
89
+ AWS_PROFILE = process.env.AWS_PROFILE || 'default';
90
+ ASSIGN_PUBLIC_IP = process.env.ECS_ASSIGN_PUBLIC_IP === 'true' ? 'ENABLED' : 'DISABLED';
91
+ }
92
+ ```
93
+
94
+ ### Environment Variables
95
+
96
+ ```bash
97
+ # AWS Configuration
98
+ AWS_REGION=us-east-1
99
+ AWS_PROFILE=default
100
+
101
+ # ECS Configuration
102
+ ECS_CLUSTER_NAME=my-application-cluster
103
+ ECS_TASK_DEFINITION=my-task-definition:1
104
+ ECS_SUBNETS=subnet-12345,subnet-67890,subnet-abcde
105
+ ECS_SECURITY_GROUPS=sg-12345,sg-67890
106
+ ECS_ASSIGN_PUBLIC_IP=false
107
+ ```
108
+
109
+ ## Services
110
+
111
+ ### EcsService
112
+
113
+ The main service for ECS operations:
114
+
115
+ ```typescript
116
+ import { EcsService } from '@onivoro/server-aws-ecs';
117
+
118
+ @Injectable()
119
+ export class BatchProcessingService {
120
+ constructor(private ecsService: EcsService) {}
121
+
122
+ async runBatchJob(jobConfig: BatchJobConfig) {
123
+ const environmentVars = EcsService.mapObjectToEcsEnvironmentArray({
124
+ BATCH_ID: jobConfig.batchId,
125
+ INPUT_BUCKET: jobConfig.inputBucket,
126
+ OUTPUT_BUCKET: jobConfig.outputBucket,
127
+ PROCESSING_MODE: jobConfig.mode,
128
+ WORKER_COUNT: jobConfig.workerCount.toString()
129
+ });
130
+
131
+ return this.ecsService.runTasks({
132
+ taskDefinition: jobConfig.taskDefinition,
133
+ cluster: jobConfig.cluster,
134
+ subnets: jobConfig.subnets,
135
+ securityGroups: jobConfig.securityGroups,
136
+ taskCount: jobConfig.taskCount,
137
+ overrides: {
138
+ containerOverrides: [{
139
+ name: jobConfig.containerName,
140
+ environment: environmentVars,
141
+ memory: jobConfig.memoryReservation,
142
+ cpu: jobConfig.cpuReservation
143
+ }]
144
+ }
145
+ });
146
+ }
147
+ }
148
+ ```
149
+
150
+ ## Usage Examples
151
+
152
+ ### Data Processing Pipeline
153
+
154
+ ```typescript
155
+ import { EcsService } from '@onivoro/server-aws-ecs';
156
+
157
+ @Injectable()
158
+ export class DataPipelineService {
159
+ constructor(private ecsService: EcsService) {}
160
+
161
+ async processDataset(dataset: DatasetConfig) {
162
+ // Step 1: Data validation task
163
+ const validationTasks = await this.runValidationTasks(dataset);
164
+ console.log(`Started ${validationTasks.length} validation tasks`);
165
+
166
+ // Step 2: Data transformation tasks (run in parallel)
167
+ const transformationTasks = await this.runTransformationTasks(dataset);
168
+ console.log(`Started ${transformationTasks.length} transformation tasks`);
169
+
170
+ // Step 3: Data aggregation task
171
+ const aggregationTasks = await this.runAggregationTasks(dataset);
172
+ console.log(`Started ${aggregationTasks.length} aggregation tasks`);
173
+
174
+ return {
175
+ validationTasks: validationTasks.length,
176
+ transformationTasks: transformationTasks.length,
177
+ aggregationTasks: aggregationTasks.length
178
+ };
179
+ }
180
+
181
+ private async runValidationTasks(dataset: DatasetConfig) {
182
+ return this.ecsService.runTasks({
183
+ taskDefinition: 'data-validator:1',
184
+ cluster: 'data-processing-cluster',
185
+ subnets: process.env.ECS_SUBNETS!,
186
+ securityGroups: process.env.ECS_SECURITY_GROUPS!,
187
+ taskCount: 1,
188
+ overrides: {
189
+ containerOverrides: [{
190
+ name: 'validator',
191
+ environment: EcsService.mapObjectToEcsEnvironmentArray({
192
+ DATASET_ID: dataset.id,
193
+ VALIDATION_RULES: JSON.stringify(dataset.validationRules),
194
+ INPUT_LOCATION: dataset.inputLocation,
195
+ VALIDATION_OUTPUT: dataset.validationOutput
196
+ })
197
+ }]
198
+ }
199
+ });
200
+ }
201
+
202
+ private async runTransformationTasks(dataset: DatasetConfig) {
203
+ const taskCount = Math.ceil(dataset.size / dataset.chunkSize);
204
+
205
+ return this.ecsService.runTasks({
206
+ taskDefinition: 'data-transformer:1',
207
+ cluster: 'data-processing-cluster',
208
+ subnets: process.env.ECS_SUBNETS!,
209
+ securityGroups: process.env.ECS_SECURITY_GROUPS!,
210
+ taskCount,
211
+ overrides: {
212
+ containerOverrides: [{
213
+ name: 'transformer',
214
+ environment: EcsService.mapObjectToEcsEnvironmentArray({
215
+ DATASET_ID: dataset.id,
216
+ CHUNK_SIZE: dataset.chunkSize.toString(),
217
+ TRANSFORMATION_CONFIG: JSON.stringify(dataset.transformationConfig),
218
+ INPUT_LOCATION: dataset.inputLocation,
219
+ OUTPUT_LOCATION: dataset.outputLocation
220
+ }),
221
+ memory: 2048,
222
+ cpu: 1024
223
+ }]
224
+ }
225
+ });
226
+ }
227
+
228
+ private async runAggregationTasks(dataset: DatasetConfig) {
229
+ return this.ecsService.runTasks({
230
+ taskDefinition: 'data-aggregator:1',
231
+ cluster: 'data-processing-cluster',
232
+ subnets: process.env.ECS_SUBNETS!,
233
+ securityGroups: process.env.ECS_SECURITY_GROUPS!,
234
+ taskCount: 1,
235
+ overrides: {
236
+ containerOverrides: [{
237
+ name: 'aggregator',
238
+ environment: EcsService.mapObjectToEcsEnvironmentArray({
239
+ DATASET_ID: dataset.id,
240
+ AGGREGATION_RULES: JSON.stringify(dataset.aggregationRules),
241
+ INPUT_LOCATION: dataset.outputLocation,
242
+ FINAL_OUTPUT: dataset.finalOutput
243
+ }),
244
+ memory: 4096,
245
+ cpu: 2048
246
+ }]
247
+ }
248
+ });
249
+ }
250
+ }
251
+ ```
252
+
253
+ ### Scheduled Task Runner
254
+
255
+ ```typescript
256
+ import { EcsService } from '@onivoro/server-aws-ecs';
257
+ import { Cron, CronExpression } from '@nestjs/schedule';
258
+
259
+ @Injectable()
260
+ export class ScheduledTaskService {
261
+ constructor(private ecsService: EcsService) {}
262
+
263
+ @Cron(CronExpression.EVERY_HOUR)
264
+ async runHourlyReports() {
265
+ console.log('Starting hourly report generation...');
266
+
267
+ const reportTasks = await this.ecsService.runTasks({
268
+ taskDefinition: 'report-generator:1',
269
+ cluster: 'reporting-cluster',
270
+ subnets: process.env.ECS_SUBNETS!,
271
+ securityGroups: process.env.ECS_SECURITY_GROUPS!,
272
+ taskCount: 1,
273
+ overrides: {
274
+ containerOverrides: [{
275
+ name: 'report-generator',
276
+ environment: EcsService.mapObjectToEcsEnvironmentArray({
277
+ REPORT_TYPE: 'HOURLY',
278
+ REPORT_TIME: new Date().toISOString(),
279
+ OUTPUT_BUCKET: 'my-reports-bucket',
280
+ NOTIFICATION_TOPIC: 'arn:aws:sns:us-east-1:123456789012:reports'
281
+ })
282
+ }]
283
+ }
284
+ });
285
+
286
+ console.log(`Started ${reportTasks.length} report generation tasks`);
287
+ }
288
+
289
+ @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
290
+ async runDailyMaintenance() {
291
+ console.log('Starting daily maintenance tasks...');
292
+
293
+ // Run multiple maintenance tasks in parallel
294
+ const maintenanceTasks = [
295
+ this.runDataCleanupTask(),
296
+ this.runBackupTask(),
297
+ this.runHealthCheckTask()
298
+ ];
299
+
300
+ const results = await Promise.allSettled(maintenanceTasks);
301
+
302
+ results.forEach((result, index) => {
303
+ if (result.status === 'fulfilled') {
304
+ console.log(`Maintenance task ${index + 1} completed successfully`);
305
+ } else {
306
+ console.error(`Maintenance task ${index + 1} failed:`, result.reason);
307
+ }
308
+ });
309
+ }
310
+
311
+ private async runDataCleanupTask() {
312
+ return this.ecsService.runTasks({
313
+ taskDefinition: 'data-cleanup:1',
314
+ cluster: 'maintenance-cluster',
315
+ subnets: process.env.ECS_SUBNETS!,
316
+ securityGroups: process.env.ECS_SECURITY_GROUPS!,
317
+ taskCount: 1,
318
+ overrides: {
319
+ containerOverrides: [{
320
+ name: 'cleanup',
321
+ environment: EcsService.mapObjectToEcsEnvironmentArray({
322
+ CLEANUP_MODE: 'DAILY',
323
+ RETENTION_DAYS: '30',
324
+ TARGET_TABLES: 'logs,temp_data,session_data'
325
+ })
326
+ }]
327
+ }
328
+ });
329
+ }
330
+
331
+ private async runBackupTask() {
332
+ return this.ecsService.runTasks({
333
+ taskDefinition: 'backup-service:1',
334
+ cluster: 'maintenance-cluster',
335
+ subnets: process.env.ECS_SUBNETS!,
336
+ securityGroups: process.env.ECS_SECURITY_GROUPS!,
337
+ taskCount: 1,
338
+ overrides: {
339
+ containerOverrides: [{
340
+ name: 'backup',
341
+ environment: EcsService.mapObjectToEcsEnvironmentArray({
342
+ BACKUP_TYPE: 'DAILY',
343
+ BACKUP_TARGET: 's3://my-backup-bucket',
344
+ DATABASE_URL: process.env.DATABASE_URL!
345
+ })
346
+ }]
347
+ }
348
+ });
349
+ }
350
+
351
+ private async runHealthCheckTask() {
352
+ return this.ecsService.runTasks({
353
+ taskDefinition: 'health-checker:1',
354
+ cluster: 'maintenance-cluster',
355
+ subnets: process.env.ECS_SUBNETS!,
356
+ securityGroups: process.env.ECS_SECURITY_GROUPS!,
357
+ taskCount: 1,
358
+ overrides: {
359
+ containerOverrides: [{
360
+ name: 'health-checker',
361
+ environment: EcsService.mapObjectToEcsEnvironmentArray({
362
+ CHECK_TYPE: 'COMPREHENSIVE',
363
+ NOTIFICATION_WEBHOOK: process.env.HEALTH_WEBHOOK_URL!,
364
+ SERVICES_TO_CHECK: 'api,database,cache,storage'
365
+ })
366
+ }]
367
+ }
368
+ });
369
+ }
370
+ }
371
+ ```
372
+
373
+ ### Dynamic Task Configuration
374
+
375
+ ```typescript
376
+ import { EcsService } from '@onivoro/server-aws-ecs';
377
+
378
+ @Injectable()
379
+ export class DynamicTaskService {
380
+ constructor(private ecsService: EcsService) {}
381
+
382
+ async runCustomTask(taskConfig: CustomTaskConfig) {
383
+ // Validate task configuration
384
+ this.validateTaskConfig(taskConfig);
385
+
386
+ // Build environment variables dynamically
387
+ const environment = EcsService.mapObjectToEcsEnvironmentArray({
388
+ ...taskConfig.environmentVariables,
389
+ TASK_ID: this.generateTaskId(),
390
+ STARTED_AT: new Date().toISOString(),
391
+ CONFIGURATION: JSON.stringify(taskConfig.configuration)
392
+ });
393
+
394
+ // Configure resource requirements based on task type
395
+ const resourceConfig = this.getResourceConfiguration(taskConfig.taskType);
396
+
397
+ return this.ecsService.runTasks({
398
+ taskDefinition: taskConfig.taskDefinition,
399
+ cluster: taskConfig.cluster || 'default-cluster',
400
+ subnets: taskConfig.subnets || process.env.ECS_SUBNETS!,
401
+ securityGroups: taskConfig.securityGroups || process.env.ECS_SECURITY_GROUPS!,
402
+ taskCount: taskConfig.taskCount || 1,
403
+ overrides: {
404
+ containerOverrides: [{
405
+ name: taskConfig.containerName,
406
+ environment,
407
+ memory: resourceConfig.memory,
408
+ cpu: resourceConfig.cpu,
409
+ ...(taskConfig.command && { command: taskConfig.command })
410
+ }],
411
+ ...(taskConfig.taskRoleArn && {
412
+ taskRoleArn: taskConfig.taskRoleArn
413
+ })
414
+ }
415
+ });
416
+ }
417
+
418
+ private validateTaskConfig(config: CustomTaskConfig) {
419
+ if (!config.taskDefinition) {
420
+ throw new Error('Task definition is required');
421
+ }
422
+ if (!config.containerName) {
423
+ throw new Error('Container name is required');
424
+ }
425
+ if (config.taskCount && (config.taskCount < 1 || config.taskCount > 100)) {
426
+ throw new Error('Task count must be between 1 and 100');
427
+ }
428
+ }
429
+
430
+ private generateTaskId(): string {
431
+ return `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
432
+ }
433
+
434
+ private getResourceConfiguration(taskType: string) {
435
+ const configurations = {
436
+ 'cpu-intensive': { memory: 4096, cpu: 2048 },
437
+ 'memory-intensive': { memory: 8192, cpu: 1024 },
438
+ 'balanced': { memory: 2048, cpu: 1024 },
439
+ 'lightweight': { memory: 512, cpu: 256 }
440
+ };
441
+
442
+ return configurations[taskType] || configurations['balanced'];
443
+ }
444
+ }
445
+ ```
446
+
447
+ ## Advanced Usage
448
+
449
+ ### Task Monitoring and Logging
450
+
451
+ ```typescript
452
+ import { ECS, DescribeTasksCommand } from '@aws-sdk/client-ecs';
453
+
454
+ @Injectable()
455
+ export class EcsTaskMonitoringService {
456
+ constructor(
457
+ private ecsService: EcsService,
458
+ private ecsClient: ECS
459
+ ) {}
460
+
461
+ async runTaskWithMonitoring(taskParams: any, monitoringOptions?: TaskMonitoringOptions) {
462
+ // Start the task
463
+ const taskResults = await this.ecsService.runTasks(taskParams);
464
+
465
+ if (!taskResults.length || !taskResults[0].tasks?.length) {
466
+ throw new Error('Failed to start tasks');
467
+ }
468
+
469
+ const taskArns = taskResults[0].tasks.map(task => task.taskArn!);
470
+
471
+ // Monitor tasks if monitoring is enabled
472
+ if (monitoringOptions?.monitor) {
473
+ this.monitorTasks(taskArns, taskParams.cluster, monitoringOptions);
474
+ }
475
+
476
+ return taskResults;
477
+ }
478
+
479
+ private async monitorTasks(taskArns: string[], cluster: string, options: TaskMonitoringOptions) {
480
+ const checkInterval = options.checkInterval || 30000; // 30 seconds
481
+ const timeout = options.timeout || 3600000; // 1 hour
482
+ const startTime = Date.now();
483
+
484
+ const monitor = setInterval(async () => {
485
+ try {
486
+ const response = await this.ecsClient.send(new DescribeTasksCommand({
487
+ cluster,
488
+ tasks: taskArns
489
+ }));
490
+
491
+ const tasks = response.tasks || [];
492
+ const runningTasks = tasks.filter(task => task.lastStatus === 'RUNNING');
493
+ const stoppedTasks = tasks.filter(task => task.lastStatus === 'STOPPED');
494
+
495
+ console.log(`Task Status - Running: ${runningTasks.length}, Stopped: ${stoppedTasks.length}`);
496
+
497
+ // Check for failed tasks
498
+ const failedTasks = stoppedTasks.filter(task => task.stopCode !== 'EssentialContainerExited' || task.containers?.some(c => c.exitCode !== 0));
499
+
500
+ if (failedTasks.length > 0) {
501
+ console.error(`${failedTasks.length} tasks failed:`);
502
+ failedTasks.forEach(task => {
503
+ console.error(`Task ${task.taskArn} failed: ${task.stoppedReason}`);
504
+ });
505
+ }
506
+
507
+ // Stop monitoring if all tasks are complete
508
+ if (stoppedTasks.length === taskArns.length) {
509
+ clearInterval(monitor);
510
+ console.log('All tasks completed');
511
+
512
+ if (options.onComplete) {
513
+ options.onComplete(tasks);
514
+ }
515
+ }
516
+
517
+ // Check timeout
518
+ if (Date.now() - startTime > timeout) {
519
+ clearInterval(monitor);
520
+ console.warn('Task monitoring timeout reached');
521
+
522
+ if (options.onTimeout) {
523
+ options.onTimeout(tasks);
524
+ }
525
+ }
526
+ } catch (error) {
527
+ console.error('Error monitoring tasks:', error);
528
+ }
529
+ }, checkInterval);
530
+ }
531
+ }
532
+ ```
533
+
534
+ ### Utility Functions
535
+
536
+ The module includes utility functions for common operations:
537
+
538
+ ```typescript
539
+ import { EcsService } from '@onivoro/server-aws-ecs';
540
+
541
+ // Convert object to ECS environment variable format
542
+ const envVars = EcsService.mapObjectToEcsEnvironmentArray({
543
+ DATABASE_URL: 'postgresql://localhost:5432/mydb',
544
+ API_KEY: 'secret-key',
545
+ LOG_LEVEL: 'info',
546
+ FEATURE_FLAGS: JSON.stringify({ newFeature: true })
547
+ });
548
+
549
+ console.log(envVars);
550
+ // Output: [
551
+ // { Name: 'DATABASE_URL', Value: 'postgresql://localhost:5432/mydb' },
552
+ // { Name: 'API_KEY', Value: 'secret-key' },
553
+ // { Name: 'LOG_LEVEL', Value: 'info' },
554
+ // { Name: 'FEATURE_FLAGS', Value: '{"newFeature":true}' }
555
+ // ]
556
+ ```
557
+
558
+ ### Error Handling and Retry Logic
559
+
560
+ ```typescript
561
+ @Injectable()
562
+ export class ResilientEcsService {
563
+ constructor(private ecsService: EcsService) {}
564
+
565
+ async runTaskWithRetry(taskParams: any, maxRetries: number = 3, backoffMs: number = 1000) {
566
+ let lastError: Error;
567
+
568
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
569
+ try {
570
+ console.log(`Attempt ${attempt}/${maxRetries} to run ECS task`);
571
+ return await this.ecsService.runTasks(taskParams);
572
+ } catch (error: any) {
573
+ lastError = error;
574
+ console.error(`Attempt ${attempt} failed:`, error.message);
575
+
576
+ if (attempt < maxRetries) {
577
+ const delay = backoffMs * Math.pow(2, attempt - 1); // Exponential backoff
578
+ console.log(`Waiting ${delay}ms before retry...`);
579
+ await this.delay(delay);
580
+ }
581
+ }
582
+ }
583
+
584
+ throw new Error(`Failed to run ECS task after ${maxRetries} attempts. Last error: ${lastError!.message}`);
585
+ }
586
+
587
+ private delay(ms: number): Promise<void> {
588
+ return new Promise(resolve => setTimeout(resolve, ms));
589
+ }
590
+ }
591
+ ```
592
+
593
+ ## Best Practices
594
+
595
+ ### 1. Resource Management
596
+
597
+ ```typescript
598
+ // Configure appropriate CPU and memory based on task requirements
599
+ const getResourceConfig = (taskType: 'cpu-intensive' | 'memory-intensive' | 'balanced') => {
600
+ switch (taskType) {
601
+ case 'cpu-intensive':
602
+ return { memory: 2048, cpu: 1024 };
603
+ case 'memory-intensive':
604
+ return { memory: 4096, cpu: 512 };
605
+ case 'balanced':
606
+ default:
607
+ return { memory: 1024, cpu: 512 };
608
+ }
609
+ };
610
+ ```
611
+
612
+ ### 2. Environment Variable Security
613
+
614
+ ```typescript
615
+ // Use AWS Systems Manager Parameter Store for sensitive data
616
+ const secureEnvVars = EcsService.mapObjectToEcsEnvironmentArray({
617
+ DATABASE_URL: '${ssm:/app/database/url}',
618
+ API_KEY: '${ssm-secure:/app/api/key}',
619
+ PUBLIC_CONFIG: 'actual-value'
620
+ });
621
+ ```
622
+
623
+ ### 3. Task Definition Versioning
624
+
625
+ ```typescript
626
+ // Always specify task definition versions
627
+ const taskDefinition = `${baseTaskDefinition}:${version}`;
628
+ ```
629
+
630
+ ## Testing
631
+
632
+ ```typescript
633
+ import { Test, TestingModule } from '@nestjs/testing';
634
+ import { ServerAwsEcsModule, EcsService } from '@onivoro/server-aws-ecs';
635
+
636
+ describe('EcsService', () => {
637
+ let service: EcsService;
638
+
639
+ beforeEach(async () => {
640
+ const module: TestingModule = await Test.createTestingModule({
641
+ imports: [ServerAwsEcsModule.configure({
642
+ AWS_REGION: 'us-east-1',
643
+ CLUSTER_NAME: 'test-cluster',
644
+ TASK_DEFINITION: 'test-task:1',
645
+ SUBNETS: 'subnet-12345',
646
+ SECURITY_GROUPS: 'sg-12345',
647
+ AWS_PROFILE: 'test'
648
+ })],
649
+ }).compile();
650
+
651
+ service = module.get<EcsService>(EcsService);
652
+ });
653
+
654
+ it('should be defined', () => {
655
+ expect(service).toBeDefined();
656
+ });
657
+
658
+ it('should map object to ECS environment array', () => {
659
+ const envObject = { KEY1: 'value1', KEY2: 'value2' };
660
+ const result = EcsService.mapObjectToEcsEnvironmentArray(envObject);
661
+
662
+ expect(result).toEqual([
663
+ { Name: 'KEY1', Value: 'value1' },
664
+ { Name: 'KEY2', Value: 'value2' }
665
+ ]);
666
+ });
667
+ });
668
+ ```
669
+
670
+ ## API Reference
671
+
672
+ ### Exported Classes
673
+ - `ServerAwsEcsConfig`: Configuration class for ECS settings
674
+ - `ServerAwsEcsModule`: NestJS module for ECS integration
675
+
676
+ ### Exported Services
677
+ - `EcsService`: Main ECS service with task execution capabilities
678
+
679
+ ### Static Methods
680
+ - `EcsService.mapObjectToEcsEnvironmentArray()`: Convert object to ECS environment variable format
681
+
682
+ ## License
683
+
684
+ This package is part of the Onivoro monorepo and follows the same licensing terms.
package/jest.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ /* eslint-disable */
2
+ export default {
3
+ displayName: 'lib-server-aws-ecs',
4
+ preset: '../../../jest.preset.js',
5
+ testEnvironment: 'node',
6
+ transform: {
7
+ '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
8
+ },
9
+ moduleFileExtensions: ['ts', 'js', 'html'],
10
+ coverageDirectory: '../../../coverage/libs/server/aws-ecs',
11
+ };