cdk-factory 0.8.7__py3-none-any.whl → 0.8.8__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.
- cdk_factory/configurations/resources/ecr.py +47 -2
- cdk_factory/configurations/resources/ecs_service.py +144 -0
- cdk_factory/constructs/ecr/ecr_construct.py +125 -53
- cdk_factory/pipeline/pipeline_factory.py +13 -1
- cdk_factory/stack_library/ecs/__init__.py +0 -0
- cdk_factory/stack_library/ecs/ecs_service_stack.py +525 -0
- cdk_factory/version.py +1 -1
- {cdk_factory-0.8.7.dist-info → cdk_factory-0.8.8.dist-info}/METADATA +1 -1
- {cdk_factory-0.8.7.dist-info → cdk_factory-0.8.8.dist-info}/RECORD +11 -8
- {cdk_factory-0.8.7.dist-info → cdk_factory-0.8.8.dist-info}/WHEEL +0 -0
- {cdk_factory-0.8.7.dist-info → cdk_factory-0.8.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -16,7 +16,7 @@ class ECRConfig(EnhancedBaseConfig):
|
|
|
16
16
|
) -> None:
|
|
17
17
|
super().__init__(config, resource_type="ecr", resource_name=config.get("name", "ecr") if config else "ecr")
|
|
18
18
|
self.__config = config
|
|
19
|
-
self.__deployment =
|
|
19
|
+
self.__deployment = deployment
|
|
20
20
|
self.__ssm_prefix_template = config.get("ssm_prefix_template", None)
|
|
21
21
|
|
|
22
22
|
@property
|
|
@@ -74,12 +74,13 @@ class ECRConfig(EnhancedBaseConfig):
|
|
|
74
74
|
Clear out untagged images after x days. This helps save costs.
|
|
75
75
|
Untagged images will stay forever if you don't clean them out.
|
|
76
76
|
"""
|
|
77
|
+
days = None
|
|
77
78
|
if self.__config and isinstance(self.__config, dict):
|
|
78
79
|
days = self.__config.get("auto_delete_untagged_images_in_days")
|
|
79
80
|
if days:
|
|
80
81
|
days = int(days)
|
|
81
82
|
|
|
82
|
-
return
|
|
83
|
+
return days
|
|
83
84
|
|
|
84
85
|
@property
|
|
85
86
|
def use_existing(self) -> bool:
|
|
@@ -118,6 +119,50 @@ class ECRConfig(EnhancedBaseConfig):
|
|
|
118
119
|
if not value:
|
|
119
120
|
raise RuntimeError("Region is not defined")
|
|
120
121
|
return value
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def cross_account_access(self) -> dict:
|
|
125
|
+
"""
|
|
126
|
+
Cross-account access configuration.
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
{
|
|
130
|
+
"enabled": true,
|
|
131
|
+
"accounts": ["123456789012", "987654321098"],
|
|
132
|
+
"services": [
|
|
133
|
+
{
|
|
134
|
+
"name": "lambda",
|
|
135
|
+
"actions": ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"],
|
|
136
|
+
"condition": {
|
|
137
|
+
"StringLike": {
|
|
138
|
+
"aws:sourceArn": "arn:aws:lambda:*:*:function:*"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "ecs-tasks",
|
|
144
|
+
"service_principal": "ecs-tasks.amazonaws.com",
|
|
145
|
+
"actions": ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "codebuild",
|
|
149
|
+
"service_principal": "codebuild.amazonaws.com",
|
|
150
|
+
"actions": ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer", "ecr:BatchCheckLayerAvailability"]
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
"""
|
|
155
|
+
if self.__config and isinstance(self.__config, dict):
|
|
156
|
+
return self.__config.get("cross_account_access", {})
|
|
157
|
+
return {}
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def cross_account_enabled(self) -> bool:
|
|
161
|
+
"""Whether cross-account access is explicitly enabled"""
|
|
162
|
+
access_config = self.cross_account_access
|
|
163
|
+
if access_config:
|
|
164
|
+
return str(access_config.get("enabled", "true")).lower() == "true"
|
|
165
|
+
return True # Default to enabled for backward compatibility
|
|
121
166
|
|
|
122
167
|
# SSM properties are now inherited from EnhancedBaseConfig
|
|
123
168
|
# Keeping these for any direct access patterns in existing code
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ECS Service Configuration
|
|
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
|
+
|
|
10
|
+
class EcsServiceConfig:
|
|
11
|
+
"""ECS Service Configuration"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config: Dict[str, Any]) -> None:
|
|
14
|
+
self._config = config
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def name(self) -> str:
|
|
18
|
+
"""Service name"""
|
|
19
|
+
return self._config.get("name", "")
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def cluster_name(self) -> Optional[str]:
|
|
23
|
+
"""ECS Cluster name"""
|
|
24
|
+
return self._config.get("cluster_name")
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def task_definition(self) -> Dict[str, Any]:
|
|
28
|
+
"""Task definition configuration"""
|
|
29
|
+
return self._config.get("task_definition", {})
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def container_definitions(self) -> List[Dict[str, Any]]:
|
|
33
|
+
"""Container definitions"""
|
|
34
|
+
return self.task_definition.get("containers", [])
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def cpu(self) -> str:
|
|
38
|
+
"""Task CPU units"""
|
|
39
|
+
return self.task_definition.get("cpu", "256")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def memory(self) -> str:
|
|
43
|
+
"""Task memory (MB)"""
|
|
44
|
+
return self.task_definition.get("memory", "512")
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def launch_type(self) -> str:
|
|
48
|
+
"""Launch type: FARGATE or EC2"""
|
|
49
|
+
return self._config.get("launch_type", "FARGATE")
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def desired_count(self) -> int:
|
|
53
|
+
"""Desired number of tasks"""
|
|
54
|
+
return self._config.get("desired_count", 2)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def min_capacity(self) -> int:
|
|
58
|
+
"""Minimum number of tasks"""
|
|
59
|
+
return self._config.get("min_capacity", 1)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def max_capacity(self) -> int:
|
|
63
|
+
"""Maximum number of tasks"""
|
|
64
|
+
return self._config.get("max_capacity", 4)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def vpc_id(self) -> Optional[str]:
|
|
68
|
+
"""VPC ID"""
|
|
69
|
+
return self._config.get("vpc_id")
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def subnet_group_name(self) -> Optional[str]:
|
|
73
|
+
"""Subnet group name for service placement"""
|
|
74
|
+
return self._config.get("subnet_group_name")
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def security_group_ids(self) -> List[str]:
|
|
78
|
+
"""Security group IDs"""
|
|
79
|
+
return self._config.get("security_group_ids", [])
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def assign_public_ip(self) -> bool:
|
|
83
|
+
"""Whether to assign public IP addresses"""
|
|
84
|
+
return self._config.get("assign_public_ip", False)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def target_group_arns(self) -> List[str]:
|
|
88
|
+
"""Target group ARNs for load balancing"""
|
|
89
|
+
return self._config.get("target_group_arns", [])
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def container_port(self) -> int:
|
|
93
|
+
"""Container port for load balancer"""
|
|
94
|
+
return self._config.get("container_port", 80)
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def health_check_grace_period(self) -> int:
|
|
98
|
+
"""Health check grace period in seconds"""
|
|
99
|
+
return self._config.get("health_check_grace_period", 60)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def enable_execute_command(self) -> bool:
|
|
103
|
+
"""Enable ECS Exec for debugging"""
|
|
104
|
+
return self._config.get("enable_execute_command", False)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def enable_auto_scaling(self) -> bool:
|
|
108
|
+
"""Enable auto-scaling"""
|
|
109
|
+
return self._config.get("enable_auto_scaling", True)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def auto_scaling_target_cpu(self) -> int:
|
|
113
|
+
"""Target CPU utilization percentage for auto-scaling"""
|
|
114
|
+
return self._config.get("auto_scaling_target_cpu", 70)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def auto_scaling_target_memory(self) -> int:
|
|
118
|
+
"""Target memory utilization percentage for auto-scaling"""
|
|
119
|
+
return self._config.get("auto_scaling_target_memory", 80)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def tags(self) -> Dict[str, str]:
|
|
123
|
+
"""Resource tags"""
|
|
124
|
+
return self._config.get("tags", {})
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def ssm_exports(self) -> Dict[str, str]:
|
|
128
|
+
"""SSM parameter exports"""
|
|
129
|
+
return self._config.get("ssm_exports", {})
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def ssm_imports(self) -> Dict[str, str]:
|
|
133
|
+
"""SSM parameter imports"""
|
|
134
|
+
return self._config.get("ssm_imports", {})
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def deployment_type(self) -> str:
|
|
138
|
+
"""Deployment type: production, maintenance, or blue-green"""
|
|
139
|
+
return self._config.get("deployment_type", "production")
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def is_maintenance_mode(self) -> bool:
|
|
143
|
+
"""Whether this is a maintenance mode deployment"""
|
|
144
|
+
return self.deployment_type == "maintenance"
|
|
@@ -33,6 +33,7 @@ class ECRConstruct(Construct, SsmParameterMixin):
|
|
|
33
33
|
|
|
34
34
|
self.scope = scope
|
|
35
35
|
self.deployment = deployment
|
|
36
|
+
self.repo = repo
|
|
36
37
|
self.ecr_name = repo.name
|
|
37
38
|
self.image_scan_on_push = repo.image_scan_on_push
|
|
38
39
|
self.empty_on_delete = repo.empty_on_delete
|
|
@@ -98,28 +99,17 @@ class ECRConstruct(Construct, SsmParameterMixin):
|
|
|
98
99
|
)
|
|
99
100
|
|
|
100
101
|
# Add dependencies to ensure SSM parameters are created after the ECR repository
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
param.node.default_child.
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
if params:
|
|
103
|
+
for param in params.values():
|
|
104
|
+
if param and hasattr(param, 'node') and param.node.default_child and isinstance(param.node.default_child, CfnResource):
|
|
105
|
+
param.node.default_child.add_dependency(
|
|
106
|
+
cast(CfnResource, self.ecr.node.default_child)
|
|
107
|
+
)
|
|
106
108
|
|
|
107
109
|
def __set_life_cycle_rules(self) -> None:
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# always keep images tagged as prod
|
|
112
|
-
self.ecr.add_lifecycle_rule(
|
|
113
|
-
tag_pattern_list=["prod*"], max_image_count=9999
|
|
114
|
-
)
|
|
115
|
-
except Exception as e: # pylint: disable=w0718
|
|
116
|
-
if "unexpected keyword argument" in str(e):
|
|
117
|
-
logger.warning(
|
|
118
|
-
"tag_pattern_list is not available in this version of the aws cdk"
|
|
119
|
-
)
|
|
120
|
-
else:
|
|
121
|
-
raise
|
|
122
|
-
|
|
110
|
+
# Note: tag_pattern_list is deprecated and causes circular dependencies in CDK synthesis
|
|
111
|
+
# Only add lifecycle rule for untagged images if configured
|
|
112
|
+
|
|
123
113
|
if not self.auto_delete_untagged_images_in_days:
|
|
124
114
|
return None
|
|
125
115
|
|
|
@@ -143,49 +133,131 @@ class ECRConstruct(Construct, SsmParameterMixin):
|
|
|
143
133
|
)
|
|
144
134
|
|
|
145
135
|
def __setup_cross_account_access_permissions(self):
|
|
146
|
-
|
|
136
|
+
"""
|
|
137
|
+
Setup cross-account access permissions with flexible configuration support.
|
|
138
|
+
|
|
139
|
+
Supports both legacy (default Lambda access) and new configurable approach.
|
|
140
|
+
"""
|
|
141
|
+
# Check if cross-account access is disabled
|
|
142
|
+
if not self.repo.cross_account_enabled:
|
|
143
|
+
logger.info(f"Cross-account access disabled for {self.ecr_name}")
|
|
144
|
+
return
|
|
147
145
|
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
# we're in the same account as the "devops" so we don't need cross account
|
|
152
|
-
# permisions
|
|
146
|
+
# Check if we're in the same account as devops
|
|
147
|
+
if self.deployment.account == self.deployment.workload.get("devops", {}).get("account"):
|
|
148
|
+
logger.info(f"Same account as devops, skipping cross-account permissions for {self.ecr_name}")
|
|
153
149
|
return
|
|
154
150
|
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
access_config = self.repo.cross_account_access
|
|
152
|
+
|
|
153
|
+
if access_config and access_config.get("services"):
|
|
154
|
+
# New configurable approach
|
|
155
|
+
logger.info(f"Setting up configurable cross-account access for {self.ecr_name}")
|
|
156
|
+
self.__setup_configurable_access(access_config)
|
|
157
|
+
else:
|
|
158
|
+
# Legacy approach - default Lambda access for backward compatibility
|
|
159
|
+
logger.info(f"Setting up legacy cross-account access (Lambda only) for {self.ecr_name}")
|
|
160
|
+
self.__setup_legacy_lambda_access()
|
|
161
|
+
|
|
162
|
+
def __setup_configurable_access(self, access_config: dict):
|
|
163
|
+
"""Setup cross-account access using configuration"""
|
|
164
|
+
|
|
165
|
+
# Get list of accounts (default to deployment account)
|
|
166
|
+
accounts = access_config.get("accounts", [self.deployment.account])
|
|
167
|
+
|
|
168
|
+
# Add account principal policies if accounts are specified
|
|
169
|
+
if accounts:
|
|
170
|
+
self.__add_account_principal_policy(accounts)
|
|
171
|
+
|
|
172
|
+
# Add service-specific policies
|
|
173
|
+
services = access_config.get("services", [])
|
|
174
|
+
for service_config in services:
|
|
175
|
+
self.__add_service_principal_policy(service_config)
|
|
176
|
+
|
|
177
|
+
def __add_account_principal_policy(self, accounts: list):
|
|
178
|
+
"""Add policy for AWS account principals"""
|
|
179
|
+
principals = [iam.AccountPrincipal(account) for account in accounts]
|
|
180
|
+
|
|
181
|
+
policy_statement = iam.PolicyStatement(
|
|
157
182
|
actions=[
|
|
158
183
|
"ecr:GetDownloadUrlForLayer",
|
|
159
184
|
"ecr:BatchGetImage",
|
|
160
185
|
"ecr:BatchCheckLayerAvailability",
|
|
161
186
|
],
|
|
162
|
-
principals=
|
|
163
|
-
iam.AccountPrincipal(self.deployment.account)
|
|
164
|
-
], # Replace with the account ID of the Lambda function
|
|
165
|
-
resources=[ecr.repository_arn],
|
|
187
|
+
principals=principals,
|
|
166
188
|
effect=iam.Effect.ALLOW,
|
|
167
189
|
)
|
|
168
190
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
response = self.ecr.add_to_resource_policy(policy_statement)
|
|
192
|
+
if not response.statement_added:
|
|
193
|
+
logger.warning(f"Failed to add account principal policy for {', '.join(accounts)}")
|
|
194
|
+
else:
|
|
195
|
+
logger.info(f"Added account principal policy for accounts: {', '.join(accounts)}")
|
|
196
|
+
|
|
197
|
+
def __add_service_principal_policy(self, service_config: dict):
|
|
198
|
+
"""Add policy for service principal (Lambda, ECS, CodeBuild, etc.)"""
|
|
199
|
+
service_name = service_config.get("name", "unknown")
|
|
200
|
+
service_principal = service_config.get("service_principal")
|
|
201
|
+
actions = service_config.get("actions", ["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"])
|
|
202
|
+
conditions = service_config.get("condition")
|
|
203
|
+
|
|
204
|
+
if not service_principal:
|
|
205
|
+
# Infer service principal from common service names
|
|
206
|
+
service_principal_map = {
|
|
207
|
+
"lambda": "lambda.amazonaws.com",
|
|
208
|
+
"ecs": "ecs-tasks.amazonaws.com",
|
|
209
|
+
"ecs-tasks": "ecs-tasks.amazonaws.com",
|
|
210
|
+
"codebuild": "codebuild.amazonaws.com",
|
|
211
|
+
"codepipeline": "codepipeline.amazonaws.com",
|
|
212
|
+
"ec2": "ec2.amazonaws.com",
|
|
213
|
+
}
|
|
214
|
+
service_principal = service_principal_map.get(service_name.lower())
|
|
215
|
+
|
|
216
|
+
if not service_principal:
|
|
217
|
+
logger.warning(f"Unknown service principal for service: {service_name}")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
policy_statement = iam.PolicyStatement(
|
|
221
|
+
effect=iam.Effect.ALLOW,
|
|
222
|
+
actions=actions,
|
|
223
|
+
principals=[iam.ServicePrincipal(service_principal)],
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Add conditions if specified
|
|
227
|
+
if conditions:
|
|
228
|
+
for condition_key, condition_value in conditions.items():
|
|
229
|
+
policy_statement.add_condition(condition_key, condition_value)
|
|
230
|
+
|
|
231
|
+
response = self.ecr.add_to_resource_policy(policy_statement)
|
|
232
|
+
if not response.statement_added:
|
|
233
|
+
logger.warning(f"Failed to add service principal policy for {service_name}")
|
|
234
|
+
else:
|
|
235
|
+
logger.info(f"Added service principal policy for {service_name} ({service_principal})")
|
|
236
|
+
|
|
237
|
+
def __setup_legacy_lambda_access(self):
|
|
238
|
+
"""Legacy method: Setup default Lambda-only cross-account access"""
|
|
239
|
+
|
|
240
|
+
# Add account principal policy
|
|
241
|
+
self.__add_account_principal_policy([self.deployment.account])
|
|
242
|
+
|
|
243
|
+
# Add Lambda service principal policy with default condition
|
|
244
|
+
lambda_policy = iam.PolicyStatement(
|
|
245
|
+
effect=iam.Effect.ALLOW,
|
|
246
|
+
actions=["ecr:BatchGetImage", "ecr:GetDownloadUrlForLayer"],
|
|
247
|
+
principals=[iam.ServicePrincipal("lambda.amazonaws.com")],
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
lambda_policy.add_condition(
|
|
251
|
+
"StringLike",
|
|
252
|
+
{
|
|
253
|
+
"aws:sourceArn": [
|
|
254
|
+
f"arn:aws:lambda:{self.deployment.region}:{self.deployment.account}:function:*"
|
|
255
|
+
]
|
|
256
|
+
}
|
|
189
257
|
)
|
|
190
258
|
|
|
191
|
-
|
|
259
|
+
response = self.ecr.add_to_resource_policy(lambda_policy)
|
|
260
|
+
if not response.statement_added:
|
|
261
|
+
logger.warning("Failed to add Lambda service principal policy")
|
|
262
|
+
else:
|
|
263
|
+
logger.info("Added legacy Lambda service principal policy")
|
|
@@ -246,7 +246,13 @@ class PipelineFactoryStack(cdk.Stack):
|
|
|
246
246
|
|
|
247
247
|
def _get_steps(self, key: str, stage_config: PipelineStageConfig):
|
|
248
248
|
"""
|
|
249
|
-
Gets the build steps from the config.json
|
|
249
|
+
Gets the build steps from the config.json.
|
|
250
|
+
|
|
251
|
+
Commands can be:
|
|
252
|
+
- A list of strings (each string is a separate command)
|
|
253
|
+
- A single multi-line string (treated as a single script block)
|
|
254
|
+
|
|
255
|
+
This allows support for complex shell constructs like if blocks, loops, etc.
|
|
250
256
|
"""
|
|
251
257
|
shell_steps: List[pipelines.ShellStep] = []
|
|
252
258
|
|
|
@@ -257,6 +263,12 @@ class PipelineFactoryStack(cdk.Stack):
|
|
|
257
263
|
for step in steps:
|
|
258
264
|
step_id = step.get("id") or step.get("name")
|
|
259
265
|
commands = step.get("commands", [])
|
|
266
|
+
|
|
267
|
+
# Normalize commands to a list
|
|
268
|
+
# If commands is a single string, wrap it in a list
|
|
269
|
+
if isinstance(commands, str):
|
|
270
|
+
commands = [commands]
|
|
271
|
+
|
|
260
272
|
shell_step = pipelines.ShellStep(
|
|
261
273
|
id=step_id,
|
|
262
274
|
commands=commands,
|
|
File without changes
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ECS Service Stack Pattern for CDK-Factory
|
|
3
|
+
Supports Fargate and EC2 launch types with auto-scaling, load balancing, and blue-green deployments.
|
|
4
|
+
Maintainers: Eric Wilson
|
|
5
|
+
MIT License. See Project Root for the license information.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, List, Optional
|
|
9
|
+
|
|
10
|
+
import aws_cdk as cdk
|
|
11
|
+
from aws_cdk import (
|
|
12
|
+
aws_ecs as ecs,
|
|
13
|
+
aws_ec2 as ec2,
|
|
14
|
+
aws_logs as logs,
|
|
15
|
+
aws_iam as iam,
|
|
16
|
+
aws_elasticloadbalancingv2 as elbv2,
|
|
17
|
+
aws_applicationautoscaling as appscaling,
|
|
18
|
+
)
|
|
19
|
+
from aws_lambda_powertools import Logger
|
|
20
|
+
from constructs import Construct
|
|
21
|
+
|
|
22
|
+
from cdk_factory.configurations.deployment import DeploymentConfig
|
|
23
|
+
from cdk_factory.configurations.stack import StackConfig
|
|
24
|
+
from cdk_factory.configurations.resources.ecs_service import EcsServiceConfig
|
|
25
|
+
from cdk_factory.interfaces.istack import IStack
|
|
26
|
+
from cdk_factory.interfaces.enhanced_ssm_parameter_mixin import EnhancedSsmParameterMixin
|
|
27
|
+
from cdk_factory.stack.stack_module_registry import register_stack
|
|
28
|
+
from cdk_factory.workload.workload_factory import WorkloadConfig
|
|
29
|
+
|
|
30
|
+
logger = Logger(service="EcsServiceStack")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@register_stack("ecs_service_library_module")
|
|
34
|
+
@register_stack("ecs_service_stack")
|
|
35
|
+
@register_stack("fargate_service_stack")
|
|
36
|
+
class EcsServiceStack(IStack, EnhancedSsmParameterMixin):
|
|
37
|
+
"""
|
|
38
|
+
Reusable stack for ECS/Fargate services with Docker container support.
|
|
39
|
+
Supports blue-green deployments, maintenance mode, and auto-scaling.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
|
|
43
|
+
super().__init__(scope, id, **kwargs)
|
|
44
|
+
self.ecs_config: Optional[EcsServiceConfig] = None
|
|
45
|
+
self.stack_config: Optional[StackConfig] = None
|
|
46
|
+
self.deployment: Optional[DeploymentConfig] = None
|
|
47
|
+
self.workload: Optional[WorkloadConfig] = None
|
|
48
|
+
self.cluster: Optional[ecs.ICluster] = None
|
|
49
|
+
self.service: Optional[ecs.FargateService] = None
|
|
50
|
+
self.task_definition: Optional[ecs.FargateTaskDefinition] = None
|
|
51
|
+
self._vpc: Optional[ec2.IVpc] = None
|
|
52
|
+
|
|
53
|
+
def build(
|
|
54
|
+
self,
|
|
55
|
+
stack_config: StackConfig,
|
|
56
|
+
deployment: DeploymentConfig,
|
|
57
|
+
workload: WorkloadConfig,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Build the ECS Service stack"""
|
|
60
|
+
self._build(stack_config, deployment, workload)
|
|
61
|
+
|
|
62
|
+
def _build(
|
|
63
|
+
self,
|
|
64
|
+
stack_config: StackConfig,
|
|
65
|
+
deployment: DeploymentConfig,
|
|
66
|
+
workload: WorkloadConfig,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Internal build method for the ECS Service stack"""
|
|
69
|
+
self.stack_config = stack_config
|
|
70
|
+
self.deployment = deployment
|
|
71
|
+
self.workload = workload
|
|
72
|
+
|
|
73
|
+
# Load ECS configuration
|
|
74
|
+
self.ecs_config = EcsServiceConfig(
|
|
75
|
+
stack_config.dictionary.get("ecs_service", {})
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
service_name = deployment.build_resource_name(self.ecs_config.name)
|
|
79
|
+
|
|
80
|
+
# Load VPC
|
|
81
|
+
self._load_vpc()
|
|
82
|
+
|
|
83
|
+
# Create or load ECS cluster
|
|
84
|
+
self._create_or_load_cluster()
|
|
85
|
+
|
|
86
|
+
# Create task definition
|
|
87
|
+
self._create_task_definition(service_name)
|
|
88
|
+
|
|
89
|
+
# Create ECS service
|
|
90
|
+
self._create_service(service_name)
|
|
91
|
+
|
|
92
|
+
# Setup auto-scaling
|
|
93
|
+
if self.ecs_config.enable_auto_scaling:
|
|
94
|
+
self._setup_auto_scaling()
|
|
95
|
+
|
|
96
|
+
# Add outputs
|
|
97
|
+
self._add_outputs(service_name)
|
|
98
|
+
|
|
99
|
+
def _load_vpc(self) -> None:
|
|
100
|
+
"""Load VPC from configuration"""
|
|
101
|
+
vpc_id = self.ecs_config.vpc_id or self.workload.vpc_id
|
|
102
|
+
|
|
103
|
+
if not vpc_id:
|
|
104
|
+
raise ValueError("VPC ID is required for ECS service")
|
|
105
|
+
|
|
106
|
+
self._vpc = ec2.Vpc.from_lookup(
|
|
107
|
+
self,
|
|
108
|
+
"VPC",
|
|
109
|
+
vpc_id=vpc_id
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _create_or_load_cluster(self) -> None:
|
|
113
|
+
"""Create a new ECS cluster or load an existing one"""
|
|
114
|
+
cluster_name = self.ecs_config.cluster_name
|
|
115
|
+
|
|
116
|
+
if cluster_name:
|
|
117
|
+
# Try to load existing cluster
|
|
118
|
+
try:
|
|
119
|
+
self.cluster = ecs.Cluster.from_cluster_attributes(
|
|
120
|
+
self,
|
|
121
|
+
"Cluster",
|
|
122
|
+
cluster_name=cluster_name,
|
|
123
|
+
vpc=self._vpc,
|
|
124
|
+
)
|
|
125
|
+
logger.info(f"Using existing cluster: {cluster_name}")
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.warning(f"Could not load cluster {cluster_name}, creating new one: {e}")
|
|
128
|
+
self._create_new_cluster(cluster_name)
|
|
129
|
+
else:
|
|
130
|
+
# Create a new cluster with auto-generated name
|
|
131
|
+
cluster_name = f"{self.deployment.workload_name}-{self.deployment.environment}-cluster"
|
|
132
|
+
self._create_new_cluster(cluster_name)
|
|
133
|
+
|
|
134
|
+
def _create_new_cluster(self, cluster_name: str) -> None:
|
|
135
|
+
"""Create a new ECS cluster"""
|
|
136
|
+
self.cluster = ecs.Cluster(
|
|
137
|
+
self,
|
|
138
|
+
"Cluster",
|
|
139
|
+
cluster_name=cluster_name,
|
|
140
|
+
vpc=self._vpc,
|
|
141
|
+
container_insights=True,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
cdk.Tags.of(self.cluster).add("Name", cluster_name)
|
|
145
|
+
cdk.Tags.of(self.cluster).add("Environment", self.deployment.environment)
|
|
146
|
+
|
|
147
|
+
def _create_task_definition(self, service_name: str) -> None:
|
|
148
|
+
"""Create ECS task definition with container definitions"""
|
|
149
|
+
|
|
150
|
+
# Create task execution role
|
|
151
|
+
execution_role = iam.Role(
|
|
152
|
+
self,
|
|
153
|
+
"TaskExecutionRole",
|
|
154
|
+
assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
|
|
155
|
+
managed_policies=[
|
|
156
|
+
iam.ManagedPolicy.from_aws_managed_policy_name(
|
|
157
|
+
"service-role/AmazonECSTaskExecutionRolePolicy"
|
|
158
|
+
),
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Create task role for application permissions
|
|
163
|
+
task_role = iam.Role(
|
|
164
|
+
self,
|
|
165
|
+
"TaskRole",
|
|
166
|
+
assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Enable ECS Exec if configured
|
|
170
|
+
if self.ecs_config.enable_execute_command:
|
|
171
|
+
task_role.add_managed_policy(
|
|
172
|
+
iam.ManagedPolicy.from_aws_managed_policy_name(
|
|
173
|
+
"CloudWatchAgentServerPolicy"
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Create task definition based on launch type
|
|
178
|
+
if self.ecs_config.launch_type == "EC2":
|
|
179
|
+
# EC2 task definition
|
|
180
|
+
network_mode = self.ecs_config.task_definition.get("network_mode", "bridge")
|
|
181
|
+
self.task_definition = ecs.Ec2TaskDefinition(
|
|
182
|
+
self,
|
|
183
|
+
"TaskDefinition",
|
|
184
|
+
family=f"{service_name}-task",
|
|
185
|
+
network_mode=ecs.NetworkMode(network_mode.upper()) if network_mode else ecs.NetworkMode.BRIDGE,
|
|
186
|
+
execution_role=execution_role,
|
|
187
|
+
task_role=task_role,
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
# Fargate task definition
|
|
191
|
+
self.task_definition = ecs.FargateTaskDefinition(
|
|
192
|
+
self,
|
|
193
|
+
"TaskDefinition",
|
|
194
|
+
family=f"{service_name}-task",
|
|
195
|
+
cpu=int(self.ecs_config.cpu),
|
|
196
|
+
memory_limit_mib=int(self.ecs_config.memory),
|
|
197
|
+
execution_role=execution_role,
|
|
198
|
+
task_role=task_role,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Add containers
|
|
202
|
+
self._add_containers_to_task()
|
|
203
|
+
|
|
204
|
+
def _add_containers_to_task(self) -> None:
|
|
205
|
+
"""Add container definitions to the task"""
|
|
206
|
+
container_definitions = self.ecs_config.container_definitions
|
|
207
|
+
|
|
208
|
+
if not container_definitions:
|
|
209
|
+
raise ValueError("At least one container definition is required")
|
|
210
|
+
|
|
211
|
+
for idx, container_config in enumerate(container_definitions):
|
|
212
|
+
container_name = container_config.get("name", f"container-{idx}")
|
|
213
|
+
image_uri = container_config.get("image")
|
|
214
|
+
|
|
215
|
+
if not image_uri:
|
|
216
|
+
raise ValueError(f"Container image is required for {container_name}")
|
|
217
|
+
|
|
218
|
+
# Create log group for container
|
|
219
|
+
log_group = logs.LogGroup(
|
|
220
|
+
self,
|
|
221
|
+
f"LogGroup-{container_name}",
|
|
222
|
+
log_group_name=f"/ecs/{self.deployment.workload_name}/{self.deployment.environment}/{container_name}",
|
|
223
|
+
retention=logs.RetentionDays.ONE_WEEK,
|
|
224
|
+
removal_policy=cdk.RemovalPolicy.DESTROY,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Build health check if configured
|
|
228
|
+
health_check_config = container_config.get("health_check")
|
|
229
|
+
health_check = None
|
|
230
|
+
if health_check_config:
|
|
231
|
+
health_check = ecs.HealthCheck(
|
|
232
|
+
command=health_check_config.get("command", ["CMD-SHELL", "exit 0"]),
|
|
233
|
+
interval=cdk.Duration.seconds(health_check_config.get("interval", 30)),
|
|
234
|
+
timeout=cdk.Duration.seconds(health_check_config.get("timeout", 5)),
|
|
235
|
+
retries=health_check_config.get("retries", 3),
|
|
236
|
+
start_period=cdk.Duration.seconds(health_check_config.get("start_period", 0)),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Add container to task definition
|
|
240
|
+
container = self.task_definition.add_container(
|
|
241
|
+
container_name,
|
|
242
|
+
image=ecs.ContainerImage.from_registry(image_uri),
|
|
243
|
+
logging=ecs.LogDriver.aws_logs(
|
|
244
|
+
stream_prefix=container_name,
|
|
245
|
+
log_group=log_group,
|
|
246
|
+
),
|
|
247
|
+
environment=container_config.get("environment", {}),
|
|
248
|
+
secrets=self._load_secrets(container_config.get("secrets", {})),
|
|
249
|
+
cpu=container_config.get("cpu"),
|
|
250
|
+
memory_limit_mib=container_config.get("memory"),
|
|
251
|
+
memory_reservation_mib=container_config.get("memory_reservation"),
|
|
252
|
+
essential=container_config.get("essential", True),
|
|
253
|
+
health_check=health_check,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Add port mappings
|
|
257
|
+
port_mappings = container_config.get("port_mappings", [])
|
|
258
|
+
for port_mapping in port_mappings:
|
|
259
|
+
container.add_port_mappings(
|
|
260
|
+
ecs.PortMapping(
|
|
261
|
+
container_port=port_mapping.get("container_port", 80),
|
|
262
|
+
protocol=ecs.Protocol.TCP,
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def _load_secrets(self, secrets_config: Dict[str, str]) -> Dict[str, ecs.Secret]:
|
|
267
|
+
"""Load secrets from Secrets Manager or SSM Parameter Store"""
|
|
268
|
+
secrets = {}
|
|
269
|
+
# Implement secret loading logic here
|
|
270
|
+
# This would integrate with AWS Secrets Manager or SSM Parameter Store
|
|
271
|
+
return secrets
|
|
272
|
+
|
|
273
|
+
def _create_service(self, service_name: str) -> None:
|
|
274
|
+
"""Create ECS service (Fargate or EC2)"""
|
|
275
|
+
|
|
276
|
+
# Load security groups
|
|
277
|
+
security_groups = self._load_security_groups()
|
|
278
|
+
|
|
279
|
+
# Load subnets
|
|
280
|
+
subnets = self._load_subnets()
|
|
281
|
+
|
|
282
|
+
# Create service based on launch type
|
|
283
|
+
if self.ecs_config.launch_type == "EC2":
|
|
284
|
+
self.service = ecs.Ec2Service(
|
|
285
|
+
self,
|
|
286
|
+
"Service",
|
|
287
|
+
service_name=service_name,
|
|
288
|
+
cluster=self.cluster,
|
|
289
|
+
task_definition=self.task_definition,
|
|
290
|
+
desired_count=self.ecs_config.desired_count,
|
|
291
|
+
enable_execute_command=self.ecs_config.enable_execute_command,
|
|
292
|
+
health_check_grace_period=cdk.Duration.seconds(
|
|
293
|
+
self.ecs_config.health_check_grace_period
|
|
294
|
+
) if self.ecs_config.target_group_arns else None,
|
|
295
|
+
circuit_breaker=ecs.DeploymentCircuitBreaker(rollback=True),
|
|
296
|
+
placement_strategies=self._get_placement_strategies(),
|
|
297
|
+
placement_constraints=self._get_placement_constraints(),
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
# Fargate service
|
|
301
|
+
self.service = ecs.FargateService(
|
|
302
|
+
self,
|
|
303
|
+
"Service",
|
|
304
|
+
service_name=service_name,
|
|
305
|
+
cluster=self.cluster,
|
|
306
|
+
task_definition=self.task_definition,
|
|
307
|
+
desired_count=self.ecs_config.desired_count,
|
|
308
|
+
security_groups=security_groups,
|
|
309
|
+
vpc_subnets=ec2.SubnetSelection(subnets=subnets) if subnets else None,
|
|
310
|
+
assign_public_ip=self.ecs_config.assign_public_ip,
|
|
311
|
+
enable_execute_command=self.ecs_config.enable_execute_command,
|
|
312
|
+
health_check_grace_period=cdk.Duration.seconds(
|
|
313
|
+
self.ecs_config.health_check_grace_period
|
|
314
|
+
) if self.ecs_config.target_group_arns else None,
|
|
315
|
+
circuit_breaker=ecs.DeploymentCircuitBreaker(rollback=True),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Attach to load balancer target groups
|
|
319
|
+
self._attach_to_load_balancer()
|
|
320
|
+
|
|
321
|
+
# Apply tags
|
|
322
|
+
for key, value in self.ecs_config.tags.items():
|
|
323
|
+
cdk.Tags.of(self.service).add(key, value)
|
|
324
|
+
|
|
325
|
+
def _get_placement_strategies(self) -> List[ecs.PlacementStrategy]:
|
|
326
|
+
"""Get placement strategies for EC2 launch type"""
|
|
327
|
+
strategies = []
|
|
328
|
+
placement_config = self.ecs_config._config.get("placement_strategies", [])
|
|
329
|
+
|
|
330
|
+
for strategy in placement_config:
|
|
331
|
+
strategy_type = strategy.get("type", "spread")
|
|
332
|
+
field = strategy.get("field", "instanceId")
|
|
333
|
+
|
|
334
|
+
if strategy_type == "spread":
|
|
335
|
+
strategies.append(ecs.PlacementStrategy.spread_across(field))
|
|
336
|
+
elif strategy_type == "binpack":
|
|
337
|
+
strategies.append(ecs.PlacementStrategy.packed_by(field))
|
|
338
|
+
elif strategy_type == "random":
|
|
339
|
+
strategies.append(ecs.PlacementStrategy.randomly())
|
|
340
|
+
|
|
341
|
+
# Default strategy if none specified
|
|
342
|
+
if not strategies:
|
|
343
|
+
strategies = [
|
|
344
|
+
ecs.PlacementStrategy.spread_across_instances(),
|
|
345
|
+
ecs.PlacementStrategy.spread_across("attribute:ecs.availability-zone"),
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
return strategies
|
|
349
|
+
|
|
350
|
+
def _get_placement_constraints(self) -> List[ecs.PlacementConstraint]:
|
|
351
|
+
"""Get placement constraints for EC2 launch type"""
|
|
352
|
+
constraints = []
|
|
353
|
+
constraint_config = self.ecs_config._config.get("placement_constraints", [])
|
|
354
|
+
|
|
355
|
+
for constraint in constraint_config:
|
|
356
|
+
constraint_type = constraint.get("type")
|
|
357
|
+
expression = constraint.get("expression", "")
|
|
358
|
+
|
|
359
|
+
if constraint_type == "distinctInstance":
|
|
360
|
+
constraints.append(ecs.PlacementConstraint.distinct_instances())
|
|
361
|
+
elif constraint_type == "memberOf" and expression:
|
|
362
|
+
constraints.append(ecs.PlacementConstraint.member_of(expression))
|
|
363
|
+
|
|
364
|
+
return constraints
|
|
365
|
+
|
|
366
|
+
def _load_security_groups(self) -> List[ec2.ISecurityGroup]:
|
|
367
|
+
"""Load security groups from IDs"""
|
|
368
|
+
security_groups = []
|
|
369
|
+
|
|
370
|
+
for sg_id in self.ecs_config.security_group_ids:
|
|
371
|
+
sg = ec2.SecurityGroup.from_security_group_id(
|
|
372
|
+
self,
|
|
373
|
+
f"SG-{sg_id[:8]}",
|
|
374
|
+
security_group_id=sg_id,
|
|
375
|
+
)
|
|
376
|
+
security_groups.append(sg)
|
|
377
|
+
|
|
378
|
+
return security_groups
|
|
379
|
+
|
|
380
|
+
def _load_subnets(self) -> Optional[List[ec2.ISubnet]]:
|
|
381
|
+
"""Load subnets by subnet group name"""
|
|
382
|
+
subnet_group_name = self.ecs_config.subnet_group_name
|
|
383
|
+
|
|
384
|
+
if not subnet_group_name:
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
# This would need to be implemented based on your subnet naming convention
|
|
388
|
+
# For now, returning None to use default VPC subnets
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
def _attach_to_load_balancer(self) -> None:
|
|
392
|
+
"""Attach service to load balancer target groups"""
|
|
393
|
+
target_group_arns = self.ecs_config.target_group_arns
|
|
394
|
+
|
|
395
|
+
if not target_group_arns:
|
|
396
|
+
# Try to load from SSM if configured
|
|
397
|
+
target_group_arns = self._load_target_groups_from_ssm()
|
|
398
|
+
|
|
399
|
+
for tg_arn in target_group_arns:
|
|
400
|
+
target_group = elbv2.ApplicationTargetGroup.from_target_group_attributes(
|
|
401
|
+
self,
|
|
402
|
+
f"TG-{tg_arn.split('/')[-1][:8]}",
|
|
403
|
+
target_group_arn=tg_arn,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
self.service.attach_to_application_target_group(target_group)
|
|
407
|
+
|
|
408
|
+
def _load_target_groups_from_ssm(self) -> List[str]:
|
|
409
|
+
"""Load target group ARNs from SSM parameters"""
|
|
410
|
+
target_group_arns = []
|
|
411
|
+
|
|
412
|
+
# Load SSM imports and look for target group ARNs
|
|
413
|
+
ssm_imports = self.ecs_config.ssm_imports
|
|
414
|
+
|
|
415
|
+
for param_key, param_name in ssm_imports.items():
|
|
416
|
+
if 'target_group' in param_key.lower() or 'tg' in param_key.lower():
|
|
417
|
+
try:
|
|
418
|
+
param_value = self.get_ssm_parameter_value(param_name)
|
|
419
|
+
if param_value and param_value.startswith('arn:'):
|
|
420
|
+
target_group_arns.append(param_value)
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.warning(f"Could not load target group from SSM {param_name}: {e}")
|
|
423
|
+
|
|
424
|
+
return target_group_arns
|
|
425
|
+
|
|
426
|
+
def _setup_auto_scaling(self) -> None:
|
|
427
|
+
"""Configure auto-scaling for the ECS service"""
|
|
428
|
+
|
|
429
|
+
scalable_target = self.service.auto_scale_task_count(
|
|
430
|
+
min_capacity=self.ecs_config.min_capacity,
|
|
431
|
+
max_capacity=self.ecs_config.max_capacity,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# CPU-based scaling
|
|
435
|
+
scalable_target.scale_on_cpu_utilization(
|
|
436
|
+
"CpuScaling",
|
|
437
|
+
target_utilization_percent=self.ecs_config.auto_scaling_target_cpu,
|
|
438
|
+
scale_in_cooldown=cdk.Duration.seconds(60),
|
|
439
|
+
scale_out_cooldown=cdk.Duration.seconds(60),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Memory-based scaling
|
|
443
|
+
scalable_target.scale_on_memory_utilization(
|
|
444
|
+
"MemoryScaling",
|
|
445
|
+
target_utilization_percent=self.ecs_config.auto_scaling_target_memory,
|
|
446
|
+
scale_in_cooldown=cdk.Duration.seconds(60),
|
|
447
|
+
scale_out_cooldown=cdk.Duration.seconds(60),
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def _add_outputs(self, service_name: str) -> None:
|
|
451
|
+
"""Add CloudFormation outputs"""
|
|
452
|
+
|
|
453
|
+
# Service name output
|
|
454
|
+
cdk.CfnOutput(
|
|
455
|
+
self,
|
|
456
|
+
"ServiceName",
|
|
457
|
+
value=self.service.service_name,
|
|
458
|
+
description=f"ECS Service Name: {service_name}",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Service ARN output
|
|
462
|
+
cdk.CfnOutput(
|
|
463
|
+
self,
|
|
464
|
+
"ServiceArn",
|
|
465
|
+
value=self.service.service_arn,
|
|
466
|
+
description=f"ECS Service ARN: {service_name}",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Cluster name output
|
|
470
|
+
cdk.CfnOutput(
|
|
471
|
+
self,
|
|
472
|
+
"ClusterName",
|
|
473
|
+
value=self.cluster.cluster_name,
|
|
474
|
+
description=f"ECS Cluster Name for {service_name}",
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Export to SSM if configured
|
|
478
|
+
self._export_to_ssm(service_name)
|
|
479
|
+
|
|
480
|
+
def _export_to_ssm(self, service_name: str) -> None:
|
|
481
|
+
"""Export resource ARNs and names to SSM Parameter Store"""
|
|
482
|
+
ssm_exports = self.ecs_config.ssm_exports
|
|
483
|
+
|
|
484
|
+
if not ssm_exports:
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
# Service name
|
|
488
|
+
if "service_name" in ssm_exports:
|
|
489
|
+
self.export_ssm_parameter(
|
|
490
|
+
scope=self,
|
|
491
|
+
id="ServiceNameParam",
|
|
492
|
+
value=self.service.service_name,
|
|
493
|
+
parameter_name=ssm_exports["service_name"],
|
|
494
|
+
description=f"ECS Service Name: {service_name}",
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# Service ARN
|
|
498
|
+
if "service_arn" in ssm_exports:
|
|
499
|
+
self.export_ssm_parameter(
|
|
500
|
+
scope=self,
|
|
501
|
+
id="ServiceArnParam",
|
|
502
|
+
value=self.service.service_arn,
|
|
503
|
+
parameter_name=ssm_exports["service_arn"],
|
|
504
|
+
description=f"ECS Service ARN: {service_name}",
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Cluster name
|
|
508
|
+
if "cluster_name" in ssm_exports:
|
|
509
|
+
self.export_ssm_parameter(
|
|
510
|
+
scope=self,
|
|
511
|
+
id="ClusterNameParam",
|
|
512
|
+
value=self.cluster.cluster_name,
|
|
513
|
+
parameter_name=ssm_exports["cluster_name"],
|
|
514
|
+
description=f"ECS Cluster Name for {service_name}",
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Task definition ARN
|
|
518
|
+
if "task_definition_arn" in ssm_exports:
|
|
519
|
+
self.export_ssm_parameter(
|
|
520
|
+
scope=self,
|
|
521
|
+
id="TaskDefinitionArnParam",
|
|
522
|
+
value=self.task_definition.task_definition_arn,
|
|
523
|
+
parameter_name=ssm_exports["task_definition_arn"],
|
|
524
|
+
description=f"ECS Task Definition ARN for {service_name}",
|
|
525
|
+
)
|
cdk_factory/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.8.
|
|
1
|
+
__version__ = "0.8.8"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
cdk_factory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
cdk_factory/app.py,sha256=xv863N7O6HPKznB68_t7O4la9JacrkG87t9TjoDUk7s,2827
|
|
3
3
|
cdk_factory/cdk.json,sha256=SKZKhJ2PBpFH78j-F8S3VDYW-lf76--Q2I3ON-ZIQfw,3106
|
|
4
|
-
cdk_factory/version.py,sha256=
|
|
4
|
+
cdk_factory/version.py,sha256=S5bBAK8bL7bybaXGJQuNE98fa3H65zGjTASMiyKGJGw,22
|
|
5
5
|
cdk_factory/builds/README.md,sha256=9BBWd7bXpyKdMU_g2UljhQwrC9i5O_Tvkb6oPvndoZk,90
|
|
6
6
|
cdk_factory/commands/command_loader.py,sha256=QbLquuP_AdxtlxlDy-2IWCQ6D-7qa58aphnDPtp_uTs,3744
|
|
7
7
|
cdk_factory/configurations/base_config.py,sha256=JKjhNsy0RCUZy1s8n5D_aXXI-upR9izaLtCTfKYiV9k,9624
|
|
@@ -28,7 +28,8 @@ cdk_factory/configurations/resources/code_repository.py,sha256=rAM6cbMtNR_S4TfiB
|
|
|
28
28
|
cdk_factory/configurations/resources/cognito.py,sha256=udX2AJ1ITLhy4f1XiJQwrva6F4i9NdbSm0zTg5PGku8,10649
|
|
29
29
|
cdk_factory/configurations/resources/docker.py,sha256=hUbuxkuhcQu9LnLX7I8_57eTmHefEAGVnOHO37MkqC4,2166
|
|
30
30
|
cdk_factory/configurations/resources/dynamodb.py,sha256=HsZMOaRwfuNPwKIzokeeE3f5zAQLTB5hRb_GzYq2ibg,2903
|
|
31
|
-
cdk_factory/configurations/resources/ecr.py,sha256=
|
|
31
|
+
cdk_factory/configurations/resources/ecr.py,sha256=o9hHzEBVPoxUvWZGXGbRJ-98FmP6fMLY5a1-qg42jL0,8253
|
|
32
|
+
cdk_factory/configurations/resources/ecs_service.py,sha256=kEzSlasDejobl1ZxZtgtM7kVYF11AT3vCGsDInQNY3I,4319
|
|
32
33
|
cdk_factory/configurations/resources/exisiting.py,sha256=EVOLnkB-DGfTlmDgyQ5DD5k2zYfpFxqI3gugDR7mifI,478
|
|
33
34
|
cdk_factory/configurations/resources/lambda_function.py,sha256=VENZ9-ABJ5mjcN8J8wdLH4KHDYr1kWO0iFDH0B2mJXA,14659
|
|
34
35
|
cdk_factory/configurations/resources/lambda_layers.py,sha256=gVeP_-LC3Eq0lkPaG_JfFUwboM5evRPr99SfKj53m7A,633
|
|
@@ -47,7 +48,7 @@ cdk_factory/configurations/resources/security_group_full_stack.py,sha256=x5MIMCa
|
|
|
47
48
|
cdk_factory/configurations/resources/sqs.py,sha256=fAh2dqttJ6PX46enFRULuiLEu3TEj0Vb2xntAOgUpYE,4346
|
|
48
49
|
cdk_factory/configurations/resources/vpc.py,sha256=sNn6w76bHFwmt6N76gZZhqpsuNB9860C1SZu6tebaXY,3835
|
|
49
50
|
cdk_factory/constructs/cloudfront/cloudfront_distribution_construct.py,sha256=7uDkDbFigeUOuYgsQ9ghA4dGlx63rUCa6Iiq4RWokEA,14627
|
|
50
|
-
cdk_factory/constructs/ecr/ecr_construct.py,sha256=
|
|
51
|
+
cdk_factory/constructs/ecr/ecr_construct.py,sha256=JLz3gWrsjlM0XghvbgxuoGlF-VIo_7IYxtgX7mTkidE,10660
|
|
51
52
|
cdk_factory/constructs/lambdas/lambda_function_construct.py,sha256=SQ5SEXn4kezVAzXuv_A_JB3o_svyBXOMi-htvfB9HQs,4516
|
|
52
53
|
cdk_factory/constructs/lambdas/lambda_function_docker_construct.py,sha256=aSyn3eh1YnuIahZ7CbZ5WswwPL8u70ZibMoS24QQifc,9907
|
|
53
54
|
cdk_factory/constructs/lambdas/lambda_function_role_construct.py,sha256=nJoxKv-4CWugX-ZkAzoG8wrfSsHqXqiMZnGRqSyS4Po,1453
|
|
@@ -62,7 +63,7 @@ cdk_factory/interfaces/istack.py,sha256=bhTBs-o9FgKwvJMSuwxjUV6D3nUlvZHVzfm27jP9
|
|
|
62
63
|
cdk_factory/interfaces/live_ssm_resolver.py,sha256=3FIr9a02SXqZmbFs3RT0WxczWEQR_CF7QSt7kWbDrVE,8163
|
|
63
64
|
cdk_factory/interfaces/ssm_parameter_mixin.py,sha256=uA2j8HmAOpuEA9ynRj51s0WjUHMVLsbLQN-QS9NKyHA,12089
|
|
64
65
|
cdk_factory/lambdas/health_handler.py,sha256=dd40ykKMxWCFEIyp2ZdQvAGNjw_ylI9CSm1N24Hp2ME,196
|
|
65
|
-
cdk_factory/pipeline/pipeline_factory.py,sha256=
|
|
66
|
+
cdk_factory/pipeline/pipeline_factory.py,sha256=OuL1pOWjThIMDYqZEBbqLzg8KK9A84qtjRMPJwnX-kk,15956
|
|
66
67
|
cdk_factory/pipeline/stage.py,sha256=Be7ExMB9A-linRM18IQDOzQ-cP_I2_ThRNzlT4FIrUg,437
|
|
67
68
|
cdk_factory/pipeline/security/policies.py,sha256=H3-S6nipz3UtF9Pc5eJYr4-aREUTCaJWMjOUyd6Rdv4,4406
|
|
68
69
|
cdk_factory/pipeline/security/roles.py,sha256=ZB_O5H_BXgotvVspS2kVad9EMcY-a_-vU7Nm1_Z5MB8,4985
|
|
@@ -84,6 +85,8 @@ cdk_factory/stack_library/cognito/cognito_stack.py,sha256=zEHkKVCIeyZywPs_GIMXCX
|
|
|
84
85
|
cdk_factory/stack_library/dynamodb/dynamodb_stack.py,sha256=TVyOrUhgaSuN8uymkpaQcpOaSA0lkYJ8QUMgakTCKus,6771
|
|
85
86
|
cdk_factory/stack_library/ecr/README.md,sha256=xw2wPx9WN03Y4BBwqvbi9lAFGNyaD1FUNpqxVJX14Oo,179
|
|
86
87
|
cdk_factory/stack_library/ecr/ecr_stack.py,sha256=1xA68sxFVyqreYjXrP_7U9I8RF9RtFeR6KeEfSWuC2U,2118
|
|
88
|
+
cdk_factory/stack_library/ecs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
|
+
cdk_factory/stack_library/ecs/ecs_service_stack.py,sha256=mFanYtkZ9GpgT8g31D9tj-2yYbmmcuHN7ADaRD-ri9o,20638
|
|
87
90
|
cdk_factory/stack_library/load_balancer/__init__.py,sha256=wZpKw2OecLJGdF5mPayCYAEhu2H3c2gJFFIxwXftGDU,52
|
|
88
91
|
cdk_factory/stack_library/load_balancer/load_balancer_stack.py,sha256=t5JUe5lMUbQCRFZR08k8nO-g-53yWY8gKB9v8ZnedBs,24391
|
|
89
92
|
cdk_factory/stack_library/rds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -112,7 +115,7 @@ cdk_factory/utilities/lambda_function_utilities.py,sha256=S1GvBsY_q2cyUiaud3HORJ
|
|
|
112
115
|
cdk_factory/utilities/os_execute.py,sha256=5Op0LY_8Y-pUm04y1k8MTpNrmQvcLmQHPQITEP7EuSU,1019
|
|
113
116
|
cdk_factory/utils/api_gateway_utilities.py,sha256=If7Xu5s_UxmuV-kL3JkXxPLBdSVUKoLtohm0IUFoiV8,4378
|
|
114
117
|
cdk_factory/workload/workload_factory.py,sha256=yBUDGIuB8-5p_mGcVFxsD2ZoZIziak3yh3LL3JvS0M4,5903
|
|
115
|
-
cdk_factory-0.8.
|
|
116
|
-
cdk_factory-0.8.
|
|
117
|
-
cdk_factory-0.8.
|
|
118
|
-
cdk_factory-0.8.
|
|
118
|
+
cdk_factory-0.8.8.dist-info/METADATA,sha256=snclFyCZwqRqLmxauiDny56lUUnERMnVSNYvMGKZWG8,2450
|
|
119
|
+
cdk_factory-0.8.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
120
|
+
cdk_factory-0.8.8.dist-info/licenses/LICENSE,sha256=NOtdOeLwg2il_XBJdXUPFPX8JlV4dqTdDGAd2-khxT8,1066
|
|
121
|
+
cdk_factory-0.8.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|