cdk-factory 0.16.15__py3-none-any.whl → 0.18.9__py3-none-any.whl
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.
Potentially problematic release.
This version of cdk-factory might be problematic. Click here for more details.
- cdk_factory/configurations/base_config.py +23 -24
- cdk_factory/configurations/cdk_config.py +1 -1
- cdk_factory/configurations/deployment.py +12 -0
- cdk_factory/configurations/devops.py +1 -1
- cdk_factory/configurations/resources/acm.py +9 -2
- cdk_factory/configurations/resources/auto_scaling.py +2 -5
- cdk_factory/configurations/resources/cloudfront.py +7 -2
- cdk_factory/configurations/resources/ecr.py +1 -1
- cdk_factory/configurations/resources/ecs_cluster.py +12 -5
- cdk_factory/configurations/resources/ecs_service.py +7 -2
- cdk_factory/configurations/resources/load_balancer.py +8 -9
- cdk_factory/configurations/resources/monitoring.py +8 -3
- cdk_factory/configurations/resources/rds.py +7 -8
- cdk_factory/configurations/resources/rum.py +7 -2
- cdk_factory/configurations/resources/s3.py +1 -1
- cdk_factory/configurations/resources/security_group_full_stack.py +7 -8
- cdk_factory/configurations/resources/vpc.py +19 -0
- cdk_factory/configurations/workload.py +32 -2
- cdk_factory/constructs/ecr/ecr_construct.py +9 -2
- cdk_factory/constructs/lambdas/policies/policy_docs.py +4 -4
- cdk_factory/interfaces/istack.py +4 -4
- cdk_factory/interfaces/networked_stack_mixin.py +6 -6
- cdk_factory/interfaces/standardized_ssm_mixin.py +657 -0
- cdk_factory/interfaces/vpc_provider_mixin.py +64 -33
- cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
- cdk_factory/pipeline/pipeline_factory.py +3 -3
- cdk_factory/stack_library/__init__.py +3 -2
- cdk_factory/stack_library/acm/acm_stack.py +2 -2
- cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +344 -535
- cdk_factory/stack_library/code_artifact/code_artifact_stack.py +2 -2
- cdk_factory/stack_library/cognito/cognito_stack.py +152 -92
- cdk_factory/stack_library/dynamodb/dynamodb_stack.py +19 -15
- cdk_factory/stack_library/ecr/ecr_stack.py +2 -2
- cdk_factory/stack_library/ecs/__init__.py +1 -3
- cdk_factory/stack_library/ecs/ecs_cluster_stack.py +157 -73
- cdk_factory/stack_library/ecs/ecs_service_stack.py +10 -26
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +2 -2
- cdk_factory/stack_library/load_balancer/load_balancer_stack.py +96 -119
- cdk_factory/stack_library/rds/rds_stack.py +73 -73
- cdk_factory/stack_library/route53/route53_stack.py +2 -2
- cdk_factory/stack_library/rum/rum_stack.py +108 -91
- cdk_factory/stack_library/security_group/security_group_full_stack.py +9 -22
- cdk_factory/stack_library/security_group/security_group_stack.py +11 -11
- cdk_factory/stack_library/stack_base.py +5 -0
- cdk_factory/stack_library/vpc/vpc_stack.py +272 -124
- cdk_factory/stack_library/websites/static_website_stack.py +1 -1
- cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
- cdk_factory/utilities/environment_services.py +5 -5
- cdk_factory/utilities/json_loading_utility.py +1 -1
- cdk_factory/validation/config_validator.py +483 -0
- cdk_factory/version.py +1 -1
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/METADATA +1 -1
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/RECORD +57 -57
- cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
- cdk_factory/interfaces/ssm_parameter_mixin.py +0 -454
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/WHEEL +0 -0
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.18.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,8 @@ MIT License. See Project Root for the license information.
|
|
|
6
6
|
|
|
7
7
|
from typing import Dict, Any, List, Optional
|
|
8
8
|
|
|
9
|
+
import base64
|
|
10
|
+
import hashlib
|
|
9
11
|
import aws_cdk as cdk
|
|
10
12
|
from aws_cdk import aws_elasticloadbalancingv2 as elbv2
|
|
11
13
|
from aws_cdk import aws_ec2 as ec2
|
|
@@ -19,7 +21,8 @@ from cdk_factory.configurations.deployment import DeploymentConfig
|
|
|
19
21
|
from cdk_factory.configurations.stack import StackConfig
|
|
20
22
|
from cdk_factory.configurations.resources.load_balancer import LoadBalancerConfig
|
|
21
23
|
from cdk_factory.interfaces.istack import IStack
|
|
22
|
-
from cdk_factory.interfaces.
|
|
24
|
+
from cdk_factory.interfaces.vpc_provider_mixin import VPCProviderMixin
|
|
25
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
|
|
23
26
|
from cdk_factory.stack.stack_module_registry import register_stack
|
|
24
27
|
from cdk_factory.workload.workload_factory import WorkloadConfig
|
|
25
28
|
|
|
@@ -30,7 +33,7 @@ logger = Logger(service="LoadBalancerStack")
|
|
|
30
33
|
@register_stack("alb_stack")
|
|
31
34
|
@register_stack("load_balancer_library_module")
|
|
32
35
|
@register_stack("load_balancer_stack")
|
|
33
|
-
class LoadBalancerStack(IStack,
|
|
36
|
+
class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
34
37
|
"""
|
|
35
38
|
Reusable stack for AWS Load Balancers.
|
|
36
39
|
Supports creating Application and Network Load Balancers with customizable configurations.
|
|
@@ -49,7 +52,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
49
52
|
self._hosted_zone = None
|
|
50
53
|
self._record_names = None
|
|
51
54
|
# SSM imported values
|
|
52
|
-
self.
|
|
55
|
+
self._ssm_imported_values: Dict[str, str] = {}
|
|
53
56
|
|
|
54
57
|
def build(
|
|
55
58
|
self,
|
|
@@ -76,8 +79,18 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
76
79
|
)
|
|
77
80
|
lb_name = deployment.build_resource_name(self.lb_config.name)
|
|
78
81
|
|
|
79
|
-
#
|
|
80
|
-
self.
|
|
82
|
+
# Setup standardized SSM integration
|
|
83
|
+
self.setup_ssm_integration(
|
|
84
|
+
scope=self,
|
|
85
|
+
config=self.lb_config,
|
|
86
|
+
resource_type="load_balancer",
|
|
87
|
+
resource_name=self.lb_config.name,
|
|
88
|
+
deployment=deployment,
|
|
89
|
+
workload=workload
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Process SSM imports
|
|
93
|
+
self.process_ssm_imports()
|
|
81
94
|
|
|
82
95
|
self._prep_dns()
|
|
83
96
|
|
|
@@ -155,16 +168,22 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
155
168
|
|
|
156
169
|
# If subnets is None, check if we have SSM-imported subnet_ids as a token
|
|
157
170
|
# We need to use Fn.Split to convert the comma-separated string to an array
|
|
158
|
-
if subnets is None
|
|
159
|
-
|
|
160
|
-
if
|
|
161
|
-
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"
|
|
166
|
-
cdk.
|
|
167
|
-
|
|
171
|
+
if subnets is None:
|
|
172
|
+
subnet_ids = self.get_subnet_ids(self.lb_config)
|
|
173
|
+
if subnet_ids:
|
|
174
|
+
# For CloudFormation token resolution, we still need Fn.split
|
|
175
|
+
# but we use the helper to determine if subnet IDs are available
|
|
176
|
+
ssm_imports = self.get_all_ssm_imports()
|
|
177
|
+
if "subnet_ids" in ssm_imports:
|
|
178
|
+
subnet_ids_value = ssm_imports["subnet_ids"]
|
|
179
|
+
if cdk.Token.is_unresolved(subnet_ids_value):
|
|
180
|
+
logger.info("Using Fn.Split to convert comma-separated subnet IDs token to array")
|
|
181
|
+
# Use CloudFormation escape hatch to set Subnets property with Fn.Split
|
|
182
|
+
cfn_lb = load_balancer.node.default_child
|
|
183
|
+
cfn_lb.add_property_override(
|
|
184
|
+
"Subnets",
|
|
185
|
+
cdk.Fn.split(",", subnet_ids_value)
|
|
186
|
+
)
|
|
168
187
|
|
|
169
188
|
# Add tags
|
|
170
189
|
for key, value in self.lb_config.tags.items():
|
|
@@ -174,100 +193,25 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
174
193
|
|
|
175
194
|
@property
|
|
176
195
|
def vpc(self) -> ec2.IVpc:
|
|
177
|
-
"""Get the VPC for the Load Balancer"""
|
|
196
|
+
"""Get the VPC for the Load Balancer using centralized VPC provider mixin."""
|
|
178
197
|
if self._vpc:
|
|
179
198
|
return self._vpc
|
|
180
|
-
|
|
181
|
-
# Check SSM imported values first (tokens from SSM parameters)
|
|
182
|
-
if "vpc_id" in self.ssm_imported_values:
|
|
183
|
-
vpc_id = self.ssm_imported_values["vpc_id"]
|
|
184
|
-
|
|
185
|
-
# Build VPC attributes
|
|
186
|
-
vpc_attrs = {
|
|
187
|
-
"vpc_id": vpc_id,
|
|
188
|
-
"availability_zones": ["us-east-1a", "us-east-1b"],
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
# If we have subnet_ids from SSM, provide dummy public subnets
|
|
192
|
-
# The actual subnets will be set via CloudFormation escape hatch
|
|
193
|
-
if "subnet_ids" in self.ssm_imported_values:
|
|
194
|
-
# Provide dummy subnet IDs - these will be overridden by the escape hatch
|
|
195
|
-
# We need at least one dummy subnet per AZ to satisfy CDK's validation
|
|
196
|
-
vpc_attrs["public_subnet_ids"] = ["subnet-dummy1", "subnet-dummy2"]
|
|
197
|
-
|
|
198
|
-
# Use from_vpc_attributes() instead of from_lookup() because SSM imports return tokens
|
|
199
|
-
self._vpc = ec2.Vpc.from_vpc_attributes(self, "VPC", **vpc_attrs)
|
|
200
|
-
elif self.lb_config.vpc_id:
|
|
201
|
-
self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.lb_config.vpc_id)
|
|
202
|
-
elif self.workload.vpc_id:
|
|
203
|
-
self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.workload.vpc_id)
|
|
204
|
-
else:
|
|
205
|
-
# Use default VPC if not provided
|
|
206
|
-
raise ValueError(
|
|
207
|
-
"VPC is not defined in the configuration. "
|
|
208
|
-
"You can provide it a the load_balancer.vpc_id in the configuration "
|
|
209
|
-
"or a top level workload.vpc_id in the workload configuration."
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
return self._vpc
|
|
213
|
-
|
|
214
|
-
def _process_ssm_imports(self) -> None:
|
|
215
|
-
"""
|
|
216
|
-
Process SSM imports from configuration.
|
|
217
|
-
Follows the same pattern as RDS and Security Group stacks.
|
|
218
|
-
"""
|
|
219
|
-
from aws_cdk import aws_ssm as ssm
|
|
220
|
-
|
|
221
|
-
ssm_imports = self.lb_config.ssm_imports
|
|
222
199
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
try:
|
|
231
|
-
# Handle list values (like security_groups)
|
|
232
|
-
if isinstance(param_value, list):
|
|
233
|
-
imported_list = []
|
|
234
|
-
for idx, param_path in enumerate(param_value):
|
|
235
|
-
if not param_path.startswith('/'):
|
|
236
|
-
param_path = f"/{param_path}"
|
|
237
|
-
|
|
238
|
-
construct_id = f"ssm-import-{param_key}-{idx}-{hash(param_path) % 10000}"
|
|
239
|
-
param = ssm.StringParameter.from_string_parameter_name(
|
|
240
|
-
self, construct_id, param_path
|
|
241
|
-
)
|
|
242
|
-
imported_list.append(param.string_value)
|
|
243
|
-
|
|
244
|
-
self.ssm_imported_values[param_key] = imported_list
|
|
245
|
-
logger.info(f"Imported SSM parameter list: {param_key} with {len(imported_list)} items")
|
|
246
|
-
else:
|
|
247
|
-
# Handle string values
|
|
248
|
-
param_path = param_value
|
|
249
|
-
if not param_path.startswith('/'):
|
|
250
|
-
param_path = f"/{param_path}"
|
|
251
|
-
|
|
252
|
-
construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
|
|
253
|
-
param = ssm.StringParameter.from_string_parameter_name(
|
|
254
|
-
self, construct_id, param_path
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
self.ssm_imported_values[param_key] = param.string_value
|
|
258
|
-
logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
|
|
259
|
-
|
|
260
|
-
except Exception as e:
|
|
261
|
-
logger.error(f"Failed to import SSM parameter {param_key}: {e}")
|
|
262
|
-
raise
|
|
200
|
+
# Use the centralized VPC resolution from VPCProviderMixin
|
|
201
|
+
self._vpc = self.resolve_vpc(
|
|
202
|
+
config=self.lb_config,
|
|
203
|
+
deployment=self.deployment,
|
|
204
|
+
workload=self.workload
|
|
205
|
+
)
|
|
206
|
+
return self._vpc
|
|
263
207
|
|
|
264
208
|
def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
|
|
265
209
|
"""Get security groups for the Load Balancer"""
|
|
266
210
|
security_groups = []
|
|
267
211
|
|
|
268
212
|
# Check SSM imported values first
|
|
269
|
-
if "security_groups" in self.
|
|
270
|
-
sg_ids = self.
|
|
213
|
+
if "security_groups" in self._ssm_imported_values:
|
|
214
|
+
sg_ids = self._ssm_imported_values["security_groups"]
|
|
271
215
|
if not isinstance(sg_ids, list):
|
|
272
216
|
sg_ids = [sg_ids]
|
|
273
217
|
else:
|
|
@@ -285,9 +229,16 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
285
229
|
"""Get subnets for the Load Balancer"""
|
|
286
230
|
subnets = []
|
|
287
231
|
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
|
|
232
|
+
# Use the standardized helper function to get subnet IDs
|
|
233
|
+
subnet_ids = self.get_subnet_ids(self.lb_config)
|
|
234
|
+
|
|
235
|
+
if not subnet_ids:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
# Check if we have unresolved tokens from SSM
|
|
239
|
+
ssm_imports = self.get_all_ssm_imports()
|
|
240
|
+
if "subnet_ids" in ssm_imports:
|
|
241
|
+
subnet_ids_value = ssm_imports["subnet_ids"]
|
|
291
242
|
|
|
292
243
|
# Check if this is a CDK token (unresolved SSM parameter)
|
|
293
244
|
if cdk.Token.is_unresolved(subnet_ids_value):
|
|
@@ -296,25 +247,40 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
296
247
|
# The ALB construct will handle the token-based subnet IDs
|
|
297
248
|
logger.info("Subnet IDs are unresolved tokens, will use vpc_subnets with token resolution")
|
|
298
249
|
return None
|
|
299
|
-
elif isinstance(subnet_ids_value, str):
|
|
300
|
-
# If it's a resolved string, split it
|
|
301
|
-
subnet_ids = [s.strip() for s in subnet_ids_value.split(',')]
|
|
302
|
-
elif isinstance(subnet_ids_value, list):
|
|
303
|
-
subnet_ids = subnet_ids_value
|
|
304
|
-
else:
|
|
305
|
-
subnet_ids = [subnet_ids_value]
|
|
306
|
-
else:
|
|
307
|
-
subnet_ids = self.lb_config.subnets
|
|
308
|
-
|
|
309
|
-
if not subnet_ids:
|
|
310
|
-
return None
|
|
311
250
|
|
|
251
|
+
# Convert subnet IDs to subnet objects
|
|
312
252
|
for idx, subnet_id in enumerate(subnet_ids):
|
|
313
253
|
subnets.append(
|
|
314
254
|
ec2.Subnet.from_subnet_id(self, f"Subnet-{idx}", subnet_id)
|
|
315
255
|
)
|
|
316
256
|
return subnets
|
|
317
257
|
|
|
258
|
+
def _generate_target_group_name(self, lb_name: str, tg_name: str, max_length: int = 32) -> str:
|
|
259
|
+
"""Generate a unique target group name that doesn't begin/end with hyphens"""
|
|
260
|
+
full_name = f"{lb_name}-{tg_name}"
|
|
261
|
+
|
|
262
|
+
if len(full_name) <= max_length:
|
|
263
|
+
# No truncation needed, just ensure no leading/trailing hyphens
|
|
264
|
+
return full_name.strip('-')
|
|
265
|
+
|
|
266
|
+
# Need to truncate - use hash suffix for uniqueness
|
|
267
|
+
# Reserve space for hash (typically 8 chars) and separator
|
|
268
|
+
hash_length = 8
|
|
269
|
+
separator_length = 1
|
|
270
|
+
max_name_length = max_length - hash_length - separator_length
|
|
271
|
+
|
|
272
|
+
# Take the prefix and ensure it doesn't end with hyphen
|
|
273
|
+
prefix = full_name[:max_name_length].rstrip('-')
|
|
274
|
+
|
|
275
|
+
# Generate hash of the full name for uniqueness
|
|
276
|
+
hash_bytes = hashlib.sha256(full_name.encode()).digest()
|
|
277
|
+
hash_suffix = base64.urlsafe_b64encode(hash_bytes).decode()[:hash_length]
|
|
278
|
+
|
|
279
|
+
# Ensure hash doesn't start with hyphen (replace any non-alphanumeric chars)
|
|
280
|
+
hash_suffix = ''.join(c for c in hash_suffix if c.isalnum())[:hash_length]
|
|
281
|
+
|
|
282
|
+
return f"{prefix}-{hash_suffix}"
|
|
283
|
+
|
|
318
284
|
def _create_target_groups(self, lb_name: str) -> None:
|
|
319
285
|
"""Create target groups for the Load Balancer"""
|
|
320
286
|
|
|
@@ -322,6 +288,9 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
322
288
|
tg_name = tg_config.get("name", f"tg-{idx}")
|
|
323
289
|
tg_id = f"{lb_name}-{tg_name}"
|
|
324
290
|
|
|
291
|
+
# Generate a unique target group name that doesn't begin/end with hyphens
|
|
292
|
+
tg_name_sanitized = self._generate_target_group_name(lb_name, tg_name)
|
|
293
|
+
|
|
325
294
|
# Configure health check
|
|
326
295
|
health_check = self._configure_health_check(
|
|
327
296
|
tg_config.get("health_check", {})
|
|
@@ -332,7 +301,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
332
301
|
target_group = elbv2.ApplicationTargetGroup(
|
|
333
302
|
self,
|
|
334
303
|
tg_id,
|
|
335
|
-
target_group_name=
|
|
304
|
+
target_group_name=tg_name_sanitized,
|
|
336
305
|
vpc=self.vpc,
|
|
337
306
|
port=tg_config.get("port", 80),
|
|
338
307
|
protocol=elbv2.ApplicationProtocol(
|
|
@@ -347,7 +316,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
347
316
|
target_group = elbv2.NetworkTargetGroup(
|
|
348
317
|
self,
|
|
349
318
|
tg_id,
|
|
350
|
-
target_group_name=
|
|
319
|
+
target_group_name=tg_name_sanitized,
|
|
351
320
|
vpc=self.vpc,
|
|
352
321
|
port=tg_config.get("port", 80),
|
|
353
322
|
protocol=elbv2.Protocol(tg_config.get("protocol", "TCP")),
|
|
@@ -357,6 +326,8 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
357
326
|
health_check=health_check,
|
|
358
327
|
)
|
|
359
328
|
|
|
329
|
+
|
|
330
|
+
|
|
360
331
|
# Store target group for later use
|
|
361
332
|
self.target_groups[tg_name] = target_group
|
|
362
333
|
|
|
@@ -397,6 +368,11 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
397
368
|
if protocol.upper() == "HTTPS":
|
|
398
369
|
certificates = self._get_certificates()
|
|
399
370
|
|
|
371
|
+
if not certificates and protocol.upper() == "HTTPS":
|
|
372
|
+
message = "No certificates found for HTTPS listener. Please attach a certificate or create a certificate stack."
|
|
373
|
+
logger.warning(message)
|
|
374
|
+
raise ValueError(message)
|
|
375
|
+
|
|
400
376
|
listener = elbv2.ApplicationListener(
|
|
401
377
|
self,
|
|
402
378
|
listener_id,
|
|
@@ -449,8 +425,9 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
449
425
|
certificates = []
|
|
450
426
|
|
|
451
427
|
# Check SSM imported values first (takes priority)
|
|
452
|
-
|
|
453
|
-
|
|
428
|
+
ssm_imports = self.get_all_ssm_imports()
|
|
429
|
+
if "certificate_arns" in ssm_imports:
|
|
430
|
+
cert_arns = ssm_imports["certificate_arns"]
|
|
454
431
|
if not isinstance(cert_arns, list):
|
|
455
432
|
cert_arns = [cert_arns]
|
|
456
433
|
for cert_arn in cert_arns:
|
|
@@ -18,7 +18,8 @@ from cdk_factory.configurations.deployment import DeploymentConfig
|
|
|
18
18
|
from cdk_factory.configurations.stack import StackConfig
|
|
19
19
|
from cdk_factory.configurations.resources.rds import RdsConfig
|
|
20
20
|
from cdk_factory.interfaces.istack import IStack
|
|
21
|
-
from cdk_factory.interfaces.
|
|
21
|
+
from cdk_factory.interfaces.vpc_provider_mixin import VPCProviderMixin
|
|
22
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
|
|
22
23
|
from cdk_factory.stack.stack_module_registry import register_stack
|
|
23
24
|
from cdk_factory.workload.workload_factory import WorkloadConfig
|
|
24
25
|
|
|
@@ -27,7 +28,7 @@ logger = Logger(service="RdsStack")
|
|
|
27
28
|
|
|
28
29
|
@register_stack("rds_library_module")
|
|
29
30
|
@register_stack("rds_stack")
|
|
30
|
-
class RdsStack(IStack,
|
|
31
|
+
class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
31
32
|
"""
|
|
32
33
|
Reusable stack for AWS RDS.
|
|
33
34
|
Supports creating RDS instances with customizable configurations.
|
|
@@ -68,8 +69,18 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
|
|
|
68
69
|
self.rds_config = RdsConfig(stack_config.dictionary.get("rds", {}), deployment)
|
|
69
70
|
db_name = deployment.build_resource_name(self.rds_config.name)
|
|
70
71
|
|
|
71
|
-
#
|
|
72
|
-
self.
|
|
72
|
+
# Setup standardized SSM integration
|
|
73
|
+
self.setup_ssm_integration(
|
|
74
|
+
scope=self,
|
|
75
|
+
config=self.rds_config,
|
|
76
|
+
resource_type="rds",
|
|
77
|
+
resource_name=self.rds_config.name,
|
|
78
|
+
deployment=deployment,
|
|
79
|
+
workload=workload
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Process SSM imports
|
|
83
|
+
self.process_ssm_imports()
|
|
73
84
|
|
|
74
85
|
# Get VPC and security groups
|
|
75
86
|
self.security_groups = self._get_security_groups()
|
|
@@ -86,63 +97,18 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
|
|
|
86
97
|
# Export to SSM Parameter Store
|
|
87
98
|
self._export_ssm_parameters(db_name)
|
|
88
99
|
|
|
89
|
-
def _process_ssm_imports(self) -> None:
|
|
90
|
-
"""Process SSM imports from configuration"""
|
|
91
|
-
ssm_imports = self.rds_config.ssm_imports
|
|
92
|
-
|
|
93
|
-
if not ssm_imports:
|
|
94
|
-
logger.debug("No SSM imports configured for RDS")
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
logger.info(f"Processing {len(ssm_imports)} SSM imports for RDS")
|
|
98
|
-
|
|
99
|
-
for param_key, param_path in ssm_imports.items():
|
|
100
|
-
try:
|
|
101
|
-
if not param_path.startswith('/'):
|
|
102
|
-
param_path = f"/{param_path}"
|
|
103
|
-
|
|
104
|
-
construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
|
|
105
|
-
param = ssm.StringParameter.from_string_parameter_name(
|
|
106
|
-
self, construct_id, param_path
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
self.ssm_imported_values[param_key] = param.string_value
|
|
110
|
-
logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
|
|
111
|
-
|
|
112
|
-
except Exception as e:
|
|
113
|
-
logger.error(f"Failed to import SSM parameter {param_key} from {param_path}: {e}")
|
|
114
|
-
raise
|
|
115
|
-
|
|
116
100
|
@property
|
|
117
101
|
def vpc(self) -> ec2.IVpc:
|
|
118
|
-
"""Get the VPC for the RDS instance"""
|
|
119
|
-
if self._vpc:
|
|
102
|
+
"""Get the VPC for the RDS instance using centralized VPC provider mixin."""
|
|
103
|
+
if hasattr(self, '_vpc') and self._vpc:
|
|
120
104
|
return self._vpc
|
|
121
105
|
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
# We'll create a DB subnet group separately instead
|
|
129
|
-
vpc_attrs = {
|
|
130
|
-
"vpc_id": vpc_id,
|
|
131
|
-
"availability_zones": ["us-east-1a", "us-east-1b"]
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
# Use from_vpc_attributes() for SSM tokens
|
|
135
|
-
self._vpc = ec2.Vpc.from_vpc_attributes(self, "VPC", **vpc_attrs)
|
|
136
|
-
elif self.rds_config.vpc_id:
|
|
137
|
-
self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.rds_config.vpc_id)
|
|
138
|
-
elif self.workload.vpc_id:
|
|
139
|
-
self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.workload.vpc_id)
|
|
140
|
-
else:
|
|
141
|
-
raise ValueError(
|
|
142
|
-
"VPC is not defined in the configuration. "
|
|
143
|
-
"You can provide it a the rds.vpc_id in the configuration "
|
|
144
|
-
"or a top level workload.vpc_id in the workload configuration."
|
|
145
|
-
)
|
|
106
|
+
# Resolve VPC using the centralized VPC provider mixin
|
|
107
|
+
self._vpc = self.resolve_vpc(
|
|
108
|
+
config=self.rds_config,
|
|
109
|
+
deployment=self.deployment,
|
|
110
|
+
workload=self.workload
|
|
111
|
+
)
|
|
146
112
|
return self._vpc
|
|
147
113
|
|
|
148
114
|
def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
|
|
@@ -150,8 +116,9 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
|
|
|
150
116
|
security_groups = []
|
|
151
117
|
|
|
152
118
|
# Check SSM imports first for security group ID
|
|
153
|
-
|
|
154
|
-
|
|
119
|
+
ssm_imports = self.get_all_ssm_imports()
|
|
120
|
+
if "security_group_rds_id" in ssm_imports:
|
|
121
|
+
sg_id = ssm_imports["security_group_rds_id"]
|
|
155
122
|
security_groups.append(
|
|
156
123
|
ec2.SecurityGroup.from_security_group_id(
|
|
157
124
|
self, "RDSSecurityGroup", sg_id
|
|
@@ -168,27 +135,60 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
|
|
|
168
135
|
|
|
169
136
|
return security_groups
|
|
170
137
|
|
|
138
|
+
def _get_subnet_selection(self) -> ec2.SubnetSelection:
|
|
139
|
+
"""
|
|
140
|
+
Get subnet selection based on available subnet types in the VPC.
|
|
141
|
+
|
|
142
|
+
RDS instances require private subnets for security, but we'll fall back
|
|
143
|
+
to available subnets if the preferred types aren't available.
|
|
144
|
+
"""
|
|
145
|
+
vpc = self.vpc
|
|
146
|
+
|
|
147
|
+
# Check for isolated subnets first (most secure for RDS)
|
|
148
|
+
if vpc.isolated_subnets:
|
|
149
|
+
logger.info("Using isolated subnets for RDS instance")
|
|
150
|
+
return ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_ISOLATED)
|
|
151
|
+
|
|
152
|
+
# Check for private subnets next
|
|
153
|
+
elif vpc.private_subnets:
|
|
154
|
+
logger.info("Using private subnets for RDS instance")
|
|
155
|
+
return ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS)
|
|
156
|
+
|
|
157
|
+
# Fall back to public subnets (not recommended for production)
|
|
158
|
+
elif vpc.public_subnets:
|
|
159
|
+
logger.warning("Using public subnets for RDS instance - not recommended for production")
|
|
160
|
+
return ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC)
|
|
161
|
+
|
|
162
|
+
else:
|
|
163
|
+
raise ValueError("No subnets available in VPC for RDS instance")
|
|
164
|
+
|
|
171
165
|
def _create_db_instance(self, db_name: str) -> rds.DatabaseInstance:
|
|
172
166
|
"""Create a new RDS instance"""
|
|
173
167
|
# Configure subnet group
|
|
174
168
|
# If we have subnet IDs from SSM, create a DB subnet group explicitly
|
|
175
169
|
db_subnet_group = None
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
170
|
+
subnet_ids = self.get_subnet_ids(self.rds_config)
|
|
171
|
+
|
|
172
|
+
if subnet_ids:
|
|
173
|
+
# For CloudFormation token resolution, we need to get the raw SSM value
|
|
174
|
+
# Use the standardized SSM imports
|
|
175
|
+
ssm_imports = self.get_all_ssm_imports()
|
|
176
|
+
if "subnet_ids" in ssm_imports:
|
|
177
|
+
subnet_ids_str = ssm_imports["subnet_ids"]
|
|
178
|
+
# Split the comma-separated token into a list for CloudFormation
|
|
179
|
+
subnet_ids_list = cdk.Fn.split(",", subnet_ids_str)
|
|
180
|
+
|
|
181
|
+
# Create DB subnet group with the token-based subnet list
|
|
182
|
+
db_subnet_group = rds.CfnDBSubnetGroup(
|
|
183
|
+
self,
|
|
184
|
+
"DBSubnetGroup",
|
|
185
|
+
db_subnet_group_description=f"Subnet group for {db_name}",
|
|
186
|
+
subnet_ids=subnet_ids_list,
|
|
187
|
+
db_subnet_group_name=f"{db_name}-subnet-group"
|
|
188
|
+
)
|
|
189
189
|
|
|
190
190
|
# Configure subnet selection for VPC (when not using SSM imports)
|
|
191
|
-
subnets = None if db_subnet_group else
|
|
191
|
+
subnets = None if db_subnet_group else self._get_subnet_selection()
|
|
192
192
|
|
|
193
193
|
# Configure engine
|
|
194
194
|
engine_version = None
|
|
@@ -18,7 +18,7 @@ from cdk_factory.configurations.deployment import DeploymentConfig
|
|
|
18
18
|
from cdk_factory.configurations.stack import StackConfig
|
|
19
19
|
from cdk_factory.configurations.resources.route53 import Route53Config
|
|
20
20
|
from cdk_factory.interfaces.istack import IStack
|
|
21
|
-
from cdk_factory.interfaces.
|
|
21
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
|
|
22
22
|
from cdk_factory.stack.stack_module_registry import register_stack
|
|
23
23
|
from cdk_factory.workload.workload_factory import WorkloadConfig
|
|
24
24
|
|
|
@@ -27,7 +27,7 @@ logger = Logger(service="Route53Stack")
|
|
|
27
27
|
|
|
28
28
|
@register_stack("route53_library_module")
|
|
29
29
|
@register_stack("route53_stack")
|
|
30
|
-
class Route53Stack(IStack,
|
|
30
|
+
class Route53Stack(IStack, StandardizedSsmMixin):
|
|
31
31
|
"""
|
|
32
32
|
Reusable stack for AWS Route53.
|
|
33
33
|
Supports creating hosted zones, DNS records, and certificate validation.
|