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.
- cdk_factory/configurations/resources/rds.py +17 -8
- cdk_factory/stack_library/auto_scaling/auto_scaling_stack.py +122 -85
- cdk_factory/stack_library/ecs/ecs_cluster_stack.py +2 -2
- cdk_factory/stack_library/ecs/ecs_service_stack.py +2 -0
- cdk_factory/stack_library/lambda_edge/functions/README.md +0 -0
- cdk_factory/stack_library/lambda_edge/functions/log_retention_manager/__init__.py +33 -0
- cdk_factory/stack_library/lambda_edge/functions/log_retention_manager/app.py +30 -0
- cdk_factory/stack_library/lambda_edge/functions/log_retention_manager/edge_log_retention.py +85 -0
- cdk_factory/stack_library/lambda_edge/functions/log_retention_manager/requirements.txt +2 -0
- cdk_factory/stack_library/lambda_edge/functions/log_retention_manager/test.py +22 -0
- cdk_factory/stack_library/lambda_edge/lambda_edge_log_retention_stack.py +0 -0
- cdk_factory/stack_library/lambda_edge/lambda_edge_stack.py +94 -8
- cdk_factory/stack_library/load_balancer/load_balancer_stack.py +4 -0
- cdk_factory/stack_library/rds/rds_stack.py +15 -13
- cdk_factory/stack_library/route53/route53_stack.py +97 -133
- cdk_factory/version.py +1 -1
- {cdk_factory-0.19.19.dist-info → cdk_factory-0.21.1.dist-info}/METADATA +1 -1
- {cdk_factory-0.19.19.dist-info → cdk_factory-0.21.1.dist-info}/RECORD +21 -15
- cdk_factory/stack_library/lambda_edge/EDGE_LOG_RETENTION_TODO.md +0 -226
- {cdk_factory-0.19.19.dist-info → cdk_factory-0.21.1.dist-info}/WHEEL +0 -0
- {cdk_factory-0.19.19.dist-info → cdk_factory-0.21.1.dist-info}/entry_points.txt +0 -0
- {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,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)
|
|
File without changes
|
|
@@ -134,7 +134,12 @@ class LambdaEdgeStack(IStack, StandardizedSsmMixin):
|
|
|
134
134
|
|
|
135
135
|
resolved_env = {}
|
|
136
136
|
|
|
137
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
|
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
|
|
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:*:{
|
|
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:*:{
|
|
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
|
-
|
|
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=
|
|
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(
|
|
92
|
+
self.db_instance = self._import_existing_db(instance_identifier)
|
|
91
93
|
else:
|
|
92
|
-
self.db_instance = self._create_db_instance(
|
|
94
|
+
self.db_instance = self._create_db_instance(instance_identifier, instance_identifier)
|
|
93
95
|
|
|
94
96
|
# Add outputs
|
|
95
|
-
self._add_outputs(
|
|
97
|
+
self._add_outputs(instance_identifier)
|
|
96
98
|
|
|
97
99
|
# Export to SSM Parameter Store
|
|
98
|
-
self._export_ssm_parameters(
|
|
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.
|
|
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,
|
|
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 "
|
|
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.
|
|
328
|
-
parameter_name=ssm_exports["
|
|
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['
|
|
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:
|