Skip to content

Commit ca6923c

Browse files
authored
Security groups at root-stack level specifically for service-to-service (#747)
1 parent fee1861 commit ca6923c

8 files changed

Lines changed: 219 additions & 127 deletions

File tree

ecs_composex/ecs/ecs_family/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,14 +417,14 @@ def finalize_services_networking_settings(self, settings: ComposeXSettings) -> N
417417
)
418418

419419
def init_network_settings(
420-
self, settings: ComposeXSettings, vpc_stack: ComposeXStack
420+
self, settings: ComposeXSettings, vpc_stack: ComposeXStack, families_sg_stack
421421
) -> None:
422422
"""
423423
Once we have figured out the compute settings (EXTERNAL vs other)
424424
"""
425425
from ecs_composex.ecs.service_networking.helpers import add_security_group
426426

427-
self.service_networking = ServiceNetworking(self)
427+
self.service_networking = ServiceNetworking(self, families_sg_stack)
428428
self.finalize_services_networking_settings(settings)
429429
if self.service_compute.launch_type == "EXTERNAL":
430430
LOG.debug(f"{self.name} Ingress cannot be set (EXTERNAL mode). Skipping")

ecs_composex/ecs/ecs_stack.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
if TYPE_CHECKING:
99
from ecs_composex.ecs.ecs_family import ComposeFamily
1010
from ecs_composex.common.settings import ComposeXSettings
11+
from ecs_composex.ecs_ingress.ecs_ingress_stack import XStack as EcsIngressStack
1112

12-
from troposphere import FindInMap, Ref
13+
from troposphere import FindInMap, GetAtt, Ref
1314

1415
from ecs_composex.common.cfn_params import ROOT_STACK_NAME, ROOT_STACK_NAME_T
1516
from ecs_composex.common.logging import LOG
@@ -88,12 +89,12 @@ def handle_families_dependencies(
8889
)
8990

9091

91-
def add_compose_families(settings: ComposeXSettings) -> None:
92+
def add_compose_families(
93+
settings: ComposeXSettings, families_sg_stack: EcsIngressStack
94+
) -> None:
9295
"""
9396
Using existing ComposeFamily in settings, creates the ServiceStack
9497
and template
95-
96-
:param ecs_composex.common.settings.ComposeXSettings settings:
9798
"""
9899
for family_name, family in settings.families.items():
99100
family.init_family()
@@ -105,6 +106,7 @@ def add_compose_families(settings: ComposeXSettings) -> None:
105106
family.iam_manager.task_role.name_param,
106107
family.iam_manager.exec_role.arn_param,
107108
family.iam_manager.exec_role.name_param,
109+
families_sg_stack.services_mappings[family.name].parameter,
108110
],
109111
)
110112
family.stack.Parameters.update(
@@ -118,6 +120,12 @@ def add_compose_families(settings: ComposeXSettings) -> None:
118120
family.iam_manager.exec_role.arn_param.title: family.iam_manager.exec_role.output_arn,
119121
family.iam_manager.exec_role.name_param.title: family.iam_manager.exec_role.output_name,
120122
ecs_params.SERVICE_HOSTNAME.title: family.family_hostname,
123+
families_sg_stack.services_mappings[
124+
family.name
125+
].parameter.title: GetAtt(
126+
families_sg_stack.services_mappings[family.name].stack.title,
127+
f"Outputs.{families_sg_stack.services_mappings[family.name].parameter.title}",
128+
),
121129
}
122130
)
123131
family.template.metadata.update(metadata)

ecs_composex/ecs/helpers/__init__.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
if TYPE_CHECKING:
99
from ecs_composex.common.settings import ComposeXSettings
1010
from ecs_composex.ecs.ecs_family import ComposeFamily
11+
from ecs_composex.ecs_ingress.ecs_ingress_stack import XStack as EcsIngressStack
1112

1213
from ecs_composex.common.stacks import ComposeXStack
1314

@@ -24,24 +25,23 @@ def add_iam_dependency(iam_stack: ComposeXStack, family: ComposeFamily):
2425

2526

2627
def handle_families_cross_dependencies(
27-
settings: ComposeXSettings, root_stack: ComposeXStack
28+
settings: ComposeXSettings,
29+
families_sg_stack: EcsIngressStack,
2830
):
2931
from ecs_composex.ecs.ecs_family import ServiceStack
3032
from ecs_composex.ecs.service_networking.ingress_helpers import (
3133
set_compose_services_ingress,
3234
)
3335

34-
families_stacks = [
35-
family
36-
for family in root_stack.stack_template.resources
37-
if (
38-
family in settings.families
39-
and isinstance(settings.families[family].stack, ServiceStack)
40-
)
41-
]
42-
for family in families_stacks:
36+
eval_families: list[ComposeFamily] = []
37+
for _family in settings.families.values():
38+
if isinstance(_family.stack, ServiceStack):
39+
eval_families.append(_family)
40+
for _dst_family in eval_families:
4341
set_compose_services_ingress(
44-
root_stack, settings.families[family], families_stacks, settings
42+
_dst_family,
43+
families_sg_stack,
44+
settings,
4545
)
4646

4747

ecs_composex/ecs/service_networking/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
if TYPE_CHECKING:
1313
from ecs_composex.ecs.ecs_family import ComposeFamily
1414
from ecs_composex.cloudmap.cloudmap_ecs import EcsDiscoveryService
15+
from ecs_composex.ecs_ingress.ecs_ingress_stack import (
16+
XStack as EcsIngressStack,
17+
ServiceSecurityGroup,
18+
)
1519

1620
from itertools import chain
1721

@@ -51,7 +55,7 @@ class ServiceNetworking:
5155

5256
self_key = "Myself"
5357

54-
def __init__(self, family: ComposeFamily):
58+
def __init__(self, family: ComposeFamily, families_sg_stack: EcsIngressStack):
5559
"""
5660
Initialize network settings for the family ServiceConfig
5761
@@ -71,7 +75,10 @@ def __init__(self, family: ComposeFamily):
7175
self.merge_networks()
7276
self.definition = merge_family_services_networking(family)
7377
self._security_group = None
74-
self.extra_security_groups = []
78+
self.inter_services_sg: ServiceSecurityGroup = (
79+
families_sg_stack.services_mappings[family.name]
80+
)
81+
self.extra_security_groups = [self.inter_services_sg.parameter]
7582
self._subnets = Ref(APP_SUBNETS)
7683
self.cloudmap_config = (
7784
merge_cloudmap_settings(family, self.ports) if self.ports else {}

ecs_composex/ecs/service_networking/ingress_helpers.py

Lines changed: 69 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
if TYPE_CHECKING:
1111
from ecs_composex.ecs.ecs_family import ComposeFamily
12+
from ecs_composex.compose.compose_services import ComposeService
1213
from ecs_composex.common.settings import ComposeXSettings
1314
from ecs_composex.common.stacks import ComposeXStack
15+
from ecs_composex.ecs_ingress.ecs_ingress_stack import XStack as EcsIngressStack
1416

1517
from json import dumps
1618

@@ -21,7 +23,7 @@
2123
from ecs_composex.cloudmap.cloudmap_params import RES_KEY as CLOUDMAP_KEY
2224
from ecs_composex.common.cfn_params import Parameter
2325
from ecs_composex.common.logging import LOG
24-
from ecs_composex.common.troposphere_tools import add_parameters
26+
from ecs_composex.common.troposphere_tools import add_parameters, add_resource
2527
from ecs_composex.ecs.ecs_params import SERVICE_NAME
2628
from ecs_composex.ingress_settings import Ingress
2729
from ecs_composex.vpc.vpc_params import SG_ID_TYPE
@@ -142,120 +144,82 @@ def merge_family_services_networking(family: ComposeFamily) -> dict:
142144
return network_config
143145

144146

145-
def add_independent_rules(
146-
dst_family: ComposeFamily, service_name: str, root_stack: ComposeXStack
147-
) -> None:
148-
"""
149-
Adds security groups rules in the root stack as both services need to be created (with their SG)
150-
before the ingress rule can be defined.
151-
152-
:param dst_family:
153-
:param service_name:
154-
:param root_stack:
155-
:return:
156-
"""
157-
src_service_stack = root_stack.stack_template.resources[service_name]
158-
for port in dst_family.service_networking.ports:
159-
target_port = set_else_none(
160-
"published", port, alt_value=set_else_none("target", port, None)
161-
)
162-
if target_port is None:
163-
raise ValueError(
164-
"Wrong port definition value for security group ingress", port
165-
)
166-
ingress_rule = SecurityGroupIngress(
167-
f"From{src_service_stack.title}To{dst_family.logical_name}On{target_port}",
168-
FromPort=target_port,
169-
ToPort=target_port,
170-
IpProtocol=port["protocol"],
171-
Description=Sub(
172-
f"From {src_service_stack.title} to {dst_family.logical_name}"
173-
f" on port {target_port}/{port['protocol']}"
174-
),
175-
GroupId=GetAtt(
176-
dst_family.stack.title,
177-
f"Outputs.{dst_family.logical_name}GroupId",
178-
),
179-
SourceSecurityGroupId=GetAtt(
180-
src_service_stack.title,
181-
f"Outputs.{src_service_stack.title}GroupId",
182-
),
183-
SourceSecurityGroupOwnerId=Ref(AWS_ACCOUNT_ID),
184-
)
185-
if ingress_rule.title not in root_stack.stack_template.resources:
186-
root_stack.stack_template.add_resource(ingress_rule)
187-
188-
189-
def add_dependant_ingress_rules(
190-
dst_family: ComposeFamily, dst_family_sg_param: Parameter, src_family: ComposeFamily
191-
) -> None:
192-
for port in dst_family.service_networking.ports:
193-
target_port = set_else_none(
194-
"published", port, alt_value=set_else_none("target", port, None)
195-
)
196-
if target_port is None:
197-
raise ValueError(
198-
"Wrong port definition value for security group ingress", port
199-
)
200-
common_args = {
201-
"FromPort": target_port,
202-
"ToPort": target_port,
203-
"IpProtocol": port["protocol"],
204-
"SourceSecurityGroupOwnerId": Ref(AWS_ACCOUNT_ID),
205-
"Description": Sub(
206-
f"From ${{{SERVICE_NAME.title}}} to {dst_family.stack.title} on port {target_port}"
207-
),
208-
}
209-
src_family.template.add_resource(
210-
SecurityGroupIngress(
211-
f"From{src_family.logical_name}To{dst_family.stack.title}On{target_port}",
212-
SourceSecurityGroupId=GetAtt(
213-
src_family.service_networking.security_group, "GroupId"
214-
),
215-
GroupId=Ref(dst_family_sg_param),
216-
**common_args,
217-
)
218-
)
219-
220-
221147
def set_compose_services_ingress(
222-
root_stack, dst_family: ComposeFamily, families: list, settings: ComposeXSettings
148+
dst_family: ComposeFamily,
149+
families_sg_stack: EcsIngressStack,
150+
settings: ComposeXSettings,
223151
) -> None:
224152
"""
225153
Function to crate SG Ingress between two families / services.
226154
Presently, the ingress rules are set after all services have been created
227-
228-
:param ecs_composex.common.stacks.ComposeXStack root_stack:
229-
:param ecs_composex.ecs.ecs_family.ComposeFamily dst_family:
230-
:param list families: The list of family names.
231-
:param ecs_composex.common.settings.ComposeXSettings settings:
232155
"""
233-
for service in dst_family.service_networking.ingress.services:
234-
service_name = service["Name"]
235-
if service_name not in families:
236-
raise KeyError(
237-
f"The service {service_name} is not among the services created together. Valid services are",
238-
families,
156+
target_family_services: list[ComposeService] = []
157+
for _target_service_def in dst_family.service_networking.ingress.services:
158+
service_name = _target_service_def["Name"]
159+
for _service in settings.services:
160+
if service_name != _service.name:
161+
continue
162+
if _service.family == dst_family:
163+
continue
164+
target_family_services.append(_service)
165+
add_service_to_service_ingress_rules(
166+
dst_family, target_family_services, families_sg_stack
167+
)
168+
169+
170+
def add_service_to_service_ingress_rules(
171+
dst_family: ComposeFamily,
172+
target_family_services: list[ComposeService],
173+
families_sg_stack: EcsIngressStack,
174+
):
175+
"""
176+
For each identified service that wants to access the `dst_family` services
177+
For each port of the `dst_family`
178+
Create an SG Ingress rule that allows service-to-service communication
179+
"""
180+
for _service in target_family_services:
181+
if families_sg_stack.title not in _service.family.stack.DependsOn:
182+
_service.family.stack.DependsOn.append(families_sg_stack.title)
183+
for _service_port_def in dst_family.service_networking.ports:
184+
target_port = set_else_none(
185+
"target",
186+
_service_port_def,
187+
set_else_none("published", _service_port_def, None),
239188
)
240-
if not keypresent("DependsOn", service):
241-
add_independent_rules(dst_family, service_name, root_stack)
242-
else:
243-
src_family = settings.families[service_name]
244-
if dst_family.stack.title not in src_family.stack.DependsOn:
245-
src_family.stack.DependsOn.append(dst_family.stack.title)
246-
dst_family_sg_param = Parameter(
247-
f"{dst_family.stack.title}GroupId", Type=SG_ID_TYPE
189+
if target_port is None:
190+
raise ValueError(
191+
"Wrong port definition value for security group ingress",
192+
_service_port_def,
193+
)
194+
common_args = {
195+
"FromPort": target_port,
196+
"ToPort": target_port,
197+
"IpProtocol": _service_port_def["protocol"],
198+
"SourceSecurityGroupOwnerId": Ref(AWS_ACCOUNT_ID),
199+
"Description": Sub(
200+
f"From ${_service.family.name} to {dst_family.name} "
201+
f"on port {target_port}/{_service_port_def['protocol']}"
202+
),
203+
}
204+
ingress_title: str = (
205+
f"From{_service.family.logical_name}To{dst_family.logical_name}"
206+
f"On{target_port}{_service_port_def['protocol'].title()}"
248207
)
249-
add_parameters(src_family.template, [dst_family_sg_param])
250-
src_family.stack.Parameters.update(
251-
{
252-
dst_family_sg_param.title: GetAtt(
253-
dst_family.stack.title,
254-
f"Outputs.{dst_family.logical_name}GroupId",
208+
add_resource(
209+
families_sg_stack.stack_template,
210+
SecurityGroupIngress(
211+
ingress_title,
212+
SourceSecurityGroupId=GetAtt(
213+
_service.family.service_networking.inter_services_sg.cfn_resource,
214+
"GroupId",
255215
),
256-
}
216+
GroupId=GetAtt(
217+
dst_family.service_networking.inter_services_sg.cfn_resource,
218+
"GroupId",
219+
),
220+
**common_args,
221+
),
257222
)
258-
add_dependant_ingress_rules(dst_family, dst_family_sg_param, src_family)
259223

260224

261225
def handle_str_cloudmap_config(

ecs_composex/ecs_composex.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
)
4242
from ecs_composex.ecs_cluster import add_ecs_cluster
4343
from ecs_composex.ecs_cluster.helpers import set_ecs_cluster_identifier
44+
from ecs_composex.ecs_ingress.ecs_ingress_stack import XStack as ServicesIngressStack
4445
from ecs_composex.iam.iam_stack import XStack as IamStack
4546
from ecs_composex.mods_manager import ModManager
4647
from ecs_composex.resource_settings import map_resource_return_value_to_services_command
@@ -248,8 +249,13 @@ def generate_full_template(settings: ComposeXSettings):
248249
iam_stack = add_resource(
249250
settings.root_stack.stack_template, IamStack("iam", settings)
250251
)
252+
families_sg_stack = add_resource(
253+
settings.root_stack.stack_template,
254+
ServicesIngressStack("ServicesNetworking", settings),
255+
)
256+
251257
add_x_resources(settings)
252-
add_compose_families(settings)
258+
add_compose_families(settings, families_sg_stack)
253259
if "x-vpc" not in settings.mod_manager.modules:
254260
vpc_module = settings.mod_manager.load_module("x-vpc", {})
255261
else:
@@ -264,9 +270,9 @@ def generate_full_template(settings: ComposeXSettings):
264270
x_cloud_lookup_and_new_vpc(settings, vpc_stack)
265271

266272
for family in settings.families.values():
267-
family.init_network_settings(settings, vpc_stack)
273+
family.init_network_settings(settings, vpc_stack, families_sg_stack)
268274

269-
handle_families_cross_dependencies(settings, settings.root_stack)
275+
handle_families_cross_dependencies(settings, families_sg_stack)
270276
update_network_resources_vpc_config(settings, vpc_stack)
271277
set_families_ecs_service(settings)
272278

@@ -304,6 +310,7 @@ def generate_full_template(settings: ComposeXSettings):
304310
set_ecs_cluster_identifier(settings.root_stack, settings)
305311
add_all_tags(settings.root_stack.stack_template, settings)
306312
set_all_mappings_to_root_stack(settings)
313+
families_sg_stack.update_vpc_settings(vpc_stack)
307314

308315
for resource in settings.x_resources:
309316
if hasattr(resource, "post_processing") and hasattr(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Copyright 2020-2022 John Mille <john@compose-x.io>
3+
4+
"""
5+
Root stack to store and manage the security groups of the services
6+
Having the security groups created before the services stacks allows to define service to service communication
7+
to be defined before the services are deployed.
8+
"""

0 commit comments

Comments
 (0)