@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.
- package/README.md +684 -0
- package/jest.config.ts +11 -0
- package/package.json +10 -45
- package/project.json +23 -0
- package/{dist/cjs/index.d.ts → src/index.ts} +2 -0
- package/src/lib/classes/server-aws-ecs-config.class.ts +4 -0
- package/src/lib/functions/parse-csv-string.function.ts +3 -0
- package/src/lib/server-aws-ecs.module.ts +33 -0
- package/src/lib/services/__snapshots__/ecs.service.spec.ts.snap +21 -0
- package/src/lib/services/ecs.service.spec.ts +13 -0
- package/src/lib/services/ecs.service.ts +41 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +21 -0
- package/dist/cjs/index.js +0 -19
- package/dist/cjs/lib/classes/server-aws-ecs-config.class.d.ts +0 -6
- package/dist/cjs/lib/classes/server-aws-ecs-config.class.js +0 -10
- package/dist/cjs/lib/functions/parse-csv-string.function.d.ts +0 -1
- package/dist/cjs/lib/functions/parse-csv-string.function.js +0 -6
- package/dist/cjs/lib/server-aws-ecs.module.d.ts +0 -4
- package/dist/cjs/lib/server-aws-ecs.module.js +0 -46
- package/dist/cjs/lib/services/ecs.service.d.ts +0 -12
- package/dist/cjs/lib/services/ecs.service.js +0 -52
- package/dist/esm/index.d.ts +0 -3
- package/dist/esm/index.js +0 -19
- package/dist/esm/lib/classes/server-aws-ecs-config.class.d.ts +0 -6
- package/dist/esm/lib/classes/server-aws-ecs-config.class.js +0 -10
- package/dist/esm/lib/functions/parse-csv-string.function.d.ts +0 -1
- package/dist/esm/lib/functions/parse-csv-string.function.js +0 -6
- package/dist/esm/lib/server-aws-ecs.module.d.ts +0 -4
- package/dist/esm/lib/server-aws-ecs.module.js +0 -46
- package/dist/esm/lib/services/ecs.service.d.ts +0 -12
- package/dist/esm/lib/services/ecs.service.js +0 -52
- package/dist/types/index.d.ts +0 -3
- package/dist/types/lib/classes/server-aws-ecs-config.class.d.ts +0 -6
- package/dist/types/lib/functions/parse-csv-string.function.d.ts +0 -1
- package/dist/types/lib/server-aws-ecs.module.d.ts +0 -4
- 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
|
+
};
|