cdk-factory 0.19.19__py3-none-any.whl → 0.21.1__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.
Files changed (22) hide show
  1. cdk_factory/configurations/resources/rds.py +17 -8
  2. cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +122 -85
  3. cdk_factory/stack_library/ecs/ecs_cluster_stack.py +2 -2
  4. cdk_factory/stack_library/ecs/ecs_service_stack.py +2 -0
  5. cdk_factory/stack_library/lambda_edge/functions/README.md +0 -0
  6. cdk_factory/stack_library/lambda_edge/functions/log_retention_manager/__init__.py +33 -0
  7. cdk_factory/stack_library/lambda_edge/functions/log_retention_manager/app.py +30 -0
  8. cdk_factory/stack_library/lambda_edge/functions/log_retention_manager/edge_log_retention.py +85 -0
  9. cdk_factory/stack_library/lambda_edge/functions/log_retention_manager/requirements.txt +2 -0
  10. cdk_factory/stack_library/lambda_edge/functions/log_retention_manager/test.py +22 -0
  11. cdk_factory/stack_library/lambda_edge/lambda_edge_log_retention_stack.py +0 -0
  12. cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +94 -8
  13. cdk_factory/stack_library/load_balancer/load_balancer_stack.py +4 -0
  14. cdk_factory/stack_library/rds/rds_stack.py +15 -13
  15. cdk_factory/stack_library/route53/route53_stack.py +97 -133
  16. cdk_factory/version.py +1 -1
  17. {cdk_factory-0.19.19.dist-info → cdk_factory-0.21.1.dist-info}/METADATA +1 -1
  18. {cdk_factory-0.19.19.dist-info → cdk_factory-0.21.1.dist-info}/RECORD +21 -15
  19. cdk_factory/stack_library/lambda_edge/EDGE_LOG_RETENTION_TODO.md +0 -226
  20. {cdk_factory-0.19.19.dist-info → cdk_factory-0.21.1.dist-info}/WHEEL +0 -0
  21. {cdk_factory-0.19.19.dist-info → cdk_factory-0.21.1.dist-info}/entry_points.txt +0 -0
  22. {cdk_factory-0.19.19.dist-info → cdk_factory-0.21.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ import boto3
3
+ from botocore.exceptions import ClientError
4
+ import os
5
+
6
+ profile_name = os.getenv('AWS_PROFILE')
7
+ session = boto3.Session(region_name='us-east-1', profile_name=profile_name)
8
+ ec2 = session.client('ec2')
9
+
10
+ def set_edge_log_retention(retention_days=7, dry_run=True):
11
+ """
12
+ Find Lambda@Edge log groups across all regions and set retention policies.
13
+
14
+ Args:
15
+ retention_days (int): Number of days to retain logs
16
+ dry_run (bool): If True, only show what would be changed
17
+ """
18
+ # Get all AWS regions
19
+ regions = [region['RegionName'] for region in ec2.describe_regions()['Regions']]
20
+
21
+ edge_log_groups = []
22
+ total_changed = 0
23
+
24
+ print(f"🔍 Hunting for Lambda@Edge log groups across {len(regions)} regions...")
25
+ print(f"🎯 Target retention: {retention_days} days")
26
+ print(f"🧪 Dry run: {dry_run}")
27
+ print("=" * 60)
28
+
29
+ for region in regions:
30
+ try:
31
+ logs = session.client('logs', region_name=region)
32
+
33
+ # Find log groups with us-east-1 prefix (indicating Edge functions)
34
+ paginator = logs.get_paginator('describe_log_groups')
35
+ for page in paginator.paginate():
36
+ for log_group in page.get('logGroups', []):
37
+ log_group_name = log_group['logGroupName']
38
+
39
+ # Check if it's a Lambda@Edge log group
40
+ if '/aws/lambda/us-east-1.' in log_group_name:
41
+ current_retention = log_group.get('retentionInDays')
42
+
43
+ edge_log_groups.append({
44
+ 'region': region,
45
+ 'name': log_group_name,
46
+ 'current_retention': current_retention,
47
+ 'stored_bytes': log_group.get('storedBytes', 0)
48
+ })
49
+
50
+ # Set retention if needed
51
+ if current_retention != retention_days:
52
+ if dry_run:
53
+ print(f"📍 {region}: Would set {log_group_name} to {retention_days} days (current: {current_retention})")
54
+ else:
55
+ try:
56
+ logs.put_retention_policy(
57
+ logGroupName=log_group_name,
58
+ retentionInDays=retention_days
59
+ )
60
+ print(f"✅ {region}: Set {log_group_name} to {retention_days} days")
61
+ total_changed += 1
62
+ except ClientError as e:
63
+ print(f"❌ {region}: Failed to set {log_group_name} - {e}")
64
+ else:
65
+ print(f"✓ {region}: {log_group_name} already has {retention_days} days retention")
66
+
67
+ except ClientError as e:
68
+ # Skip regions where CloudWatch Logs isn't available
69
+ continue
70
+
71
+ print("=" * 60)
72
+ print(f"📊 Summary:")
73
+ print(f" Found {len(edge_log_groups)} Lambda@Edge log groups")
74
+ print(f" Total storage: {sum(g['stored_bytes'] for g in edge_log_groups) / (1024**3):.2f} GB")
75
+ if not dry_run:
76
+ print(f" Changed {total_changed} log groups")
77
+
78
+ return edge_log_groups
79
+
80
+ if __name__ == "__main__":
81
+ # Dry run first to see what would be changed
82
+ edge_logs = set_edge_log_retention(retention_days=7, dry_run=True)
83
+
84
+ # Uncomment the line below to actually make changes
85
+ # set_edge_log_retention(retention_days=7, dry_run=False)
@@ -0,0 +1,2 @@
1
+ # aws-lambda-powertools
2
+ aws-lambda-powertools
@@ -0,0 +1,22 @@
1
+ from app import lambda_handler
2
+
3
+ if __name__ == "__main__":
4
+
5
+ event = {
6
+ "version": "0",
7
+ "id": "12345678-1234-1234-1234-123456789012",
8
+ "detail-type": "Scheduled Event",
9
+ "source": "aws.events",
10
+ "account": "123456789012",
11
+ "time": "2024-01-15T10:00:00Z",
12
+ "region": "us-east-1",
13
+ "resources": [
14
+ "arn:aws:events:us-east-1:123456789012:rule/LogRetentionManager"
15
+ ],
16
+ "detail": {
17
+ "days": 7,
18
+ "dry_run": False
19
+ }
20
+ }
21
+
22
+ lambda_handler(event, None)
@@ -134,7 +134,12 @@ class LambdaEdgeStack(IStack, StandardizedSsmMixin):
134
134
 
135
135
  resolved_env = {}
136
136
 
137
- for key, value in self.edge_config.environment.items():
137
+ # Use the new simplified configuration structure
138
+ configuration = self.edge_config.dictionary.get("configuration", {})
139
+ runtime_config = configuration.get("runtime", {})
140
+ ui_config = configuration.get("ui", {})
141
+
142
+ for key, value in runtime_config.items():
138
143
  # Check if value is an SSM parameter reference
139
144
  if isinstance(value, str) and value.startswith("{{ssm:") and value.endswith("}}"):
140
145
  # Extract SSM parameter path
@@ -216,11 +221,23 @@ class LambdaEdgeStack(IStack, StandardizedSsmMixin):
216
221
  # Since Lambda@Edge doesn't support environment variables, we bundle a config file
217
222
  # Use the full function_name (e.g., "tech-talk-dev-ip-gate") not just the base name
218
223
  resolved_env = self._resolve_environment_variables()
224
+
225
+ # Get the UI configuration
226
+ configuration = self.edge_config.dictionary.get("configuration", {})
227
+ ui_config = configuration.get("ui", {})
228
+
229
+
230
+ workload_name = self.deployment.workload.get("name")
231
+
232
+ if not workload_name:
233
+ raise ValueError("Workload name is required for Lambda@Edge function")
219
234
  runtime_config = {
220
235
  'environment': self.deployment.environment,
236
+ 'workload': workload_name,
221
237
  'function_name': function_name,
222
238
  'region': self.deployment.region,
223
- 'environment_variables': resolved_env # Add actual environment variables
239
+ 'runtime': resolved_env, # Runtime variables (SSM, etc.)
240
+ 'ui': ui_config # UI configuration (colors, messages, etc.)
224
241
  }
225
242
 
226
243
  runtime_config_path = temp_code_dir / 'runtime_config.json'
@@ -250,12 +267,15 @@ class LambdaEdgeStack(IStack, StandardizedSsmMixin):
250
267
  )
251
268
 
252
269
  # Log warning if environment variables are configured
253
- if self.edge_config.environment:
270
+ configuration = self.edge_config.dictionary.get("configuration", {})
271
+ runtime_config = configuration.get("runtime", {})
272
+
273
+ if runtime_config:
254
274
  logger.warning(
255
275
  f"Lambda@Edge function '{function_name}' has environment variables configured, "
256
276
  "but Lambda@Edge does not support environment variables. The function must fetch these values from SSM Parameter Store at runtime."
257
277
  )
258
- for key, value in self.edge_config.environment.items():
278
+ for key, value in runtime_config.items():
259
279
  logger.warning(f" - {key}: {value}")
260
280
 
261
281
  # Create execution role with CloudWatch Logs and SSM permissions
@@ -276,7 +296,7 @@ class LambdaEdgeStack(IStack, StandardizedSsmMixin):
276
296
  )
277
297
 
278
298
  # Add SSM read permissions if environment variables reference SSM parameters
279
- if self.edge_config.environment:
299
+ if runtime_config:
280
300
  execution_role.add_to_policy(
281
301
  iam.PolicyStatement(
282
302
  effect=iam.Effect.ALLOW,
@@ -286,7 +306,7 @@ class LambdaEdgeStack(IStack, StandardizedSsmMixin):
286
306
  "ssm:GetParametersByPath"
287
307
  ],
288
308
  resources=[
289
- f"arn:aws:ssm:*:{cdk.Aws.ACCOUNT_ID}:parameter/*"
309
+ f"arn:aws:ssm:*:{self.deployment.account}:parameter/*"
290
310
  ]
291
311
  )
292
312
  )
@@ -300,7 +320,70 @@ class LambdaEdgeStack(IStack, StandardizedSsmMixin):
300
320
  "secretsmanager:DescribeSecret"
301
321
  ],
302
322
  resources=[
303
- f"arn:aws:secretsmanager:*:{cdk.Aws.ACCOUNT_ID}:secret:{self.deployment.environment}/{self.workload.name}/origin-secret*"
323
+ f"arn:aws:secretsmanager:*:{self.deployment.account}:secret:{self.deployment.environment}/{self.workload.name}/origin-secret*"
324
+ ]
325
+ )
326
+ )
327
+
328
+ # Add ELB permissions for target health API access
329
+ execution_role.add_to_policy(
330
+ iam.PolicyStatement(
331
+ effect=iam.Effect.ALLOW,
332
+ actions=[
333
+ "elasticloadbalancing:DescribeTargetHealth",
334
+ "elasticloadbalancing:DescribeTargetGroups",
335
+ "elasticloadbalancing:DescribeLoadBalancers",
336
+ "elasticloadbalancing:DescribeListeners",
337
+ "elasticloadbalancing:DescribeTags"
338
+ ],
339
+ resources=[
340
+ "*"
341
+ ]
342
+ )
343
+ )
344
+
345
+ # Add ACM permissions for certificate validation
346
+ execution_role.add_to_policy(
347
+ iam.PolicyStatement(
348
+ effect=iam.Effect.ALLOW,
349
+ actions=[
350
+ "acm:DescribeCertificate",
351
+ "acm:ListCertificates"
352
+ ],
353
+ resources=[
354
+ f"arn:aws:acm:*:{self.deployment.account}:certificate/*"
355
+ ]
356
+ )
357
+ )
358
+
359
+ # Add Route 53 permissions for health check access
360
+ execution_role.add_to_policy(
361
+ iam.PolicyStatement(
362
+ effect=iam.Effect.ALLOW,
363
+ actions=[
364
+ "route53:GetHealthCheckStatus",
365
+ "route53:ListHealthChecks",
366
+ "route53:GetHealthCheck"
367
+ ],
368
+ resources=[
369
+ f"arn:aws:route53:::{self.deployment.account}:health-check/*"
370
+ ]
371
+ )
372
+ )
373
+
374
+ # Add CloudWatch permissions for enhanced logging and metrics
375
+ execution_role.add_to_policy(
376
+ iam.PolicyStatement(
377
+ effect=iam.Effect.ALLOW,
378
+ actions=[
379
+ "logs:CreateLogGroup",
380
+ "logs:CreateLogStream",
381
+ "logs:PutLogEvents",
382
+ "cloudwatch:PutMetricData"
383
+ ],
384
+ resources=[
385
+ f"arn:aws:logs:*:{self.deployment.account}:log-group:/aws/lambda/*",
386
+ f"arn:aws:cloudwatch:*:{self.deployment.account}:metric:*"
304
387
  ]
305
388
  )
306
389
  )
@@ -437,8 +520,11 @@ class LambdaEdgeStack(IStack, StandardizedSsmMixin):
437
520
  configuration = self.edge_config.dictionary.get("configuration", {})
438
521
  environment_variables = configuration.get("environment_variables", {})
439
522
 
523
+ # Build full configuration that Lambda@Edge expects
440
524
  full_config = {
441
- "environment_variables": environment_variables
525
+ "environment_variables": environment_variables,
526
+ "runtime": configuration.get("runtime", {}),
527
+ "ui": configuration.get("ui", {})
442
528
  }
443
529
 
444
530
  self.export_ssm_parameter(
@@ -459,6 +459,10 @@ class LoadBalancerStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
459
459
  # Parse AWS ALB conditions format
460
460
  aws_conditions = rule_config.get("conditions", [])
461
461
  for condition in aws_conditions:
462
+ enabled = str(condition.get("enabled", True)).lower()
463
+ if enabled != "true":
464
+ continue
465
+
462
466
  field = condition.get("field")
463
467
  if field == "http-header" and "http_header_config" in condition:
464
468
  header_config = condition["http_header_config"]
@@ -67,14 +67,16 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
67
67
  self.workload = workload
68
68
 
69
69
  self.rds_config = RdsConfig(stack_config.dictionary.get("rds", {}), deployment)
70
- db_name = deployment.build_resource_name(self.rds_config.name)
70
+ # Use stable construct ID to prevent CloudFormation logical ID changes on pipeline rename
71
+ # The construct ID determines the CloudFormation logical ID, not the physical resource names
72
+ instance_identifier = self.rds_config.instance_identifier
71
73
 
72
74
  # Setup standardized SSM integration
73
75
  self.setup_ssm_integration(
74
76
  scope=self,
75
77
  config=self.rds_config,
76
78
  resource_type="rds",
77
- resource_name=self.rds_config.name,
79
+ resource_name=instance_identifier,
78
80
  deployment=deployment,
79
81
  workload=workload
80
82
  )
@@ -87,15 +89,15 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
87
89
 
88
90
  # Create RDS instance or import existing
89
91
  if self.rds_config.existing_instance_id:
90
- self.db_instance = self._import_existing_db(db_name)
92
+ self.db_instance = self._import_existing_db(instance_identifier)
91
93
  else:
92
- self.db_instance = self._create_db_instance(db_name)
94
+ self.db_instance = self._create_db_instance(instance_identifier, instance_identifier)
93
95
 
94
96
  # Add outputs
95
- self._add_outputs(db_name)
97
+ self._add_outputs(instance_identifier)
96
98
 
97
99
  # Export to SSM Parameter Store
98
- self._export_ssm_parameters(db_name)
100
+ self._export_ssm_parameters(instance_identifier)
99
101
 
100
102
  @property
101
103
  def vpc(self) -> ec2.IVpc:
@@ -162,7 +164,7 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
162
164
  else:
163
165
  raise ValueError("No subnets available in VPC for RDS instance")
164
166
 
165
- def _create_db_instance(self, db_name: str) -> rds.DatabaseInstance:
167
+ def _create_db_instance(self, db_name: str, stable_db_id: str) -> rds.DatabaseInstance:
166
168
  """Create a new RDS instance"""
167
169
  # Configure subnet group
168
170
  # If we have subnet IDs from SSM, create a DB subnet group explicitly
@@ -232,7 +234,7 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
232
234
  "vpc": self.vpc,
233
235
  "instance_type": instance_type,
234
236
  "credentials": rds.Credentials.from_generated_secret(
235
- username=self.rds_config.username,
237
+ username=self.rds_config.master_username,
236
238
  secret_name=self.rds_config.secret_name,
237
239
  ),
238
240
  "database_name": self.rds_config.database_name,
@@ -264,7 +266,7 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
264
266
  else:
265
267
  db_props["vpc_subnets"] = subnets
266
268
 
267
- db_instance = rds.DatabaseInstance(self, db_name, **db_props)
269
+ db_instance = rds.DatabaseInstance(self, stable_db_id, **db_props)
268
270
 
269
271
  # Add tags
270
272
  for key, value in self.rds_config.tags.items():
@@ -320,15 +322,15 @@ class RdsStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
320
322
  logger.info(f"Exported SSM parameter: {ssm_exports['db_port']}")
321
323
 
322
324
  # Export database name
323
- if "db_name" in ssm_exports and self.rds_config.database_name:
325
+ if "db_instance_identifier" in ssm_exports and self.rds_config.instance_identifier:
324
326
  self.export_ssm_parameter(
325
327
  scope=self,
326
328
  id="SsmExportDbName",
327
- value=self.rds_config.database_name,
328
- parameter_name=ssm_exports["db_name"],
329
+ value=self.rds_config.instance_identifier,
330
+ parameter_name=ssm_exports["db_instance_identifier"],
329
331
  description=f"RDS database name for {db_name}",
330
332
  )
331
- logger.info(f"Exported SSM parameter: {ssm_exports['db_name']}")
333
+ logger.info(f"Exported SSM parameter: {ssm_exports['db_instance_identifier']}")
332
334
 
333
335
  # Export secret ARN (contains username and password)
334
336
  if "db_secret_arn" in ssm_exports: