@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,276 @@
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.NlbConstruct = 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 elbv2Targets = __importStar(require("aws-cdk-lib/aws-elasticloadbalancingv2-targets"));
43
+ const acm = __importStar(require("aws-cdk-lib/aws-certificatemanager"));
44
+ /**
45
+ * NLB Construct - สร้าง NLB (internet-facing) + Security Group + Listeners
46
+ *
47
+ * Pattern: NLB (internet-facing) → ALB (internal)
48
+ * - NLB SG: allow inbound from internet
49
+ * - ALB SG: auto-add ingress from NLB SG (via CfnSecurityGroupIngress เพื่อหลีกเลี่ยง circular dependency)
50
+ * - Target Group: type ALB, forward traffic ไปที่ internal ALB
51
+ */
52
+ class NlbConstruct extends constructs_1.Construct {
53
+ constructor(scope, id, props) {
54
+ super(scope, id);
55
+ const { config, vpcConstruct, albConstruct } = props;
56
+ this.removalPolicy = config.removalPolicy === 'retain'
57
+ ? aws_cdk_lib_1.RemovalPolicy.RETAIN
58
+ : aws_cdk_lib_1.RemovalPolicy.DESTROY;
59
+ // A. Resolve VPC & Subnets
60
+ const { vpc, subnets } = this.resolveVpc(config, vpcConstruct);
61
+ // B. Create NLB Security Group
62
+ this.securityGroup = this.createSecurityGroup(vpc, config);
63
+ // C. Create NLB
64
+ this.nlb = this.createNlb(config, vpc, subnets);
65
+ // D. Create Listeners + Target Groups (ALB as target)
66
+ this.createListeners(config, albConstruct);
67
+ // E. Add ingress rule to ALB SG from NLB SG
68
+ this.addAlbSgIngress(config, albConstruct);
69
+ // F. Apply Removal Policy
70
+ this.nlb.applyRemovalPolicy(this.removalPolicy);
71
+ this.securityGroup.applyRemovalPolicy(this.removalPolicy);
72
+ // G. Outputs
73
+ this.createOutputs(config);
74
+ }
75
+ // ==========================================
76
+ // Resolve VPC
77
+ // ==========================================
78
+ resolveVpc(config, vpcConstruct) {
79
+ const subnetName = config.source.subnetName ?? 'public';
80
+ const cfnSubnets = vpcConstruct.subnetsByName.get(subnetName);
81
+ if (!cfnSubnets || cfnSubnets.length === 0) {
82
+ throw new Error(`No subnets found with name "${subnetName}" in VPC construct. ` +
83
+ `Available: ${Array.from(vpcConstruct.subnetsByName.keys()).join(', ')}`);
84
+ }
85
+ // Build subnets with routeTableId from VpcConstruct
86
+ const subnetIds = [];
87
+ const subnets = [];
88
+ const azs = [];
89
+ const routeTableIds = [];
90
+ vpcConstruct.subnets.forEach((cfnSubnet, subnetKey) => {
91
+ if (subnetKey.startsWith(`${subnetName}-`)) {
92
+ const az = subnetKey.replace(`${subnetName}-`, '');
93
+ const routeTableId = vpcConstruct.subnetRouteTableMap.get(subnetKey);
94
+ subnetIds.push(cfnSubnet.ref);
95
+ azs.push(cfnSubnet.availabilityZone);
96
+ if (routeTableId)
97
+ routeTableIds.push(routeTableId);
98
+ subnets.push(ec2.Subnet.fromSubnetAttributes(this, `Subnet-${az}`, {
99
+ subnetId: cfnSubnet.ref,
100
+ availabilityZone: cfnSubnet.availabilityZone,
101
+ routeTableId,
102
+ }));
103
+ }
104
+ });
105
+ const hasRtIds = routeTableIds.length === subnetIds.length;
106
+ const vpc = ec2.Vpc.fromVpcAttributes(this, 'Vpc', {
107
+ vpcId: vpcConstruct.vpc.ref,
108
+ availabilityZones: azs,
109
+ publicSubnetIds: subnetIds,
110
+ publicSubnetRouteTableIds: hasRtIds ? routeTableIds : undefined,
111
+ });
112
+ return { vpc, subnets };
113
+ }
114
+ // ==========================================
115
+ // Security Group
116
+ // ==========================================
117
+ createSecurityGroup(vpc, config) {
118
+ const security = config.security ?? {};
119
+ const sg = new ec2.SecurityGroup(this, 'NlbSecurityGroup', {
120
+ vpc,
121
+ securityGroupName: config.securityGroupName,
122
+ description: `Security group for ${config.nlbName} NLB (internet-facing)`,
123
+ allowAllOutbound: true,
124
+ });
125
+ // Allow from anywhere on listener ports (default: true)
126
+ const allowFromAnywhere = security.allowFromAnywhere ?? true;
127
+ if (allowFromAnywhere) {
128
+ config.listeners.forEach((listener) => {
129
+ sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(listener.port), `Allow inbound on port ${listener.port}`);
130
+ });
131
+ }
132
+ // Additional CIDRs
133
+ if (security.allowFromCidrs) {
134
+ security.allowFromCidrs.forEach((cidr, index) => {
135
+ sg.addIngressRule(ec2.Peer.ipv4(cidr), ec2.Port.allTraffic(), `Allow from CIDR ${index + 1}: ${cidr}`);
136
+ });
137
+ }
138
+ return sg;
139
+ }
140
+ // ==========================================
141
+ // Create NLB
142
+ // ==========================================
143
+ createNlb(config, vpc, subnets) {
144
+ return new elbv2.NetworkLoadBalancer(this, 'NetworkLoadBalancer', {
145
+ vpc,
146
+ vpcSubnets: { subnets },
147
+ loadBalancerName: config.nlbName,
148
+ internetFacing: true,
149
+ securityGroups: [this.securityGroup],
150
+ crossZoneEnabled: config.crossZoneEnabled ?? true,
151
+ deletionProtection: config.deletionProtection ?? false,
152
+ });
153
+ }
154
+ // ==========================================
155
+ // Create Listeners + ALB Target Groups
156
+ // ==========================================
157
+ createListeners(config, albConstruct) {
158
+ const healthCheckConfig = config.healthCheck ?? {};
159
+ // Determine health check protocol
160
+ // Target type ALB รองรับเฉพาะ HTTP/HTTPS (ไม่รองรับ TCP)
161
+ let hcProtocol;
162
+ if (healthCheckConfig.protocol === 'HTTPS') {
163
+ hcProtocol = elbv2.Protocol.HTTPS;
164
+ }
165
+ else {
166
+ hcProtocol = elbv2.Protocol.HTTP; // default HTTP สำหรับ ALB target
167
+ }
168
+ config.listeners.forEach((listenerConfig, index) => {
169
+ const targetPort = listenerConfig.targetPort ?? listenerConfig.port;
170
+ // Listener protocol
171
+ const protocol = listenerConfig.protocol === 'TLS'
172
+ ? elbv2.Protocol.TLS
173
+ : elbv2.Protocol.TCP;
174
+ // Certificates for TLS
175
+ const certificates = listenerConfig.protocol === 'TLS' && listenerConfig.certificateArn
176
+ ? [acm.Certificate.fromCertificateArn(this, `Certificate-${index}`, listenerConfig.certificateArn)]
177
+ : undefined;
178
+ // Create listener
179
+ const listener = this.nlb.addListener(`Listener-${index}`, {
180
+ port: listenerConfig.port,
181
+ protocol,
182
+ certificates,
183
+ });
184
+ // เลือก ALB listener ที่ตรงกับ targetPort
185
+ // AlbListenerTarget สร้าง dependency ระหว่าง NLB target group → ALB listener อัตโนมัติ
186
+ const httpsPort = config.listeners.find(l => (l.targetPort ?? l.port) === 443) ? 443 : undefined;
187
+ const albListener = targetPort === 443 && albConstruct.httpsListener
188
+ ? albConstruct.httpsListener
189
+ : targetPort === 80 && albConstruct.httpListener
190
+ ? albConstruct.httpListener
191
+ : albConstruct.httpsListener ?? albConstruct.httpListener;
192
+ if (!albListener) {
193
+ throw new Error(`No ALB listener found for target port ${targetPort}. Ensure ALB has HTTP or HTTPS listener enabled.`);
194
+ }
195
+ // Target group ต้องใช้ TCP เสมอสำหรับ target type ALB
196
+ // (AWS ไม่รองรับ TLS protocol กับ ALB target type)
197
+ // Health check protocol ต้องตรงกับ ALB listener (HTTP/HTTPS)
198
+ const hcProto = targetPort === 443 ? elbv2.Protocol.HTTPS : hcProtocol;
199
+ listener.addTargets(`AlbTarget-${index}`, {
200
+ targets: [new elbv2Targets.AlbListenerTarget(albListener)],
201
+ port: targetPort,
202
+ protocol: elbv2.Protocol.TCP,
203
+ targetGroupName: listenerConfig.targetGroupName,
204
+ healthCheck: {
205
+ enabled: true,
206
+ protocol: hcProto,
207
+ path: healthCheckConfig.path ?? '/',
208
+ port: targetPort.toString(),
209
+ interval: healthCheckConfig.interval
210
+ ? cdk.Duration.seconds(healthCheckConfig.interval)
211
+ : undefined,
212
+ healthyThresholdCount: healthCheckConfig.healthyThresholdCount,
213
+ unhealthyThresholdCount: healthCheckConfig.unhealthyThresholdCount,
214
+ healthyHttpCodes: healthCheckConfig.healthyHttpCodes,
215
+ },
216
+ });
217
+ });
218
+ }
219
+ // ==========================================
220
+ // ALB SG Ingress from NLB SG
221
+ // ==========================================
222
+ /**
223
+ * เพิ่ม ingress rule ใน ALB SG ให้รับ traffic จาก NLB SG
224
+ * ใช้ CfnSecurityGroupIngress (L1) แทน sg.addIngressRule (L2)
225
+ * เพื่อหลีกเลี่ยง circular cross-stack dependency
226
+ *
227
+ * Resource นี้จะอยู่ใน NlbStack → reference AlbStack's SG ID ผ่าน Fn::ImportValue (one-way)
228
+ */
229
+ addAlbSgIngress(config, albConstruct) {
230
+ // Collect unique target ports
231
+ const targetPorts = new Set();
232
+ config.listeners.forEach((listener) => {
233
+ targetPorts.add(listener.targetPort ?? listener.port);
234
+ });
235
+ let index = 0;
236
+ targetPorts.forEach((port) => {
237
+ new ec2.CfnSecurityGroupIngress(this, `AlbSgIngress-${index}`, {
238
+ groupId: albConstruct.securityGroup.securityGroupId,
239
+ sourceSecurityGroupId: this.securityGroup.securityGroupId,
240
+ ipProtocol: 'tcp',
241
+ fromPort: port,
242
+ toPort: port,
243
+ description: `Allow from NLB ${config.nlbName} on port ${port}`,
244
+ });
245
+ index++;
246
+ });
247
+ }
248
+ // ==========================================
249
+ // Outputs
250
+ // ==========================================
251
+ createOutputs(config) {
252
+ const stack = cdk.Stack.of(this);
253
+ const prefix = config.stackName;
254
+ new cdk.CfnOutput(stack, 'NlbArn', {
255
+ value: this.nlb.loadBalancerArn,
256
+ description: 'Network Load Balancer ARN',
257
+ exportName: `${prefix}-NLB-Arn`,
258
+ });
259
+ new cdk.CfnOutput(stack, 'NlbDnsName', {
260
+ value: this.nlb.loadBalancerDnsName,
261
+ description: 'NLB DNS Name',
262
+ exportName: `${prefix}-NLB-DnsName`,
263
+ });
264
+ new cdk.CfnOutput(stack, 'NlbSecurityGroupId', {
265
+ value: this.securityGroup.securityGroupId,
266
+ description: 'NLB Security Group ID',
267
+ exportName: `${prefix}-NLB-SecurityGroup-Id`,
268
+ });
269
+ new cdk.CfnOutput(stack, 'NlbFullName', {
270
+ value: this.nlb.loadBalancerFullName,
271
+ description: 'NLB Full Name (for CloudWatch metrics)',
272
+ exportName: `${prefix}-NLB-FullName`,
273
+ });
274
+ }
275
+ }
276
+ exports.NlbConstruct = NlbConstruct;
@@ -0,0 +1,40 @@
1
+ import { Construct } from 'constructs';
2
+ import * as ec2 from 'aws-cdk-lib/aws-ec2';
3
+ import * as rds from 'aws-cdk-lib/aws-rds';
4
+ import { RdsConfig } from '../interfaces/rds-config';
5
+ import { VpcConstruct } from './vpc';
6
+ export interface RdsConstructProps {
7
+ config: RdsConfig;
8
+ /** VpcConstruct from NetworkStack */
9
+ vpcConstruct: VpcConstruct;
10
+ }
11
+ /**
12
+ * RDS Construct - สร้าง RDS Database Instance
13
+ *
14
+ * Features:
15
+ * - Multi-AZ for high availability
16
+ * - Encryption at rest
17
+ * - Performance Insights & Enhanced Monitoring
18
+ * - Automated backups
19
+ */
20
+ export declare class RdsConstruct extends Construct {
21
+ readonly instance: rds.DatabaseInstance;
22
+ readonly securityGroup: ec2.SecurityGroup;
23
+ readonly subnetGroup: rds.SubnetGroup;
24
+ private readonly removalPolicy;
25
+ constructor(scope: Construct, id: string, props: RdsConstructProps);
26
+ private getRemovalPolicy;
27
+ private resolveVpc;
28
+ private createSecurityGroup;
29
+ private createSubnetGroup;
30
+ private createParameterGroup;
31
+ private getDefaultParameterGroupFamily;
32
+ private getDatabaseEngine;
33
+ private createInstance;
34
+ private getStorageType;
35
+ private createOutputs;
36
+ /**
37
+ * Allow database access from a security group
38
+ */
39
+ allowDbFrom(sg: ec2.ISecurityGroup, port?: number, description?: string): void;
40
+ }
@@ -0,0 +1,320 @@
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.RdsConstruct = 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 rds = __importStar(require("aws-cdk-lib/aws-rds"));
42
+ const secretsmanager = __importStar(require("aws-cdk-lib/aws-secretsmanager"));
43
+ const iam = __importStar(require("aws-cdk-lib/aws-iam"));
44
+ /**
45
+ * RDS Construct - สร้าง RDS Database Instance
46
+ *
47
+ * Features:
48
+ * - Multi-AZ for high availability
49
+ * - Encryption at rest
50
+ * - Performance Insights & Enhanced Monitoring
51
+ * - Automated backups
52
+ */
53
+ class RdsConstruct extends constructs_1.Construct {
54
+ constructor(scope, id, props) {
55
+ super(scope, id);
56
+ const { config, vpcConstruct } = props;
57
+ this.removalPolicy = this.getRemovalPolicy(config.removalPolicy);
58
+ // A. Resolve VPC & Subnets
59
+ const { vpc, subnets } = this.resolveVpc(config, vpcConstruct);
60
+ // B. Create Security Group
61
+ this.securityGroup = this.createSecurityGroup(vpc, config);
62
+ // C. Create Subnet Group
63
+ this.subnetGroup = this.createSubnetGroup(config, subnets);
64
+ // D. Create Parameter Group (optional)
65
+ const parameterGroup = this.createParameterGroup(config);
66
+ // E. Create RDS Instance
67
+ this.instance = this.createInstance(config, vpc, parameterGroup);
68
+ // F. Apply Removal Policy
69
+ this.instance.applyRemovalPolicy(this.removalPolicy);
70
+ this.securityGroup.applyRemovalPolicy(this.removalPolicy);
71
+ // G. Outputs
72
+ this.createOutputs(config);
73
+ }
74
+ getRemovalPolicy(policy) {
75
+ switch (policy) {
76
+ case 'destroy':
77
+ return aws_cdk_lib_1.RemovalPolicy.DESTROY;
78
+ case 'snapshot':
79
+ return aws_cdk_lib_1.RemovalPolicy.SNAPSHOT;
80
+ default:
81
+ return aws_cdk_lib_1.RemovalPolicy.RETAIN;
82
+ }
83
+ }
84
+ // ==========================================
85
+ // Resolve VPC
86
+ // ==========================================
87
+ resolveVpc(config, vpcConstruct) {
88
+ const subnetName = config.source.subnetName ?? 'db';
89
+ const cfnSubnets = vpcConstruct.subnetsByName.get(subnetName);
90
+ if (!cfnSubnets || cfnSubnets.length === 0) {
91
+ throw new Error(`No subnets found with name "${subnetName}" in VPC construct. ` +
92
+ `Available: ${Array.from(vpcConstruct.subnetsByName.keys()).join(', ')}`);
93
+ }
94
+ // Build subnets with routeTableId from VpcConstruct
95
+ const subnetIds = [];
96
+ const subnets = [];
97
+ const azs = [];
98
+ const routeTableIds = [];
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, `Subnet-${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, 'Vpc', {
116
+ vpcId: vpcConstruct.vpc.ref,
117
+ availabilityZones: azs,
118
+ isolatedSubnetIds: subnetIds,
119
+ isolatedSubnetRouteTableIds: hasRtIds ? routeTableIds : undefined,
120
+ });
121
+ return { vpc, subnets };
122
+ }
123
+ // ==========================================
124
+ // Security Group
125
+ // ==========================================
126
+ createSecurityGroup(vpc, config) {
127
+ const sg = new ec2.SecurityGroup(this, 'RdsSecurityGroup', {
128
+ vpc,
129
+ securityGroupName: config.securityGroupName,
130
+ description: `Security group for ${config.instanceIdentifier} RDS`,
131
+ allowAllOutbound: true, // Allow response traffic back to clients (ECS, Bastion)
132
+ });
133
+ // Ingress rules will be added by ECS service construct
134
+ return sg;
135
+ }
136
+ // ==========================================
137
+ // Subnet Group
138
+ // ==========================================
139
+ createSubnetGroup(config, subnets) {
140
+ return new rds.SubnetGroup(this, 'SubnetGroup', {
141
+ description: `Subnet group for ${config.instanceIdentifier} RDS`,
142
+ vpc: ec2.Vpc.fromVpcAttributes(this, 'SubnetGroupVpc', {
143
+ vpcId: cdk.Fn.importValue(`${config.source.vpcStackName}-VpcId`),
144
+ availabilityZones: cdk.Stack.of(this).availabilityZones,
145
+ }),
146
+ vpcSubnets: { subnets },
147
+ subnetGroupName: `${config.instanceIdentifier}-subnet-group`,
148
+ });
149
+ }
150
+ // ==========================================
151
+ // Parameter Group
152
+ // ==========================================
153
+ createParameterGroup(config) {
154
+ if (!config.parameters || Object.keys(config.parameters).length === 0) {
155
+ return undefined;
156
+ }
157
+ const family = config.parameterGroupFamily ?? this.getDefaultParameterGroupFamily(config);
158
+ return new rds.ParameterGroup(this, 'ParameterGroup', {
159
+ engine: this.getDatabaseEngine(config),
160
+ description: `Parameter group for ${config.instanceIdentifier}`,
161
+ parameters: config.parameters,
162
+ });
163
+ }
164
+ getDefaultParameterGroupFamily(config) {
165
+ const version = config.engineVersion.split('.')[0];
166
+ switch (config.engine) {
167
+ case 'mariadb':
168
+ return `mariadb${config.engineVersion}`;
169
+ case 'mysql':
170
+ return `mysql${version}.0`;
171
+ case 'postgres':
172
+ return `postgres${version}`;
173
+ default:
174
+ return `mariadb${config.engineVersion}`;
175
+ }
176
+ }
177
+ getDatabaseEngine(config) {
178
+ switch (config.engine) {
179
+ case 'mysql':
180
+ return rds.DatabaseInstanceEngine.mysql({
181
+ version: rds.MysqlEngineVersion.of(config.engineVersion, config.engineVersion),
182
+ });
183
+ case 'postgres':
184
+ return rds.DatabaseInstanceEngine.postgres({
185
+ version: rds.PostgresEngineVersion.of(config.engineVersion, config.engineVersion),
186
+ });
187
+ case 'mariadb':
188
+ default:
189
+ return rds.DatabaseInstanceEngine.mariaDb({
190
+ version: rds.MariaDbEngineVersion.of(config.engineVersion, config.engineVersion),
191
+ });
192
+ }
193
+ }
194
+ // ==========================================
195
+ // RDS Instance
196
+ // ==========================================
197
+ createInstance(config, vpc, parameterGroup) {
198
+ const port = config.port ?? (config.engine === 'postgres' ? 5432 : 3306);
199
+ // Get credentials
200
+ let credentials;
201
+ if (config.masterPasswordSecretArn) {
202
+ const secret = secretsmanager.Secret.fromSecretCompleteArn(this, 'MasterPasswordSecret', config.masterPasswordSecretArn);
203
+ credentials = rds.Credentials.fromSecret(secret, config.masterUsername);
204
+ }
205
+ else if (config.masterPassword) {
206
+ credentials = rds.Credentials.fromPassword(config.masterUsername, cdk.SecretValue.unsafePlainText(config.masterPassword));
207
+ }
208
+ else {
209
+ // Generate new secret
210
+ credentials = rds.Credentials.fromGeneratedSecret(config.masterUsername, {
211
+ secretName: `${config.instanceIdentifier}/master-credentials`,
212
+ });
213
+ }
214
+ // Determine instance class
215
+ const instanceClass = config.instanceClass ?? 'db.t3.medium';
216
+ const [instanceFamily, instanceSize] = instanceClass.replace('db.', '').split('.');
217
+ const instanceType = ec2.InstanceType.of(instanceFamily, instanceSize);
218
+ // Monitoring role for Enhanced Monitoring
219
+ let monitoringRole;
220
+ if (config.enhancedMonitoringEnabled ?? true) {
221
+ monitoringRole = new iam.Role(this, 'MonitoringRole', {
222
+ assumedBy: new iam.ServicePrincipal('monitoring.rds.amazonaws.com'),
223
+ managedPolicies: [
224
+ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonRDSEnhancedMonitoringRole'),
225
+ ],
226
+ });
227
+ }
228
+ const instance = new rds.DatabaseInstance(this, 'Instance', {
229
+ engine: this.getDatabaseEngine(config),
230
+ instanceType,
231
+ vpc,
232
+ vpcSubnets: { subnets: this.subnetGroup.node.tryFindChild('SubnetGroup') },
233
+ subnetGroup: this.subnetGroup,
234
+ securityGroups: [this.securityGroup],
235
+ credentials,
236
+ databaseName: config.databaseName,
237
+ instanceIdentifier: config.instanceIdentifier,
238
+ port,
239
+ allocatedStorage: config.allocatedStorage ?? 20,
240
+ maxAllocatedStorage: config.maxAllocatedStorage,
241
+ storageType: this.getStorageType(config.storageType),
242
+ iops: config.iops,
243
+ storageThroughput: config.storageThroughput,
244
+ storageEncrypted: config.storageEncrypted ?? true,
245
+ multiAz: config.multiAz ?? true,
246
+ publiclyAccessible: config.publiclyAccessible ?? false,
247
+ deletionProtection: config.deletionProtection ?? true,
248
+ backupRetention: cdk.Duration.days(config.backupRetentionPeriod ?? 7),
249
+ preferredBackupWindow: config.preferredBackupWindow ?? '03:00-04:00',
250
+ preferredMaintenanceWindow: config.preferredMaintenanceWindow ?? 'sun:04:00-sun:05:00',
251
+ enablePerformanceInsights: config.performanceInsightsEnabled ?? true,
252
+ performanceInsightRetention: (config.performanceInsightsEnabled ?? true)
253
+ ? rds.PerformanceInsightRetention.DEFAULT
254
+ : undefined,
255
+ monitoringInterval: (config.enhancedMonitoringEnabled ?? true)
256
+ ? cdk.Duration.seconds(config.monitoringInterval ?? 60)
257
+ : undefined,
258
+ monitoringRole: (config.enhancedMonitoringEnabled ?? true) ? monitoringRole : undefined,
259
+ autoMinorVersionUpgrade: config.autoMinorVersionUpgrade ?? true,
260
+ parameterGroup,
261
+ });
262
+ return instance;
263
+ }
264
+ getStorageType(type) {
265
+ switch (type) {
266
+ case 'gp2':
267
+ return rds.StorageType.GP2;
268
+ case 'io1':
269
+ return rds.StorageType.IO1;
270
+ case 'io2':
271
+ return rds.StorageType.IO2;
272
+ case 'gp3':
273
+ default:
274
+ return rds.StorageType.GP3;
275
+ }
276
+ }
277
+ // ==========================================
278
+ // Outputs
279
+ // ==========================================
280
+ createOutputs(config) {
281
+ new cdk.CfnOutput(this, 'InstanceIdentifier', {
282
+ value: this.instance.instanceIdentifier,
283
+ description: 'RDS Instance Identifier',
284
+ exportName: `${config.stackName}-InstanceIdentifier`,
285
+ });
286
+ new cdk.CfnOutput(this, 'Endpoint', {
287
+ value: this.instance.dbInstanceEndpointAddress,
288
+ description: 'RDS Endpoint Address',
289
+ exportName: `${config.stackName}-Endpoint`,
290
+ });
291
+ new cdk.CfnOutput(this, 'Port', {
292
+ value: this.instance.dbInstanceEndpointPort,
293
+ description: 'RDS Port',
294
+ exportName: `${config.stackName}-Port`,
295
+ });
296
+ new cdk.CfnOutput(this, 'SecurityGroupId', {
297
+ value: this.securityGroup.securityGroupId,
298
+ description: 'RDS Security Group ID',
299
+ exportName: `${config.stackName}-RdsSecurityGroupId`,
300
+ });
301
+ if (this.instance.secret) {
302
+ new cdk.CfnOutput(this, 'SecretArn', {
303
+ value: this.instance.secret.secretArn,
304
+ description: 'RDS Credentials Secret ARN',
305
+ exportName: `${config.stackName}-SecretArn`,
306
+ });
307
+ }
308
+ }
309
+ // ==========================================
310
+ // Public Methods
311
+ // ==========================================
312
+ /**
313
+ * Allow database access from a security group
314
+ */
315
+ allowDbFrom(sg, port, description) {
316
+ const dbPort = port ?? this.instance.dbInstanceEndpointPort ?? 3306;
317
+ this.securityGroup.addIngressRule(ec2.Peer.securityGroupId(sg.securityGroupId), ec2.Port.tcp(dbPort), description ?? 'Allow database access from specified security group');
318
+ }
319
+ }
320
+ exports.RdsConstruct = RdsConstruct;