Skip to content

Commit 3313039

Browse files
authored
Feature - ECS Service Connect (#745)
1 parent ca6923c commit 3313039

16 files changed

Lines changed: 520 additions & 97 deletions

File tree

docs/syntax/compose_x/ecs.details/network.rst

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ services.x-network
1414
services:
1515
serviceA:
1616
x-network:
17+
x-ecs_connect: {}
1718
AssignPublicIp: bool
1819
Ingress: {}
1920
x-cloudmap: {}
@@ -41,6 +42,103 @@ This flag allows to assign an Elastic IP to the container when using ``awsvpc``
4142
To select which subnets to place the services, see :ref:`compose_networks_syntax_reference`
4243

4344

45+
x-ecs_connect (1.1.0)
46+
======================
47+
48+
This configuration section allows you to define ECS Service Connect configuration.
49+
It's made up of two options, `Properties` and `MacroParameters`
50+
51+
`Properties` must match exactly the `ECS Service Connect properties`_ and must be all valid to work.
52+
53+
.. attention::
54+
55+
No changes to input or validation will be made when set. Be sure to have everything valid.
56+
57+
`MacroParameters` however, is an attempt at creating a shorthand syntax to this.
58+
59+
service connect - client only
60+
------------------------------
61+
62+
You might have applications that you want to act only as clients to other services. This will only tell ECS to make sure
63+
to provision the Service Connect sidecar which will be there to handle the proxy-ing to server services.
64+
65+
To enable the client config, you simply need to enable the feature as show below
66+
67+
.. code-block::
68+
69+
x-cloudmap:
70+
PrivateNamespace:
71+
Name: compose-x.internal
72+
73+
services:
74+
yelb-ui:
75+
x-network:
76+
AssignPublicIp: true
77+
x-ecs_connect:
78+
MacroParameters:
79+
x-cloudmap: PrivateNamespace
80+
Ingress:
81+
ExtSources:
82+
- IPv4: 0.0.0.0/0
83+
Name: ANY
84+
85+
service connect - server
86+
----------------------------
87+
88+
For services that you want to act as client & server, you need to declare which ports you want to declare to Service Connect.
89+
That's mandatory.
90+
91+
For example, we have the following two services: appserver will act as both a client and a server. It will serve requests
92+
for our yelb-ui service (the client above), and a client to the redis-server
93+
94+
.. code-block::
95+
96+
x-cloudmap:
97+
PrivateNamespace:
98+
Name: compose-x.internal
99+
100+
services:
101+
yelb-appserver:
102+
image: mreferre/yelb-appserver:0.7
103+
depends_on:
104+
- redis-server
105+
ports:
106+
- 4567:4567
107+
environment:
108+
redishost: redis-server
109+
x-network:
110+
Ingress:
111+
Services:
112+
- Name: yelb-ui
113+
x-ecs_connect:
114+
MacroParameters:
115+
service_ports:
116+
tcp_4567:
117+
DnsName: yelb-appserver
118+
CloudMapServiceName: yelb-appserver
119+
x-cloudmap: PrivateNamespace
120+
121+
122+
redis-server:
123+
image: redis:4.0.2
124+
ports:
125+
- 6379:6379
126+
x-network:
127+
x-ecs_connect:
128+
MacroParameters:
129+
service_ports:
130+
tcp_6379:
131+
DnsName: redis-server
132+
CloudMapServiceName: redis-server
133+
x-cloudmap: PrivateNamespace
134+
Ingress:
135+
Services:
136+
- Name: yelb-appserver
137+
138+
.. hint::
139+
140+
See `the full connect example`_ uses to perform functional testing of the feature.
141+
44142
Ingress
45143
======================
46144

@@ -148,3 +246,6 @@ Definition
148246
-----------
149247

150248
.. literalinclude:: ../../../../ecs_composex/specs/services.x-network.spec.json
249+
250+
.. _ECS Service Connect properties: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-serviceconnectconfiguration.html
251+
.. _the full connect example: https://github.com/compose-x/ecs_composex/tree/main/use-cases/yelb.yaml

ecs_composex/cloudmap/cloudmap_params.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@
4848
AllowedPattern=ZONES_PATTERN.pattern,
4949
)
5050

51+
PRIVATE_NAMESPACE_ARN_T: str = "PrivateNamespaceArn"
52+
PRIVATE_NAMESPACE_ARN: Parameter = Parameter(
53+
PRIVATE_NAMESPACE_ARN_T,
54+
group_label=LABEL,
55+
return_value="Arn",
56+
Type="String",
57+
)
58+
59+
5160
ECS_SERVICE_NAMESPACE_SERVICE_ID_T = "EcsCloudMapServiceName"
5261
ECS_SERVICE_NAMESPACE_SERVICE_ID = Parameter(
5362
ECS_SERVICE_NAMESPACE_SERVICE_ID_T,

ecs_composex/cloudmap/cloudmap_stack.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
MOD_KEY,
4444
PRIVATE_DNS_ZONE_ID,
4545
PRIVATE_DNS_ZONE_NAME,
46+
PRIVATE_NAMESPACE_ARN,
4647
PRIVATE_NAMESPACE_ID,
4748
)
4849
from .cloudmap_x_resources import handle_resource_cloudmap_settings
@@ -99,6 +100,12 @@ def init_outputs(self):
99100
self.zone_name,
100101
False,
101102
),
103+
PRIVATE_NAMESPACE_ARN: (
104+
f"{self.logical_name}{PRIVATE_NAMESPACE_ARN.return_value}",
105+
self.cfn_resource,
106+
GetAtt,
107+
PRIVATE_NAMESPACE_ARN.return_value,
108+
),
102109
}
103110

104111
@property

ecs_composex/common/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ def get_resource_attribute(self, compose_resource_arn: str) -> tuple:
281281
]
282282
return resource, parameter
283283
except LookupError:
284+
print(f"Not found {compose_resource_arn}")
284285
return None, None
285286

286287
@property

ecs_composex/compose/compose_services/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def __init__(
105105
self.x_scaling = set_else_none("x-scaling", self.definition, None, False)
106106
self.x_network = set_else_none("x-network", self.definition, None, False)
107107
self.x_cloudmap = set_else_none("x-cloudmap", self.x_network, None, False)
108+
self.x_ecs_connect = set_else_none("x-ecs_connect", self.x_network, None)
108109
self.x_ecs = set_else_none("x-ecs", self.definition, {})
109110
self.ecr_config = set_else_none("x-ecr", self.definition, None)
110111
self.x_ecr = set_else_none("x-ecr", self.definition, {})

ecs_composex/ecs/ecs_family/__init__.py

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
if TYPE_CHECKING:
1313
from troposphere.ecs import Service as CfnService
1414
from ecs_composex.common.settings import ComposeXSettings
15+
from ecs_composex.ecs.ecs_service import EcsService
1516

1617
import re
1718
from itertools import chain
@@ -72,8 +73,8 @@ class ComposeFamily:
7273
"""
7374

7475
def __init__(self, services: list[ComposeService], family_name):
75-
self._compose_services = services
76-
self.ordered_services = services
76+
self._compose_services: list[ComposeService] = services
77+
self.ordered_services: list[ComposeService] = services
7778
self.managed_sidecars = []
7879
self.name = family_name
7980
self.family_hostname = self.name.replace("_", "-").lower()
@@ -92,7 +93,7 @@ def __init__(self, services: list[ComposeService], family_name):
9293
self.task_definition = None
9394
self.service_tags = None
9495
self.enable_execute_command = False
95-
self.ecs_service = None
96+
self.ecs_service: EcsService | None = None
9697
self.runtime_cpu_arch = None
9798
self.runtime_os_family = None
9899
self.outputs = []
@@ -103,9 +104,9 @@ def __init__(self, services: list[ComposeService], family_name):
103104
self.iam_manager = TaskIam(self)
104105
self.iam_manager.init_update_policies()
105106
self.service_scaling = None
106-
self.service_networking = None
107+
self.service_networking: ServiceNetworking | None = None
107108
self.task_compute = None
108-
self.service_compute = ServiceCompute(self)
109+
self.service_compute: ServiceCompute = ServiceCompute(self)
109110
self.set_enable_execute_command()
110111
set_family_hostname(self)
111112

@@ -447,41 +448,29 @@ def init_network_settings(
447448
self.service_networking.ingress.associate_ext_ingress_rules(self.template)
448449
self.service_networking.add_self_ingress()
449450

450-
def finalize_family_settings(self):
451+
def finalize_family_settings(self, settings: ComposeXSettings):
451452
"""
452453
Once all services have been added, we add the sidecars and deal with appropriate permissions and settings
453454
Will add xray / prometheus sidecars
454455
"""
455-
from .family_helpers import set_service_dependency_on_all_iam_policies
456+
from ecs_composex.ecs.ecs_family.family_helpers import (
457+
set_service_dependency_on_all_iam_policies,
458+
)
459+
from ecs_composex.ecs.ecs_family.family_helpers.compute_finalizers import (
460+
finalize_family_compute,
461+
finalize_scaling_settings,
462+
)
463+
from ecs_composex.ecs.ecs_family.family_helpers.network_finalizers import (
464+
finalize_lb_settings,
465+
finalize_network_settings,
466+
)
456467

457-
self.add_containers_images_cfn_parameters()
458-
self.task_compute.set_task_compute_parameter()
459-
self.task_compute.unlock_compute_for_main_container()
460-
if self.service_compute.ecs_capacity_providers:
461-
self.service_compute.apply_capacity_providers_to_service(
462-
self.service_compute.ecs_capacity_providers
463-
)
468+
finalize_network_settings(self, settings)
469+
finalize_family_compute(self)
464470

465471
set_service_dependency_on_all_iam_policies(self)
466-
if self.service_compute.launch_type == "EXTERNAL":
467-
if hasattr(self.service_definition, "LoadBalancers"):
468-
setattr(self.service_definition, "LoadBalancers", NoValue)
469-
if hasattr(self.service_definition, "ServiceRegistries"):
470-
setattr(self.service_definition, "ServiceRegistries", NoValue)
471-
for container in self.task_definition.ContainerDefinitions:
472-
if hasattr(container, "LinuxParameters"):
473-
parameters = getattr(container, "LinuxParameters")
474-
setattr(parameters, "InitProcessEnabled", False)
475-
if (
476-
self.service_definition
477-
and self.service_definition.title in self.template.resources
478-
) and (
479-
self.service_scaling
480-
and self.service_scaling.scalable_target
481-
and self.service_scaling.scalable_target.title
482-
not in self.template.resources
483-
):
484-
self.template.add_resource(self.service_scaling.scalable_target)
472+
finalize_lb_settings(self)
473+
finalize_scaling_settings(self)
485474
self.generate_outputs()
486475
service_configs = [
487476
[0, service]
File renamed without changes.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Copyright 2024 John Mille <john@compose-x.io>
3+
4+
"""Functions to finalize the family compute & scaling settings"""
5+
6+
from __future__ import annotations
7+
8+
from typing import TYPE_CHECKING
9+
10+
if TYPE_CHECKING:
11+
from ecs_composex.ecs.ecs_family import ComposeFamily
12+
13+
14+
def finalize_family_compute(family: ComposeFamily) -> None:
15+
"""Finalizes the family compute settings"""
16+
family.add_containers_images_cfn_parameters()
17+
family.task_compute.set_task_compute_parameter()
18+
family.task_compute.unlock_compute_for_main_container()
19+
if family.service_compute.ecs_capacity_providers:
20+
family.service_compute.apply_capacity_providers_to_service(
21+
family.service_compute.ecs_capacity_providers
22+
)
23+
24+
25+
def finalize_scaling_settings(family: ComposeFamily) -> None:
26+
"""If family has scaling target configured, ensures that the scalable target gets created."""
27+
if (
28+
family.service_definition
29+
and family.service_definition.title in family.template.resources
30+
) and (
31+
family.service_scaling
32+
and family.service_scaling.scalable_target
33+
and family.service_scaling.scalable_target.title
34+
not in family.template.resources
35+
):
36+
family.template.add_resource(family.service_scaling.scalable_target)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Copyright 2024 John Mille <john@compose-x.io>
3+
4+
"""Functions to finalize the family networking settings"""
5+
6+
from __future__ import annotations
7+
8+
from typing import TYPE_CHECKING
9+
10+
if TYPE_CHECKING:
11+
from ecs_composex.common.settings import ComposeXSettings
12+
from ecs_composex.ecs.ecs_family import ComposeFamily
13+
14+
from troposphere import NoValue
15+
16+
17+
def finalize_network_settings(
18+
family: ComposeFamily, settings: ComposeXSettings
19+
) -> None:
20+
"""
21+
Evaluates the ECS Connect settings to be configured by the service.
22+
If there is a configuration to be set, ensures it's set on the ECS Service definition.
23+
"""
24+
family.service_networking.set_ecs_connect(settings)
25+
if family.service_networking.ecs_connect_config and family.ecs_service:
26+
setattr(
27+
family.ecs_service.ecs_service,
28+
"ServiceConnectConfiguration",
29+
family.service_networking.ecs_connect_config,
30+
)
31+
32+
33+
def finalize_lb_settings(family: ComposeFamily) -> None:
34+
"""
35+
Ensures that the LoadBalancers & ServiceRegistries (LB & CloudMap) are set appropriately based on
36+
the deployment settings. Especially, resets properties if the service is deployed to ECS Anywhere.
37+
Ensures correctness of LinuxParameters for each of the services.
38+
"""
39+
if family.service_compute.launch_type == "EXTERNAL":
40+
if hasattr(family.service_definition, "LoadBalancers"):
41+
setattr(family.service_definition, "LoadBalancers", NoValue)
42+
if hasattr(family.service_definition, "ServiceRegistries"):
43+
setattr(family.service_definition, "ServiceRegistries", NoValue)
44+
for container in family.task_definition.ContainerDefinitions:
45+
if hasattr(container, "LinuxParameters"):
46+
parameters = getattr(container, "LinuxParameters")
47+
setattr(parameters, "InitProcessEnabled", False)

0 commit comments

Comments
 (0)