Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add Support for Multiple Hostnames #159

Merged
merged 1 commit into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions cloudlift/config/service_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,18 +183,23 @@ def _validate_changes(self, configuration):
"type": "string",
"pattern": "^(cluster|dedicated)$"
},
"hostname": {
"type": "string",
# Regex for FQDN: https://stackoverflow.com/a/20204811/9716730
"pattern": "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)"
"hostnames": {
"type": "array",
"items": {
"type": "string",
# Regex for FQDN: https://stackoverflow.com/a/20204811/9716730
"pattern": "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)"
},
"minItems": 1,
"maxItems": 5
}
},
"required": [
"internal",
"restrict_access_to",
"container_port",
"alb_mode",
"hostname"
"hostnames"
]
},
"custom_metrics": {
Expand Down Expand Up @@ -332,8 +337,16 @@ def _validate_changes(self, configuration):
except ValidationError as validation_error:
log_err("Schema validation failed!")
if validation_error.relative_path:
raise UnrecoverableException(validation_error.message + " in " +
str(".".join(list(validation_error.relative_path))))
path = []
for item in validation_error.relative_path:
if isinstance(item, str):
path.append(item)
elif isinstance(item, int):
path.append(str(item))
else:
path.append(str(item))
path_string = ".".join(path)
raise UnrecoverableException(f"{validation_error.message} in {path_string}")
else:
raise UnrecoverableException(validation_error.message)
log_bold("Schema valid!")
Expand All @@ -350,7 +363,7 @@ def _default_service_configuration(self):
'container_port': 80,
'health_check_path': '/elb-check',
'alb_mode': default_alb_mode,
'hostname': '',
'hostnames': [''],
},
'memory_reservation': 250,
'command': None,
Expand Down
55 changes: 31 additions & 24 deletions cloudlift/deployment/service_template_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ def _add_service(self, service_name, config):
# if 'alb_mode' not in http_interface, then fallback to environment default
alb_mode = http_interface.get('alb_mode', environment_default_alb_mode)

alb, lb, service_listener, alb_sg = self._add_alb(cd, service_name, config, launch_type, alb_mode)
alb, lb, service_listener, alb_sg, cluster_alb_listener_rules = self._add_alb(cd, service_name, config, launch_type, alb_mode)

if launch_type == self.LAUNCH_TYPE_FARGATE:
# if launch type is ec2, then services inherit the ec2 instance security group
Expand Down Expand Up @@ -454,11 +454,17 @@ def _add_service(self, service_name, config):
'Role': Ref(self.ecs_service_role),
'PlacementStrategies': self.PLACEMENT_STRATEGIES
}
service_depends_on = {}
service_dependencies = []

if service_listener:
service_depends_on = {
'DependsOn': service_listener.title
}
service_dependencies.append(
service_listener.title
)
if cluster_alb_listener_rules:
service_dependencies.extend(
[rule.title for rule in cluster_alb_listener_rules]
)

svc = Service(
service_name,
LoadBalancers=[lb],
Expand All @@ -469,7 +475,7 @@ def _add_service(self, service_name, config):
**launch_type_svc,
Tags=Tags(Team=self.team_name, environment=self.env),
**placement_constraint,
**service_depends_on
DependsOn=service_dependencies
)
self.template.add_output(
Output(
Expand Down Expand Up @@ -615,6 +621,7 @@ def _gen_log_config(self, service_name, config):
)

def _add_alb(self, cd, service_name, config, launch_type, alb_mode):
cluster_alb_listener_rules = None # default value
target_group_name = "TargetGroup" + service_name
if alb_mode == 'cluster':
# suffix 'Cluster' denotes that the target group is for a cluster ALB
Expand Down Expand Up @@ -652,7 +659,9 @@ def _add_alb(self, cd, service_name, config, launch_type, alb_mode):
{"Key": "Team", "Value": self.team_name},
{'Key': 'environment', 'Value': self.env},
{'Key': 'alb_mode', 'Value': alb_mode},
{'Key': 'alb_scheme', 'Value': alb_scheme}
{'Key': 'alb_scheme', 'Value': alb_scheme},
{'Key': 'cloudlift_app', 'Value': self.application_name},
{'Key': 'service_name', 'Value': service_name}
]
)

Expand Down Expand Up @@ -748,12 +757,12 @@ def _add_alb(self, cd, service_name, config, launch_type, alb_mode):
self._add_alb_alarms(service_name, alb)
else:
is_alb_internal = config.get('http_interface', {}).get('internal', True)
self._add_listener_rules_to_cluster_alb(config, service_name, service_target_group, is_alb_internal)
cluster_alb_listener_rules = self._add_listener_rules_to_cluster_alb(config, service_name, service_target_group, is_alb_internal)
service_listener = None
alb = None
svc_alb_sg = self._fetch_cluster_alb_sg_id(is_alb_internal)

return alb, lb, service_listener, svc_alb_sg
return alb, lb, service_listener, svc_alb_sg, cluster_alb_listener_rules

def _fetch_cluster_alb_sg_id(self, is_alb_internal):
sg_outputs = list(
Expand All @@ -771,9 +780,9 @@ def _fetch_cluster_alb_sg_id(self, is_alb_internal):
elif not is_alb_internal and sg_output["OutputKey"].startswith("SGPublic"):
return sg_output["OutputValue"]

def _add_listener_rules_to_cluster_alb(self, config, service_name, target_group, is_alb_internal):
def _add_listener_rules_to_cluster_alb(self, config, service_name, target_group, is_alb_internal) -> list[ListenerRule]:
# get the http_interface scheme
hostname = config.get('http_interface', {}).get('hostname')
hostnames = config.get('http_interface', {}).get('hostnames')
internal_listeners, public_listeners = self._fetch_alb_listeners_arns()

if is_alb_internal and not internal_listeners:
Expand All @@ -784,9 +793,8 @@ def _add_listener_rules_to_cluster_alb(self, config, service_name, target_group,
target_group_arn = Ref(target_group)
# create listener rules
if is_alb_internal:
self._create_listener_rules(service_name, is_alb_internal, hostname, internal_listeners, target_group_arn)
else:
self._create_listener_rules(service_name, is_alb_internal, hostname, public_listeners, target_group_arn)
return self._create_listener_rules(service_name, is_alb_internal, hostnames, internal_listeners, target_group_arn)
return self._create_listener_rules(service_name, is_alb_internal, hostnames, public_listeners, target_group_arn)

def _fetch_alb_listeners_arns(self) -> list[dict[str, str]]:
internal_listeners = []
Expand All @@ -812,8 +820,8 @@ def _fetch_alb_listeners_arns(self) -> list[dict[str, str]]:


def _create_listener_rules(
self, service_name: str, is_internal: bool, hostname: str, alb_listeners: list[dict[str, str]], target_group_arn: str
):
self, service_name: str, is_internal: bool, hostnames: list[str], alb_listeners: list[dict[str, str]], target_group_arn: str
) -> list[ListenerRule]:
if not alb_listeners:
raise UnrecoverableException("No listener found on the cluster ALB")

Expand All @@ -837,16 +845,16 @@ def _create_listener_rules(
for index, http_listener_arn in enumerate(http_listener_arns):
index = index + 1

priority = self._get_listener_rule_priority(http_listener_arn, hostname)
http_listener_rule = self._get_listener_rule("HTTP", index, is_internal, http_listener_arn, priority, hostname, service_name, target_group_arn)
priority = self._get_listener_rule_priority(http_listener_arn, hostnames[0])
http_listener_rule = self._get_listener_rule("HTTP", index, is_internal, http_listener_arn, priority, hostnames, service_name, target_group_arn)

http_listener_rules.append(http_listener_rule)

for index, https_listener_arn in enumerate(https_listener_arns):
index = index + 1

priority = self._get_listener_rule_priority(https_listener_arn, hostname)
https_listener_rule = self._get_listener_rule("HTTPS", index, is_internal, https_listener_arn, priority, hostname, service_name, target_group_arn)
priority = self._get_listener_rule_priority(https_listener_arn, hostnames[0])
https_listener_rule = self._get_listener_rule("HTTPS", index, is_internal, https_listener_arn, priority, hostnames, service_name, target_group_arn)

https_listener_rules.append(https_listener_rule)

Expand All @@ -858,6 +866,7 @@ def _create_listener_rules(
else:
for rule in https_listener_rules:
self.template.add_resource(rule)
return https_listener_rules + http_listener_rules

def _get_listener_rule_priority(self, listener_arn: str, hostname: str) -> int:
MIN_PRIORITY = 100
Expand Down Expand Up @@ -898,7 +907,7 @@ def _get_listener_rule(
is_internal: bool,
listener_arn: str,
priority: int,
hostname: str,
hostnames: list[str],
service_name: str,
target_group_arn: str
) -> ListenerRule:
Expand All @@ -909,9 +918,7 @@ def _get_listener_rule(
condition = Condition(
Field="host-header",
HostHeaderConfig=HostHeaderConfig(
Values=[
hostname
]
Values=hostnames
),
)

Expand Down