@dga-itc/aws-cdk-constructs 1.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 +219 -0
- package/dist/aws-cdk/constructs/acm.d.ts +28 -0
- package/dist/aws-cdk/constructs/acm.js +239 -0
- package/dist/aws-cdk/constructs/alb.d.ts +28 -0
- package/dist/aws-cdk/constructs/alb.js +304 -0
- package/dist/aws-cdk/constructs/bastion.d.ts +46 -0
- package/dist/aws-cdk/constructs/bastion.js +332 -0
- package/dist/aws-cdk/constructs/cloudfront.d.ts +45 -0
- package/dist/aws-cdk/constructs/cloudfront.js +261 -0
- package/dist/aws-cdk/constructs/ecr.d.ts +17 -0
- package/dist/aws-cdk/constructs/ecr.js +143 -0
- package/dist/aws-cdk/constructs/ecs-cluster.d.ts +21 -0
- package/dist/aws-cdk/constructs/ecs-cluster.js +124 -0
- package/dist/aws-cdk/constructs/ecs-service.d.ts +72 -0
- package/dist/aws-cdk/constructs/ecs-service.js +682 -0
- package/dist/aws-cdk/constructs/efs.d.ts +31 -0
- package/dist/aws-cdk/constructs/efs.js +241 -0
- package/dist/aws-cdk/constructs/elasticache.d.ts +35 -0
- package/dist/aws-cdk/constructs/elasticache.js +210 -0
- package/dist/aws-cdk/constructs/nacl.d.ts +37 -0
- package/dist/aws-cdk/constructs/nacl.js +88 -0
- package/dist/aws-cdk/constructs/nlb.d.ts +39 -0
- package/dist/aws-cdk/constructs/nlb.js +276 -0
- package/dist/aws-cdk/constructs/rds.d.ts +40 -0
- package/dist/aws-cdk/constructs/rds.js +320 -0
- package/dist/aws-cdk/constructs/self-signed-cert.d.ts +83 -0
- package/dist/aws-cdk/constructs/self-signed-cert.js +215 -0
- package/dist/aws-cdk/constructs/sqs.d.ts +30 -0
- package/dist/aws-cdk/constructs/sqs.js +268 -0
- package/dist/aws-cdk/constructs/vpc.d.ts +30 -0
- package/dist/aws-cdk/constructs/vpc.js +423 -0
- package/dist/aws-cdk/constructs/waf.d.ts +37 -0
- package/dist/aws-cdk/constructs/waf.js +350 -0
- package/dist/aws-cdk/interfaces/account-config.d.ts +18 -0
- package/dist/aws-cdk/interfaces/account-config.js +2 -0
- package/dist/aws-cdk/interfaces/acm-config.d.ts +94 -0
- package/dist/aws-cdk/interfaces/acm-config.js +14 -0
- package/dist/aws-cdk/interfaces/alb-config.d.ts +72 -0
- package/dist/aws-cdk/interfaces/alb-config.js +2 -0
- package/dist/aws-cdk/interfaces/bastion-config.d.ts +77 -0
- package/dist/aws-cdk/interfaces/bastion-config.js +10 -0
- package/dist/aws-cdk/interfaces/cloudfront-config.d.ts +154 -0
- package/dist/aws-cdk/interfaces/cloudfront-config.js +15 -0
- package/dist/aws-cdk/interfaces/ecr-config.d.ts +40 -0
- package/dist/aws-cdk/interfaces/ecr-config.js +2 -0
- package/dist/aws-cdk/interfaces/ecs-cluster-config.d.ts +30 -0
- package/dist/aws-cdk/interfaces/ecs-cluster-config.js +2 -0
- package/dist/aws-cdk/interfaces/ecs-service-config.d.ts +237 -0
- package/dist/aws-cdk/interfaces/ecs-service-config.js +2 -0
- package/dist/aws-cdk/interfaces/efs-config.d.ts +56 -0
- package/dist/aws-cdk/interfaces/efs-config.js +7 -0
- package/dist/aws-cdk/interfaces/elasticache-config.d.ts +56 -0
- package/dist/aws-cdk/interfaces/elasticache-config.js +7 -0
- package/dist/aws-cdk/interfaces/nacl-config.d.ts +1 -0
- package/dist/aws-cdk/interfaces/nacl-config.js +3 -0
- package/dist/aws-cdk/interfaces/nlb-config.d.ts +69 -0
- package/dist/aws-cdk/interfaces/nlb-config.js +2 -0
- package/dist/aws-cdk/interfaces/rds-config.d.ts +84 -0
- package/dist/aws-cdk/interfaces/rds-config.js +7 -0
- package/dist/aws-cdk/interfaces/sqs-config.d.ts +145 -0
- package/dist/aws-cdk/interfaces/sqs-config.js +12 -0
- package/dist/aws-cdk/interfaces/tag-config.d.ts +18 -0
- package/dist/aws-cdk/interfaces/tag-config.js +2 -0
- package/dist/aws-cdk/interfaces/vpc-config.d.ts +72 -0
- package/dist/aws-cdk/interfaces/vpc-config.js +2 -0
- package/dist/aws-cdk/interfaces/waf-config.d.ts +180 -0
- package/dist/aws-cdk/interfaces/waf-config.js +2 -0
- package/dist/aws-cdk/utils/priority-tracker.d.ts +60 -0
- package/dist/aws-cdk/utils/priority-tracker.js +131 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +55 -0
- package/dist/terraform-cdk/constructs/alb-listener-rule.d.ts +33 -0
- package/dist/terraform-cdk/constructs/alb-listener-rule.js +81 -0
- package/dist/terraform-cdk/constructs/ecs-service.d.ts +29 -0
- package/dist/terraform-cdk/constructs/ecs-service.js +238 -0
- package/dist/terraform-cdk/interfaces/ecs-service-config.d.ts +53 -0
- package/dist/terraform-cdk/interfaces/ecs-service-config.js +25 -0
- package/dist/terraform-cdk/interfaces/infrastructure-refs.d.ts +16 -0
- package/dist/terraform-cdk/interfaces/infrastructure-refs.js +8 -0
- package/dist/terraform-cdk/utils/priority-tracker.d.ts +60 -0
- package/dist/terraform-cdk/utils/priority-tracker.js +131 -0
- package/package.json +46 -0
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.EcsServiceConstruct = void 0;
|
|
37
|
+
const constructs_1 = require("constructs");
|
|
38
|
+
const cdk = __importStar(require("aws-cdk-lib"));
|
|
39
|
+
const aws_cdk_lib_1 = require("aws-cdk-lib");
|
|
40
|
+
const ec2 = __importStar(require("aws-cdk-lib/aws-ec2"));
|
|
41
|
+
const ecs = __importStar(require("aws-cdk-lib/aws-ecs"));
|
|
42
|
+
const elbv2 = __importStar(require("aws-cdk-lib/aws-elasticloadbalancingv2"));
|
|
43
|
+
const logs = __importStar(require("aws-cdk-lib/aws-logs"));
|
|
44
|
+
const iam = __importStar(require("aws-cdk-lib/aws-iam"));
|
|
45
|
+
const ecr = __importStar(require("aws-cdk-lib/aws-ecr"));
|
|
46
|
+
/**
|
|
47
|
+
* ECS Service Construct - สร้าง Fargate Service + SecurityGroup + LogGroup + TargetGroup
|
|
48
|
+
*
|
|
49
|
+
* Modes:
|
|
50
|
+
* - web: สร้าง TargetGroup + ListenerRule ที่ ALB
|
|
51
|
+
* - worker: สร้างแค่ Service (ไม่มี ALB integration)
|
|
52
|
+
*
|
|
53
|
+
* อ้างอิง VpcConstruct, EcsClusterConstruct, AlbConstruct จาก stacks อื่น
|
|
54
|
+
*/
|
|
55
|
+
class EcsServiceConstruct extends constructs_1.Construct {
|
|
56
|
+
constructor(scope, id, props) {
|
|
57
|
+
super(scope, id);
|
|
58
|
+
const { config, vpcConstruct, ecsClusterConstruct, albConstruct, efsConstruct, rdsConstruct, elastiCacheConstruct } = props;
|
|
59
|
+
this.isWorkerMode = config.serviceMode === 'worker';
|
|
60
|
+
this.removalPolicy = config.removalPolicy === 'retain'
|
|
61
|
+
? aws_cdk_lib_1.RemovalPolicy.RETAIN
|
|
62
|
+
: aws_cdk_lib_1.RemovalPolicy.DESTROY;
|
|
63
|
+
// Validate: web mode requires ALB construct + routing
|
|
64
|
+
if (!this.isWorkerMode) {
|
|
65
|
+
if (!albConstruct) {
|
|
66
|
+
throw new Error(`ECS Service "${config.container.name}" is web mode but no ALB construct provided. ` +
|
|
67
|
+
`Set serviceMode: 'worker' or provide albStackName in source.`);
|
|
68
|
+
}
|
|
69
|
+
if (!config.routing) {
|
|
70
|
+
throw new Error(`ECS Service "${config.container.name}" is web mode but no routing config provided.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// A. Resolve VPC & Subnets
|
|
74
|
+
const { vpc, subnets } = this.resolveVpc(config, vpcConstruct);
|
|
75
|
+
// B. Create Security Group
|
|
76
|
+
this.securityGroup = this.createSecurityGroup(config, vpc, albConstruct);
|
|
77
|
+
// C. Create Log Group
|
|
78
|
+
this.logGroup = this.createLogGroup(config);
|
|
79
|
+
// D. Resolve IAM Roles
|
|
80
|
+
const { taskRole, executionRole } = this.resolveIamRoles(config);
|
|
81
|
+
// E. Create Task Definition
|
|
82
|
+
const taskDef = this.createTaskDefinition(config, taskRole, executionRole);
|
|
83
|
+
// F. Create Fargate Service
|
|
84
|
+
this.service = this.createFargateService(config, ecsClusterConstruct, taskDef, subnets);
|
|
85
|
+
// G. Create Target Group + Listener Rule (web mode only)
|
|
86
|
+
if (!this.isWorkerMode && albConstruct && config.routing) {
|
|
87
|
+
this.targetGroup = this.createTargetGroup(config, vpc);
|
|
88
|
+
this.createListenerRule(config, albConstruct);
|
|
89
|
+
}
|
|
90
|
+
// H. Setup upstream SG access (EFS / RDS / Redis)
|
|
91
|
+
this.setupUpstreamSecurityAccess(config, efsConstruct, rdsConstruct, elastiCacheConstruct);
|
|
92
|
+
// I. Apply Removal Policy
|
|
93
|
+
this.securityGroup.applyRemovalPolicy(this.removalPolicy);
|
|
94
|
+
this.logGroup.applyRemovalPolicy(this.removalPolicy);
|
|
95
|
+
// J. Outputs
|
|
96
|
+
this.createOutputs(config);
|
|
97
|
+
}
|
|
98
|
+
// ==========================================
|
|
99
|
+
// Upstream SG Access (EFS / RDS / Redis)
|
|
100
|
+
// ==========================================
|
|
101
|
+
/**
|
|
102
|
+
* เพิ่ม ingress rule ที่ EFS/RDS/Redis SG ให้ ECS SG เข้าถึงได้
|
|
103
|
+
* ใช้ CfnSecurityGroupIngress เพื่อหลีกเลี่ยง cyclic dependency
|
|
104
|
+
*/
|
|
105
|
+
setupUpstreamSecurityAccess(config, efsConstruct, rdsConstruct, elastiCacheConstruct) {
|
|
106
|
+
// ECS → EFS (NFS port 2049)
|
|
107
|
+
if (efsConstruct) {
|
|
108
|
+
new ec2.CfnSecurityGroupIngress(this, 'EfsNfsIngress', {
|
|
109
|
+
groupId: efsConstruct.securityGroup.securityGroupId,
|
|
110
|
+
sourceSecurityGroupId: this.securityGroup.securityGroupId,
|
|
111
|
+
ipProtocol: 'tcp',
|
|
112
|
+
fromPort: 2049,
|
|
113
|
+
toPort: 2049,
|
|
114
|
+
description: `Allow NFS from ECS ${config.container.name}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// ECS → RDS (MySQL/MariaDB/PostgreSQL port)
|
|
118
|
+
if (rdsConstruct) {
|
|
119
|
+
const dbPort = config.container.environmentVariables?.WORDPRESS_DB_PORT
|
|
120
|
+
? parseInt(config.container.environmentVariables.WORDPRESS_DB_PORT)
|
|
121
|
+
: 3306;
|
|
122
|
+
new ec2.CfnSecurityGroupIngress(this, 'RdsDbIngress', {
|
|
123
|
+
groupId: rdsConstruct.securityGroup.securityGroupId,
|
|
124
|
+
sourceSecurityGroupId: this.securityGroup.securityGroupId,
|
|
125
|
+
ipProtocol: 'tcp',
|
|
126
|
+
fromPort: dbPort,
|
|
127
|
+
toPort: dbPort,
|
|
128
|
+
description: `Allow DB from ECS ${config.container.name}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// ECS → Redis (port 6379)
|
|
132
|
+
if (elastiCacheConstruct) {
|
|
133
|
+
const redisPort = config.container.environmentVariables?.WP_REDIS_PORT
|
|
134
|
+
? parseInt(config.container.environmentVariables.WP_REDIS_PORT)
|
|
135
|
+
: 6379;
|
|
136
|
+
new ec2.CfnSecurityGroupIngress(this, 'RedisIngress', {
|
|
137
|
+
groupId: elastiCacheConstruct.securityGroup.securityGroupId,
|
|
138
|
+
sourceSecurityGroupId: this.securityGroup.securityGroupId,
|
|
139
|
+
ipProtocol: 'tcp',
|
|
140
|
+
fromPort: redisPort,
|
|
141
|
+
toPort: redisPort,
|
|
142
|
+
description: `Allow Redis from ECS ${config.container.name}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ==========================================
|
|
147
|
+
// Resolve VPC & Subnets
|
|
148
|
+
// ==========================================
|
|
149
|
+
resolveVpc(config, vpcConstruct) {
|
|
150
|
+
const subnetName = config.source.subnetName ?? 'private';
|
|
151
|
+
const cfnSubnets = vpcConstruct.subnetsByName.get(subnetName);
|
|
152
|
+
if (!cfnSubnets || cfnSubnets.length === 0) {
|
|
153
|
+
throw new Error(`No subnets found with name "${subnetName}" in VPC construct. ` +
|
|
154
|
+
`Available: ${Array.from(vpcConstruct.subnetsByName.keys()).join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
// Build subnets with routeTableId from VpcConstruct
|
|
157
|
+
const subnetIds = [];
|
|
158
|
+
const subnets = [];
|
|
159
|
+
const allAzs = new Set();
|
|
160
|
+
const routeTableIds = [];
|
|
161
|
+
vpcConstruct.subnets.forEach((cfnSubnet, subnetKey) => {
|
|
162
|
+
if (subnetKey.startsWith(`${subnetName}-`)) {
|
|
163
|
+
const az = subnetKey.replace(`${subnetName}-`, '');
|
|
164
|
+
const routeTableId = vpcConstruct.subnetRouteTableMap.get(subnetKey);
|
|
165
|
+
subnetIds.push(cfnSubnet.ref);
|
|
166
|
+
if (cfnSubnet.availabilityZone)
|
|
167
|
+
allAzs.add(cfnSubnet.availabilityZone);
|
|
168
|
+
if (routeTableId)
|
|
169
|
+
routeTableIds.push(routeTableId);
|
|
170
|
+
subnets.push(ec2.Subnet.fromSubnetAttributes(this, `ServiceSubnet-${az}`, {
|
|
171
|
+
subnetId: cfnSubnet.ref,
|
|
172
|
+
availabilityZone: cfnSubnet.availabilityZone,
|
|
173
|
+
routeTableId,
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
const hasRtIds = routeTableIds.length === subnetIds.length;
|
|
178
|
+
const vpc = ec2.Vpc.fromVpcAttributes(this, 'ServiceVpc', {
|
|
179
|
+
vpcId: vpcConstruct.vpc.ref,
|
|
180
|
+
availabilityZones: Array.from(allAzs),
|
|
181
|
+
privateSubnetIds: subnetIds,
|
|
182
|
+
privateSubnetRouteTableIds: hasRtIds ? routeTableIds : undefined,
|
|
183
|
+
});
|
|
184
|
+
return { vpc, subnets };
|
|
185
|
+
}
|
|
186
|
+
// ==========================================
|
|
187
|
+
// Security Group
|
|
188
|
+
// ==========================================
|
|
189
|
+
createSecurityGroup(config, vpc, albConstruct) {
|
|
190
|
+
const sgConfig = config.securityGroup ?? {};
|
|
191
|
+
const serviceType = this.isWorkerMode ? 'worker' : 'web';
|
|
192
|
+
const sg = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
|
|
193
|
+
vpc,
|
|
194
|
+
securityGroupName: config.naming?.securityGroupName,
|
|
195
|
+
description: `Security group for ${config.container.name} ECS ${serviceType} service`,
|
|
196
|
+
allowAllOutbound: true,
|
|
197
|
+
});
|
|
198
|
+
// Allow traffic from ALB (web mode only)
|
|
199
|
+
if (!this.isWorkerMode && albConstruct) {
|
|
200
|
+
const allowAll = sgConfig.allowAllFromAlb !== false; // default: true
|
|
201
|
+
if (allowAll) {
|
|
202
|
+
// ใช้ CfnSecurityGroupIngress เพื่อหลีกเลี่ยง circular dependency
|
|
203
|
+
new ec2.CfnSecurityGroupIngress(this, 'AlbToServiceIngress', {
|
|
204
|
+
ipProtocol: 'tcp',
|
|
205
|
+
fromPort: 0,
|
|
206
|
+
toPort: 65535,
|
|
207
|
+
groupId: sg.securityGroupId,
|
|
208
|
+
sourceSecurityGroupId: albConstruct.securityGroup.securityGroupId,
|
|
209
|
+
description: 'Allow all TCP traffic from ALB',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
new ec2.CfnSecurityGroupIngress(this, 'AlbToServiceIngress', {
|
|
214
|
+
ipProtocol: 'tcp',
|
|
215
|
+
fromPort: config.container.port,
|
|
216
|
+
toPort: config.container.port,
|
|
217
|
+
groupId: sg.securityGroupId,
|
|
218
|
+
sourceSecurityGroupId: albConstruct.securityGroup.securityGroupId,
|
|
219
|
+
description: `Allow traffic from ALB on container port ${config.container.port}`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Additional source security groups
|
|
224
|
+
if (sgConfig.additionalSourceSgIds) {
|
|
225
|
+
sgConfig.additionalSourceSgIds.forEach((sourceSgId, index) => {
|
|
226
|
+
new ec2.CfnSecurityGroupIngress(this, `AdditionalSgIngress${index + 1}`, {
|
|
227
|
+
ipProtocol: 'tcp',
|
|
228
|
+
fromPort: 0,
|
|
229
|
+
toPort: 65535,
|
|
230
|
+
groupId: sg.securityGroupId,
|
|
231
|
+
sourceSecurityGroupId: sourceSgId,
|
|
232
|
+
description: `Allow all TCP from additional SG ${index + 1}`,
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// CIDR rules
|
|
237
|
+
if (sgConfig.allowFromCidrs) {
|
|
238
|
+
sgConfig.allowFromCidrs.forEach((cidr, index) => {
|
|
239
|
+
sg.addIngressRule(ec2.Peer.ipv4(cidr), ec2.Port.allTcp(), `Allow all TCP from CIDR ${index + 1}: ${cidr}`);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return sg;
|
|
243
|
+
}
|
|
244
|
+
// ==========================================
|
|
245
|
+
// IAM Roles
|
|
246
|
+
// ==========================================
|
|
247
|
+
resolveIamRoles(config) {
|
|
248
|
+
const iamConfig = config.iam;
|
|
249
|
+
let taskRole;
|
|
250
|
+
let executionRole;
|
|
251
|
+
// ── Task Execution Role ──
|
|
252
|
+
if (iamConfig?.executionRoleArn) {
|
|
253
|
+
executionRole = iam.Role.fromRoleArn(this, 'ImportedExecRole', iamConfig.executionRoleArn, {
|
|
254
|
+
mutable: false,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
else if (iamConfig?.executionRolePresets && iamConfig.executionRolePresets.length > 0) {
|
|
258
|
+
// สร้าง Execution Role เอง + เพิ่ม preset policies
|
|
259
|
+
const execRole = new iam.Role(this, 'ExecutionRole', {
|
|
260
|
+
roleName: config.naming?.executionRoleName,
|
|
261
|
+
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
|
|
262
|
+
description: `Execution Role for ${config.container.name} ECS service`,
|
|
263
|
+
managedPolicies: [
|
|
264
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'),
|
|
265
|
+
],
|
|
266
|
+
});
|
|
267
|
+
// Add execution role preset policies
|
|
268
|
+
this.applyExecutionRolePresets(execRole, iamConfig.executionRolePresets);
|
|
269
|
+
execRole.applyRemovalPolicy(this.removalPolicy);
|
|
270
|
+
executionRole = execRole;
|
|
271
|
+
}
|
|
272
|
+
// ถ้าไม่ระบุ CDK จะ auto-create พร้อม AmazonECSTaskExecutionRolePolicy
|
|
273
|
+
// ── Task Role ──
|
|
274
|
+
const hasPresets = iamConfig?.taskRolePresets && iamConfig.taskRolePresets.length > 0;
|
|
275
|
+
const hasInlinePolicies = iamConfig?.taskRolePolicies && iamConfig.taskRolePolicies.length > 0;
|
|
276
|
+
const hasManagedPolicies = iamConfig?.taskRoleManagedPolicyArns && iamConfig.taskRoleManagedPolicyArns.length > 0;
|
|
277
|
+
if (iamConfig?.taskRoleArn) {
|
|
278
|
+
// Import existing role
|
|
279
|
+
taskRole = iam.Role.fromRoleArn(this, 'ImportedTaskRole', iamConfig.taskRoleArn, {
|
|
280
|
+
mutable: false,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else if (hasPresets || hasInlinePolicies || hasManagedPolicies) {
|
|
284
|
+
// สร้าง Task Role พร้อม preset + inline + managed policies
|
|
285
|
+
const role = new iam.Role(this, 'TaskRole', {
|
|
286
|
+
roleName: config.naming?.taskRoleName,
|
|
287
|
+
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
|
|
288
|
+
description: `Task Role for ${config.container.name} ECS service`,
|
|
289
|
+
});
|
|
290
|
+
// Apply preset policies
|
|
291
|
+
if (hasPresets) {
|
|
292
|
+
this.applyTaskRolePresets(role, iamConfig.taskRolePresets);
|
|
293
|
+
}
|
|
294
|
+
// Add inline policies
|
|
295
|
+
if (hasInlinePolicies) {
|
|
296
|
+
iamConfig.taskRolePolicies.forEach((policy, index) => {
|
|
297
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
298
|
+
effect: policy.effect === 'Deny' ? iam.Effect.DENY : iam.Effect.ALLOW,
|
|
299
|
+
actions: policy.actions,
|
|
300
|
+
resources: policy.resources,
|
|
301
|
+
}));
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// Attach managed policies
|
|
305
|
+
if (hasManagedPolicies) {
|
|
306
|
+
iamConfig.taskRoleManagedPolicyArns.forEach((arn, index) => {
|
|
307
|
+
role.addManagedPolicy(iam.ManagedPolicy.fromManagedPolicyArn(this, `ManagedPolicy${index + 1}`, arn));
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
role.applyRemovalPolicy(this.removalPolicy);
|
|
311
|
+
taskRole = role;
|
|
312
|
+
}
|
|
313
|
+
// ถ้าไม่ระบุอะไรเลย CDK จะ auto-create empty role
|
|
314
|
+
return { taskRole, executionRole };
|
|
315
|
+
}
|
|
316
|
+
// ==========================================
|
|
317
|
+
// IAM Preset Policies
|
|
318
|
+
// ==========================================
|
|
319
|
+
/**
|
|
320
|
+
* Apply preset policies ให้ Task Role
|
|
321
|
+
* ใช้ AWS Managed Policies เป็นหลัก, fallback เป็น inline สำหรับ services ที่ไม่มี managed policy ที่เหมาะสม
|
|
322
|
+
*/
|
|
323
|
+
applyTaskRolePresets(role, presets) {
|
|
324
|
+
// AWS Managed Policy ARNs mapping
|
|
325
|
+
const managedPolicyMap = {
|
|
326
|
+
'ssm-parameters': 'arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess',
|
|
327
|
+
'sqs': 'arn:aws:iam::aws:policy/AmazonSQSFullAccess',
|
|
328
|
+
's3-read': 'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess',
|
|
329
|
+
's3-full': 'arn:aws:iam::aws:policy/AmazonS3FullAccess',
|
|
330
|
+
'dynamodb': 'arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess',
|
|
331
|
+
'sns': 'arn:aws:iam::aws:policy/AmazonSNSFullAccess',
|
|
332
|
+
'ses': 'arn:aws:iam::aws:policy/AmazonSESFullAccess',
|
|
333
|
+
'xray': 'arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess',
|
|
334
|
+
'cloudwatch-metrics': 'arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy',
|
|
335
|
+
'bedrock': 'arn:aws:iam::aws:policy/AmazonBedrockFullAccess',
|
|
336
|
+
};
|
|
337
|
+
// Inline policies สำหรับ services ที่ไม่มี managed policy ที่เหมาะสม
|
|
338
|
+
const inlinePolicyMap = {
|
|
339
|
+
'secrets-manager': {
|
|
340
|
+
actions: [
|
|
341
|
+
'secretsmanager:GetSecretValue',
|
|
342
|
+
'secretsmanager:DescribeSecret',
|
|
343
|
+
'secretsmanager:ListSecretVersionIds',
|
|
344
|
+
],
|
|
345
|
+
resources: ['*'],
|
|
346
|
+
},
|
|
347
|
+
'rds-connect': {
|
|
348
|
+
actions: ['rds-db:connect'],
|
|
349
|
+
resources: ['*'],
|
|
350
|
+
},
|
|
351
|
+
'ecs-exec': {
|
|
352
|
+
actions: [
|
|
353
|
+
'ssmmessages:CreateControlChannel',
|
|
354
|
+
'ssmmessages:CreateDataChannel',
|
|
355
|
+
'ssmmessages:OpenControlChannel',
|
|
356
|
+
'ssmmessages:OpenDataChannel',
|
|
357
|
+
],
|
|
358
|
+
resources: ['*'],
|
|
359
|
+
},
|
|
360
|
+
'kms': {
|
|
361
|
+
actions: [
|
|
362
|
+
'kms:Decrypt',
|
|
363
|
+
'kms:GenerateDataKey',
|
|
364
|
+
'kms:DescribeKey',
|
|
365
|
+
],
|
|
366
|
+
resources: ['*'],
|
|
367
|
+
},
|
|
368
|
+
'efs': {
|
|
369
|
+
actions: [
|
|
370
|
+
'elasticfilesystem:ClientMount',
|
|
371
|
+
'elasticfilesystem:ClientWrite',
|
|
372
|
+
'elasticfilesystem:ClientRootAccess',
|
|
373
|
+
],
|
|
374
|
+
resources: ['*'],
|
|
375
|
+
},
|
|
376
|
+
'elasticache': {
|
|
377
|
+
actions: [
|
|
378
|
+
'elasticache:Connect',
|
|
379
|
+
'elasticache:DescribeCacheClusters',
|
|
380
|
+
'elasticache:DescribeReplicationGroups',
|
|
381
|
+
'elasticache:DescribeCacheSubnetGroups',
|
|
382
|
+
],
|
|
383
|
+
resources: ['*'],
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
const allKnownPresets = [...Object.keys(managedPolicyMap), ...Object.keys(inlinePolicyMap)];
|
|
387
|
+
presets.forEach((presetName) => {
|
|
388
|
+
// ลองหา managed policy ก่อน
|
|
389
|
+
const managedPolicyArn = managedPolicyMap[presetName];
|
|
390
|
+
if (managedPolicyArn) {
|
|
391
|
+
role.addManagedPolicy(iam.ManagedPolicy.fromManagedPolicyArn(this, `ManagedPolicy-${presetName}`, managedPolicyArn));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// ถ้าไม่มี managed policy ใช้ inline
|
|
395
|
+
const inlinePolicy = inlinePolicyMap[presetName];
|
|
396
|
+
if (inlinePolicy) {
|
|
397
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
398
|
+
effect: iam.Effect.ALLOW,
|
|
399
|
+
actions: inlinePolicy.actions,
|
|
400
|
+
resources: inlinePolicy.resources,
|
|
401
|
+
}));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
throw new Error(`Unknown Task Role preset policy: "${presetName}". Available: ${allKnownPresets.join(', ')}`);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Apply preset policies ให้ Execution Role
|
|
409
|
+
*/
|
|
410
|
+
applyExecutionRolePresets(role, presets) {
|
|
411
|
+
const presetMap = {
|
|
412
|
+
'exec-secrets-injection': {
|
|
413
|
+
actions: [
|
|
414
|
+
'secretsmanager:GetSecretValue',
|
|
415
|
+
],
|
|
416
|
+
resources: ['*'],
|
|
417
|
+
},
|
|
418
|
+
'exec-ssm-injection': {
|
|
419
|
+
actions: [
|
|
420
|
+
'ssm:GetParameters',
|
|
421
|
+
],
|
|
422
|
+
resources: ['*'],
|
|
423
|
+
},
|
|
424
|
+
'exec-kms': {
|
|
425
|
+
actions: [
|
|
426
|
+
'kms:Decrypt',
|
|
427
|
+
],
|
|
428
|
+
resources: ['*'],
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
presets.forEach((presetName) => {
|
|
432
|
+
const preset = presetMap[presetName];
|
|
433
|
+
if (!preset) {
|
|
434
|
+
throw new Error(`Unknown Execution Role preset policy: "${presetName}". Available: ${Object.keys(presetMap).join(', ')}`);
|
|
435
|
+
}
|
|
436
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
437
|
+
effect: iam.Effect.ALLOW,
|
|
438
|
+
actions: preset.actions,
|
|
439
|
+
resources: preset.resources,
|
|
440
|
+
}));
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// ==========================================
|
|
444
|
+
// Log Group
|
|
445
|
+
// ==========================================
|
|
446
|
+
createLogGroup(config) {
|
|
447
|
+
const logConfig = config.logging ?? {};
|
|
448
|
+
const logRemoval = logConfig.removalPolicy === 'retain'
|
|
449
|
+
? aws_cdk_lib_1.RemovalPolicy.RETAIN
|
|
450
|
+
: aws_cdk_lib_1.RemovalPolicy.DESTROY;
|
|
451
|
+
return new logs.LogGroup(this, 'LogGroup', {
|
|
452
|
+
logGroupName: `/ecs/${config.stackName}/${config.container.name}`,
|
|
453
|
+
retention: logConfig.retention ?? logs.RetentionDays.ONE_WEEK,
|
|
454
|
+
removalPolicy: logRemoval,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// ==========================================
|
|
458
|
+
// Task Definition
|
|
459
|
+
// ==========================================
|
|
460
|
+
createTaskDefinition(config, taskRole, executionRole) {
|
|
461
|
+
const { container } = config;
|
|
462
|
+
// Use custom family name or generate from container name + stack name
|
|
463
|
+
const familyName = config.naming?.taskDefinitionFamily ?? `${container.name}-${config.stackName}`;
|
|
464
|
+
const taskDef = new ecs.FargateTaskDefinition(this, 'TaskDefinition', {
|
|
465
|
+
family: familyName,
|
|
466
|
+
cpu: container.cpu,
|
|
467
|
+
memoryLimitMiB: container.memory,
|
|
468
|
+
ephemeralStorageGiB: container.ephemeralStorageGiB,
|
|
469
|
+
taskRole,
|
|
470
|
+
executionRole,
|
|
471
|
+
});
|
|
472
|
+
const image = this.resolveContainerImage(container.image);
|
|
473
|
+
// initProcessEnabled จำเป็นสำหรับ ECS Exec (ใช้ tini เป็น PID 1)
|
|
474
|
+
const initProcessEnabled = container.initProcessEnabled ?? config.enableExecuteCommand ?? true;
|
|
475
|
+
const containerDef = taskDef.addContainer('MainContainer', {
|
|
476
|
+
image,
|
|
477
|
+
containerName: container.name,
|
|
478
|
+
logging: ecs.LogDrivers.awsLogs({
|
|
479
|
+
logGroup: this.logGroup,
|
|
480
|
+
streamPrefix: 'ecs',
|
|
481
|
+
}),
|
|
482
|
+
environment: container.environmentVariables,
|
|
483
|
+
command: container.command,
|
|
484
|
+
secrets: container.secrets
|
|
485
|
+
? Object.fromEntries(Object.entries(container.secrets).map(([envName, secretValue]) => {
|
|
486
|
+
// Support format: 'arn:...:secret:name-suffix:jsonField'
|
|
487
|
+
// Secrets Manager ARN has exactly 6 parts separated by ':'
|
|
488
|
+
// arn:aws:secretsmanager:region:account:secret:name-suffix
|
|
489
|
+
// If there's a 7th part, it's the JSON field name
|
|
490
|
+
//
|
|
491
|
+
// When the ARN is a CDK Token (cross-stack reference), we can't split by ':'
|
|
492
|
+
// because the token is not a real ARN string at synth time.
|
|
493
|
+
// Use a separator that won't appear in ARNs: '::' (double colon)
|
|
494
|
+
let secretArn;
|
|
495
|
+
let jsonField;
|
|
496
|
+
if (secretValue.includes('::')) {
|
|
497
|
+
// New format: 'arn:...:secret:name::jsonField' (double colon separator)
|
|
498
|
+
const separatorIndex = secretValue.indexOf('::');
|
|
499
|
+
secretArn = secretValue.substring(0, separatorIndex);
|
|
500
|
+
jsonField = secretValue.substring(separatorIndex + 2);
|
|
501
|
+
}
|
|
502
|
+
else if (!cdk.Token.isUnresolved(secretValue)) {
|
|
503
|
+
// Legacy format: only works with resolved (literal) strings
|
|
504
|
+
const parts = secretValue.split(':');
|
|
505
|
+
if (parts.length > 7) {
|
|
506
|
+
secretArn = parts.slice(0, 7).join(':');
|
|
507
|
+
jsonField = parts.slice(7).join(':');
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
secretArn = secretValue;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
// Token without '::' separator — treat entire value as ARN (no JSON field)
|
|
515
|
+
secretArn = secretValue;
|
|
516
|
+
}
|
|
517
|
+
const secret = cdk.aws_secretsmanager.Secret.fromSecretCompleteArn(this, `Secret-${envName}`, secretArn);
|
|
518
|
+
return [
|
|
519
|
+
envName,
|
|
520
|
+
jsonField
|
|
521
|
+
? ecs.Secret.fromSecretsManager(secret, jsonField)
|
|
522
|
+
: ecs.Secret.fromSecretsManager(secret),
|
|
523
|
+
];
|
|
524
|
+
}))
|
|
525
|
+
: undefined,
|
|
526
|
+
linuxParameters: new ecs.LinuxParameters(this, 'LinuxParams', {
|
|
527
|
+
initProcessEnabled,
|
|
528
|
+
}),
|
|
529
|
+
});
|
|
530
|
+
containerDef.addPortMappings({
|
|
531
|
+
containerPort: container.port,
|
|
532
|
+
protocol: ecs.Protocol.TCP,
|
|
533
|
+
});
|
|
534
|
+
return taskDef;
|
|
535
|
+
}
|
|
536
|
+
// ==========================================
|
|
537
|
+
// Resolve Container Image
|
|
538
|
+
// ==========================================
|
|
539
|
+
/**
|
|
540
|
+
* ถ้า image URL เป็น ECR pattern (*.dkr.ecr.*.amazonaws.com/repo:tag)
|
|
541
|
+
* ใช้ fromEcrRepository เพื่อให้ CDK auto-grant ECR pull policy
|
|
542
|
+
* ไม่งั้นใช้ fromRegistry ปกติ (Docker Hub, public ECR, etc.)
|
|
543
|
+
*/
|
|
544
|
+
resolveContainerImage(imageUri) {
|
|
545
|
+
// Local path pattern: starts with './', '../', or '/' → use fromAsset (CDK builds & pushes to ECR)
|
|
546
|
+
if (imageUri.startsWith('./') || imageUri.startsWith('../') || imageUri.startsWith('/')) {
|
|
547
|
+
return ecs.ContainerImage.fromAsset(imageUri);
|
|
548
|
+
}
|
|
549
|
+
// ECR private pattern: {account}.dkr.ecr.{region}.amazonaws.com/{repo}:{tag}
|
|
550
|
+
const ecrMatch = imageUri.match(/^(\d+)\.dkr\.ecr\.([^.]+)\.amazonaws\.com\/([^:]+)(?::(.+))?$/);
|
|
551
|
+
if (ecrMatch) {
|
|
552
|
+
const [, , region, repoName, tag] = ecrMatch;
|
|
553
|
+
const repo = ecr.Repository.fromRepositoryAttributes(this, 'EcrRepo', {
|
|
554
|
+
repositoryArn: `arn:aws:ecr:${region}:${cdk.Stack.of(this).account}:repository/${repoName}`,
|
|
555
|
+
repositoryName: repoName,
|
|
556
|
+
});
|
|
557
|
+
return ecs.ContainerImage.fromEcrRepository(repo, tag ?? 'latest');
|
|
558
|
+
}
|
|
559
|
+
return ecs.ContainerImage.fromRegistry(imageUri);
|
|
560
|
+
}
|
|
561
|
+
// ==========================================
|
|
562
|
+
// Fargate Service
|
|
563
|
+
// ==========================================
|
|
564
|
+
createFargateService(config, ecsClusterConstruct, taskDef, subnets) {
|
|
565
|
+
return new ecs.FargateService(this, 'FargateService', {
|
|
566
|
+
cluster: ecsClusterConstruct.cluster,
|
|
567
|
+
taskDefinition: taskDef,
|
|
568
|
+
desiredCount: config.container.desiredCount,
|
|
569
|
+
securityGroups: [this.securityGroup],
|
|
570
|
+
vpcSubnets: { subnets },
|
|
571
|
+
serviceName: `${config.container.name}`,
|
|
572
|
+
enableExecuteCommand: config.enableExecuteCommand ?? true,
|
|
573
|
+
circuitBreaker: (config.circuitBreakerEnabled ?? true) ? { rollback: true } : undefined,
|
|
574
|
+
minHealthyPercent: config.minHealthyPercent ?? 50,
|
|
575
|
+
maxHealthyPercent: config.maxHealthyPercent ?? 200,
|
|
576
|
+
assignPublicIp: config.assignPublicIp ?? false,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
// ==========================================
|
|
580
|
+
// Target Group (web mode only)
|
|
581
|
+
// ==========================================
|
|
582
|
+
createTargetGroup(config, vpc) {
|
|
583
|
+
const routing = config.routing;
|
|
584
|
+
// Target Group name: use custom name or generate from container name (max 32 chars)
|
|
585
|
+
let tgName;
|
|
586
|
+
if (config.naming?.targetGroupName) {
|
|
587
|
+
tgName = config.naming.targetGroupName.substring(0, 32);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
const tgNameBase = `${config.container.name}`;
|
|
591
|
+
tgName = tgNameBase.length > 29
|
|
592
|
+
? `${tgNameBase.substring(0, 29)}-tg`
|
|
593
|
+
: `${tgNameBase}-tg`;
|
|
594
|
+
}
|
|
595
|
+
return new elbv2.ApplicationTargetGroup(this, 'TargetGroup', {
|
|
596
|
+
vpc,
|
|
597
|
+
targetGroupName: tgName,
|
|
598
|
+
port: config.container.port,
|
|
599
|
+
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
600
|
+
targets: [this.service],
|
|
601
|
+
targetType: elbv2.TargetType.IP,
|
|
602
|
+
healthCheck: {
|
|
603
|
+
path: routing.healthCheckPath,
|
|
604
|
+
interval: cdk.Duration.seconds(routing.healthCheckInterval ?? 30),
|
|
605
|
+
timeout: cdk.Duration.seconds(routing.healthCheckTimeout ?? 5),
|
|
606
|
+
healthyThresholdCount: routing.healthyThresholdCount ?? 2,
|
|
607
|
+
unhealthyThresholdCount: routing.unhealthyThresholdCount ?? 3,
|
|
608
|
+
},
|
|
609
|
+
deregistrationDelay: cdk.Duration.seconds(routing.deregistrationDelay ?? 30),
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
// ==========================================
|
|
613
|
+
// Listener Rule (web mode only)
|
|
614
|
+
// ==========================================
|
|
615
|
+
createListenerRule(config, albConstruct) {
|
|
616
|
+
const routing = config.routing;
|
|
617
|
+
// ใช้ HTTPS listener (ถ้ามี), fallback ไป HTTP
|
|
618
|
+
const listener = albConstruct.httpsListener ?? albConstruct.httpListener;
|
|
619
|
+
if (!listener) {
|
|
620
|
+
throw new Error('ALB has no listeners available for ECS Service routing');
|
|
621
|
+
}
|
|
622
|
+
const conditions = [];
|
|
623
|
+
// Host header condition (optional)
|
|
624
|
+
if (routing.domainName && routing.domainName !== '*') {
|
|
625
|
+
conditions.push(elbv2.ListenerCondition.hostHeaders([routing.domainName]));
|
|
626
|
+
}
|
|
627
|
+
// Path pattern condition
|
|
628
|
+
conditions.push(elbv2.ListenerCondition.pathPatterns([routing.pathPattern]));
|
|
629
|
+
return new elbv2.ApplicationListenerRule(this, 'ListenerRule', {
|
|
630
|
+
listener,
|
|
631
|
+
priority: routing.priority,
|
|
632
|
+
conditions,
|
|
633
|
+
targetGroups: [this.targetGroup],
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
// ==========================================
|
|
637
|
+
// Outputs
|
|
638
|
+
// ==========================================
|
|
639
|
+
createOutputs(config) {
|
|
640
|
+
const stack = cdk.Stack.of(this);
|
|
641
|
+
const prefix = config.stackName;
|
|
642
|
+
const svcName = config.container.name;
|
|
643
|
+
new cdk.CfnOutput(stack, `${svcName}-ServiceArn`, {
|
|
644
|
+
value: this.service.serviceArn,
|
|
645
|
+
description: `ECS Service ARN (${svcName})`,
|
|
646
|
+
exportName: `${prefix}-${svcName}-Service-Arn`,
|
|
647
|
+
});
|
|
648
|
+
new cdk.CfnOutput(stack, `${svcName}-ServiceName`, {
|
|
649
|
+
value: this.service.serviceName,
|
|
650
|
+
description: `ECS Service Name (${svcName})`,
|
|
651
|
+
exportName: `${prefix}-${svcName}-Service-Name`,
|
|
652
|
+
});
|
|
653
|
+
if (!this.isWorkerMode && this.targetGroup) {
|
|
654
|
+
new cdk.CfnOutput(stack, `${svcName}-TargetGroupArn`, {
|
|
655
|
+
value: this.targetGroup.targetGroupArn,
|
|
656
|
+
description: `Target Group ARN (${svcName})`,
|
|
657
|
+
exportName: `${prefix}-${svcName}-TargetGroup-Arn`,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
new cdk.CfnOutput(stack, `${svcName}-SecurityGroupId`, {
|
|
661
|
+
value: this.securityGroup.securityGroupId,
|
|
662
|
+
description: `Service Security Group ID (${svcName})`,
|
|
663
|
+
exportName: `${prefix}-${svcName}-SecurityGroup-Id`,
|
|
664
|
+
});
|
|
665
|
+
new cdk.CfnOutput(stack, `${svcName}-ServiceMode`, {
|
|
666
|
+
value: this.isWorkerMode ? 'worker' : 'web',
|
|
667
|
+
description: `Service Mode (${svcName})`,
|
|
668
|
+
});
|
|
669
|
+
// ECS Exec command
|
|
670
|
+
if (config.enableExecuteCommand !== false) {
|
|
671
|
+
new cdk.CfnOutput(stack, `${svcName}-EcsExecCommand`, {
|
|
672
|
+
value: `aws ecs execute-command --cluster ${config.source.ecsClusterStackName} --task TASK_ID --container ${svcName} --interactive --command "/bin/sh"`,
|
|
673
|
+
description: `ECS Exec command - replace TASK_ID with actual task ID (${svcName})`,
|
|
674
|
+
});
|
|
675
|
+
new cdk.CfnOutput(stack, `${svcName}-ListTasksCommand`, {
|
|
676
|
+
value: `aws ecs list-tasks --cluster ${config.source.ecsClusterStackName} --service-name ${svcName} --query 'taskArns[0]' --output text`,
|
|
677
|
+
description: `List running tasks to get TASK_ID (${svcName})`,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
exports.EcsServiceConstruct = EcsServiceConstruct;
|