@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.
Files changed (82) hide show
  1. package/README.md +219 -0
  2. package/dist/aws-cdk/constructs/acm.d.ts +28 -0
  3. package/dist/aws-cdk/constructs/acm.js +239 -0
  4. package/dist/aws-cdk/constructs/alb.d.ts +28 -0
  5. package/dist/aws-cdk/constructs/alb.js +304 -0
  6. package/dist/aws-cdk/constructs/bastion.d.ts +46 -0
  7. package/dist/aws-cdk/constructs/bastion.js +332 -0
  8. package/dist/aws-cdk/constructs/cloudfront.d.ts +45 -0
  9. package/dist/aws-cdk/constructs/cloudfront.js +261 -0
  10. package/dist/aws-cdk/constructs/ecr.d.ts +17 -0
  11. package/dist/aws-cdk/constructs/ecr.js +143 -0
  12. package/dist/aws-cdk/constructs/ecs-cluster.d.ts +21 -0
  13. package/dist/aws-cdk/constructs/ecs-cluster.js +124 -0
  14. package/dist/aws-cdk/constructs/ecs-service.d.ts +72 -0
  15. package/dist/aws-cdk/constructs/ecs-service.js +682 -0
  16. package/dist/aws-cdk/constructs/efs.d.ts +31 -0
  17. package/dist/aws-cdk/constructs/efs.js +241 -0
  18. package/dist/aws-cdk/constructs/elasticache.d.ts +35 -0
  19. package/dist/aws-cdk/constructs/elasticache.js +210 -0
  20. package/dist/aws-cdk/constructs/nacl.d.ts +37 -0
  21. package/dist/aws-cdk/constructs/nacl.js +88 -0
  22. package/dist/aws-cdk/constructs/nlb.d.ts +39 -0
  23. package/dist/aws-cdk/constructs/nlb.js +276 -0
  24. package/dist/aws-cdk/constructs/rds.d.ts +40 -0
  25. package/dist/aws-cdk/constructs/rds.js +320 -0
  26. package/dist/aws-cdk/constructs/self-signed-cert.d.ts +83 -0
  27. package/dist/aws-cdk/constructs/self-signed-cert.js +215 -0
  28. package/dist/aws-cdk/constructs/sqs.d.ts +30 -0
  29. package/dist/aws-cdk/constructs/sqs.js +268 -0
  30. package/dist/aws-cdk/constructs/vpc.d.ts +30 -0
  31. package/dist/aws-cdk/constructs/vpc.js +423 -0
  32. package/dist/aws-cdk/constructs/waf.d.ts +37 -0
  33. package/dist/aws-cdk/constructs/waf.js +350 -0
  34. package/dist/aws-cdk/interfaces/account-config.d.ts +18 -0
  35. package/dist/aws-cdk/interfaces/account-config.js +2 -0
  36. package/dist/aws-cdk/interfaces/acm-config.d.ts +94 -0
  37. package/dist/aws-cdk/interfaces/acm-config.js +14 -0
  38. package/dist/aws-cdk/interfaces/alb-config.d.ts +72 -0
  39. package/dist/aws-cdk/interfaces/alb-config.js +2 -0
  40. package/dist/aws-cdk/interfaces/bastion-config.d.ts +77 -0
  41. package/dist/aws-cdk/interfaces/bastion-config.js +10 -0
  42. package/dist/aws-cdk/interfaces/cloudfront-config.d.ts +154 -0
  43. package/dist/aws-cdk/interfaces/cloudfront-config.js +15 -0
  44. package/dist/aws-cdk/interfaces/ecr-config.d.ts +40 -0
  45. package/dist/aws-cdk/interfaces/ecr-config.js +2 -0
  46. package/dist/aws-cdk/interfaces/ecs-cluster-config.d.ts +30 -0
  47. package/dist/aws-cdk/interfaces/ecs-cluster-config.js +2 -0
  48. package/dist/aws-cdk/interfaces/ecs-service-config.d.ts +237 -0
  49. package/dist/aws-cdk/interfaces/ecs-service-config.js +2 -0
  50. package/dist/aws-cdk/interfaces/efs-config.d.ts +56 -0
  51. package/dist/aws-cdk/interfaces/efs-config.js +7 -0
  52. package/dist/aws-cdk/interfaces/elasticache-config.d.ts +56 -0
  53. package/dist/aws-cdk/interfaces/elasticache-config.js +7 -0
  54. package/dist/aws-cdk/interfaces/nacl-config.d.ts +1 -0
  55. package/dist/aws-cdk/interfaces/nacl-config.js +3 -0
  56. package/dist/aws-cdk/interfaces/nlb-config.d.ts +69 -0
  57. package/dist/aws-cdk/interfaces/nlb-config.js +2 -0
  58. package/dist/aws-cdk/interfaces/rds-config.d.ts +84 -0
  59. package/dist/aws-cdk/interfaces/rds-config.js +7 -0
  60. package/dist/aws-cdk/interfaces/sqs-config.d.ts +145 -0
  61. package/dist/aws-cdk/interfaces/sqs-config.js +12 -0
  62. package/dist/aws-cdk/interfaces/tag-config.d.ts +18 -0
  63. package/dist/aws-cdk/interfaces/tag-config.js +2 -0
  64. package/dist/aws-cdk/interfaces/vpc-config.d.ts +72 -0
  65. package/dist/aws-cdk/interfaces/vpc-config.js +2 -0
  66. package/dist/aws-cdk/interfaces/waf-config.d.ts +180 -0
  67. package/dist/aws-cdk/interfaces/waf-config.js +2 -0
  68. package/dist/aws-cdk/utils/priority-tracker.d.ts +60 -0
  69. package/dist/aws-cdk/utils/priority-tracker.js +131 -0
  70. package/dist/index.d.ts +33 -0
  71. package/dist/index.js +55 -0
  72. package/dist/terraform-cdk/constructs/alb-listener-rule.d.ts +33 -0
  73. package/dist/terraform-cdk/constructs/alb-listener-rule.js +81 -0
  74. package/dist/terraform-cdk/constructs/ecs-service.d.ts +29 -0
  75. package/dist/terraform-cdk/constructs/ecs-service.js +238 -0
  76. package/dist/terraform-cdk/interfaces/ecs-service-config.d.ts +53 -0
  77. package/dist/terraform-cdk/interfaces/ecs-service-config.js +25 -0
  78. package/dist/terraform-cdk/interfaces/infrastructure-refs.d.ts +16 -0
  79. package/dist/terraform-cdk/interfaces/infrastructure-refs.js +8 -0
  80. package/dist/terraform-cdk/utils/priority-tracker.d.ts +60 -0
  81. package/dist/terraform-cdk/utils/priority-tracker.js +131 -0
  82. 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;