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
@@ -13,6 +13,8 @@ from aws_cdk import (
13
13
  aws_cloudfront as cloudfront,
14
14
  aws_cloudfront_origins as origins,
15
15
  aws_certificatemanager as acm,
16
+ aws_route53 as route53,
17
+ aws_s3 as s3,
16
18
  aws_lambda as _lambda,
17
19
  aws_ssm as ssm,
18
20
  CfnOutput,
@@ -143,7 +145,7 @@ class CloudFrontStack(IStack):
143
145
  return
144
146
 
145
147
  # Check if certificate ARN is provided
146
- cert_arn = cert_config.get("arn")
148
+ cert_arn = self.resolve_ssm_value(self, cert_config.get("arn"), "CertificateARN")
147
149
  if cert_arn:
148
150
  self.certificate = acm.Certificate.from_certificate_arn(
149
151
  self, "Certificate", certificate_arn=cert_arn
@@ -161,8 +163,36 @@ class CloudFrontStack(IStack):
161
163
  logger.info(f"Using certificate from SSM: {ssm_param}")
162
164
  return
163
165
 
166
+ # Create new certificate from domain name
167
+ domain_name = cert_config.get("domain_name")
168
+ if domain_name and self.cf_config.aliases:
169
+ # CloudFront certificates must be in us-east-1
170
+ if self.region != "us-east-1":
171
+ logger.warning(
172
+ f"Certificate creation requested but stack is in {self.region}. "
173
+ "CloudFront certificates must be created in us-east-1"
174
+ )
175
+ return
176
+
177
+ # Create the certificate
178
+ # Get hosted zone from SSM imports
179
+ hosted_zone_id = cert_config.get("hosted_zone_id")
180
+ hosted_zone = route53.HostedZone.from_hosted_zone_id(
181
+ self, "HostedZone", hosted_zone_id
182
+ )
183
+
184
+ self.certificate = acm.Certificate(
185
+ self,
186
+ "Certificate",
187
+ domain_name=domain_name,
188
+ subject_alternative_names=self.cf_config.aliases,
189
+ validation=acm.CertificateValidation.from_dns(hosted_zone=hosted_zone),
190
+ )
191
+ logger.info(f"Created new ACM certificate for domain: {domain_name}")
192
+ return
193
+
164
194
  logger.warning(
165
- "No certificate ARN provided - CloudFront will use default certificate"
195
+ "No certificate ARN or domain name provided - CloudFront will use default certificate"
166
196
  )
167
197
 
168
198
  def _create_origins(self) -> None:
@@ -193,27 +223,29 @@ class CloudFrontStack(IStack):
193
223
 
194
224
  def _create_custom_origin(self, config: Dict[str, Any]) -> cloudfront.IOrigin:
195
225
  """Create custom origin (ALB, API Gateway, etc.)"""
196
- domain_name = config.get("domain_name")
226
+ domain_name = self.resolve_ssm_value(
227
+ self, config.get("domain_name"), config.get("domain_name")
228
+ )
197
229
  origin_id = config.get("id")
198
230
 
199
231
  if not domain_name:
200
232
  raise ValueError("domain_name is required for custom origin")
201
233
 
202
- # Check if domain name is a placeholder from ssm_imports
203
- if domain_name.startswith("{{") and domain_name.endswith("}}"):
204
- placeholder_key = domain_name[2:-2] # Remove {{ and }}
205
- if placeholder_key in self.ssm_imported_values:
206
- domain_name = self.ssm_imported_values[placeholder_key]
207
- logger.info(f"Resolved domain from SSM import: {placeholder_key}")
208
- else:
209
- logger.warning(f"Placeholder {domain_name} not found in SSM imports")
210
-
211
- # Legacy support: Check if domain name is an SSM parameter reference
212
- elif domain_name.startswith("{{ssm:") and domain_name.endswith("}}"):
213
- # Extract SSM parameter name
214
- ssm_param = domain_name[6:-2] # Remove {{ssm: and }}
215
- domain_name = ssm.StringParameter.value_from_lookup(self, ssm_param)
216
- logger.info(f"Resolved domain from SSM lookup {ssm_param}: {domain_name}")
234
+ # # Check if domain name is a placeholder from ssm_imports
235
+ # if domain_name.startswith("{{") and domain_name.endswith("}}"):
236
+ # placeholder_key = domain_name[2:-2] # Remove {{ and }}
237
+ # if placeholder_key in self.ssm_imported_values:
238
+ # domain_name = self.ssm_imported_values[placeholder_key]
239
+ # logger.info(f"Resolved domain from SSM import: {placeholder_key}")
240
+ # else:
241
+ # logger.warning(f"Placeholder {domain_name} not found in SSM imports")
242
+
243
+ # # Legacy support: Check if domain name is an SSM parameter reference
244
+ # elif domain_name.startswith("{{ssm:") and domain_name.endswith("}}"):
245
+ # # Extract SSM parameter name
246
+ # ssm_param = domain_name[6:-2] # Remove {{ssm: and }}
247
+ # domain_name = ssm.StringParameter.value_from_lookup(self, ssm_param)
248
+ # logger.info(f"Resolved domain from SSM lookup {ssm_param}: {domain_name}")
217
249
 
218
250
  # Build custom headers (e.g., X-Origin-Secret)
219
251
  custom_headers = {}
@@ -267,12 +299,34 @@ class CloudFrontStack(IStack):
267
299
 
268
300
  def _create_s3_origin(self, config: Dict[str, Any]) -> cloudfront.IOrigin:
269
301
  """Create S3 origin"""
270
- # S3 origin implementation
271
- # This would use origins.S3Origin
272
- raise NotImplementedError(
273
- "S3 origin support - use existing static_website_stack for S3"
302
+ bucket_name = self.resolve_ssm_value(
303
+ self, config.get("bucket_name"), config.get("bucket_name")
304
+ )
305
+
306
+ origin_path = config.get("origin_path", "")
307
+
308
+ if not bucket_name:
309
+ raise ValueError("S3 origin requires 'bucket_name' configuration")
310
+
311
+ # For S3 origins, we need to import the bucket by name
312
+ bucket = s3.Bucket.from_bucket_name(
313
+ self,
314
+ id=f"S3OriginBucket-{config.get('id', 'unknown')}",
315
+ bucket_name=bucket_name,
274
316
  )
275
317
 
318
+ # Create S3 origin with OAC (Origin Access Control) for security
319
+ origin = origins.S3BucketOrigin.with_origin_access_control(
320
+ bucket,
321
+ origin_path=origin_path,
322
+ origin_access_levels=[
323
+ cloudfront.AccessLevel.READ,
324
+ cloudfront.AccessLevel.LIST,
325
+ ],
326
+ )
327
+
328
+ return origin
329
+
276
330
  def _create_distribution(self) -> None:
277
331
  """Create CloudFront distribution"""
278
332
 
@@ -140,32 +140,10 @@ class CodeArtifactStack(IStack, StandardizedSsmMixin):
140
140
 
141
141
  def _add_outputs(self) -> None:
142
142
  """Add CloudFormation outputs for the CodeArtifact resources"""
143
+
144
+
143
145
  # Domain outputs
144
146
  if self.domain:
145
147
  domain_name = self.code_artifact_config.domain_name
146
148
 
147
- # Domain ARN
148
- cdk.CfnOutput(
149
- self,
150
- f"{domain_name}-domain-arn",
151
- value=self.domain.attr_arn,
152
- export_name=f"{self.deployment.build_resource_name(domain_name)}-domain-arn"
153
- )
154
-
155
- # Domain URL
156
- cdk.CfnOutput(
157
- self,
158
- f"{domain_name}-domain-url",
159
- value=f"https://{self.code_artifact_config.account}.d.codeartifact.{self.code_artifact_config.region}.amazonaws.com/",
160
- export_name=f"{self.deployment.build_resource_name(domain_name)}-domain-url"
161
- )
162
-
163
- # Repository outputs
164
- for repo_name, repo in self.repositories.items():
165
- # Repository ARN
166
- cdk.CfnOutput(
167
- self,
168
- f"{repo_name}-repo-arn",
169
- value=repo.attr_arn,
170
- export_name=f"{self.deployment.build_resource_name(repo_name)}-repo-arn"
171
- )
149
+
@@ -564,7 +564,7 @@ class CognitoStack(IStack, StandardizedSsmMixin):
564
564
  # Setup enhanced SSM integration with proper resource type and name
565
565
  # Use "user-pool" as resource identifier for SSM paths, not the full pool name
566
566
 
567
- self.setup_standardized_ssm_integration(
567
+ self.setup_ssm_integration(
568
568
  scope=self,
569
569
  config=self.stack_config.dictionary.get("cognito", {}),
570
570
  resource_type="cognito",
@@ -591,7 +591,7 @@ class CognitoStack(IStack, StandardizedSsmMixin):
591
591
  # or retrieve via AWS Console/CLI if needed.
592
592
 
593
593
  # Use enhanced SSM parameter export
594
- exported_params = self.export_standardized_ssm_parameters(resource_values)
594
+ exported_params = self.export_ssm_parameters(resource_values)
595
595
 
596
596
  if exported_params:
597
597
  logger.info(f"Exported {len(exported_params)} Cognito parameters to SSM")
@@ -152,7 +152,7 @@ class DynamoDBStack(IStack, StandardizedSsmMixin):
152
152
  # Setup enhanced SSM integration with proper resource type and name
153
153
  # Use "app-table" as resource identifier for SSM paths, not the full table name
154
154
 
155
- self.setup_standardized_ssm_integration(
155
+ self.setup_ssm_integration(
156
156
  scope=self,
157
157
  config=self.stack_config.dictionary.get("dynamodb", {}),
158
158
  resource_type="dynamodb",
@@ -178,7 +178,7 @@ class DynamoDBStack(IStack, StandardizedSsmMixin):
178
178
  resource_values = {k: v for k, v in resource_values.items() if v is not None}
179
179
 
180
180
  # Use enhanced SSM parameter export
181
- exported_params = self.export_standardized_ssm_parameters(resource_values)
181
+ exported_params = self.export_ssm_parameters(resource_values)
182
182
 
183
183
  if exported_params:
184
184
  logger.info(f"Exported {len(exported_params)} DynamoDB parameters to SSM")
@@ -5,10 +5,8 @@ Contains ECS-related stack modules for creating and managing
5
5
  ECS clusters, services, and related resources.
6
6
  """
7
7
 
8
- from .ecs_cluster_stack_standardized import EcsClusterStack
9
- from .ecs_service_stack import EcsServiceStack
8
+ from .ecs_cluster_stack import EcsClusterStack
10
9
 
11
10
  __all__ = [
12
- "EcsClusterStack",
13
- "EcsServiceStack"
11
+ "EcsClusterStack"
14
12
  ]
@@ -86,17 +86,20 @@ class EcsClusterStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
86
86
  # Initialize VPC cache from mixin
87
87
  self._initialize_vpc_cache()
88
88
 
89
- # Load ECS cluster configuration
90
- self.ecs_config: EcsClusterConfig = EcsClusterConfig(
91
- stack_config.dictionary.get("ecs_cluster", {})
92
- )
89
+ # Load ECS cluster configuration with full stack config for SSM access
90
+ ecs_cluster_dict = stack_config.dictionary.get("ecs_cluster", {})
91
+ # Merge SSM config from root level into ECS config for VPC resolution
92
+ if "ssm" in stack_config.dictionary:
93
+ ecs_cluster_dict["ssm"] = stack_config.dictionary["ssm"]
94
+
95
+ self.ecs_config: EcsClusterConfig = EcsClusterConfig(ecs_cluster_dict)
93
96
 
94
97
  cluster_name = deployment.build_resource_name(self.ecs_config.name)
95
98
 
96
99
  logger.info(f"Creating ECS Cluster stack: {cluster_name}")
97
100
 
98
101
  # Setup standardized SSM integration
99
- self.setup_standardized_ssm_integration(
102
+ self.setup_ssm_integration(
100
103
  scope=self,
101
104
  config=self.ecs_config,
102
105
  resource_type="ecs_cluster",
@@ -106,7 +109,7 @@ class EcsClusterStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
106
109
  )
107
110
 
108
111
  # Process SSM imports using standardized method
109
- self.process_standardized_ssm_imports()
112
+ self.process_ssm_imports()
110
113
 
111
114
  # Create the ECS cluster
112
115
  self._create_ecs_cluster()
@@ -118,7 +121,9 @@ class EcsClusterStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
118
121
  self._export_cluster_info()
119
122
 
120
123
  # Export SSM parameters
124
+ logger.info("Starting SSM parameter export for ECS cluster")
121
125
  self._export_ssm_parameters()
126
+ logger.info("Completed SSM parameter export for ECS cluster")
122
127
 
123
128
  logger.info(f"ECS Cluster stack created: {cluster_name}")
124
129
 
@@ -131,7 +136,7 @@ class EcsClusterStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
131
136
 
132
137
  # Add container insights if enabled
133
138
  if self.ecs_config.container_insights:
134
- cluster_settings.append({"name": "containerInsights", "value": "enabled"})
139
+ cluster_settings.append({"name": "containerInsightsV2", "value": "enabled"})
135
140
 
136
141
  # Add custom cluster settings
137
142
  if self.ecs_config.cluster_settings:
@@ -146,7 +151,7 @@ class EcsClusterStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
146
151
  "ECSCluster",
147
152
  cluster_name=self.ecs_config.name,
148
153
  vpc=self.vpc,
149
- container_insights=self.ecs_config.container_insights,
154
+ container_insights_v2=ecs.ContainerInsights.ENABLED if self.ecs_config.container_insights else ecs.ContainerInsights.DISABLED,
150
155
  default_cloud_map_namespace=(
151
156
  self.ecs_config.cloud_map_namespace
152
157
  if self.ecs_config.cloud_map_namespace
@@ -165,7 +170,8 @@ class EcsClusterStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
165
170
  """
166
171
  Get VPC using the centralized VPC provider mixin.
167
172
  """
168
- # Use the centralized VPC resolution from VPCProviderMixin
173
+
174
+ # Use the stack_config (not ecs_config) to ensure SSM imports are available
169
175
  return self.resolve_vpc(
170
176
  config=self.ecs_config,
171
177
  deployment=self.deployment,
@@ -174,6 +180,8 @@ class EcsClusterStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
174
180
 
175
181
  def _create_iam_roles(self):
176
182
  """Create IAM roles for the ECS cluster if configured."""
183
+ logger.info(f"create_instance_role setting: {self.ecs_config.create_instance_role}")
184
+
177
185
  if not self.ecs_config.create_instance_role:
178
186
  logger.info("Skipping instance role creation (disabled in config)")
179
187
  return
@@ -186,21 +194,25 @@ class EcsClusterStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
186
194
  "ECSInstanceRole",
187
195
  assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"),
188
196
  managed_policies=[
189
- iam.ManagedPolicy.from_aws_managed_policy_name("AmazonEC2ContainerServiceforEC2Role"),
197
+ iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AmazonEC2ContainerServiceforEC2Role"),
190
198
  iam.ManagedPolicy.from_aws_managed_policy_name("AmazonEC2ContainerRegistryReadOnly"),
191
199
  iam.ManagedPolicy.from_aws_managed_policy_name("AmazonSSMManagedInstanceCore"),
192
200
  ],
193
201
  role_name=f"{self.ecs_config.name}-ecs-instance-role",
194
202
  )
195
203
 
204
+ logger.info(f"Created ECS instance role: {self.instance_role.role_name}")
205
+
196
206
  # Create instance profile
197
207
  self.instance_profile = iam.CfnInstanceProfile(
198
208
  self,
199
209
  "ECSInstanceProfile",
200
- roles=[self.instance_role.role_name],
201
210
  instance_profile_name=f"{self.ecs_config.name}-ecs-instance-profile",
211
+ roles=[self.instance_role.role_name],
202
212
  )
203
213
 
214
+ logger.info(f"Created ECS instance profile: {self.instance_profile.instance_profile_name}")
215
+
204
216
  logger.info("ECS instance role and profile created")
205
217
 
206
218
  def _export_cluster_info(self):
@@ -254,52 +266,51 @@ class EcsClusterStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
254
266
 
255
267
  def _export_ssm_parameters(self) -> None:
256
268
  """Export SSM parameters using standardized approach"""
269
+ logger.info("=== Starting SSM Parameter Export ===")
270
+
257
271
  if not self.ecs_cluster:
258
272
  logger.warning("No ECS cluster to export")
259
273
  return
260
274
 
275
+ logger.info(f"ECS cluster found: {self.ecs_cluster.cluster_name}")
276
+ logger.info(f"SSM exports configured: {self.ssm_config.get('exports', {})}")
277
+
261
278
  # Prepare resource values for export
262
279
  resource_values = {
263
- "ecs_cluster_name": self.ecs_cluster.cluster_name,
264
- "ecs_cluster_arn": self.ecs_cluster.cluster_arn,
280
+ "cluster_name": self.ecs_cluster.cluster_name,
281
+ "cluster_arn": self.ecs_cluster.cluster_arn,
265
282
  }
266
283
 
284
+ # Add instance role ARN if created
285
+ if self.instance_role:
286
+ resource_values["instance_role_arn"] = self.instance_role.role_arn
287
+ logger.info(f"Instance role ARN added: {self.instance_role.role_name}")
288
+ else:
289
+ logger.info("No instance role to export")
290
+
267
291
  # Add security group ID if available
268
292
  if hasattr(self.ecs_cluster, 'connections') and self.ecs_cluster.connections:
269
293
  security_groups = self.ecs_cluster.connections.security_groups
270
294
  if security_groups:
271
- resource_values["ecs_cluster_security_group_id"] = security_groups[0].security_group_id
295
+ resource_values["security_group_id"] = security_groups[0].security_group_id
296
+ logger.info(f"Security group ID added: {security_groups[0].security_group_id}")
272
297
 
273
298
  # Add instance profile ARN if created
274
299
  if self.instance_profile:
275
- resource_values["ecs_instance_profile_arn"] = self.instance_profile.attr_arn
300
+ resource_values["instance_profile_arn"] = self.instance_profile.attr_arn
301
+ logger.info(f"Instance profile ARN added: {self.instance_profile.instance_profile_name}")
276
302
 
277
303
  # Export using standardized SSM mixin
278
- exported_params = self.export_standardized_ssm_parameters(resource_values)
279
-
280
- logger.info(f"Exported SSM parameters: {exported_params}")
281
-
282
- # Backward compatibility methods
283
- def process_ssm_imports(self, config: Any, deployment: DeploymentConfig, resource_type: str = "resource") -> None:
284
- """Backward compatibility method for existing modules."""
285
- # Extract SSM configuration from old format
286
- if hasattr(config, 'ssm_imports'):
287
- # Convert old ssm_imports format to new format
288
- old_imports = config.ssm_imports
289
- new_imports = {}
304
+ logger.info(f"Resource values available for export: {list(resource_values.keys())}")
305
+ for key, value in resource_values.items():
306
+ logger.info(f" {key}: {value}")
290
307
 
291
- for key, value in old_imports.items():
292
- # Resolve template variables using old method
293
- if isinstance(value, str) and not value.startswith('/'):
294
- value = f"/{deployment.environment}/{deployment.workload_name}/{value}"
295
- new_imports[key] = value
296
-
297
- # Update SSM config
298
- self.ssm_config = {"imports": new_imports}
299
-
300
- # Process imports using standardized method
301
- self.process_standardized_ssm_imports()
302
-
303
-
304
- # Backward compatibility alias
308
+ try:
309
+ exported_params = self.export_ssm_parameters(resource_values)
310
+ logger.info(f"Successfully exported SSM parameters: {exported_params}")
311
+ except Exception as e:
312
+ logger.error(f"Failed to export SSM parameters: {str(e)}")
313
+ raise
314
+
315
+ # Backward compatibility alias
305
316
  EcsClusterStackStandardized = EcsClusterStack
@@ -101,6 +101,7 @@ class EcsServiceStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
101
101
 
102
102
  # Add outputs
103
103
  self._add_outputs(service_name)
104
+ self._export_to_ssm(service_name)
104
105
 
105
106
  def _load_vpc(self) -> None:
106
107
  """Load VPC using the centralized VPC provider mixin."""
@@ -225,6 +226,9 @@ class EcsServiceStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
225
226
  "CloudWatchAgentServerPolicy"
226
227
  )
227
228
  )
229
+
230
+ # add any custom policies
231
+ self._add_custom_task_policies(task_role)
228
232
 
229
233
  # Create task definition based on launch type
230
234
  if self.ecs_config.launch_type == "EC2":
@@ -237,6 +241,7 @@ class EcsServiceStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
237
241
  network_mode=ecs.NetworkMode(network_mode.upper()) if network_mode else ecs.NetworkMode.BRIDGE,
238
242
  execution_role=execution_role,
239
243
  task_role=task_role,
244
+ inference_accelerators=None
240
245
  )
241
246
  else:
242
247
  # Fargate task definition
@@ -248,6 +253,7 @@ class EcsServiceStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
248
253
  memory_limit_mib=int(self.ecs_config.memory),
249
254
  execution_role=execution_role,
250
255
  task_role=task_role,
256
+ inference_accelerators=None
251
257
  )
252
258
 
253
259
  # Add volumes to task definition
@@ -256,6 +262,37 @@ class EcsServiceStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
256
262
  # Add containers
257
263
  self._add_containers_to_task()
258
264
 
265
+ def _add_custom_task_policies(self, task_role: iam.Role) -> None:
266
+ """
267
+ Add custom task policies to the task definition.
268
+ """
269
+ for policy in self.ecs_config.task_definition.get("policies", []):
270
+
271
+ effect = policy.get("effect", "Allow")
272
+ action = policy.get("action", None)
273
+ actions = policy.get("actions", [])
274
+ if action:
275
+ actions.append(action)
276
+ resources = policy.get("resources", [])
277
+ resource = policy.get("resource", None)
278
+ if resource:
279
+ resources.append(resource)
280
+
281
+ if effect == "Allow" and actions:
282
+ effect = iam.Effect.ALLOW
283
+ if effect == "Deny" and actions:
284
+ effect = iam.Effect.DENY
285
+
286
+ sid = policy.get("sid", None)
287
+ task_role.add_to_policy(
288
+ iam.PolicyStatement(
289
+ effect=effect,
290
+ actions=actions,
291
+ resources=resources,
292
+ sid=sid,
293
+ )
294
+ )
295
+
259
296
  def _add_volumes_to_task(self) -> None:
260
297
  """
261
298
  Add volumes to the task definition.
@@ -540,6 +577,14 @@ class EcsServiceStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
540
577
  """Attach service to load balancer target groups"""
541
578
  target_group_arns = self.ecs_config.target_group_arns
542
579
 
580
+ if target_group_arns:
581
+ tmp = []
582
+ for tg_arn in target_group_arns:
583
+ import hashlib
584
+ unique_id = hashlib.md5(tg_arn.encode()).hexdigest()
585
+ tmp.append(self.resolve_ssm_value(self, tg_arn, unique_id))
586
+ target_group_arns = tmp
587
+
543
588
  if not target_group_arns:
544
589
  # Try to load from SSM if configured
545
590
  target_group_arns = self._load_target_groups_from_ssm()
@@ -563,7 +608,7 @@ class EcsServiceStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
563
608
  for param_key, param_name in ssm_imports.items():
564
609
  if 'target_group' in param_key.lower() or 'tg' in param_key.lower():
565
610
  try:
566
- param_value = self.get_ssm_parameter_value(param_name)
611
+ param_value = self.get_ssm_imported_value(param_name)
567
612
  if param_value and param_value.startswith('arn:'):
568
613
  target_group_arns.append(param_value)
569
614
  except Exception as e:
@@ -595,35 +640,13 @@ class EcsServiceStack(IStack, VPCProviderMixin, StandardizedSsmMixin):
595
640
  scale_out_cooldown=cdk.Duration.seconds(60),
596
641
  )
597
642
 
643
+
644
+
598
645
  def _add_outputs(self, service_name: str) -> None:
599
646
  """Add CloudFormation outputs"""
647
+ return
600
648
 
601
- # Service name output
602
- cdk.CfnOutput(
603
- self,
604
- "ServiceName",
605
- value=self.service.service_name,
606
- description=f"ECS Service Name: {service_name}",
607
- )
608
649
 
609
- # Service ARN output
610
- cdk.CfnOutput(
611
- self,
612
- "ServiceArn",
613
- value=self.service.service_arn,
614
- description=f"ECS Service ARN: {service_name}",
615
- )
616
-
617
- # Cluster name output
618
- cdk.CfnOutput(
619
- self,
620
- "ClusterName",
621
- value=self.cluster.cluster_name,
622
- description=f"ECS Cluster Name for {service_name}",
623
- )
624
-
625
- # Export to SSM if configured
626
- self._export_to_ssm(service_name)
627
650
 
628
651
  def _export_to_ssm(self, service_name: str) -> None:
629
652
  """Export resource ARNs and names to SSM Parameter Store"""