cdk-factory 0.17.6__py3-none-any.whl → 0.20.2__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.

Files changed (44) hide show
  1. cdk_factory/configurations/deployment.py +12 -0
  2. cdk_factory/configurations/resources/acm.py +9 -2
  3. cdk_factory/configurations/resources/auto_scaling.py +7 -5
  4. cdk_factory/configurations/resources/ecs_cluster.py +5 -0
  5. cdk_factory/configurations/resources/ecs_service.py +24 -2
  6. cdk_factory/configurations/resources/lambda_edge.py +18 -4
  7. cdk_factory/configurations/resources/rds.py +1 -1
  8. cdk_factory/configurations/resources/route53.py +5 -0
  9. cdk_factory/configurations/resources/s3.py +9 -1
  10. cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py +1 -1
  11. cdk_factory/constructs/lambdas/policies/policy_docs.py +1 -1
  12. cdk_factory/interfaces/networked_stack_mixin.py +1 -1
  13. cdk_factory/interfaces/standardized_ssm_mixin.py +82 -10
  14. cdk_factory/stack_library/acm/acm_stack.py +5 -15
  15. cdk_factory/stack_library/api_gateway/api_gateway_stack.py +2 -2
  16. cdk_factory/stack_library/auto_scaling/{auto_scaling_stack_standardized.py → auto_scaling_stack.py} +213 -105
  17. cdk_factory/stack_library/cloudfront/cloudfront_stack.py +76 -22
  18. cdk_factory/stack_library/code_artifact/code_artifact_stack.py +3 -25
  19. cdk_factory/stack_library/cognito/cognito_stack.py +2 -2
  20. cdk_factory/stack_library/dynamodb/dynamodb_stack.py +2 -2
  21. cdk_factory/stack_library/ecs/__init__.py +2 -4
  22. cdk_factory/stack_library/ecs/{ecs_cluster_stack_standardized.py → ecs_cluster_stack.py} +52 -41
  23. cdk_factory/stack_library/ecs/ecs_service_stack.py +49 -26
  24. cdk_factory/stack_library/lambda_edge/EDGE_LOG_RETENTION_TODO.md +226 -0
  25. cdk_factory/stack_library/lambda_edge/LAMBDA_EDGE_LOG_RETENTION_BLOG.md +215 -0
  26. cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +241 -81
  27. cdk_factory/stack_library/load_balancer/load_balancer_stack.py +128 -177
  28. cdk_factory/stack_library/rds/rds_stack.py +65 -72
  29. cdk_factory/stack_library/route53/route53_stack.py +244 -38
  30. cdk_factory/stack_library/rum/rum_stack.py +3 -3
  31. cdk_factory/stack_library/security_group/security_group_full_stack.py +1 -31
  32. cdk_factory/stack_library/security_group/security_group_stack.py +1 -8
  33. cdk_factory/stack_library/simple_queue_service/sqs_stack.py +1 -34
  34. cdk_factory/stack_library/stack_base.py +5 -0
  35. cdk_factory/stack_library/vpc/{vpc_stack_standardized.py → vpc_stack.py} +6 -109
  36. cdk_factory/stack_library/websites/static_website_stack.py +7 -3
  37. cdk_factory/utilities/api_gateway_integration_utility.py +2 -2
  38. cdk_factory/utilities/environment_services.py +2 -2
  39. cdk_factory/version.py +1 -1
  40. {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.2.dist-info}/METADATA +1 -1
  41. {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.2.dist-info}/RECORD +44 -42
  42. {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.2.dist-info}/WHEEL +0 -0
  43. {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.2.dist-info}/entry_points.txt +0 -0
  44. {cdk_factory-0.17.6.dist-info → cdk_factory-0.20.2.dist-info}/licenses/LICENSE +0 -0
@@ -9,11 +9,11 @@ from typing import Dict, Any, List, Optional
9
9
  import aws_cdk as cdk
10
10
  from aws_cdk import aws_ec2 as ec2
11
11
  from aws_cdk import aws_autoscaling as autoscaling
12
- from aws_cdk import aws_cloudwatch as cloudwatch
13
12
  from aws_cdk import aws_iam as iam
14
- from aws_cdk import aws_ssm as ssm
15
13
  from aws_cdk import aws_ecs as ecs
16
- from aws_cdk import Duration, Stack
14
+ from aws_cdk import Duration
15
+
16
+ from aws_cdk.aws_autoscaling import HealthChecks, AdditionalHealthCheckType
17
17
  from aws_lambda_powertools import Logger
18
18
  from constructs import Construct
19
19
 
@@ -34,10 +34,10 @@ logger = Logger(service="AutoScalingStackStandardized")
34
34
  class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
35
35
  """
36
36
  Reusable stack for AWS Auto Scaling Groups with standardized SSM integration.
37
-
37
+
38
38
  This version uses the StandardizedSsmMixin to provide consistent SSM parameter
39
39
  handling across all CDK Factory modules.
40
-
40
+
41
41
  Key Features:
42
42
  - Standardized SSM import/export patterns
43
43
  - Template variable resolution
@@ -49,10 +49,10 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
49
49
  def __init__(self, scope: Construct, id: str, **kwargs) -> None:
50
50
  # Initialize parent classes properly
51
51
  super().__init__(scope, id, **kwargs)
52
-
52
+
53
53
  # Initialize VPC cache from mixin
54
54
  self._initialize_vpc_cache()
55
-
55
+
56
56
  # Initialize module attributes
57
57
  self.asg_config = None
58
58
  self.stack_config = None
@@ -92,17 +92,17 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
92
92
  asg_name = deployment.build_resource_name(self.asg_config.name)
93
93
 
94
94
  # Setup standardized SSM integration
95
- self.setup_standardized_ssm_integration(
95
+ self.setup_ssm_integration(
96
96
  scope=self,
97
97
  config=self.asg_config,
98
98
  resource_type="auto_scaling",
99
99
  resource_name=asg_name,
100
100
  deployment=deployment,
101
- workload=workload
101
+ workload=workload,
102
102
  )
103
103
 
104
104
  # Process SSM imports using standardized method
105
- self.process_standardized_ssm_imports()
105
+ self.process_ssm_imports()
106
106
 
107
107
  # Get security groups using standardized approach
108
108
  self.security_groups = self._get_security_groups()
@@ -127,7 +127,7 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
127
127
 
128
128
  # Add scaling policies
129
129
  self._add_scaling_policies()
130
-
130
+
131
131
  # Add update policy
132
132
  self._add_update_policy()
133
133
 
@@ -143,12 +143,12 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
143
143
  def _get_security_groups(self) -> List[ec2.ISecurityGroup]:
144
144
  """
145
145
  Get security groups for the Auto Scaling Group using standardized SSM imports.
146
-
146
+
147
147
  Returns:
148
148
  List of security group references
149
149
  """
150
150
  security_groups = []
151
-
151
+
152
152
  # Primary method: Use standardized SSM imports
153
153
  ssm_imports = self._get_ssm_imports()
154
154
  if "security_group_ids" in ssm_imports:
@@ -160,7 +160,9 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
160
160
  self, f"SecurityGroup-SSM-{idx}", sg_id
161
161
  )
162
162
  )
163
- logger.info(f"Added {len(imported_sg_ids)} security groups from SSM imports")
163
+ logger.info(
164
+ f"Added {len(imported_sg_ids)} security groups from SSM imports"
165
+ )
164
166
  else:
165
167
  security_groups.append(
166
168
  ec2.SecurityGroup.from_security_group_id(
@@ -168,10 +170,12 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
168
170
  )
169
171
  )
170
172
  logger.info(f"Added security group from SSM imports")
171
-
173
+
172
174
  # Fallback: Check for direct configuration (backward compatibility)
173
175
  elif self.asg_config.security_group_ids:
174
- logger.warning("Using direct security group configuration - consider migrating to SSM imports")
176
+ logger.warning(
177
+ "Using direct security group configuration - consider migrating to SSM imports"
178
+ )
175
179
  for idx, sg_id in enumerate(self.asg_config.security_group_ids):
176
180
  logger.info(f"Adding security group from direct config: {sg_id}")
177
181
  # Handle comma-separated security group IDs
@@ -180,7 +184,9 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
180
184
  for block_idx, block in enumerate(blocks):
181
185
  security_groups.append(
182
186
  ec2.SecurityGroup.from_security_group_id(
183
- self, f"SecurityGroup-Direct-{idx}-{block_idx}", block.strip()
187
+ self,
188
+ f"SecurityGroup-Direct-{idx}-{block_idx}",
189
+ block.strip(),
184
190
  )
185
191
  )
186
192
  else:
@@ -190,8 +196,10 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
190
196
  )
191
197
  )
192
198
  else:
193
- logger.warning("No security groups found from SSM imports or direct configuration")
194
-
199
+ logger.warning(
200
+ "No security groups found from SSM imports or direct configuration"
201
+ )
202
+
195
203
  return security_groups
196
204
 
197
205
  def _get_vpc_id(self) -> str:
@@ -200,9 +208,7 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
200
208
  """
201
209
  # Use the centralized VPC resolution from VPCProviderMixin
202
210
  vpc = self.resolve_vpc(
203
- config=self.asg_config,
204
- deployment=self.deployment,
205
- workload=self.workload
211
+ config=self.asg_config, deployment=self.deployment, workload=self.workload
206
212
  )
207
213
  return vpc.vpc_id
208
214
 
@@ -211,21 +217,11 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
211
217
  Get subnet IDs using standardized SSM approach.
212
218
  """
213
219
  # 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 []
220
+ # ssm_imports = self._get_ssm_imports()
221
+
222
+ subnet_ids = self.get_subnet_ids(self.asg_config)
223
+
224
+ return subnet_ids
229
225
 
230
226
  def _create_instance_role(self, asg_name: str) -> iam.Role:
231
227
  """Create IAM role for EC2 instances"""
@@ -248,13 +244,14 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
248
244
  def _create_user_data(self) -> ec2.UserData:
249
245
  """Create user data for EC2 instances"""
250
246
  user_data = ec2.UserData.for_linux()
251
-
247
+
252
248
  # Add basic setup commands
253
- user_data.add_commands(
254
- "#!/bin/bash",
255
- "yum update -y",
256
- "yum install -y aws-cfn-bootstrap",
257
- )
249
+ # this will break amazon linux 2023 which uses dnf instead of yum
250
+ # user_data.add_commands(
251
+ # "#!/bin/bash",
252
+ # "yum update -y",
253
+ # "yum install -y aws-cfn-bootstrap",
254
+ # )
258
255
 
259
256
  # Add user data commands from configuration
260
257
  if self.asg_config.user_data_commands:
@@ -264,11 +261,13 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
264
261
  for command in self.asg_config.user_data_commands:
265
262
  processed_command = command
266
263
  # 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)
264
+ if "cluster_name" in ssm_imports and "{{cluster_name}}" in command:
265
+ cluster_name = ssm_imports["cluster_name"]
266
+ processed_command = command.replace(
267
+ "{{cluster_name}}", cluster_name
268
+ )
270
269
  processed_commands.append(processed_command)
271
-
270
+
272
271
  user_data.add_commands(*processed_commands)
273
272
  self.user_data_commands = processed_commands
274
273
 
@@ -276,11 +275,11 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
276
275
  if self.ecs_cluster:
277
276
  # Use the SSM-imported cluster name if available, otherwise fallback to default format
278
277
  ssm_imports = self._get_ssm_imports()
279
- if "ecs_cluster_name" in ssm_imports:
280
- cluster_name = ssm_imports["ecs_cluster_name"]
278
+ if "cluster_name" in ssm_imports:
279
+ cluster_name = ssm_imports["cluster_name"]
281
280
  ecs_commands = [
282
281
  f"echo 'ECS_CLUSTER={cluster_name}' >> /etc/ecs/ecs.config",
283
- "systemctl restart ecs"
282
+ "systemctl restart ecs",
284
283
  ]
285
284
  else:
286
285
  # Fallback to default naming pattern
@@ -288,11 +287,13 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
288
287
  "echo 'ECS_CLUSTER={}{}' >> /etc/ecs/ecs.config".format(
289
288
  self.deployment.workload_name, self.deployment.environment
290
289
  ),
291
- "systemctl restart ecs"
290
+ "systemctl restart ecs",
292
291
  ]
293
292
  user_data.add_commands(*ecs_commands)
294
293
 
295
- logger.info(f"Created user data with {len(self.user_data_commands)} custom commands")
294
+ logger.info(
295
+ f"Created user data with {len(self.user_data_commands)} custom commands"
296
+ )
296
297
  return user_data
297
298
 
298
299
  def _get_or_create_vpc(self) -> ec2.Vpc:
@@ -300,14 +301,18 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
300
301
  if self._vpc is None:
301
302
  vpc_id = self._get_vpc_id()
302
303
  subnet_ids = self._get_subnet_ids()
303
-
304
+
304
305
  # Create VPC and subnets from imported values
305
306
  self._vpc = ec2.Vpc.from_vpc_attributes(
306
- self, "ImportedVPC",
307
+ self,
308
+ "ImportedVPC",
307
309
  vpc_id=vpc_id,
308
- availability_zones=["us-east-1a", "us-east-1b"] # Add required availability zones
310
+ availability_zones=[
311
+ "us-east-1a",
312
+ "us-east-1b",
313
+ ], # Add required availability zones
309
314
  )
310
-
315
+
311
316
  # Create and store subnets if we have subnet IDs
312
317
  self._subnets = []
313
318
  if subnet_ids:
@@ -319,41 +324,40 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
319
324
  else:
320
325
  # Use default subnets from VPC
321
326
  self._subnets = self._vpc.public_subnets
322
-
327
+
323
328
  return self._vpc
324
329
 
325
330
  def _get_subnets(self) -> List[ec2.Subnet]:
326
331
  """Get the subnets from the shared VPC"""
327
- return getattr(self, '_subnets', [])
332
+ return getattr(self, "_subnets", [])
328
333
 
329
334
  def _create_ecs_cluster_if_needed(self) -> Optional[ecs.Cluster]:
330
335
  """Create ECS cluster if ECS configuration is detected"""
331
336
  # Check if user data contains ECS configuration (use raw config since user_data_commands might not be set yet)
332
337
  ecs_detected = False
333
338
  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
-
339
+ ecs_detected = any(
340
+ "ECS_CLUSTER" in cmd for cmd in self.asg_config.user_data_commands
341
+ )
342
+
336
343
  if ecs_detected:
337
344
  ssm_imports = self._get_ssm_imports()
338
- if "ecs_cluster_name" in ssm_imports:
339
- cluster_name = ssm_imports["ecs_cluster_name"]
340
-
345
+ if "cluster_name" in ssm_imports:
346
+ cluster_name = ssm_imports["cluster_name"]
347
+
341
348
  # Use the shared VPC
342
349
  vpc = self._get_or_create_vpc()
343
-
350
+
344
351
  self.ecs_cluster = ecs.Cluster.from_cluster_attributes(
345
- self,
346
- "ImportedECSCluster",
347
- cluster_name=cluster_name,
348
- vpc=vpc
352
+ self, "ImportedECSCluster", cluster_name=cluster_name, vpc=vpc
349
353
  )
350
354
  logger.info(f"Connected to existing ECS cluster: {cluster_name}")
351
-
355
+
352
356
  return self.ecs_cluster
353
357
 
354
358
  def _create_launch_template(self, asg_name: str) -> ec2.LaunchTemplate:
355
359
  """Create launch template for Auto Scaling Group"""
356
-
360
+
357
361
  # Use the configured AMI ID or fall back to appropriate lookup
358
362
  if self.asg_config.ami_id:
359
363
  # Use explicit AMI ID provided by user
@@ -367,14 +371,19 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
367
371
  elif self.asg_config.ami_type.upper() == "AMAZON-LINUX-2":
368
372
  machine_image = ec2.MachineImage.latest_amazon_linux2()
369
373
  elif self.asg_config.ami_type.upper() == "ECS_OPTIMIZED":
370
- machine_image = ec2.MachineImage.latest_amazon_linux2023()
374
+ # Use ECS-optimized AMI from SSM parameter
375
+ from aws_cdk import aws_ssm as ssm
376
+
377
+ machine_image = ec2.MachineImage.from_ssm_parameter(
378
+ parameter_name="/aws/service/ecs/optimized-ami/amazon-linux-2023/recommended/image_id"
379
+ )
371
380
  else:
372
381
  # Default to latest Amazon Linux
373
382
  machine_image = ec2.MachineImage.latest_amazon_linux2023()
374
383
  else:
375
384
  # Default fallback
376
385
  machine_image = ec2.MachineImage.latest_amazon_linux2023()
377
-
386
+
378
387
  launch_template = ec2.LaunchTemplate(
379
388
  self,
380
389
  f"{asg_name}-LaunchTemplate",
@@ -385,17 +394,27 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
385
394
  security_group=self.security_groups[0] if self.security_groups else None,
386
395
  key_name=self.asg_config.key_name,
387
396
  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),
397
+ block_devices=(
398
+ [
399
+ ec2.BlockDevice(
400
+ device_name=block_device.get("device_name", "/dev/xvda"),
401
+ volume=ec2.BlockDeviceVolume.ebs(
402
+ volume_size=block_device.get("volume_size", 8),
403
+ volume_type=getattr(
404
+ ec2.EbsDeviceVolumeType,
405
+ block_device.get("volume_type", "GP3").upper(),
406
+ ),
407
+ delete_on_termination=block_device.get(
408
+ "delete_on_termination", True
409
+ ),
410
+ encrypted=block_device.get("encrypted", False),
411
+ ),
396
412
  )
397
- ) for block_device in self.asg_config.block_devices
398
- ] if self.asg_config.block_devices else None,
413
+ for block_device in self.asg_config.block_devices
414
+ ]
415
+ if self.asg_config.block_devices
416
+ else None
417
+ ),
399
418
  )
400
419
 
401
420
  logger.info(f"Created launch template: {launch_template.launch_template_name}")
@@ -407,6 +426,22 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
407
426
  vpc = self._get_or_create_vpc()
408
427
  subnets = self._get_subnets()
409
428
 
429
+ health_checks = (
430
+ # ELB + EC2 (EC2 is always included; ELB is “additional”)
431
+ HealthChecks.with_additional_checks(
432
+ additional_types=[AdditionalHealthCheckType.ELB],
433
+ grace_period=Duration.seconds(
434
+ self.asg_config.health_check_grace_period
435
+ ),
436
+ )
437
+ if self.asg_config.health_check_type.upper() == "ELB"
438
+ # EC2-only
439
+ else HealthChecks.ec2(
440
+ grace_period=Duration.seconds(
441
+ self.asg_config.health_check_grace_period
442
+ ),
443
+ )
444
+ )
410
445
  auto_scaling_group = autoscaling.AutoScalingGroup(
411
446
  self,
412
447
  f"{asg_name}-ASG",
@@ -416,18 +451,18 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
416
451
  min_capacity=self.asg_config.min_capacity,
417
452
  max_capacity=self.asg_config.max_capacity,
418
453
  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
- ),
454
+ health_checks=health_checks,
424
455
  cooldown=cdk.Duration.seconds(self.asg_config.cooldown),
425
456
  termination_policies=[
426
- getattr(autoscaling.TerminationPolicy, policy.upper())
457
+ getattr(autoscaling.TerminationPolicy, policy.upper())
427
458
  for policy in self.asg_config.termination_policies
428
459
  ],
429
460
  )
430
461
 
462
+ # Add instance refresh if configured
463
+ if self.asg_config.instance_refresh:
464
+ self._configure_instance_refresh(auto_scaling_group)
465
+
431
466
  # Attach target groups if configured
432
467
  self._attach_target_groups(auto_scaling_group)
433
468
 
@@ -449,7 +484,7 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
449
484
  def _get_target_group_arns(self) -> List[str]:
450
485
  """Get target group ARNs using standardized SSM approach"""
451
486
  target_group_arns = []
452
-
487
+
453
488
  # Use standardized SSM imports
454
489
  ssm_imports = self._get_ssm_imports()
455
490
  if "target_group_arns" in ssm_imports:
@@ -458,11 +493,11 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
458
493
  target_group_arns.extend(imported_arns)
459
494
  else:
460
495
  target_group_arns.append(imported_arns)
461
-
496
+
462
497
  # Fallback: Direct configuration
463
498
  elif self.asg_config.target_group_arns:
464
499
  target_group_arns.extend(self.asg_config.target_group_arns)
465
-
500
+
466
501
  return target_group_arns
467
502
 
468
503
  def _add_scaling_policies(self) -> None:
@@ -482,30 +517,40 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
482
517
  target_value=policy_config.get("target_cpu", 70),
483
518
  predefined_metric_specification=autoscaling.CfnScalingPolicy.PredefinedMetricSpecificationProperty(
484
519
  predefined_metric_type="ASGAverageCPUUtilization"
485
- )
486
- )
520
+ ),
521
+ ),
487
522
  )
488
523
  logger.info("Added CPU utilization scaling policy")
489
524
 
490
525
  def _add_update_policy(self) -> None:
491
526
  """Add update policy to the Auto Scaling Group"""
492
527
  update_policy = self.asg_config.update_policy
493
-
528
+
494
529
  if not update_policy:
530
+ # No update policy configured, don't add one
495
531
  return
496
-
532
+
497
533
  # Get the underlying CloudFormation resource to add update policy
498
534
  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", {
535
+
536
+ # Get CDK's default policy first (if any)
537
+ default_policy = getattr(cfn_asg, "update_policy", {})
538
+
539
+ # Merge with defaults, then use the robust add_override method
540
+ merged_policy = {
541
+ **default_policy, # Preserve CDK defaults
502
542
  "AutoScalingRollingUpdate": {
503
- "MinInstancesInService": update_policy.get("min_instances_in_service", 1),
543
+ "MinInstancesInService": update_policy.get(
544
+ "min_instances_in_service", 1
545
+ ),
504
546
  "MaxBatchSize": update_policy.get("max_batch_size", 1),
505
- "PauseTime": f"PT{update_policy.get('pause_time', 300)}S"
506
- }
507
- })
508
-
547
+ "PauseTime": f"PT{update_policy.get('pause_time', 300)}S",
548
+ },
549
+ }
550
+
551
+ # Use the robust CDK-documented approach
552
+ cfn_asg.add_override("UpdatePolicy", merged_policy)
553
+
509
554
  logger.info("Added rolling update policy to Auto Scaling Group")
510
555
 
511
556
  def _export_ssm_parameters(self) -> None:
@@ -521,10 +566,73 @@ class AutoScalingStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
521
566
  }
522
567
 
523
568
  # Export using standardized SSM mixin
524
- exported_params = self.export_standardized_ssm_parameters(resource_values)
525
-
569
+ exported_params = self.export_ssm_parameters(resource_values)
570
+
526
571
  logger.info(f"Exported SSM parameters: {exported_params}")
527
572
 
573
+ def _configure_instance_refresh(self, asg: autoscaling.AutoScalingGroup) -> None:
574
+ """Configure instance refresh for rolling updates"""
575
+ instance_refresh_config = self.asg_config.instance_refresh
576
+
577
+ if not instance_refresh_config.get("enabled", False):
578
+ return
579
+
580
+ logger.warning("Instance refresh is not supported in this version of the CDK")
581
+ return
582
+
583
+ # Get the CloudFormation ASG resource
584
+ cfn_asg = asg.node.default_child
585
+
586
+ # Configure instance refresh using CloudFormation UpdatePolicy
587
+ # UpdatePolicy is added at the resource level, not as a property
588
+ update_policy = {
589
+ "AutoScalingRollingUpdate": {
590
+ "PauseTime": "PT300S", # 5 minutes pause
591
+ "MinInstancesInService": "1",
592
+ "MaxBatchSize": "1",
593
+ "WaitOnResourceSignals": True,
594
+ "SuspendProcesses": [
595
+ "HealthCheck",
596
+ "ReplaceUnhealthy",
597
+ "AZRebalance",
598
+ "AlarmNotification",
599
+ "ScheduledActions",
600
+ ],
601
+ }
602
+ }
603
+
604
+ # # Apply instance refresh using CloudFormation's cfn_options.update_policy
605
+ # cfn_asg.cfn_options.update_policy = cdk.CfnUpdatePolicy.from_rolling_update(
606
+ # pause_time=cdk.Duration.seconds(300),
607
+ # min_instances_in_service=1,
608
+ # max_batch_size=1,
609
+ # wait_on_resource_signals=True
610
+ # )
611
+
612
+ # Grab the L1 to attach UpdatePolicy.InstanceRefresh
613
+ cfn_asg: autoscaling.CfnAutoScalingGroup = asg.node.default_child
614
+
615
+ # cfn_asg.cfn_options.update_policy = CfnUpdatePolicy.from_auto_scaling_instance_refresh(
616
+ # # Triggers tell CFN *what* changes should start a refresh
617
+ # triggers=[CfnUpdatePolicy.InstanceRefreshTrigger.LAUNCH_TEMPLATE],
618
+ # preferences=CfnUpdatePolicy.InstanceRefreshPreferences(
619
+ # # warmup is like “grace” before counting a new instance healthy
620
+ # instance_warmup=Duration.minutes(5),
621
+ # # how aggressive the refresh is; 90 keeps capacity high
622
+ # min_healthy_percentage=90,
623
+ # # skip instances that already match the new LT (fast when only userdata/env tweaked)
624
+ # skip_matching=True,
625
+ # # optional: put instances in Standby first; default is rolling terminate/launch
626
+ # # standby_instances=CfnUpdatePolicy.StandbyInstances.TERMINATE,
627
+ # # checkpoint_percentages=[25, 50, 75], # optional: progressive checkpoints
628
+ # # checkpoint_delay=Duration.minutes(2), # optional delay at checkpoints
629
+ # ),
630
+ # )
631
+ logger.info(f"Configured instance refresh via CDK CfnUpdatePolicy")
632
+
633
+ # Note: This provides rolling update functionality similar to instance refresh
634
+ # For true instance refresh with preferences, we would need CDK v2.80+ or custom CloudFormation
635
+
528
636
 
529
637
  # Backward compatibility alias
530
638
  AutoScalingStackStandardized = AutoScalingStack