cdk-factory 0.16.15__py3-none-any.whl → 0.17.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.
- cdk_factory/configurations/base_config.py +23 -24
- cdk_factory/configurations/cdk_config.py +1 -1
- cdk_factory/configurations/devops.py +1 -1
- cdk_factory/configurations/resources/cloudfront.py +7 -2
- cdk_factory/configurations/resources/ecr.py +1 -1
- cdk_factory/configurations/resources/ecs_cluster.py +7 -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 +612 -0
- cdk_factory/interfaces/vpc_provider_mixin.py +53 -29
- 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_standardized.py +530 -0
- 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 -1
- cdk_factory/stack_library/ecs/ecs_cluster_stack_standardized.py +305 -0
- 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 +11 -35
- cdk_factory/stack_library/rds/rds_stack.py +10 -27
- cdk_factory/stack_library/route53/route53_stack.py +2 -2
- cdk_factory/stack_library/rum/rum_stack.py +102 -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/vpc/vpc_stack_standardized.py +411 -0
- cdk_factory/utilities/api_gateway_integration_utility.py +24 -16
- cdk_factory/utilities/environment_services.py +3 -3
- 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.17.0.dist-info}/METADATA +1 -1
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.17.0.dist-info}/RECORD +52 -52
- cdk_factory/interfaces/enhanced_ssm_parameter_mixin.py +0 -321
- cdk_factory/interfaces/ssm_parameter_mixin.py +0 -454
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +0 -721
- cdk_factory/stack_library/ecs/ecs_cluster_stack.py +0 -232
- cdk_factory/stack_library/vpc/vpc_stack.py +0 -298
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.17.0.dist-info}/WHEEL +0 -0
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.17.0.dist-info}/entry_points.txt +0 -0
- {cdk_factory-0.16.15.dist-info → cdk_factory-0.17.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto Scaling Group Stack Pattern for CDK-Factory (Standardized SSM Version)
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Any, List, Optional
|
|
8
|
+
|
|
9
|
+
import aws_cdk as cdk
|
|
10
|
+
from aws_cdk import aws_ec2 as ec2
|
|
11
|
+
from aws_cdk import aws_autoscaling as autoscaling
|
|
12
|
+
from aws_cdk import aws_cloudwatch as cloudwatch
|
|
13
|
+
from aws_cdk import aws_iam as iam
|
|
14
|
+
from aws_cdk import aws_ssm as ssm
|
|
15
|
+
from aws_cdk import aws_ecs as ecs
|
|
16
|
+
from aws_cdk import Duration, Stack
|
|
17
|
+
from aws_lambda_powertools import Logger
|
|
18
|
+
from constructs import Construct
|
|
19
|
+
|
|
20
|
+
from cdk_factory.configurations.deployment import DeploymentConfig
|
|
21
|
+
from cdk_factory.configurations.stack import StackConfig
|
|
22
|
+
from cdk_factory.configurations.resources.auto_scaling import AutoScalingConfig
|
|
23
|
+
from cdk_factory.interfaces.istack import IStack
|
|
24
|
+
from cdk_factory.interfaces.vpc_provider_mixin import VPCProviderMixin
|
|
25
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
|
|
26
|
+
from cdk_factory.stack.stack_module_registry import register_stack
|
|
27
|
+
from cdk_factory.workload.workload_factory import WorkloadConfig
|
|
28
|
+
|
|
29
|
+
logger = Logger(service="AutoScalingStackStandardized")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@register_stack("auto_scaling_library_module")
|
|
33
|
+
@register_stack("auto_scaling_stack")
|
|
34
|
+
class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
|
|
35
|
+
"""
|
|
36
|
+
Reusable stack for AWS Auto Scaling Groups with standardized SSM integration.
|
|
37
|
+
|
|
38
|
+
This version uses the StandardizedSsmMixin to provide consistent SSM parameter
|
|
39
|
+
handling across all CDK Factory modules.
|
|
40
|
+
|
|
41
|
+
Key Features:
|
|
42
|
+
- Standardized SSM import/export patterns
|
|
43
|
+
- Template variable resolution
|
|
44
|
+
- Comprehensive validation
|
|
45
|
+
- Clear error handling
|
|
46
|
+
- Backward compatibility
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
|
|
50
|
+
# Initialize parent classes properly
|
|
51
|
+
super().__init__(scope, id, **kwargs)
|
|
52
|
+
|
|
53
|
+
# Initialize VPC cache from mixin
|
|
54
|
+
self._initialize_vpc_cache()
|
|
55
|
+
|
|
56
|
+
# Initialize module attributes
|
|
57
|
+
self.asg_config = None
|
|
58
|
+
self.stack_config = None
|
|
59
|
+
self.deployment = None
|
|
60
|
+
self.workload = None
|
|
61
|
+
self.security_groups = []
|
|
62
|
+
self.auto_scaling_group = None
|
|
63
|
+
self.launch_template = None
|
|
64
|
+
self.instance_role = None
|
|
65
|
+
self.user_data = None
|
|
66
|
+
self.user_data_commands = [] # Store raw commands for ECS cluster detection
|
|
67
|
+
self.ecs_cluster = None
|
|
68
|
+
|
|
69
|
+
def build(
|
|
70
|
+
self,
|
|
71
|
+
stack_config: StackConfig,
|
|
72
|
+
deployment: DeploymentConfig,
|
|
73
|
+
workload: WorkloadConfig,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Build the Auto Scaling Group stack"""
|
|
76
|
+
self._build(stack_config, deployment, workload)
|
|
77
|
+
|
|
78
|
+
def _build(
|
|
79
|
+
self,
|
|
80
|
+
stack_config: StackConfig,
|
|
81
|
+
deployment: DeploymentConfig,
|
|
82
|
+
workload: WorkloadConfig,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Internal build method for the Auto Scaling Group stack"""
|
|
85
|
+
self.stack_config = stack_config
|
|
86
|
+
self.deployment = deployment
|
|
87
|
+
self.workload = workload
|
|
88
|
+
|
|
89
|
+
self.asg_config = AutoScalingConfig(
|
|
90
|
+
stack_config.dictionary.get("auto_scaling", {}), deployment
|
|
91
|
+
)
|
|
92
|
+
asg_name = deployment.build_resource_name(self.asg_config.name)
|
|
93
|
+
|
|
94
|
+
# Setup standardized SSM integration
|
|
95
|
+
self.setup_standardized_ssm_integration(
|
|
96
|
+
scope=self,
|
|
97
|
+
config=self.asg_config,
|
|
98
|
+
resource_type="auto_scaling",
|
|
99
|
+
resource_name=asg_name,
|
|
100
|
+
deployment=deployment,
|
|
101
|
+
workload=workload
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Process SSM imports using standardized method
|
|
105
|
+
self.process_standardized_ssm_imports()
|
|
106
|
+
|
|
107
|
+
# Get security groups using standardized approach
|
|
108
|
+
self.security_groups = self._get_security_groups()
|
|
109
|
+
|
|
110
|
+
# Create IAM role for instances
|
|
111
|
+
self.instance_role = self._create_instance_role(asg_name)
|
|
112
|
+
|
|
113
|
+
# Create VPC once to be reused by both ECS cluster and ASG
|
|
114
|
+
self._vpc = None # Store VPC for reuse
|
|
115
|
+
|
|
116
|
+
# Create ECS cluster if ECS configuration is detected
|
|
117
|
+
self.ecs_cluster = self._create_ecs_cluster_if_needed()
|
|
118
|
+
|
|
119
|
+
# Create user data (after ECS cluster so it can reference it)
|
|
120
|
+
self.user_data = self._create_user_data()
|
|
121
|
+
|
|
122
|
+
# Create launch template
|
|
123
|
+
self.launch_template = self._create_launch_template(asg_name)
|
|
124
|
+
|
|
125
|
+
# Create Auto Scaling Group
|
|
126
|
+
self.auto_scaling_group = self._create_auto_scaling_group(asg_name)
|
|
127
|
+
|
|
128
|
+
# Add scaling policies
|
|
129
|
+
self._add_scaling_policies()
|
|
130
|
+
|
|
131
|
+
# Add update policy
|
|
132
|
+
self._add_update_policy()
|
|
133
|
+
|
|
134
|
+
# Export SSM parameters
|
|
135
|
+
self._export_ssm_parameters()
|
|
136
|
+
|
|
137
|
+
logger.info(f"Auto Scaling Group {asg_name} built successfully")
|
|
138
|
+
|
|
139
|
+
def _get_ssm_imports(self) -> Dict[str, Any]:
|
|
140
|
+
"""Get SSM imports from configuration"""
|
|
141
|
+
return self.asg_config.ssm_imports
|
|
142
|
+
|
|
143
|
+
def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
|
|
144
|
+
"""
|
|
145
|
+
Get security groups for the Auto Scaling Group using standardized SSM imports.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of security group references
|
|
149
|
+
"""
|
|
150
|
+
security_groups = []
|
|
151
|
+
|
|
152
|
+
# Primary method: Use standardized SSM imports
|
|
153
|
+
ssm_imports = self._get_ssm_imports()
|
|
154
|
+
if "security_group_ids" in ssm_imports:
|
|
155
|
+
imported_sg_ids = ssm_imports["security_group_ids"]
|
|
156
|
+
if isinstance(imported_sg_ids, list):
|
|
157
|
+
for idx, sg_id in enumerate(imported_sg_ids):
|
|
158
|
+
security_groups.append(
|
|
159
|
+
ec2.SecurityGroup.from_security_group_id(
|
|
160
|
+
self, f"SecurityGroup-SSM-{idx}", sg_id
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
logger.info(f"Added {len(imported_sg_ids)} security groups from SSM imports")
|
|
164
|
+
else:
|
|
165
|
+
security_groups.append(
|
|
166
|
+
ec2.SecurityGroup.from_security_group_id(
|
|
167
|
+
self, f"SecurityGroup-SSM-0", imported_sg_ids
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
logger.info(f"Added security group from SSM imports")
|
|
171
|
+
|
|
172
|
+
# Fallback: Check for direct configuration (backward compatibility)
|
|
173
|
+
elif self.asg_config.security_group_ids:
|
|
174
|
+
logger.warning("Using direct security group configuration - consider migrating to SSM imports")
|
|
175
|
+
for idx, sg_id in enumerate(self.asg_config.security_group_ids):
|
|
176
|
+
logger.info(f"Adding security group from direct config: {sg_id}")
|
|
177
|
+
# Handle comma-separated security group IDs
|
|
178
|
+
if "," in sg_id:
|
|
179
|
+
blocks = sg_id.split(",")
|
|
180
|
+
for block_idx, block in enumerate(blocks):
|
|
181
|
+
security_groups.append(
|
|
182
|
+
ec2.SecurityGroup.from_security_group_id(
|
|
183
|
+
self, f"SecurityGroup-Direct-{idx}-{block_idx}", block.strip()
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
security_groups.append(
|
|
188
|
+
ec2.SecurityGroup.from_security_group_id(
|
|
189
|
+
self, f"SecurityGroup-Direct-{idx}", sg_id
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
logger.warning("No security groups found from SSM imports or direct configuration")
|
|
194
|
+
|
|
195
|
+
return security_groups
|
|
196
|
+
|
|
197
|
+
def _get_vpc_id(self) -> str:
|
|
198
|
+
"""
|
|
199
|
+
Get VPC ID using the centralized VPC provider mixin.
|
|
200
|
+
"""
|
|
201
|
+
# Use the centralized VPC resolution from VPCProviderMixin
|
|
202
|
+
vpc = self.resolve_vpc(
|
|
203
|
+
config=self.asg_config,
|
|
204
|
+
deployment=self.deployment,
|
|
205
|
+
workload=self.workload
|
|
206
|
+
)
|
|
207
|
+
return vpc.vpc_id
|
|
208
|
+
|
|
209
|
+
def _get_subnet_ids(self) -> List[str]:
|
|
210
|
+
"""
|
|
211
|
+
Get subnet IDs using standardized SSM approach.
|
|
212
|
+
"""
|
|
213
|
+
# Primary method: Use standardized SSM imports
|
|
214
|
+
ssm_imports = self._get_ssm_imports()
|
|
215
|
+
if "subnet_ids" in ssm_imports:
|
|
216
|
+
return ssm_imports["subnet_ids"]
|
|
217
|
+
|
|
218
|
+
# Fallback: Use VPC provider mixin (backward compatibility)
|
|
219
|
+
elif hasattr(self, '_get_subnets_from_provider'):
|
|
220
|
+
return self._get_subnets_from_provider()
|
|
221
|
+
|
|
222
|
+
# Final fallback: Direct configuration
|
|
223
|
+
elif hasattr(self.asg_config, 'subnet_ids') and self.asg_config.subnet_ids:
|
|
224
|
+
return self.asg_config.subnet_ids
|
|
225
|
+
|
|
226
|
+
else:
|
|
227
|
+
logger.warning("No subnet IDs found, using default behavior")
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
def _create_instance_role(self, asg_name: str) -> iam.Role:
|
|
231
|
+
"""Create IAM role for EC2 instances"""
|
|
232
|
+
role = iam.Role(
|
|
233
|
+
self,
|
|
234
|
+
f"{asg_name}-InstanceRole",
|
|
235
|
+
assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"),
|
|
236
|
+
role_name=f"{asg_name}-role",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Add managed policies
|
|
240
|
+
for policy_name in self.asg_config.managed_policies:
|
|
241
|
+
role.add_managed_policy(
|
|
242
|
+
iam.ManagedPolicy.from_aws_managed_policy_name(policy_name)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
logger.info(f"Created instance role: {role.role_name}")
|
|
246
|
+
return role
|
|
247
|
+
|
|
248
|
+
def _create_user_data(self) -> ec2.UserData:
|
|
249
|
+
"""Create user data for EC2 instances"""
|
|
250
|
+
user_data = ec2.UserData.for_linux()
|
|
251
|
+
|
|
252
|
+
# Add basic setup commands
|
|
253
|
+
user_data.add_commands(
|
|
254
|
+
"#!/bin/bash",
|
|
255
|
+
"yum update -y",
|
|
256
|
+
"yum install -y aws-cfn-bootstrap",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Add user data commands from configuration
|
|
260
|
+
if self.asg_config.user_data_commands:
|
|
261
|
+
# Process template variables in user data commands
|
|
262
|
+
processed_commands = []
|
|
263
|
+
ssm_imports = self._get_ssm_imports()
|
|
264
|
+
for command in self.asg_config.user_data_commands:
|
|
265
|
+
processed_command = command
|
|
266
|
+
# Substitute SSM-imported values
|
|
267
|
+
if "ecs_cluster_name" in ssm_imports and "{{ecs_cluster_name}}" in command:
|
|
268
|
+
cluster_name = ssm_imports["ecs_cluster_name"]
|
|
269
|
+
processed_command = command.replace("{{ecs_cluster_name}}", cluster_name)
|
|
270
|
+
processed_commands.append(processed_command)
|
|
271
|
+
|
|
272
|
+
user_data.add_commands(*processed_commands)
|
|
273
|
+
self.user_data_commands = processed_commands
|
|
274
|
+
|
|
275
|
+
# Add ECS cluster configuration if needed
|
|
276
|
+
if self.ecs_cluster:
|
|
277
|
+
# Use the SSM-imported cluster name if available, otherwise fallback to default format
|
|
278
|
+
ssm_imports = self._get_ssm_imports()
|
|
279
|
+
if "ecs_cluster_name" in ssm_imports:
|
|
280
|
+
cluster_name = ssm_imports["ecs_cluster_name"]
|
|
281
|
+
ecs_commands = [
|
|
282
|
+
f"echo 'ECS_CLUSTER={cluster_name}' >> /etc/ecs/ecs.config",
|
|
283
|
+
"systemctl restart ecs"
|
|
284
|
+
]
|
|
285
|
+
else:
|
|
286
|
+
# Fallback to default naming pattern
|
|
287
|
+
ecs_commands = [
|
|
288
|
+
"echo 'ECS_CLUSTER={}{}' >> /etc/ecs/ecs.config".format(
|
|
289
|
+
self.deployment.workload_name, self.deployment.environment
|
|
290
|
+
),
|
|
291
|
+
"systemctl restart ecs"
|
|
292
|
+
]
|
|
293
|
+
user_data.add_commands(*ecs_commands)
|
|
294
|
+
|
|
295
|
+
logger.info(f"Created user data with {len(self.user_data_commands)} custom commands")
|
|
296
|
+
return user_data
|
|
297
|
+
|
|
298
|
+
def _get_or_create_vpc(self) -> ec2.Vpc:
|
|
299
|
+
"""Get or create VPC for reuse across the stack"""
|
|
300
|
+
if self._vpc is None:
|
|
301
|
+
vpc_id = self._get_vpc_id()
|
|
302
|
+
subnet_ids = self._get_subnet_ids()
|
|
303
|
+
|
|
304
|
+
# Create VPC and subnets from imported values
|
|
305
|
+
self._vpc = ec2.Vpc.from_vpc_attributes(
|
|
306
|
+
self, "ImportedVPC",
|
|
307
|
+
vpc_id=vpc_id,
|
|
308
|
+
availability_zones=["us-east-1a", "us-east-1b"] # Add required availability zones
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Create and store subnets if we have subnet IDs
|
|
312
|
+
self._subnets = []
|
|
313
|
+
if subnet_ids:
|
|
314
|
+
for i, subnet_id in enumerate(subnet_ids):
|
|
315
|
+
subnet = ec2.Subnet.from_subnet_id(
|
|
316
|
+
self, f"ImportedSubnet-{i}", subnet_id
|
|
317
|
+
)
|
|
318
|
+
self._subnets.append(subnet)
|
|
319
|
+
else:
|
|
320
|
+
# Use default subnets from VPC
|
|
321
|
+
self._subnets = self._vpc.public_subnets
|
|
322
|
+
|
|
323
|
+
return self._vpc
|
|
324
|
+
|
|
325
|
+
def _get_subnets(self) -> List[ec2.Subnet]:
|
|
326
|
+
"""Get the subnets from the shared VPC"""
|
|
327
|
+
return getattr(self, '_subnets', [])
|
|
328
|
+
|
|
329
|
+
def _create_ecs_cluster_if_needed(self) -> Optional[ecs.Cluster]:
|
|
330
|
+
"""Create ECS cluster if ECS configuration is detected"""
|
|
331
|
+
# Check if user data contains ECS configuration (use raw config since user_data_commands might not be set yet)
|
|
332
|
+
ecs_detected = False
|
|
333
|
+
if self.asg_config.user_data_commands:
|
|
334
|
+
ecs_detected = any("ECS_CLUSTER" in cmd for cmd in self.asg_config.user_data_commands)
|
|
335
|
+
|
|
336
|
+
if ecs_detected:
|
|
337
|
+
ssm_imports = self._get_ssm_imports()
|
|
338
|
+
if "ecs_cluster_name" in ssm_imports:
|
|
339
|
+
cluster_name = ssm_imports["ecs_cluster_name"]
|
|
340
|
+
|
|
341
|
+
# Use the shared VPC
|
|
342
|
+
vpc = self._get_or_create_vpc()
|
|
343
|
+
|
|
344
|
+
self.ecs_cluster = ecs.Cluster.from_cluster_attributes(
|
|
345
|
+
self,
|
|
346
|
+
"ImportedECSCluster",
|
|
347
|
+
cluster_name=cluster_name,
|
|
348
|
+
vpc=vpc
|
|
349
|
+
)
|
|
350
|
+
logger.info(f"Connected to existing ECS cluster: {cluster_name}")
|
|
351
|
+
|
|
352
|
+
return self.ecs_cluster
|
|
353
|
+
|
|
354
|
+
def _create_launch_template(self, asg_name: str) -> ec2.LaunchTemplate:
|
|
355
|
+
"""Create launch template for Auto Scaling Group"""
|
|
356
|
+
|
|
357
|
+
# Use the configured AMI ID or fall back to appropriate lookup
|
|
358
|
+
if self.asg_config.ami_id:
|
|
359
|
+
# Use explicit AMI ID provided by user
|
|
360
|
+
machine_image = ec2.MachineImage.lookup(name=self.asg_config.ami_id)
|
|
361
|
+
elif self.asg_config.ami_type:
|
|
362
|
+
# Use AMI type for dynamic lookup
|
|
363
|
+
if self.asg_config.ami_type.upper() == "AMAZON-LINUX-2023":
|
|
364
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2023()
|
|
365
|
+
elif self.asg_config.ami_type.upper() == "AMAZON-LINUX-2022":
|
|
366
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2022()
|
|
367
|
+
elif self.asg_config.ami_type.upper() == "AMAZON-LINUX-2":
|
|
368
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2()
|
|
369
|
+
elif self.asg_config.ami_type.upper() == "ECS_OPTIMIZED":
|
|
370
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2023()
|
|
371
|
+
else:
|
|
372
|
+
# Default to latest Amazon Linux
|
|
373
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2023()
|
|
374
|
+
else:
|
|
375
|
+
# Default fallback
|
|
376
|
+
machine_image = ec2.MachineImage.latest_amazon_linux2023()
|
|
377
|
+
|
|
378
|
+
launch_template = ec2.LaunchTemplate(
|
|
379
|
+
self,
|
|
380
|
+
f"{asg_name}-LaunchTemplate",
|
|
381
|
+
instance_type=ec2.InstanceType(self.asg_config.instance_type),
|
|
382
|
+
machine_image=machine_image,
|
|
383
|
+
role=self.instance_role,
|
|
384
|
+
user_data=self.user_data,
|
|
385
|
+
security_group=self.security_groups[0] if self.security_groups else None,
|
|
386
|
+
key_name=self.asg_config.key_name,
|
|
387
|
+
detailed_monitoring=self.asg_config.detailed_monitoring,
|
|
388
|
+
block_devices=[
|
|
389
|
+
ec2.BlockDevice(
|
|
390
|
+
device_name=block_device.get("device_name", "/dev/xvda"),
|
|
391
|
+
volume=ec2.BlockDeviceVolume.ebs(
|
|
392
|
+
volume_size=block_device.get("volume_size", 8),
|
|
393
|
+
volume_type=getattr(ec2.EbsDeviceVolumeType, block_device.get("volume_type", "GP3").upper()),
|
|
394
|
+
delete_on_termination=block_device.get("delete_on_termination", True),
|
|
395
|
+
encrypted=block_device.get("encrypted", False),
|
|
396
|
+
)
|
|
397
|
+
) for block_device in self.asg_config.block_devices
|
|
398
|
+
] if self.asg_config.block_devices else None,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
logger.info(f"Created launch template: {launch_template.launch_template_name}")
|
|
402
|
+
return launch_template
|
|
403
|
+
|
|
404
|
+
def _create_auto_scaling_group(self, asg_name: str) -> autoscaling.AutoScalingGroup:
|
|
405
|
+
"""Create Auto Scaling Group"""
|
|
406
|
+
# Use the shared VPC and subnets
|
|
407
|
+
vpc = self._get_or_create_vpc()
|
|
408
|
+
subnets = self._get_subnets()
|
|
409
|
+
|
|
410
|
+
auto_scaling_group = autoscaling.AutoScalingGroup(
|
|
411
|
+
self,
|
|
412
|
+
f"{asg_name}-ASG",
|
|
413
|
+
vpc=vpc,
|
|
414
|
+
vpc_subnets=ec2.SubnetSelection(subnets=subnets),
|
|
415
|
+
launch_template=self.launch_template,
|
|
416
|
+
min_capacity=self.asg_config.min_capacity,
|
|
417
|
+
max_capacity=self.asg_config.max_capacity,
|
|
418
|
+
desired_capacity=self.asg_config.desired_capacity,
|
|
419
|
+
health_check=autoscaling.HealthCheck.elb(
|
|
420
|
+
grace=cdk.Duration.seconds(self.asg_config.health_check_grace_period)
|
|
421
|
+
) if self.asg_config.health_check_type.upper() == "ELB" else autoscaling.HealthCheck.ec2(
|
|
422
|
+
grace=cdk.Duration.seconds(self.asg_config.health_check_grace_period)
|
|
423
|
+
),
|
|
424
|
+
cooldown=cdk.Duration.seconds(self.asg_config.cooldown),
|
|
425
|
+
termination_policies=[
|
|
426
|
+
getattr(autoscaling.TerminationPolicy, policy.upper())
|
|
427
|
+
for policy in self.asg_config.termination_policies
|
|
428
|
+
],
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Attach target groups if configured
|
|
432
|
+
self._attach_target_groups(auto_scaling_group)
|
|
433
|
+
|
|
434
|
+
logger.info(f"Created Auto Scaling Group: {asg_name}")
|
|
435
|
+
return auto_scaling_group
|
|
436
|
+
|
|
437
|
+
def _attach_target_groups(self, asg: autoscaling.AutoScalingGroup) -> None:
|
|
438
|
+
"""Attach the Auto Scaling Group to target groups"""
|
|
439
|
+
target_group_arns = self._get_target_group_arns()
|
|
440
|
+
|
|
441
|
+
if not target_group_arns:
|
|
442
|
+
logger.warning("No target group ARNs found for Auto Scaling Group")
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
# Get the underlying CloudFormation resource to add target group ARNs
|
|
446
|
+
cfn_asg = asg.node.default_child
|
|
447
|
+
cfn_asg.add_property_override("TargetGroupARNs", target_group_arns)
|
|
448
|
+
|
|
449
|
+
def _get_target_group_arns(self) -> List[str]:
|
|
450
|
+
"""Get target group ARNs using standardized SSM approach"""
|
|
451
|
+
target_group_arns = []
|
|
452
|
+
|
|
453
|
+
# Use standardized SSM imports
|
|
454
|
+
ssm_imports = self._get_ssm_imports()
|
|
455
|
+
if "target_group_arns" in ssm_imports:
|
|
456
|
+
imported_arns = ssm_imports["target_group_arns"]
|
|
457
|
+
if isinstance(imported_arns, list):
|
|
458
|
+
target_group_arns.extend(imported_arns)
|
|
459
|
+
else:
|
|
460
|
+
target_group_arns.append(imported_arns)
|
|
461
|
+
|
|
462
|
+
# Fallback: Direct configuration
|
|
463
|
+
elif self.asg_config.target_group_arns:
|
|
464
|
+
target_group_arns.extend(self.asg_config.target_group_arns)
|
|
465
|
+
|
|
466
|
+
return target_group_arns
|
|
467
|
+
|
|
468
|
+
def _add_scaling_policies(self) -> None:
|
|
469
|
+
"""Add scaling policies to the Auto Scaling Group"""
|
|
470
|
+
if not self.asg_config.scaling_policies:
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
for policy_config in self.asg_config.scaling_policies:
|
|
474
|
+
if policy_config.get("type") == "target_tracking":
|
|
475
|
+
# Create a target tracking scaling policy for CPU utilization
|
|
476
|
+
scaling_policy = autoscaling.CfnScalingPolicy(
|
|
477
|
+
self,
|
|
478
|
+
"CPUScalingPolicy",
|
|
479
|
+
auto_scaling_group_name=self.auto_scaling_group.auto_scaling_group_name,
|
|
480
|
+
policy_type="TargetTrackingScaling",
|
|
481
|
+
target_tracking_configuration=autoscaling.CfnScalingPolicy.TargetTrackingConfigurationProperty(
|
|
482
|
+
target_value=policy_config.get("target_cpu", 70),
|
|
483
|
+
predefined_metric_specification=autoscaling.CfnScalingPolicy.PredefinedMetricSpecificationProperty(
|
|
484
|
+
predefined_metric_type="ASGAverageCPUUtilization"
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
logger.info("Added CPU utilization scaling policy")
|
|
489
|
+
|
|
490
|
+
def _add_update_policy(self) -> None:
|
|
491
|
+
"""Add update policy to the Auto Scaling Group"""
|
|
492
|
+
update_policy = self.asg_config.update_policy
|
|
493
|
+
|
|
494
|
+
if not update_policy:
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
# Get the underlying CloudFormation resource to add update policy
|
|
498
|
+
cfn_asg = self.auto_scaling_group.node.default_child
|
|
499
|
+
|
|
500
|
+
# Clear any existing update policy and set the rolling update policy
|
|
501
|
+
cfn_asg.add_property_override("UpdatePolicy", {
|
|
502
|
+
"AutoScalingRollingUpdate": {
|
|
503
|
+
"MinInstancesInService": update_policy.get("min_instances_in_service", 1),
|
|
504
|
+
"MaxBatchSize": update_policy.get("max_batch_size", 1),
|
|
505
|
+
"PauseTime": f"PT{update_policy.get('pause_time', 300)}S"
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
logger.info("Added rolling update policy to Auto Scaling Group")
|
|
510
|
+
|
|
511
|
+
def _export_ssm_parameters(self) -> None:
|
|
512
|
+
"""Export SSM parameters using standardized approach"""
|
|
513
|
+
if not self.auto_scaling_group:
|
|
514
|
+
logger.warning("No Auto Scaling Group to export")
|
|
515
|
+
return
|
|
516
|
+
|
|
517
|
+
# Prepare resource values for export
|
|
518
|
+
resource_values = {
|
|
519
|
+
"auto_scaling_group_name": self.auto_scaling_group.auto_scaling_group_name,
|
|
520
|
+
"auto_scaling_group_arn": self.auto_scaling_group.auto_scaling_group_arn,
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
# Export using standardized SSM mixin
|
|
524
|
+
exported_params = self.export_standardized_ssm_parameters(resource_values)
|
|
525
|
+
|
|
526
|
+
logger.info(f"Exported SSM parameters: {exported_params}")
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
# Backward compatibility alias
|
|
530
|
+
AutoScalingStackStandardized = AutoScalingStack
|
|
@@ -15,7 +15,7 @@ from cdk_factory.configurations.deployment import DeploymentConfig
|
|
|
15
15
|
from cdk_factory.configurations.stack import StackConfig
|
|
16
16
|
from cdk_factory.configurations.resources.code_artifact import CodeArtifactConfig
|
|
17
17
|
from cdk_factory.interfaces.istack import IStack
|
|
18
|
-
from cdk_factory.interfaces.
|
|
18
|
+
from cdk_factory.interfaces.standardized_ssm_mixin import StandardizedSsmMixin
|
|
19
19
|
from cdk_factory.stack.stack_module_registry import register_stack
|
|
20
20
|
from cdk_factory.workload.workload_factory import WorkloadConfig
|
|
21
21
|
|
|
@@ -24,7 +24,7 @@ logger = Logger(service="CodeArtifactStack")
|
|
|
24
24
|
|
|
25
25
|
@register_stack("code_artifact_library_module")
|
|
26
26
|
@register_stack("code_artifact_stack")
|
|
27
|
-
class CodeArtifactStack(IStack,
|
|
27
|
+
class CodeArtifactStack(IStack, StandardizedSsmMixin):
|
|
28
28
|
"""
|
|
29
29
|
Reusable stack for AWS CodeArtifact.
|
|
30
30
|
Supports creating domains and repositories with configurable settings.
|