@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,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;
|