@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,304 @@
|
|
|
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.AlbConstruct = 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 elbv2 = __importStar(require("aws-cdk-lib/aws-elasticloadbalancingv2"));
|
|
42
|
+
const acm = __importStar(require("aws-cdk-lib/aws-certificatemanager"));
|
|
43
|
+
/**
|
|
44
|
+
* ALB Construct - สร้าง ALB + Security Group + Listeners
|
|
45
|
+
*
|
|
46
|
+
* อ้างอิง VpcConstruct จาก NetworkStack เพื่อดึง VPC + Subnets
|
|
47
|
+
*/
|
|
48
|
+
class AlbConstruct extends constructs_1.Construct {
|
|
49
|
+
constructor(scope, id, props) {
|
|
50
|
+
super(scope, id);
|
|
51
|
+
const { config, vpcConstruct } = props;
|
|
52
|
+
this.removalPolicy = config.removalPolicy === 'retain'
|
|
53
|
+
? aws_cdk_lib_1.RemovalPolicy.RETAIN
|
|
54
|
+
: aws_cdk_lib_1.RemovalPolicy.DESTROY;
|
|
55
|
+
// ------------------------------------------
|
|
56
|
+
// A. Resolve VPC & Subnets
|
|
57
|
+
// ------------------------------------------
|
|
58
|
+
const { vpc, subnets } = this.resolveVpc(config, vpcConstruct);
|
|
59
|
+
// ------------------------------------------
|
|
60
|
+
// B. Create Security Group
|
|
61
|
+
// ------------------------------------------
|
|
62
|
+
this.securityGroup = this.createSecurityGroup(vpc, config);
|
|
63
|
+
// ------------------------------------------
|
|
64
|
+
// C. Create ALB
|
|
65
|
+
// ------------------------------------------
|
|
66
|
+
this.alb = this.createAlb(config, vpc, subnets);
|
|
67
|
+
// ------------------------------------------
|
|
68
|
+
// D. Create Listeners
|
|
69
|
+
// ------------------------------------------
|
|
70
|
+
const { httpListener, httpsListener } = this.createListeners(config);
|
|
71
|
+
this.httpListener = httpListener;
|
|
72
|
+
this.httpsListener = httpsListener;
|
|
73
|
+
// ------------------------------------------
|
|
74
|
+
// E. Apply Removal Policy
|
|
75
|
+
// ------------------------------------------
|
|
76
|
+
this.alb.applyRemovalPolicy(this.removalPolicy);
|
|
77
|
+
this.securityGroup.applyRemovalPolicy(this.removalPolicy);
|
|
78
|
+
// ------------------------------------------
|
|
79
|
+
// F. Outputs
|
|
80
|
+
// ------------------------------------------
|
|
81
|
+
this.createOutputs(config);
|
|
82
|
+
}
|
|
83
|
+
// ==========================================
|
|
84
|
+
// Resolve VPC
|
|
85
|
+
// ==========================================
|
|
86
|
+
resolveVpc(config, vpcConstruct) {
|
|
87
|
+
const subnetName = config.source.subnetName ?? 'public';
|
|
88
|
+
const cfnSubnets = vpcConstruct.subnetsByName.get(subnetName);
|
|
89
|
+
if (!cfnSubnets || cfnSubnets.length === 0) {
|
|
90
|
+
throw new Error(`No subnets found with name "${subnetName}" in VPC construct. ` +
|
|
91
|
+
`Available: ${Array.from(vpcConstruct.subnetsByName.keys()).join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
// Build subnets with routeTableId from VpcConstruct
|
|
94
|
+
const subnetIds = [];
|
|
95
|
+
const subnets = [];
|
|
96
|
+
const azs = [];
|
|
97
|
+
const routeTableIds = [];
|
|
98
|
+
// Find matching subnet keys from vpcConstruct.subnets map
|
|
99
|
+
vpcConstruct.subnets.forEach((cfnSubnet, subnetKey) => {
|
|
100
|
+
if (subnetKey.startsWith(`${subnetName}-`)) {
|
|
101
|
+
const az = subnetKey.replace(`${subnetName}-`, '');
|
|
102
|
+
const routeTableId = vpcConstruct.subnetRouteTableMap.get(subnetKey);
|
|
103
|
+
subnetIds.push(cfnSubnet.ref);
|
|
104
|
+
azs.push(cfnSubnet.availabilityZone);
|
|
105
|
+
if (routeTableId)
|
|
106
|
+
routeTableIds.push(routeTableId);
|
|
107
|
+
subnets.push(ec2.Subnet.fromSubnetAttributes(this, `VpcRefSubnet-${az}`, {
|
|
108
|
+
subnetId: cfnSubnet.ref,
|
|
109
|
+
availabilityZone: cfnSubnet.availabilityZone,
|
|
110
|
+
routeTableId,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
const hasRtIds = routeTableIds.length === subnetIds.length;
|
|
115
|
+
const vpc = ec2.Vpc.fromVpcAttributes(this, 'VpcRefVpc', {
|
|
116
|
+
vpcId: vpcConstruct.vpc.ref,
|
|
117
|
+
availabilityZones: azs,
|
|
118
|
+
publicSubnetIds: config.scheme === 'internet-facing' ? subnetIds : undefined,
|
|
119
|
+
publicSubnetRouteTableIds: config.scheme === 'internet-facing' && hasRtIds ? routeTableIds : undefined,
|
|
120
|
+
privateSubnetIds: config.scheme === 'internal' ? subnetIds : undefined,
|
|
121
|
+
privateSubnetRouteTableIds: config.scheme === 'internal' && hasRtIds ? routeTableIds : undefined,
|
|
122
|
+
});
|
|
123
|
+
return { vpc, subnets };
|
|
124
|
+
}
|
|
125
|
+
// ==========================================
|
|
126
|
+
// Security Group
|
|
127
|
+
// ==========================================
|
|
128
|
+
createSecurityGroup(vpc, config) {
|
|
129
|
+
const security = config.security ?? {};
|
|
130
|
+
const isInternetFacing = config.scheme === 'internet-facing';
|
|
131
|
+
const sg = new ec2.SecurityGroup(this, 'AlbSecurityGroup', {
|
|
132
|
+
vpc,
|
|
133
|
+
securityGroupName: config.securityGroupName,
|
|
134
|
+
description: `Security group for ${config.albName} ALB (${config.scheme})`,
|
|
135
|
+
allowAllOutbound: true,
|
|
136
|
+
});
|
|
137
|
+
// HTTP Ingress
|
|
138
|
+
const allowHttp = security.allowHttpFromAnywhere ?? isInternetFacing;
|
|
139
|
+
if (allowHttp) {
|
|
140
|
+
const httpPort = config.listener?.httpPort ?? 80;
|
|
141
|
+
sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(httpPort), `Allow HTTP from anywhere (port ${httpPort})`);
|
|
142
|
+
}
|
|
143
|
+
// HTTPS Ingress
|
|
144
|
+
const allowHttps = security.allowHttpsFromAnywhere ?? isInternetFacing;
|
|
145
|
+
if (allowHttps) {
|
|
146
|
+
const httpsPort = config.listener?.httpsPort ?? 443;
|
|
147
|
+
sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(httpsPort), `Allow HTTPS from anywhere (port ${httpsPort})`);
|
|
148
|
+
}
|
|
149
|
+
// Additional CIDRs
|
|
150
|
+
if (security.allowFromCidrs) {
|
|
151
|
+
security.allowFromCidrs.forEach((cidr, index) => {
|
|
152
|
+
sg.addIngressRule(ec2.Peer.ipv4(cidr), ec2.Port.allTraffic(), `Allow from additional CIDR ${index + 1}: ${cidr}`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// Additional Security Groups
|
|
156
|
+
if (security.allowFromSecurityGroupIds) {
|
|
157
|
+
security.allowFromSecurityGroupIds.forEach((sgId, index) => {
|
|
158
|
+
sg.addIngressRule(ec2.Peer.securityGroupId(sgId), ec2.Port.allTraffic(), `Allow from additional SG ${index + 1}: ${sgId}`);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return sg;
|
|
162
|
+
}
|
|
163
|
+
// ==========================================
|
|
164
|
+
// Create ALB
|
|
165
|
+
// ==========================================
|
|
166
|
+
createAlb(config, vpc, subnets) {
|
|
167
|
+
const security = config.security ?? {};
|
|
168
|
+
return new elbv2.ApplicationLoadBalancer(this, 'ApplicationLoadBalancer', {
|
|
169
|
+
vpc,
|
|
170
|
+
vpcSubnets: { subnets },
|
|
171
|
+
loadBalancerName: config.albName,
|
|
172
|
+
internetFacing: config.scheme === 'internet-facing',
|
|
173
|
+
securityGroup: this.securityGroup,
|
|
174
|
+
deletionProtection: config.deletionProtection ?? false,
|
|
175
|
+
idleTimeout: cdk.Duration.seconds(security.idleTimeoutSeconds ?? 60),
|
|
176
|
+
dropInvalidHeaderFields: security.dropInvalidHeaderFields ?? true,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// ==========================================
|
|
180
|
+
// Create Listeners
|
|
181
|
+
// ==========================================
|
|
182
|
+
createListeners(config) {
|
|
183
|
+
const listenerConfig = config.listener ?? {};
|
|
184
|
+
let httpListener;
|
|
185
|
+
let httpsListener;
|
|
186
|
+
// HTTPS Listener
|
|
187
|
+
const httpsEnabled = listenerConfig.httpsEnabled ?? true;
|
|
188
|
+
if (httpsEnabled) {
|
|
189
|
+
if (!listenerConfig.certificateArn) {
|
|
190
|
+
throw new Error('HTTPS listener requires certificateArn');
|
|
191
|
+
}
|
|
192
|
+
const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate', listenerConfig.certificateArn);
|
|
193
|
+
httpsListener = this.alb.addListener('HttpsListener', {
|
|
194
|
+
port: listenerConfig.httpsPort ?? 443,
|
|
195
|
+
protocol: elbv2.ApplicationProtocol.HTTPS,
|
|
196
|
+
certificates: [certificate],
|
|
197
|
+
sslPolicy: listenerConfig.sslPolicy ?? elbv2.SslPolicy.RECOMMENDED_TLS,
|
|
198
|
+
defaultAction: elbv2.ListenerAction.fixedResponse(404, {
|
|
199
|
+
contentType: 'text/plain',
|
|
200
|
+
messageBody: 'Not Found',
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
// /health path → 200 OK (สำหรับ NLB health check)
|
|
204
|
+
httpsListener.addAction('HealthCheck', {
|
|
205
|
+
priority: 1,
|
|
206
|
+
conditions: [elbv2.ListenerCondition.pathPatterns(['/health'])],
|
|
207
|
+
action: elbv2.ListenerAction.fixedResponse(200, {
|
|
208
|
+
contentType: 'text/plain',
|
|
209
|
+
messageBody: 'OK',
|
|
210
|
+
}),
|
|
211
|
+
});
|
|
212
|
+
// Additional certificates
|
|
213
|
+
if (listenerConfig.additionalCertificateArns) {
|
|
214
|
+
listenerConfig.additionalCertificateArns.forEach((certArn, index) => {
|
|
215
|
+
const additionalCert = acm.Certificate.fromCertificateArn(this, `AdditionalCertificate${index + 1}`, certArn);
|
|
216
|
+
httpsListener.addCertificates(`AdditionalCerts${index + 1}`, [additionalCert]);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// HTTP Listener
|
|
221
|
+
const httpEnabled = listenerConfig.httpEnabled ?? true;
|
|
222
|
+
if (httpEnabled) {
|
|
223
|
+
const httpRedirectToHttps = listenerConfig.httpRedirectToHttps ?? httpsEnabled;
|
|
224
|
+
if (httpRedirectToHttps && httpsListener) {
|
|
225
|
+
httpListener = this.alb.addListener('HttpListener', {
|
|
226
|
+
port: listenerConfig.httpPort ?? 80,
|
|
227
|
+
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
228
|
+
defaultAction: elbv2.ListenerAction.redirect({
|
|
229
|
+
protocol: 'HTTPS',
|
|
230
|
+
port: String(listenerConfig.httpsPort ?? 443),
|
|
231
|
+
permanent: true,
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
httpListener = this.alb.addListener('HttpListener', {
|
|
237
|
+
port: listenerConfig.httpPort ?? 80,
|
|
238
|
+
protocol: elbv2.ApplicationProtocol.HTTP,
|
|
239
|
+
defaultAction: elbv2.ListenerAction.fixedResponse(404, {
|
|
240
|
+
contentType: 'text/plain',
|
|
241
|
+
messageBody: 'Not Found',
|
|
242
|
+
}),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// /health path → 200 OK (สำหรับ NLB health check)
|
|
246
|
+
httpListener.addAction('HealthCheck', {
|
|
247
|
+
priority: 1,
|
|
248
|
+
conditions: [elbv2.ListenerCondition.pathPatterns(['/health'])],
|
|
249
|
+
action: elbv2.ListenerAction.fixedResponse(200, {
|
|
250
|
+
contentType: 'text/plain',
|
|
251
|
+
messageBody: 'OK',
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return { httpListener, httpsListener };
|
|
256
|
+
}
|
|
257
|
+
// ==========================================
|
|
258
|
+
// Outputs
|
|
259
|
+
// ==========================================
|
|
260
|
+
createOutputs(config) {
|
|
261
|
+
const stack = cdk.Stack.of(this);
|
|
262
|
+
const prefix = config.stackName;
|
|
263
|
+
new cdk.CfnOutput(stack, 'AlbArn', {
|
|
264
|
+
value: this.alb.loadBalancerArn,
|
|
265
|
+
description: 'Application Load Balancer ARN',
|
|
266
|
+
exportName: `${prefix}-ALB-Arn`,
|
|
267
|
+
});
|
|
268
|
+
new cdk.CfnOutput(stack, 'AlbDnsName', {
|
|
269
|
+
value: this.alb.loadBalancerDnsName,
|
|
270
|
+
description: 'ALB DNS Name',
|
|
271
|
+
exportName: `${prefix}-ALB-DnsName`,
|
|
272
|
+
});
|
|
273
|
+
new cdk.CfnOutput(stack, 'AlbCanonicalHostedZoneId', {
|
|
274
|
+
value: this.alb.loadBalancerCanonicalHostedZoneId,
|
|
275
|
+
description: 'ALB Canonical Hosted Zone ID (for Route53 alias records)',
|
|
276
|
+
exportName: `${prefix}-ALB-HostedZoneId`,
|
|
277
|
+
});
|
|
278
|
+
new cdk.CfnOutput(stack, 'SecurityGroupId', {
|
|
279
|
+
value: this.securityGroup.securityGroupId,
|
|
280
|
+
description: 'ALB Security Group ID',
|
|
281
|
+
exportName: `${prefix}-ALB-SecurityGroup-Id`,
|
|
282
|
+
});
|
|
283
|
+
if (this.httpListener) {
|
|
284
|
+
new cdk.CfnOutput(stack, 'HttpListenerArn', {
|
|
285
|
+
value: this.httpListener.listenerArn,
|
|
286
|
+
description: 'HTTP Listener ARN',
|
|
287
|
+
exportName: `${prefix}-ALB-HTTPListener-Arn`,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (this.httpsListener) {
|
|
291
|
+
new cdk.CfnOutput(stack, 'HttpsListenerArn', {
|
|
292
|
+
value: this.httpsListener.listenerArn,
|
|
293
|
+
description: 'HTTPS Listener ARN',
|
|
294
|
+
exportName: `${prefix}-ALB-HTTPSListener-Arn`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
new cdk.CfnOutput(stack, 'AlbFullName', {
|
|
298
|
+
value: this.alb.loadBalancerFullName,
|
|
299
|
+
description: 'ALB Full Name (for CloudWatch metrics)',
|
|
300
|
+
exportName: `${prefix}-ALB-FullName`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
exports.AlbConstruct = AlbConstruct;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
|
|
3
|
+
import { BastionConfig } from '../interfaces/bastion-config';
|
|
4
|
+
import { VpcConstruct } from './vpc';
|
|
5
|
+
import { EfsConstruct } from './efs';
|
|
6
|
+
import { RdsConstruct } from './rds';
|
|
7
|
+
import { ElastiCacheConstruct } from './elasticache';
|
|
8
|
+
export interface BastionConstructProps {
|
|
9
|
+
config: BastionConfig;
|
|
10
|
+
/** VpcConstruct from NetworkStack */
|
|
11
|
+
vpcConstruct: VpcConstruct;
|
|
12
|
+
/** EfsConstruct (optional) - สำหรับ mount EFS */
|
|
13
|
+
efsConstruct?: EfsConstruct;
|
|
14
|
+
/** RdsConstruct (optional) - สำหรับเปิด ingress rule */
|
|
15
|
+
rdsConstruct?: RdsConstruct;
|
|
16
|
+
/** ElastiCacheConstruct (optional) - สำหรับเปิด ingress rule */
|
|
17
|
+
elastiCacheConstruct?: ElastiCacheConstruct;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Bastion Host Construct - สร้าง EC2 Bastion Host
|
|
21
|
+
*
|
|
22
|
+
* Features:
|
|
23
|
+
* - SSH Key Pair (สร้างใหม่ หรือใช้ existing)
|
|
24
|
+
* - Auto-mount EFS (optional)
|
|
25
|
+
* - Auto allow RDS security group ingress (optional)
|
|
26
|
+
* - Install MySQL/MariaDB client
|
|
27
|
+
* - SSM Session Manager support
|
|
28
|
+
*
|
|
29
|
+
* Key Pair จะถูกเก็บใน AWS Systems Manager Parameter Store
|
|
30
|
+
* ดึง private key: aws ssm get-parameter --name /ec2/keypair/{key-pair-id} --with-decryption
|
|
31
|
+
*/
|
|
32
|
+
export declare class BastionConstruct extends Construct {
|
|
33
|
+
readonly instance: ec2.Instance;
|
|
34
|
+
readonly securityGroup: ec2.SecurityGroup;
|
|
35
|
+
readonly keyPair: ec2.KeyPair | undefined;
|
|
36
|
+
private readonly removalPolicy;
|
|
37
|
+
private readonly resolvedRegion;
|
|
38
|
+
constructor(scope: Construct, id: string, props: BastionConstructProps);
|
|
39
|
+
private resolveVpc;
|
|
40
|
+
private createKeyPair;
|
|
41
|
+
private createSecurityGroup;
|
|
42
|
+
private buildUserData;
|
|
43
|
+
private createInstance;
|
|
44
|
+
private setupSecurityAccess;
|
|
45
|
+
private createOutputs;
|
|
46
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
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.BastionConstruct = 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 iam = __importStar(require("aws-cdk-lib/aws-iam"));
|
|
42
|
+
/**
|
|
43
|
+
* Bastion Host Construct - สร้าง EC2 Bastion Host
|
|
44
|
+
*
|
|
45
|
+
* Features:
|
|
46
|
+
* - SSH Key Pair (สร้างใหม่ หรือใช้ existing)
|
|
47
|
+
* - Auto-mount EFS (optional)
|
|
48
|
+
* - Auto allow RDS security group ingress (optional)
|
|
49
|
+
* - Install MySQL/MariaDB client
|
|
50
|
+
* - SSM Session Manager support
|
|
51
|
+
*
|
|
52
|
+
* Key Pair จะถูกเก็บใน AWS Systems Manager Parameter Store
|
|
53
|
+
* ดึง private key: aws ssm get-parameter --name /ec2/keypair/{key-pair-id} --with-decryption
|
|
54
|
+
*/
|
|
55
|
+
class BastionConstruct extends constructs_1.Construct {
|
|
56
|
+
constructor(scope, id, props) {
|
|
57
|
+
super(scope, id);
|
|
58
|
+
const { config, vpcConstruct, efsConstruct, rdsConstruct, elastiCacheConstruct } = props;
|
|
59
|
+
// Resolve region: ใช้ config.region ถ้ามี, ไม่งั้น fallback ไป Stack region
|
|
60
|
+
this.resolvedRegion = config.region ?? cdk.Stack.of(this).region;
|
|
61
|
+
this.removalPolicy = config.removalPolicy === 'retain'
|
|
62
|
+
? aws_cdk_lib_1.RemovalPolicy.RETAIN
|
|
63
|
+
: aws_cdk_lib_1.RemovalPolicy.DESTROY;
|
|
64
|
+
// A. Resolve VPC & Subnets
|
|
65
|
+
const { vpc, subnets } = this.resolveVpc(config, vpcConstruct);
|
|
66
|
+
// B. Create Key Pair
|
|
67
|
+
this.keyPair = this.createKeyPair(config);
|
|
68
|
+
// C. Create Security Group
|
|
69
|
+
this.securityGroup = this.createSecurityGroup(vpc, config);
|
|
70
|
+
// D. Build User Data
|
|
71
|
+
const userData = this.buildUserData(config, efsConstruct);
|
|
72
|
+
// E. Create EC2 Instance
|
|
73
|
+
this.instance = this.createInstance(config, vpc, subnets, userData);
|
|
74
|
+
// F. Setup cross-stack security (EFS + RDS + Redis ingress)
|
|
75
|
+
this.setupSecurityAccess(config, efsConstruct, rdsConstruct, elastiCacheConstruct);
|
|
76
|
+
// G. Apply Removal Policy
|
|
77
|
+
this.instance.applyRemovalPolicy(this.removalPolicy);
|
|
78
|
+
this.securityGroup.applyRemovalPolicy(this.removalPolicy);
|
|
79
|
+
// H. Outputs
|
|
80
|
+
this.createOutputs(config);
|
|
81
|
+
}
|
|
82
|
+
// ==========================================
|
|
83
|
+
// Resolve VPC
|
|
84
|
+
// ==========================================
|
|
85
|
+
resolveVpc(config, vpcConstruct) {
|
|
86
|
+
const subnetName = config.source.subnetName ?? 'private';
|
|
87
|
+
const cfnSubnets = vpcConstruct.subnetsByName.get(subnetName);
|
|
88
|
+
if (!cfnSubnets || cfnSubnets.length === 0) {
|
|
89
|
+
throw new Error(`No subnets found with name "${subnetName}" in VPC construct. ` +
|
|
90
|
+
`Available: ${Array.from(vpcConstruct.subnetsByName.keys()).join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
// Build subnets with routeTableId from VpcConstruct
|
|
93
|
+
const subnetIds = [];
|
|
94
|
+
const subnets = [];
|
|
95
|
+
const azs = [];
|
|
96
|
+
const routeTableIds = [];
|
|
97
|
+
vpcConstruct.subnets.forEach((cfnSubnet, subnetKey) => {
|
|
98
|
+
if (subnetKey.startsWith(`${subnetName}-`)) {
|
|
99
|
+
const az = subnetKey.replace(`${subnetName}-`, '');
|
|
100
|
+
const routeTableId = vpcConstruct.subnetRouteTableMap.get(subnetKey);
|
|
101
|
+
subnetIds.push(cfnSubnet.ref);
|
|
102
|
+
azs.push(cfnSubnet.availabilityZone);
|
|
103
|
+
if (routeTableId)
|
|
104
|
+
routeTableIds.push(routeTableId);
|
|
105
|
+
subnets.push(ec2.Subnet.fromSubnetAttributes(this, `Subnet-${az}`, {
|
|
106
|
+
subnetId: cfnSubnet.ref,
|
|
107
|
+
availabilityZone: cfnSubnet.availabilityZone,
|
|
108
|
+
routeTableId,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
const isPublic = config.associatePublicIpAddress === true;
|
|
113
|
+
const hasRtIds = routeTableIds.length === subnetIds.length;
|
|
114
|
+
const vpc = ec2.Vpc.fromVpcAttributes(this, 'Vpc', {
|
|
115
|
+
vpcId: vpcConstruct.vpc.ref,
|
|
116
|
+
availabilityZones: azs,
|
|
117
|
+
...(isPublic
|
|
118
|
+
? { publicSubnetIds: subnetIds, publicSubnetRouteTableIds: hasRtIds ? routeTableIds : undefined }
|
|
119
|
+
: { privateSubnetIds: subnetIds, privateSubnetRouteTableIds: hasRtIds ? routeTableIds : undefined }),
|
|
120
|
+
});
|
|
121
|
+
return { vpc, subnets };
|
|
122
|
+
}
|
|
123
|
+
// ==========================================
|
|
124
|
+
// Key Pair
|
|
125
|
+
// ==========================================
|
|
126
|
+
createKeyPair(config) {
|
|
127
|
+
if (config.createKeyPair === false && config.keyPairName) {
|
|
128
|
+
// ใช้ existing key pair
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const keyPairName = config.keyPairName ?? `${config.instanceName}-keypair`;
|
|
132
|
+
const keyPair = new ec2.KeyPair(this, 'KeyPair', {
|
|
133
|
+
keyPairName,
|
|
134
|
+
type: ec2.KeyPairType.RSA,
|
|
135
|
+
format: ec2.KeyPairFormat.PEM,
|
|
136
|
+
});
|
|
137
|
+
keyPair.applyRemovalPolicy(this.removalPolicy);
|
|
138
|
+
return keyPair;
|
|
139
|
+
}
|
|
140
|
+
// ==========================================
|
|
141
|
+
// Security Group
|
|
142
|
+
// ==========================================
|
|
143
|
+
createSecurityGroup(vpc, config) {
|
|
144
|
+
const sg = new ec2.SecurityGroup(this, 'BastionSecurityGroup', {
|
|
145
|
+
vpc,
|
|
146
|
+
securityGroupName: config.securityGroupName ?? `${config.instanceName}-sg`,
|
|
147
|
+
description: `Security group for ${config.instanceName} Bastion Host`,
|
|
148
|
+
allowAllOutbound: true, // Bastion ต้องการ outbound สำหรับ yum/apt, SSM, etc.
|
|
149
|
+
});
|
|
150
|
+
// เปิด SSH จาก CIDR ที่กำหนด (ถ้ามี)
|
|
151
|
+
if (config.allowedSshCidrs && config.allowedSshCidrs.length > 0) {
|
|
152
|
+
for (const cidr of config.allowedSshCidrs) {
|
|
153
|
+
sg.addIngressRule(ec2.Peer.ipv4(cidr), ec2.Port.tcp(22), `Allow SSH from ${cidr}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return sg;
|
|
157
|
+
}
|
|
158
|
+
// ==========================================
|
|
159
|
+
// User Data
|
|
160
|
+
// ==========================================
|
|
161
|
+
buildUserData(config, efsConstruct) {
|
|
162
|
+
const userData = ec2.UserData.forLinux();
|
|
163
|
+
// Base packages
|
|
164
|
+
userData.addCommands('#!/bin/bash', 'set -euxo pipefail', '', '# Update system', 'dnf update -y', '', '# Install essential tools', 'dnf install -y amazon-efs-utils nfs-utils htop jq unzip');
|
|
165
|
+
// Install MySQL/MariaDB client
|
|
166
|
+
if (config.installMysqlClient !== false) {
|
|
167
|
+
userData.addCommands('', '# Install MariaDB client', 'dnf install -y mariadb105');
|
|
168
|
+
}
|
|
169
|
+
// Mount EFS
|
|
170
|
+
if (config.efsMount && efsConstruct) {
|
|
171
|
+
const mountPath = config.efsMount.mountPath ?? '/mnt/efs';
|
|
172
|
+
userData.addCommands('', `# Mount EFS at ${mountPath}`, `mkdir -p ${mountPath}`, `echo "${efsConstruct.fileSystem.fileSystemId}:/ ${mountPath} efs _netdev,tls,iam 0 0" >> /etc/fstab`, `mount -a`, `chmod 755 ${mountPath}`, '', '# Create uploads directory with www-data permissions', `mkdir -p ${mountPath}/uploads`, `chown 33:33 ${mountPath}/uploads`, `chmod 755 ${mountPath}/uploads`);
|
|
173
|
+
}
|
|
174
|
+
// Additional user data
|
|
175
|
+
if (config.additionalUserData) {
|
|
176
|
+
userData.addCommands('', '# Additional user data', config.additionalUserData);
|
|
177
|
+
}
|
|
178
|
+
// Completion marker
|
|
179
|
+
userData.addCommands('', '# Signal completion', 'echo "Bastion setup completed at $(date)" > /var/log/bastion-setup.log');
|
|
180
|
+
return userData;
|
|
181
|
+
}
|
|
182
|
+
// ==========================================
|
|
183
|
+
// EC2 Instance
|
|
184
|
+
// ==========================================
|
|
185
|
+
createInstance(config, vpc, subnets, userData) {
|
|
186
|
+
// Instance type
|
|
187
|
+
const instanceTypeStr = config.instanceType ?? 't3.micro';
|
|
188
|
+
const [family, size] = instanceTypeStr.split('.');
|
|
189
|
+
const instanceType = ec2.InstanceType.of(family, size);
|
|
190
|
+
// AMI - Amazon Linux 2023
|
|
191
|
+
const machineImage = config.amiId
|
|
192
|
+
? ec2.MachineImage.genericLinux({ [this.resolvedRegion]: config.amiId })
|
|
193
|
+
: ec2.MachineImage.latestAmazonLinux2023();
|
|
194
|
+
// IAM Role สำหรับ SSM Session Manager + EFS
|
|
195
|
+
const role = new iam.Role(this, 'BastionRole', {
|
|
196
|
+
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
|
|
197
|
+
managedPolicies: [
|
|
198
|
+
// SSM Session Manager - เข้า bastion ผ่าน console ได้โดยไม่ต้องเปิด SSH
|
|
199
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
|
|
200
|
+
// EFS access
|
|
201
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonElasticFileSystemClientReadWriteAccess'),
|
|
202
|
+
],
|
|
203
|
+
description: `Role for ${config.instanceName} Bastion Host`,
|
|
204
|
+
});
|
|
205
|
+
// Key pair reference
|
|
206
|
+
let keyName;
|
|
207
|
+
if (this.keyPair) {
|
|
208
|
+
keyName = this.keyPair.keyPairName;
|
|
209
|
+
}
|
|
210
|
+
else if (config.keyPairName) {
|
|
211
|
+
keyName = config.keyPairName;
|
|
212
|
+
}
|
|
213
|
+
const isPublic = config.associatePublicIpAddress === true;
|
|
214
|
+
const instance = new ec2.Instance(this, 'BastionInstance', {
|
|
215
|
+
vpc,
|
|
216
|
+
vpcSubnets: isPublic
|
|
217
|
+
? { subnetType: ec2.SubnetType.PUBLIC }
|
|
218
|
+
: { subnets: [subnets[0]] },
|
|
219
|
+
instanceType,
|
|
220
|
+
machineImage,
|
|
221
|
+
securityGroup: this.securityGroup,
|
|
222
|
+
keyPair: this.keyPair,
|
|
223
|
+
role,
|
|
224
|
+
userData,
|
|
225
|
+
instanceName: config.instanceName,
|
|
226
|
+
associatePublicIpAddress: isPublic,
|
|
227
|
+
blockDevices: [
|
|
228
|
+
{
|
|
229
|
+
deviceName: '/dev/xvda',
|
|
230
|
+
volume: ec2.BlockDeviceVolume.ebs(config.volumeSize ?? 20, {
|
|
231
|
+
volumeType: config.volumeType === 'gp2'
|
|
232
|
+
? ec2.EbsDeviceVolumeType.GP2
|
|
233
|
+
: ec2.EbsDeviceVolumeType.GP3,
|
|
234
|
+
encrypted: true,
|
|
235
|
+
}),
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
return instance;
|
|
240
|
+
}
|
|
241
|
+
// ==========================================
|
|
242
|
+
// Cross-Stack Security Access
|
|
243
|
+
// ==========================================
|
|
244
|
+
setupSecurityAccess(config, efsConstruct, rdsConstruct, elastiCacheConstruct) {
|
|
245
|
+
// Allow Bastion → EFS (NFS port 2049)
|
|
246
|
+
// สร้าง ingress rule ใน Bastion stack เพื่อหลีกเลี่ยง cyclic dependency
|
|
247
|
+
if (efsConstruct) {
|
|
248
|
+
new ec2.CfnSecurityGroupIngress(this, 'EfsNfsIngress', {
|
|
249
|
+
groupId: efsConstruct.securityGroup.securityGroupId,
|
|
250
|
+
sourceSecurityGroupId: this.securityGroup.securityGroupId,
|
|
251
|
+
ipProtocol: 'tcp',
|
|
252
|
+
fromPort: 2049,
|
|
253
|
+
toPort: 2049,
|
|
254
|
+
description: `Allow NFS from ${config.instanceName} Bastion`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// Allow Bastion → RDS (MySQL/MariaDB port)
|
|
258
|
+
if (rdsConstruct) {
|
|
259
|
+
const dbPort = config.rdsAccess?.port ?? 3306;
|
|
260
|
+
new ec2.CfnSecurityGroupIngress(this, 'RdsDbIngress', {
|
|
261
|
+
groupId: rdsConstruct.securityGroup.securityGroupId,
|
|
262
|
+
sourceSecurityGroupId: this.securityGroup.securityGroupId,
|
|
263
|
+
ipProtocol: 'tcp',
|
|
264
|
+
fromPort: dbPort,
|
|
265
|
+
toPort: dbPort,
|
|
266
|
+
description: `Allow DB access from ${config.instanceName} Bastion`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
// Allow Bastion → Redis (port 6379)
|
|
270
|
+
if (elastiCacheConstruct) {
|
|
271
|
+
const redisPort = config.redisAccess?.port ?? 6379;
|
|
272
|
+
new ec2.CfnSecurityGroupIngress(this, 'RedisIngress', {
|
|
273
|
+
groupId: elastiCacheConstruct.securityGroup.securityGroupId,
|
|
274
|
+
sourceSecurityGroupId: this.securityGroup.securityGroupId,
|
|
275
|
+
ipProtocol: 'tcp',
|
|
276
|
+
fromPort: redisPort,
|
|
277
|
+
toPort: redisPort,
|
|
278
|
+
description: `Allow Redis access from ${config.instanceName} Bastion`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// ==========================================
|
|
283
|
+
// Outputs
|
|
284
|
+
// ==========================================
|
|
285
|
+
createOutputs(config) {
|
|
286
|
+
new cdk.CfnOutput(this, 'InstanceId', {
|
|
287
|
+
value: this.instance.instanceId,
|
|
288
|
+
description: 'Bastion EC2 Instance ID',
|
|
289
|
+
exportName: `${config.stackName}-InstanceId`,
|
|
290
|
+
});
|
|
291
|
+
new cdk.CfnOutput(this, 'PrivateIp', {
|
|
292
|
+
value: this.instance.instancePrivateIp,
|
|
293
|
+
description: 'Bastion Private IP Address',
|
|
294
|
+
exportName: `${config.stackName}-PrivateIp`,
|
|
295
|
+
});
|
|
296
|
+
if (config.associatePublicIpAddress) {
|
|
297
|
+
new cdk.CfnOutput(this, 'PublicIp', {
|
|
298
|
+
value: this.instance.instancePublicIp,
|
|
299
|
+
description: 'Bastion Public IP Address',
|
|
300
|
+
exportName: `${config.stackName}-PublicIp`,
|
|
301
|
+
});
|
|
302
|
+
new cdk.CfnOutput(this, 'SshCommand', {
|
|
303
|
+
value: `ssh -i standard-bastion.pem ec2-user@<PUBLIC_IP>`,
|
|
304
|
+
description: 'SSH command to connect to Bastion (replace <PUBLIC_IP> with actual IP)',
|
|
305
|
+
exportName: `${config.stackName}-SshCommand`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
new cdk.CfnOutput(this, 'SecurityGroupId', {
|
|
309
|
+
value: this.securityGroup.securityGroupId,
|
|
310
|
+
description: 'Bastion Security Group ID',
|
|
311
|
+
exportName: `${config.stackName}-BastionSecurityGroupId`,
|
|
312
|
+
});
|
|
313
|
+
if (this.keyPair) {
|
|
314
|
+
new cdk.CfnOutput(this, 'KeyPairId', {
|
|
315
|
+
value: this.keyPair.keyPairId,
|
|
316
|
+
description: 'Key Pair ID - ดึง private key: aws ssm get-parameter --name /ec2/keypair/{this-id} --with-decryption --query Parameter.Value --output text',
|
|
317
|
+
exportName: `${config.stackName}-KeyPairId`,
|
|
318
|
+
});
|
|
319
|
+
new cdk.CfnOutput(this, 'GetPrivateKeyCommand', {
|
|
320
|
+
value: `aws ssm get-parameter --name /ec2/keypair/${this.keyPair.keyPairId} --with-decryption --query Parameter.Value --output text --region ${this.resolvedRegion}`,
|
|
321
|
+
description: 'Command to retrieve SSH private key from SSM Parameter Store',
|
|
322
|
+
exportName: `${config.stackName}-GetPrivateKeyCommand`,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
new cdk.CfnOutput(this, 'SsmConnectCommand', {
|
|
326
|
+
value: `aws ssm start-session --target ${this.instance.instanceId} --region ${this.resolvedRegion}`,
|
|
327
|
+
description: 'Command to connect via SSM Session Manager (no SSH key needed)',
|
|
328
|
+
exportName: `${config.stackName}-SsmConnectCommand`,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
exports.BastionConstruct = BastionConstruct;
|