cdk-factory 0.15.10__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 +6 -4
- cdk_factory/configurations/deployment.py +12 -0
- cdk_factory/configurations/devops.py +1 -1
- cdk_factory/configurations/pipeline_stage.py +29 -5
- cdk_factory/configurations/resources/acm.py +85 -0
- cdk_factory/configurations/resources/auto_scaling.py +7 -5
- cdk_factory/configurations/resources/cloudfront.py +7 -2
- cdk_factory/configurations/resources/ecr.py +1 -1
- cdk_factory/configurations/resources/ecs_cluster.py +108 -0
- cdk_factory/configurations/resources/ecs_service.py +17 -2
- cdk_factory/configurations/resources/load_balancer.py +17 -4
- cdk_factory/configurations/resources/monitoring.py +8 -3
- cdk_factory/configurations/resources/rds.py +305 -19
- 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 +6 -3
- cdk_factory/interfaces/networked_stack_mixin.py +75 -0
- cdk_factory/interfaces/standardized_ssm_mixin.py +657 -0
- cdk_factory/interfaces/vpc_provider_mixin.py +210 -0
- cdk_factory/lambdas/edge/ip_gate/handler.py +42 -40
- cdk_factory/pipeline/pipeline_factory.py +222 -27
- cdk_factory/stack/stack_factory.py +34 -0
- cdk_factory/stack_library/__init__.py +3 -2
- cdk_factory/stack_library/acm/__init__.py +6 -0
- cdk_factory/stack_library/acm/acm_stack.py +169 -0
- cdk_factory/stack_library/api_gateway/api_gateway_stack.py +84 -59
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +366 -408
- 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 +12 -0
- cdk_factory/stack_library/ecs/ecs_cluster_stack.py +316 -0
- cdk_factory/stack_library/ecs/ecs_service_stack.py +20 -39
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +2 -2
- cdk_factory/stack_library/load_balancer/load_balancer_stack.py +151 -118
- cdk_factory/stack_library/rds/rds_stack.py +85 -74
- cdk_factory/stack_library/route53/route53_stack.py +8 -3
- 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 +12 -3
- cdk_factory/validation/config_validator.py +483 -0
- cdk_factory/version.py +1 -1
- cdk_factory/workload/workload_factory.py +1 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/METADATA +1 -1
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/RECORD +61 -54
- cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
- cdk_factory/interfaces/ssm_parameter_mixin.py +0 -329
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/WHEEL +0 -0
- {cdk_factory-0.15.10.dist-info → cdk_factory-0.18.9.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.15.10.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
|
|
|
@@ -112,34 +125,65 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
112
125
|
# Get subnets
|
|
113
126
|
subnets = self._get_subnets()
|
|
114
127
|
|
|
128
|
+
# Prepare vpc_subnets parameter
|
|
129
|
+
# If subnets is None, we'll handle it via escape hatch after creation
|
|
130
|
+
vpc_subnets_param = ec2.SubnetSelection(subnets=subnets) if subnets else None
|
|
131
|
+
|
|
115
132
|
# Create the Load Balancer based on type
|
|
116
133
|
if self.lb_config.type == "APPLICATION":
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
load_balancer_name
|
|
121
|
-
vpc
|
|
122
|
-
internet_facing
|
|
123
|
-
security_group
|
|
134
|
+
# When vpc_subnets is None and we have token-based subnet_ids,
|
|
135
|
+
# we need to create the ALB without vpc_subnets to avoid VPC subnet lookup errors
|
|
136
|
+
alb_props = {
|
|
137
|
+
"load_balancer_name": lb_name,
|
|
138
|
+
"vpc": self.vpc,
|
|
139
|
+
"internet_facing": self.lb_config.internet_facing,
|
|
140
|
+
"security_group": (
|
|
124
141
|
security_groups[0]
|
|
125
142
|
if security_groups and len(security_groups) > 0
|
|
126
143
|
else None
|
|
127
144
|
),
|
|
128
|
-
deletion_protection
|
|
129
|
-
idle_timeout
|
|
130
|
-
http2_enabled
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
"deletion_protection": self.lb_config.deletion_protection,
|
|
146
|
+
"idle_timeout": cdk.Duration.seconds(self.lb_config.idle_timeout),
|
|
147
|
+
"http2_enabled": self.lb_config.http2_enabled,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Only add vpc_subnets if we have concrete subnet objects
|
|
151
|
+
if vpc_subnets_param:
|
|
152
|
+
alb_props["vpc_subnets"] = vpc_subnets_param
|
|
153
|
+
|
|
154
|
+
load_balancer = elbv2.ApplicationLoadBalancer(self, lb_name, **alb_props)
|
|
133
155
|
else: # NETWORK
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
nlb_props = {
|
|
157
|
+
"load_balancer_name": lb_name,
|
|
158
|
+
"vpc": self.vpc,
|
|
159
|
+
"internet_facing": self.lb_config.internet_facing,
|
|
160
|
+
"deletion_protection": self.lb_config.deletion_protection,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Only add vpc_subnets if we have concrete subnet objects
|
|
164
|
+
if vpc_subnets_param:
|
|
165
|
+
nlb_props["vpc_subnets"] = vpc_subnets_param
|
|
166
|
+
|
|
167
|
+
load_balancer = elbv2.NetworkLoadBalancer(self, lb_name, **nlb_props)
|
|
168
|
+
|
|
169
|
+
# If subnets is None, check if we have SSM-imported subnet_ids as a token
|
|
170
|
+
# We need to use Fn.Split to convert the comma-separated string to an array
|
|
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
|
+
)
|
|
143
187
|
|
|
144
188
|
# Add tags
|
|
145
189
|
for key, value in self.lb_config.tags.items():
|
|
@@ -149,93 +193,25 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
149
193
|
|
|
150
194
|
@property
|
|
151
195
|
def vpc(self) -> ec2.IVpc:
|
|
152
|
-
"""Get the VPC for the Load Balancer"""
|
|
196
|
+
"""Get the VPC for the Load Balancer using centralized VPC provider mixin."""
|
|
153
197
|
if self._vpc:
|
|
154
198
|
return self._vpc
|
|
155
|
-
|
|
156
|
-
# Check SSM imported values first (tokens from SSM parameters)
|
|
157
|
-
if "vpc_id" in self.ssm_imported_values:
|
|
158
|
-
vpc_id = self.ssm_imported_values["vpc_id"]
|
|
159
|
-
|
|
160
|
-
# Build VPC attributes
|
|
161
|
-
vpc_attrs = {
|
|
162
|
-
"vpc_id": vpc_id,
|
|
163
|
-
"availability_zones": ["us-east-1a", "us-east-1b"]
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
# Use from_vpc_attributes() instead of from_lookup() because SSM imports return tokens
|
|
167
|
-
self._vpc = ec2.Vpc.from_vpc_attributes(self, "VPC", **vpc_attrs)
|
|
168
|
-
elif self.lb_config.vpc_id:
|
|
169
|
-
self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.lb_config.vpc_id)
|
|
170
|
-
elif self.workload.vpc_id:
|
|
171
|
-
self._vpc = ec2.Vpc.from_lookup(self, "VPC", vpc_id=self.workload.vpc_id)
|
|
172
|
-
else:
|
|
173
|
-
# Use default VPC if not provided
|
|
174
|
-
raise ValueError(
|
|
175
|
-
"VPC is not defined in the configuration. "
|
|
176
|
-
"You can provide it a the load_balancer.vpc_id in the configuration "
|
|
177
|
-
"or a top level workload.vpc_id in the workload configuration."
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
return self._vpc
|
|
181
|
-
|
|
182
|
-
def _process_ssm_imports(self) -> None:
|
|
183
|
-
"""
|
|
184
|
-
Process SSM imports from configuration.
|
|
185
|
-
Follows the same pattern as RDS and Security Group stacks.
|
|
186
|
-
"""
|
|
187
|
-
from aws_cdk import aws_ssm as ssm
|
|
188
|
-
|
|
189
|
-
ssm_imports = self.lb_config.ssm_imports
|
|
190
199
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
# Handle list values (like security_groups)
|
|
200
|
-
if isinstance(param_value, list):
|
|
201
|
-
imported_list = []
|
|
202
|
-
for idx, param_path in enumerate(param_value):
|
|
203
|
-
if not param_path.startswith('/'):
|
|
204
|
-
param_path = f"/{param_path}"
|
|
205
|
-
|
|
206
|
-
construct_id = f"ssm-import-{param_key}-{idx}-{hash(param_path) % 10000}"
|
|
207
|
-
param = ssm.StringParameter.from_string_parameter_name(
|
|
208
|
-
self, construct_id, param_path
|
|
209
|
-
)
|
|
210
|
-
imported_list.append(param.string_value)
|
|
211
|
-
|
|
212
|
-
self.ssm_imported_values[param_key] = imported_list
|
|
213
|
-
logger.info(f"Imported SSM parameter list: {param_key} with {len(imported_list)} items")
|
|
214
|
-
else:
|
|
215
|
-
# Handle string values
|
|
216
|
-
param_path = param_value
|
|
217
|
-
if not param_path.startswith('/'):
|
|
218
|
-
param_path = f"/{param_path}"
|
|
219
|
-
|
|
220
|
-
construct_id = f"ssm-import-{param_key}-{hash(param_path) % 10000}"
|
|
221
|
-
param = ssm.StringParameter.from_string_parameter_name(
|
|
222
|
-
self, construct_id, param_path
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
self.ssm_imported_values[param_key] = param.string_value
|
|
226
|
-
logger.info(f"Imported SSM parameter: {param_key} from {param_path}")
|
|
227
|
-
|
|
228
|
-
except Exception as e:
|
|
229
|
-
logger.error(f"Failed to import SSM parameter {param_key}: {e}")
|
|
230
|
-
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
|
|
231
207
|
|
|
232
208
|
def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
|
|
233
209
|
"""Get security groups for the Load Balancer"""
|
|
234
210
|
security_groups = []
|
|
235
211
|
|
|
236
212
|
# Check SSM imported values first
|
|
237
|
-
if "security_groups" in self.
|
|
238
|
-
sg_ids = self.
|
|
213
|
+
if "security_groups" in self._ssm_imported_values:
|
|
214
|
+
sg_ids = self._ssm_imported_values["security_groups"]
|
|
239
215
|
if not isinstance(sg_ids, list):
|
|
240
216
|
sg_ids = [sg_ids]
|
|
241
217
|
else:
|
|
@@ -253,23 +229,58 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
253
229
|
"""Get subnets for the Load Balancer"""
|
|
254
230
|
subnets = []
|
|
255
231
|
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
subnet_ids = [s.strip() for s in subnet_ids.split(',')]
|
|
262
|
-
elif not isinstance(subnet_ids, list):
|
|
263
|
-
subnet_ids = [subnet_ids]
|
|
264
|
-
else:
|
|
265
|
-
subnet_ids = self.lb_config.subnets
|
|
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
|
|
266
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"]
|
|
242
|
+
|
|
243
|
+
# Check if this is a CDK token (unresolved SSM parameter)
|
|
244
|
+
if cdk.Token.is_unresolved(subnet_ids_value):
|
|
245
|
+
# For tokens, we can't split at synth time
|
|
246
|
+
# Return None to signal that subnets should be resolved via SubnetSelection
|
|
247
|
+
# The ALB construct will handle the token-based subnet IDs
|
|
248
|
+
logger.info("Subnet IDs are unresolved tokens, will use vpc_subnets with token resolution")
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
# Convert subnet IDs to subnet objects
|
|
267
252
|
for idx, subnet_id in enumerate(subnet_ids):
|
|
268
253
|
subnets.append(
|
|
269
254
|
ec2.Subnet.from_subnet_id(self, f"Subnet-{idx}", subnet_id)
|
|
270
255
|
)
|
|
271
256
|
return subnets
|
|
272
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
|
+
|
|
273
284
|
def _create_target_groups(self, lb_name: str) -> None:
|
|
274
285
|
"""Create target groups for the Load Balancer"""
|
|
275
286
|
|
|
@@ -277,6 +288,9 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
277
288
|
tg_name = tg_config.get("name", f"tg-{idx}")
|
|
278
289
|
tg_id = f"{lb_name}-{tg_name}"
|
|
279
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
|
+
|
|
280
294
|
# Configure health check
|
|
281
295
|
health_check = self._configure_health_check(
|
|
282
296
|
tg_config.get("health_check", {})
|
|
@@ -287,7 +301,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
287
301
|
target_group = elbv2.ApplicationTargetGroup(
|
|
288
302
|
self,
|
|
289
303
|
tg_id,
|
|
290
|
-
target_group_name=
|
|
304
|
+
target_group_name=tg_name_sanitized,
|
|
291
305
|
vpc=self.vpc,
|
|
292
306
|
port=tg_config.get("port", 80),
|
|
293
307
|
protocol=elbv2.ApplicationProtocol(
|
|
@@ -302,7 +316,7 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
302
316
|
target_group = elbv2.NetworkTargetGroup(
|
|
303
317
|
self,
|
|
304
318
|
tg_id,
|
|
305
|
-
target_group_name=
|
|
319
|
+
target_group_name=tg_name_sanitized,
|
|
306
320
|
vpc=self.vpc,
|
|
307
321
|
port=tg_config.get("port", 80),
|
|
308
322
|
protocol=elbv2.Protocol(tg_config.get("protocol", "TCP")),
|
|
@@ -312,6 +326,8 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
312
326
|
health_check=health_check,
|
|
313
327
|
)
|
|
314
328
|
|
|
329
|
+
|
|
330
|
+
|
|
315
331
|
# Store target group for later use
|
|
316
332
|
self.target_groups[tg_name] = target_group
|
|
317
333
|
|
|
@@ -352,6 +368,11 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
352
368
|
if protocol.upper() == "HTTPS":
|
|
353
369
|
certificates = self._get_certificates()
|
|
354
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
|
+
|
|
355
376
|
listener = elbv2.ApplicationListener(
|
|
356
377
|
self,
|
|
357
378
|
listener_id,
|
|
@@ -402,8 +423,20 @@ class LoadBalancerStack(IStack, EnhancedSsmParameterMixin):
|
|
|
402
423
|
def _get_certificates(self) -> List[elbv2.ListenerCertificate]:
|
|
403
424
|
"""Get certificates for HTTPS listeners"""
|
|
404
425
|
certificates = []
|
|
405
|
-
|
|
406
|
-
|
|
426
|
+
|
|
427
|
+
# Check SSM imported values first (takes priority)
|
|
428
|
+
ssm_imports = self.get_all_ssm_imports()
|
|
429
|
+
if "certificate_arns" in ssm_imports:
|
|
430
|
+
cert_arns = ssm_imports["certificate_arns"]
|
|
431
|
+
if not isinstance(cert_arns, list):
|
|
432
|
+
cert_arns = [cert_arns]
|
|
433
|
+
for cert_arn in cert_arns:
|
|
434
|
+
certificates.append(elbv2.ListenerCertificate.from_arn(cert_arn))
|
|
435
|
+
logger.info(f"Using {len(cert_arns)} certificate(s) from SSM")
|
|
436
|
+
else:
|
|
437
|
+
# Fall back to config values
|
|
438
|
+
for cert_arn in self.lb_config.certificate_arns:
|
|
439
|
+
certificates.append(elbv2.ListenerCertificate.from_arn(cert_arn))
|
|
407
440
|
|
|
408
441
|
if self.ssl_certificate:
|
|
409
442
|
certificates.append(
|
|
@@ -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
|
|
@@ -211,8 +211,10 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
|
|
|
211
211
|
raise ValueError(f"Unsupported database engine: {self.rds_config.engine}")
|
|
212
212
|
|
|
213
213
|
# Configure instance type
|
|
214
|
+
# Strip 'db.' prefix if present since ec2.InstanceType expects just the instance family/size
|
|
214
215
|
instance_class = self.rds_config.instance_class
|
|
215
|
-
|
|
216
|
+
instance_class_name = instance_class.replace("db.", "") if instance_class.startswith("db.") else instance_class
|
|
217
|
+
instance_type = ec2.InstanceType(instance_class_name)
|
|
216
218
|
|
|
217
219
|
# Configure removal policy
|
|
218
220
|
removal_policy = None
|
|
@@ -242,9 +244,18 @@ class RdsStack(IStack, EnhancedSsmParameterMixin):
|
|
|
242
244
|
"backup_retention": Duration.days(self.rds_config.backup_retention),
|
|
243
245
|
"cloudwatch_logs_exports": self.rds_config.cloudwatch_logs_exports,
|
|
244
246
|
"enable_performance_insights": self.rds_config.enable_performance_insights,
|
|
247
|
+
"allow_major_version_upgrade": self.rds_config.allow_major_version_upgrade,
|
|
245
248
|
"removal_policy": removal_policy,
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
# Add storage auto-scaling if max_allocated_storage is configured
|
|
252
|
+
if self.rds_config.max_allocated_storage:
|
|
253
|
+
db_props["max_allocated_storage"] = self.rds_config.max_allocated_storage
|
|
254
|
+
logger.info(
|
|
255
|
+
f"Storage auto-scaling enabled: {self.rds_config.allocated_storage}GB "
|
|
256
|
+
f"-> {self.rds_config.max_allocated_storage}GB"
|
|
257
|
+
)
|
|
258
|
+
|
|
248
259
|
# Use either subnet group or vpc_subnets depending on what's available
|
|
249
260
|
if db_subnet_group:
|
|
250
261
|
db_props["subnet_group"] = rds.SubnetGroup.from_subnet_group_name(
|
|
@@ -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.
|
|
@@ -58,8 +58,13 @@ class Route53Stack(IStack, EnhancedSsmParameterMixin):
|
|
|
58
58
|
# Get or create hosted zone
|
|
59
59
|
self.hosted_zone = self._get_or_create_hosted_zone()
|
|
60
60
|
|
|
61
|
-
# Create certificate if needed
|
|
61
|
+
# Create certificate if needed (DEPRECATED - use dedicated ACM stack)
|
|
62
62
|
if self.route53_config.create_certificate:
|
|
63
|
+
logger.warning(
|
|
64
|
+
"Creating certificates in Route53Stack is deprecated. "
|
|
65
|
+
"Please use the dedicated 'acm_stack' module for certificate management. "
|
|
66
|
+
"This feature will be maintained for backward compatibility."
|
|
67
|
+
)
|
|
63
68
|
self.certificate = self._create_certificate()
|
|
64
69
|
|
|
65
70
|
# Create DNS records
|