cdk-factory 0.17.6__py3-none-any.whl → 0.20.0__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/deployment.py +12 -0
- cdk_factory/configurations/resources/acm.py +9 -2
- cdk_factory/configurations/resources/auto_scaling.py +7 -5
- cdk_factory/configurations/resources/ecs_cluster.py +5 -0
- cdk_factory/configurations/resources/ecs_service.py +24 -2
- cdk_factory/configurations/resources/lambda_edge.py +18 -4
- cdk_factory/configurations/resources/rds.py +1 -1
- cdk_factory/configurations/resources/route53.py +5 -0
- cdk_factory/configurations/resources/s3.py +9 -1
- cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py +1 -1
- cdk_factory/constructs/lambdas/policies/policy_docs.py +1 -1
- cdk_factory/interfaces/networked_stack_mixin.py +1 -1
- cdk_factory/interfaces/standardized_ssm_mixin.py +82 -10
- cdk_factory/stack_library/acm/acm_stack.py +5 -15
- cdk_factory/stack_library/api_gateway/api_gateway_stack.py +2 -2
- cdk_factory/stack_library/auto_scaling/{auto_scaling_stack_standardized.py → auto_scaling_stack.py} +213 -105
- cdk_factory/stack_library/cloudfront/cloudfront_stack.py +76 -22
- cdk_factory/stack_library/code_artifact/code_artifact_stack.py +3 -25
- cdk_factory/stack_library/cognito/cognito_stack.py +2 -2
- cdk_factory/stack_library/dynamodb/dynamodb_stack.py +2 -2
- cdk_factory/stack_library/ecs/__init__.py +2 -4
- cdk_factory/stack_library/ecs/{ecs_cluster_stack_standardized.py → ecs_cluster_stack.py} +52 -41
- cdk_factory/stack_library/ecs/ecs_service_stack.py +49 -26
- cdk_factory/stack_library/lambda_edge/EDGE_LOG_RETENTION_TODO.md +226 -0
- cdk_factory/stack_library/lambda_edge/LAMBDA_EDGE_LOG_RETENTION_BLOG.md +215 -0
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +238 -81
- cdk_factory/stack_library/load_balancer/load_balancer_stack.py +128 -177
- cdk_factory/stack_library/rds/rds_stack.py +65 -72
- cdk_factory/stack_library/route53/route53_stack.py +244 -38
- cdk_factory/stack_library/rum/rum_stack.py +3 -3
- cdk_factory/stack_library/security_group/security_group_full_stack.py +1 -31
- cdk_factory/stack_library/security_group/security_group_stack.py +1 -8
- cdk_factory/stack_library/simple_queue_service/sqs_stack.py +1 -34
- cdk_factory/stack_library/stack_base.py +5 -0
- cdk_factory/stack_library/vpc/{vpc_stack_standardized.py → vpc_stack.py} +6 -109
- cdk_factory/stack_library/websites/static_website_stack.py +7 -3
- cdk_factory/utilities/api_gateway_integration_utility.py +2 -2
- cdk_factory/utilities/environment_services.py +2 -2
- cdk_factory/version.py +1 -1
- {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.0.dist-info}/METADATA +1 -1
- {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.0.dist-info}/RECORD +44 -42
- {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.0.dist-info}/WHEEL +0 -0
- {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.0.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.0.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
|
|
@@ -50,7 +52,7 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
50
52
|
self._hosted_zone = None
|
|
51
53
|
self._record_names = None
|
|
52
54
|
# SSM imported values
|
|
53
|
-
self.
|
|
55
|
+
self._ssm_imported_values: Dict[str, str] = {}
|
|
54
56
|
|
|
55
57
|
def build(
|
|
56
58
|
self,
|
|
@@ -77,8 +79,18 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
77
79
|
)
|
|
78
80
|
lb_name = deployment.build_resource_name(self.lb_config.name)
|
|
79
81
|
|
|
80
|
-
#
|
|
81
|
-
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()
|
|
82
94
|
|
|
83
95
|
self._prep_dns()
|
|
84
96
|
|
|
@@ -156,16 +168,22 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
156
168
|
|
|
157
169
|
# If subnets is None, check if we have SSM-imported subnet_ids as a token
|
|
158
170
|
# We need to use Fn.Split to convert the comma-separated string to an array
|
|
159
|
-
if subnets is None
|
|
160
|
-
|
|
161
|
-
if
|
|
162
|
-
|
|
163
|
-
#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"
|
|
167
|
-
cdk.
|
|
168
|
-
|
|
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
|
+
)
|
|
169
187
|
|
|
170
188
|
# Add tags
|
|
171
189
|
for key, value in self.lb_config.tags.items():
|
|
@@ -187,63 +205,13 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
187
205
|
)
|
|
188
206
|
return self._vpc
|
|
189
207
|
|
|
190
|
-
def _process_ssm_imports(self) -> None:
|
|
191
|
-
"""
|
|
192
|
-
Process SSM imports from configuration.
|
|
193
|
-
Follows the same pattern as RDS and Security Group stacks.
|
|
194
|
-
"""
|
|
195
|
-
from aws_cdk import aws_ssm as ssm
|
|
196
|
-
|
|
197
|
-
ssm_imports = self.lb_config.ssm_imports
|
|
198
|
-
|
|
199
|
-
if not ssm_imports:
|
|
200
|
-
logger.debug("No SSM imports configured for Load Balancer")
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
logger.info(f"Processing {len(ssm_imports)} SSM imports for Load Balancer")
|
|
204
|
-
|
|
205
|
-
for param_key, param_value in ssm_imports.items():
|
|
206
|
-
try:
|
|
207
|
-
# Handle list values (like security_groups)
|
|
208
|
-
if isinstance(param_value, list):
|
|
209
|
-
imported_list = []
|
|
210
|
-
for idx, param_path in enumerate(param_value):
|
|
211
|
-
if not param_path.startswith('/'):
|
|
212
|
-
param_path = f"/{param_path}"
|
|
213
|
-
|
|
214
|
-
construct_id = f"ssm-import-{param_key}-{idx}-{hash(param_path) % 10000}"
|
|
215
|
-
param = ssm.StringParameter.from_string_parameter_name(
|
|
216
|
-
self, construct_id, param_path
|
|
217
|
-
)
|
|
218
|
-
imported_list.append(param.string_value)
|
|
219
|
-
|
|
220
|
-
self.ssm_imported_values[param_key] = imported_list
|
|
221
|
-
logger.info(f"Imported SSM parameter list: {param_key} with {len(imported_list)} items")
|
|
222
|
-
else:
|
|
223
|
-
# Handle string values
|
|
224
|
-
param_path = param_value
|
|
225
|
-
if not param_path.startswith('/'):
|
|
226
|
-
param_path = f"/{param_path}"
|
|
227
|
-
|
|
228
|
-
construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
|
|
229
|
-
param = ssm.StringParameter.from_string_parameter_name(
|
|
230
|
-
self, construct_id, param_path
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
self.ssm_imported_values[param_key] = param.string_value
|
|
234
|
-
logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
|
|
235
|
-
|
|
236
|
-
except Exception as e:
|
|
237
|
-
logger.error(f"Failed to import SSM parameter {param_key}: {e}")
|
|
238
|
-
raise
|
|
239
|
-
|
|
240
208
|
def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
|
|
241
209
|
"""Get security groups for the Load Balancer"""
|
|
242
210
|
security_groups = []
|
|
243
211
|
|
|
244
212
|
# Check SSM imported values first
|
|
245
|
-
if "security_groups" in self.
|
|
246
|
-
sg_ids = self.
|
|
213
|
+
if "security_groups" in self._ssm_imported_values:
|
|
214
|
+
sg_ids = self._ssm_imported_values["security_groups"]
|
|
247
215
|
if not isinstance(sg_ids, list):
|
|
248
216
|
sg_ids = [sg_ids]
|
|
249
217
|
else:
|
|
@@ -261,9 +229,16 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
261
229
|
"""Get subnets for the Load Balancer"""
|
|
262
230
|
subnets = []
|
|
263
231
|
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
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"]
|
|
267
242
|
|
|
268
243
|
# Check if this is a CDK token (unresolved SSM parameter)
|
|
269
244
|
if cdk.Token.is_unresolved(subnet_ids_value):
|
|
@@ -272,25 +247,40 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
272
247
|
# The ALB construct will handle the token-based subnet IDs
|
|
273
248
|
logger.info("Subnet IDs are unresolved tokens, will use vpc_subnets with token resolution")
|
|
274
249
|
return None
|
|
275
|
-
elif isinstance(subnet_ids_value, str):
|
|
276
|
-
# If it's a resolved string, split it
|
|
277
|
-
subnet_ids = [s.strip() for s in subnet_ids_value.split(',')]
|
|
278
|
-
elif isinstance(subnet_ids_value, list):
|
|
279
|
-
subnet_ids = subnet_ids_value
|
|
280
|
-
else:
|
|
281
|
-
subnet_ids = [subnet_ids_value]
|
|
282
|
-
else:
|
|
283
|
-
subnet_ids = self.lb_config.subnets
|
|
284
|
-
|
|
285
|
-
if not subnet_ids:
|
|
286
|
-
return None
|
|
287
250
|
|
|
251
|
+
# Convert subnet IDs to subnet objects
|
|
288
252
|
for idx, subnet_id in enumerate(subnet_ids):
|
|
289
253
|
subnets.append(
|
|
290
254
|
ec2.Subnet.from_subnet_id(self, f"Subnet-{idx}", subnet_id)
|
|
291
255
|
)
|
|
292
256
|
return subnets
|
|
293
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
|
+
|
|
294
284
|
def _create_target_groups(self, lb_name: str) -> None:
|
|
295
285
|
"""Create target groups for the Load Balancer"""
|
|
296
286
|
|
|
@@ -298,6 +288,9 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
298
288
|
tg_name = tg_config.get("name", f"tg-{idx}")
|
|
299
289
|
tg_id = f"{lb_name}-{tg_name}"
|
|
300
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
|
+
|
|
301
294
|
# Configure health check
|
|
302
295
|
health_check = self._configure_health_check(
|
|
303
296
|
tg_config.get("health_check", {})
|
|
@@ -308,7 +301,7 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
308
301
|
target_group = elbv2.ApplicationTargetGroup(
|
|
309
302
|
self,
|
|
310
303
|
tg_id,
|
|
311
|
-
target_group_name=
|
|
304
|
+
target_group_name=tg_name_sanitized,
|
|
312
305
|
vpc=self.vpc,
|
|
313
306
|
port=tg_config.get("port", 80),
|
|
314
307
|
protocol=elbv2.ApplicationProtocol(
|
|
@@ -323,7 +316,7 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
323
316
|
target_group = elbv2.NetworkTargetGroup(
|
|
324
317
|
self,
|
|
325
318
|
tg_id,
|
|
326
|
-
target_group_name=
|
|
319
|
+
target_group_name=tg_name_sanitized,
|
|
327
320
|
vpc=self.vpc,
|
|
328
321
|
port=tg_config.get("port", 80),
|
|
329
322
|
protocol=elbv2.Protocol(tg_config.get("protocol", "TCP")),
|
|
@@ -333,6 +326,8 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
333
326
|
health_check=health_check,
|
|
334
327
|
)
|
|
335
328
|
|
|
329
|
+
|
|
330
|
+
|
|
336
331
|
# Store target group for later use
|
|
337
332
|
self.target_groups[tg_name] = target_group
|
|
338
333
|
|
|
@@ -373,6 +368,11 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
373
368
|
if protocol.upper() == "HTTPS":
|
|
374
369
|
certificates = self._get_certificates()
|
|
375
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
|
+
|
|
376
376
|
listener = elbv2.ApplicationListener(
|
|
377
377
|
self,
|
|
378
378
|
listener_id,
|
|
@@ -425,8 +425,9 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
425
425
|
certificates = []
|
|
426
426
|
|
|
427
427
|
# Check SSM imported values first (takes priority)
|
|
428
|
-
|
|
429
|
-
|
|
428
|
+
ssm_imports = self.get_all_ssm_imports()
|
|
429
|
+
if "certificate_arns" in ssm_imports:
|
|
430
|
+
cert_arns = ssm_imports["certificate_arns"]
|
|
430
431
|
if not isinstance(cert_arns, list):
|
|
431
432
|
cert_arns = [cert_arns]
|
|
432
433
|
for cert_arn in cert_arns:
|
|
@@ -455,44 +456,42 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
455
456
|
# Configure conditions
|
|
456
457
|
conditions = []
|
|
457
458
|
|
|
458
|
-
#
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
http_headers = rule_config.get("http_headers", {})
|
|
470
|
-
for header_name, header_values in http_headers.items():
|
|
471
|
-
conditions.append(
|
|
472
|
-
elbv2.ListenerCondition.http_header(header_name, header_values)
|
|
473
|
-
)
|
|
474
|
-
|
|
475
|
-
# Query strings
|
|
476
|
-
query_strings = rule_config.get("query_strings", [])
|
|
477
|
-
if query_strings:
|
|
478
|
-
query_string_conditions = []
|
|
479
|
-
for qs in query_strings:
|
|
480
|
-
query_string_conditions.append(
|
|
481
|
-
elbv2.QueryStringCondition(
|
|
482
|
-
key=qs.get("key"), value=qs.get("value")
|
|
459
|
+
# Parse AWS ALB conditions format
|
|
460
|
+
aws_conditions = rule_config.get("conditions", [])
|
|
461
|
+
for condition in aws_conditions:
|
|
462
|
+
field = condition.get("field")
|
|
463
|
+
if field == "http-header" and "http_header_config" in condition:
|
|
464
|
+
header_config = condition["http_header_config"]
|
|
465
|
+
header_name = header_config.get("header_name")
|
|
466
|
+
header_values = header_config.get("values", [])
|
|
467
|
+
if header_name and header_values:
|
|
468
|
+
conditions.append(
|
|
469
|
+
elbv2.ListenerCondition.http_header(header_name, header_values)
|
|
483
470
|
)
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
471
|
+
elif field == "path-pattern" and "values" in condition:
|
|
472
|
+
path_patterns = condition.get("values", [])
|
|
473
|
+
if path_patterns:
|
|
474
|
+
conditions.append(elbv2.ListenerCondition.path_patterns(path_patterns))
|
|
475
|
+
elif field == "host-header" and "values" in condition:
|
|
476
|
+
host_headers = condition.get("values", [])
|
|
477
|
+
if host_headers:
|
|
478
|
+
conditions.append(elbv2.ListenerCondition.host_headers(host_headers))
|
|
488
479
|
|
|
489
480
|
# Configure actions
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
)
|
|
481
|
+
target_group = None
|
|
482
|
+
|
|
483
|
+
# Parse AWS ALB actions format
|
|
484
|
+
aws_actions = rule_config.get("actions", [])
|
|
485
|
+
for action in aws_actions:
|
|
486
|
+
if action.get("type") == "forward":
|
|
487
|
+
target_group_name = action.get("target_group")
|
|
488
|
+
target_group = (
|
|
489
|
+
self.target_groups.get(target_group_name) if target_group_name else None
|
|
490
|
+
)
|
|
491
|
+
break # Use first forward action
|
|
494
492
|
|
|
495
|
-
|
|
493
|
+
# Validate that we have both conditions and target group before creating rule
|
|
494
|
+
if target_group and conditions:
|
|
496
495
|
# Create rule with forward action
|
|
497
496
|
elbv2.ApplicationListenerRule(
|
|
498
497
|
self,
|
|
@@ -502,6 +501,15 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
502
501
|
conditions=conditions,
|
|
503
502
|
target_groups=[target_group],
|
|
504
503
|
)
|
|
504
|
+
elif not conditions:
|
|
505
|
+
logger.warning(
|
|
506
|
+
f"Skipping listener rule '{rule_id}' - no conditions defined. "
|
|
507
|
+
f"CDK requires at least one condition for every rule."
|
|
508
|
+
)
|
|
509
|
+
elif not target_group:
|
|
510
|
+
logger.warning(
|
|
511
|
+
f"Skipping listener rule '{rule_id}' - no valid target group found."
|
|
512
|
+
)
|
|
505
513
|
|
|
506
514
|
def _add_ip_whitelist_rules(
|
|
507
515
|
self,
|
|
@@ -648,36 +656,7 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
648
656
|
|
|
649
657
|
def _export_cfn_outputs(self, lb_name: str) -> None:
|
|
650
658
|
"""Add CloudFormation outputs for the Load Balancer"""
|
|
651
|
-
|
|
652
|
-
# Load Balancer DNS Name
|
|
653
|
-
cdk.CfnOutput(
|
|
654
|
-
self,
|
|
655
|
-
f"{lb_name}-dns-name",
|
|
656
|
-
value=self.load_balancer.load_balancer_dns_name,
|
|
657
|
-
export_name=f"{self.deployment.build_resource_name(lb_name)}-dns-name",
|
|
658
|
-
)
|
|
659
|
-
|
|
660
|
-
# Load Balancer ARN
|
|
661
|
-
cdk.CfnOutput(
|
|
662
|
-
self,
|
|
663
|
-
f"{lb_name}-arn",
|
|
664
|
-
value=self.load_balancer.load_balancer_arn,
|
|
665
|
-
export_name=f"{self.deployment.build_resource_name(lb_name)}-arn",
|
|
666
|
-
)
|
|
667
|
-
|
|
668
|
-
# Target Group ARNs
|
|
669
|
-
for tg_name, target_group in self.target_groups.items():
|
|
670
|
-
# Normalize target group name for consistent CloudFormation export naming
|
|
671
|
-
normalized_tg_name = self.normalize_resource_name(
|
|
672
|
-
tg_name, for_export=True
|
|
673
|
-
)
|
|
674
|
-
cdk.CfnOutput(
|
|
675
|
-
self,
|
|
676
|
-
f"{lb_name}-{normalized_tg_name}-arn",
|
|
677
|
-
value=target_group.target_group_arn,
|
|
678
|
-
export_name=f"{self.deployment.build_resource_name(lb_name)}-{normalized_tg_name}-arn",
|
|
679
|
-
)
|
|
680
|
-
|
|
659
|
+
return
|
|
681
660
|
def _export_ssm_parameters(self, lb_name: str) -> None:
|
|
682
661
|
"""Export Load Balancer resources to SSM Parameter Store if configured"""
|
|
683
662
|
if not self.load_balancer:
|
|
@@ -708,32 +687,4 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
708
687
|
|
|
709
688
|
def _export_cfn_outputs(self, lb_name: str) -> None:
|
|
710
689
|
"""Add CloudFormation outputs for the Load Balancer"""
|
|
711
|
-
|
|
712
|
-
# Load Balancer DNS Name
|
|
713
|
-
cdk.CfnOutput(
|
|
714
|
-
self,
|
|
715
|
-
f"{lb_name}-dns-name",
|
|
716
|
-
value=self.load_balancer.load_balancer_dns_name,
|
|
717
|
-
export_name=f"{self.deployment.build_resource_name(lb_name)}-dns-name",
|
|
718
|
-
)
|
|
719
|
-
|
|
720
|
-
# Load Balancer ARN
|
|
721
|
-
cdk.CfnOutput(
|
|
722
|
-
self,
|
|
723
|
-
f"{lb_name}-arn",
|
|
724
|
-
value=self.load_balancer.load_balancer_arn,
|
|
725
|
-
export_name=f"{self.deployment.build_resource_name(lb_name)}-arn",
|
|
726
|
-
)
|
|
727
|
-
|
|
728
|
-
# Target Group ARNs
|
|
729
|
-
for tg_name, target_group in self.target_groups.items():
|
|
730
|
-
# Normalize target group name for consistent CloudFormation export naming
|
|
731
|
-
normalized_tg_name = self.normalize_resource_name(
|
|
732
|
-
tg_name, for_export=True
|
|
733
|
-
)
|
|
734
|
-
cdk.CfnOutput(
|
|
735
|
-
self,
|
|
736
|
-
f"{lb_name}-{normalized_tg_name}-arn",
|
|
737
|
-
value=target_group.target_group_arn,
|
|
738
|
-
export_name=f"{self.deployment.build_resource_name(lb_name)}-{normalized_tg_name}-arn",
|
|
739
|
-
)
|
|
690
|
+
return
|
|
@@ -69,8 +69,18 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
69
69
|
self.rds_config = RdsConfig(stack_config.dictionary.get("rds", {}), deployment)
|
|
70
70
|
db_name = deployment.build_resource_name(self.rds_config.name)
|
|
71
71
|
|
|
72
|
-
#
|
|
73
|
-
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()
|
|
74
84
|
|
|
75
85
|
# Get VPC and security groups
|
|
76
86
|
self.security_groups = self._get_security_groups()
|
|
@@ -87,40 +97,13 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
87
97
|
# Export to SSM Parameter Store
|
|
88
98
|
self._export_ssm_parameters(db_name)
|
|
89
99
|
|
|
90
|
-
def _process_ssm_imports(self) -> None:
|
|
91
|
-
"""Process SSM imports from configuration"""
|
|
92
|
-
ssm_imports = self.rds_config.ssm_imports
|
|
93
|
-
|
|
94
|
-
if not ssm_imports:
|
|
95
|
-
logger.debug("No SSM imports configured for RDS")
|
|
96
|
-
return
|
|
97
|
-
|
|
98
|
-
logger.info(f"Processing {len(ssm_imports)} SSM imports for RDS")
|
|
99
|
-
|
|
100
|
-
for param_key, param_path in ssm_imports.items():
|
|
101
|
-
try:
|
|
102
|
-
if not param_path.startswith('/'):
|
|
103
|
-
param_path = f"/{param_path}"
|
|
104
|
-
|
|
105
|
-
construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
|
|
106
|
-
param = ssm.StringParameter.from_string_parameter_name(
|
|
107
|
-
self, construct_id, param_path
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
self.ssm_imported_values[param_key] = param.string_value
|
|
111
|
-
logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
|
|
112
|
-
|
|
113
|
-
except Exception as e:
|
|
114
|
-
logger.error(f"Failed to import SSM parameter {param_key} from {param_path}: {e}")
|
|
115
|
-
raise
|
|
116
|
-
|
|
117
100
|
@property
|
|
118
101
|
def vpc(self) -> ec2.IVpc:
|
|
119
102
|
"""Get the VPC for the RDS instance using centralized VPC provider mixin."""
|
|
120
|
-
if self._vpc:
|
|
103
|
+
if hasattr(self, '_vpc') and self._vpc:
|
|
121
104
|
return self._vpc
|
|
122
105
|
|
|
123
|
-
#
|
|
106
|
+
# Resolve VPC using the centralized VPC provider mixin
|
|
124
107
|
self._vpc = self.resolve_vpc(
|
|
125
108
|
config=self.rds_config,
|
|
126
109
|
deployment=self.deployment,
|
|
@@ -133,8 +116,9 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
133
116
|
security_groups = []
|
|
134
117
|
|
|
135
118
|
# Check SSM imports first for security group ID
|
|
136
|
-
|
|
137
|
-
|
|
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"]
|
|
138
122
|
security_groups.append(
|
|
139
123
|
ec2.SecurityGroup.from_security_group_id(
|
|
140
124
|
self, "RDSSecurityGroup", sg_id
|
|
@@ -151,27 +135,60 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
151
135
|
|
|
152
136
|
return security_groups
|
|
153
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
|
+
|
|
154
165
|
def _create_db_instance(self, db_name: str) -> rds.DatabaseInstance:
|
|
155
166
|
"""Create a new RDS instance"""
|
|
156
167
|
# Configure subnet group
|
|
157
168
|
# If we have subnet IDs from SSM, create a DB subnet group explicitly
|
|
158
169
|
db_subnet_group = None
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
)
|
|
172
189
|
|
|
173
190
|
# Configure subnet selection for VPC (when not using SSM imports)
|
|
174
|
-
subnets = None if db_subnet_group else
|
|
191
|
+
subnets = None if db_subnet_group else self._get_subnet_selection()
|
|
175
192
|
|
|
176
193
|
# Configure engine
|
|
177
194
|
engine_version = None
|
|
@@ -268,31 +285,7 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
|
268
285
|
|
|
269
286
|
def _add_outputs(self, db_name: str) -> None:
|
|
270
287
|
"""Add CloudFormation outputs for the RDS instance"""
|
|
271
|
-
|
|
272
|
-
# Database endpoint
|
|
273
|
-
cdk.CfnOutput(
|
|
274
|
-
self,
|
|
275
|
-
f"{db_name}-endpoint",
|
|
276
|
-
value=self.db_instance.db_instance_endpoint_address,
|
|
277
|
-
export_name=f"{self.deployment.build_resource_name(db_name)}-endpoint",
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
# Database port
|
|
281
|
-
cdk.CfnOutput(
|
|
282
|
-
self,
|
|
283
|
-
f"{db_name}-port",
|
|
284
|
-
value=self.db_instance.db_instance_endpoint_port,
|
|
285
|
-
export_name=f"{self.deployment.build_resource_name(db_name)}-port",
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
# Secret ARN (if available)
|
|
289
|
-
if hasattr(self.db_instance, "secret") and self.db_instance.secret:
|
|
290
|
-
cdk.CfnOutput(
|
|
291
|
-
self,
|
|
292
|
-
f"{db_name}-secret-arn",
|
|
293
|
-
value=self.db_instance.secret.secret_arn,
|
|
294
|
-
export_name=f"{self.deployment.build_resource_name(db_name)}-secret-arn",
|
|
295
|
-
)
|
|
288
|
+
return
|
|
296
289
|
|
|
297
290
|
def _export_ssm_parameters(self, db_name: str) -> None:
|
|
298
291
|
"""Export RDS connection info and credentials to SSM Parameter Store"""
|