def create_root_template(new_resources: list, module_res_key: str) -> Template: """ Function to create the root stack template for profiles :param list[CodeProfiler] new_resources: :param str module_res_key: :return: The template wit the profiles :rtype: troposphere.Template """ root_tpl = build_template(f"Root stack to manage {module_res_key}") for res in new_resources: try: props = import_record_properties(res.properties, ProfilingGroup, ignore_missing_required=False) if res.parameters and keyisset("AppendStackId", res.parameters): props["ProfilingGroupName"] = Sub( f"{res.properties['ProfilingGroupName']}-${{StackId}}", StackId=STACK_ID_SHORT, ) except KeyError: props = import_record_properties(res.properties, ProfilingGroup, ignore_missing_required=True) props["ProfilingGroupName"] = Sub( f"{res.logical_name}-${{StackId}}", StackId=STACK_ID_SHORT) res.cfn_resource = ProfilingGroup(res.logical_name, **props) res.init_outputs() res.generate_outputs() add_outputs(root_tpl, res.outputs) root_tpl.add_resource(res.cfn_resource) return root_tpl
def generate_outputs(self): """ Generates a list of CFN outputs for the ECS Service and Task Definition """ if self.service_networking.security_group: self.outputs.append( CfnOutput( f"{self.logical_name}GroupId", Value=GetAtt(self.service_networking.security_group, "GroupId"), ) ) self.outputs.append( CfnOutput( ecs_params.SERVICE_SUBNETS.title, Value=Join(",", self.service_networking.subnets_output), ) ) self.outputs.append( CfnOutput(self.task_definition.title, Value=Ref(self.task_definition)) ) if ( self.service_scaling and self.service_scaling.scalable_target and self.service_scaling.scalable_target.title in self.template.resources ): self.outputs.append( CfnOutput( self.service_scaling.scalable_target.title, Value=Ref(self.service_scaling.scalable_target), ) ) add_outputs(self.template, self.outputs)
def define_table(table, template): """ Function to create the DynamoDB table resource :param table: :type table: ecs_composex.common.compose_resources.Table """ table_props = import_record_properties(table.properties, dynamodb.Table) table_props.update({ "Metadata": metadata, "Tags": Tags( Name=table.name, ResourceName=table.logical_name, CreatedByComposex=True, RootStackName=Ref(ROOT_STACK_NAME), ), }) cfn_table = dynamodb.Table(table.logical_name, **table_props) table.cfn_resource = cfn_table if table.scaling: add_autoscaling(table, template) table.init_outputs() table.generate_outputs() add_resource(template, table.cfn_resource) add_outputs(template, table.outputs)
def create_alarms(template: Template, new_alarms: list[Alarm]) -> None: """ Main function to create new alarms Rules out CompositeAlarms first, creates "Simple" alarms, and then link these to ComopsiteAlarms if so declared. """ for alarm in new_alarms: if (alarm.properties and not alarm.parameters or (alarm.parameters and not keyisset("CompositeExpression", alarm.parameters))): try: import_record_properties( alarm.properties, CompositeAlarm, ignore_missing_required=False, ) except KeyError: props = import_record_properties(alarm.properties, CWAlarm) alarm.cfn_resource = CWAlarm(alarm.logical_name, **props) if alarm.cfn_resource.title not in template.resources: alarm.init_outputs() alarm.generate_outputs() add_resource(template, alarm.cfn_resource) add_outputs(template, alarm.outputs) elif alarm.parameters and keyisset("CompositeExpression", alarm.parameters): continue add_composite_alarms(template, new_alarms)
def render_new_parameters(new_resources: list[SsmParameter], root_stack: ComposeXStack) -> None: """ :param list[SsmParameter] new_resources: :param ecs_composex.common.stacks.ComposeXStack root_stack: """ for new_res in new_resources: value = None if (keyisset("Type", new_res.definition) and new_res.definition["Type"] == "SecureString"): raise ValueError( f"{new_res.name} AWS CFN does not support SecureString.") if new_res.parameters and keyisset("FromFile", new_res.parameters): value = import_value_from_file(new_res) if keyisset("Value", new_res.properties): if value: LOG.warn( "Both Value and FromFile properties were set. Using Value from Properties" ) value = new_res.properties["Value"] if not value: raise ValueError(f"{new_res.name} - Failed to determine the value") if keyisset("EncodeToBase64", new_res.parameters): value = Base64(value) new_res.properties.update({"Value": value}) param_props = import_record_properties(new_res.properties, CfnSsmParameter, ignore_missing_required=False) new_res.cfn_resource = CfnSsmParameter(new_res.logical_name, **param_props) root_stack.stack_template.add_resource(new_res.cfn_resource) new_res.init_outputs() new_res.generate_outputs() add_outputs(root_stack.stack_template, new_res.outputs)
def add_service_actions(alarm, alarms_stack, target, scaling_in_policy, scaling_out_policy): """ Function to update the alarm properties with OKActions and AlarmActions :param ecs_composex.alarms.alarms_stack.Alarm alarm: :param ecs_composex.common.stacks.ComposeXStack alarms_stack: :param tuple target: :param scaling_in_policy: :param scaling_out_policy: """ setattr( alarm, "Threshold", float(scaling_out_policy.StepScalingPolicyConfiguration. StepAdjustments[0].MetricIntervalLowerBound), ) if not alarm.cfn_resource: raise AttributeError( f"Alarm {alarm.logical_name} has no CFN object associated") service_scaling_in_policy_param = Parameter( f"{target[0].logical_name}ScaleInPolicy", Type="String") service_scaling_out_policy_param = Parameter( f"{target[0].logical_name}ScaleOutPolicy", Type="String") add_parameters( alarms_stack.stack_template, [service_scaling_in_policy_param, service_scaling_out_policy_param], ) add_outputs( target[0].template, [ Output( f"{target[0].logical_name}ScaleInPolicy", Value=Ref(scaling_in_policy), ), Output( f"{target[0].logical_name}ScaleOutPolicy", Value=Ref(scaling_out_policy), ), ], ) alarms_stack.Parameters.update({ service_scaling_in_policy_param.title: GetAtt( target[0].logical_name, f"Outputs.{target[0].logical_name}ScaleInPolicy", ), service_scaling_out_policy_param.title: GetAtt( target[0].logical_name, f"Outputs.{target[0].logical_name}ScaleOutPolicy", ), }) actions = get_alarm_actions(alarm) actions[0].append(Ref(service_scaling_in_policy_param)) actions[1].append(Ref(service_scaling_out_policy_param))
def create_record(name, route53_zone, route53_stack, target_elbv2, elbv2_stack) -> None: """ Create a new RecordResource with the given DNS Name pointing to the ELB :param str name: :param ecs_composex.route53.route53_zone.HostedZone route53_zone: :param ecs_composex.route53.route53_stack.XStack route53_stack: :param ecs_composex.elbv2.elbv2_stack.Elbv2 target_elbv2: :param ComposeXStack elbv2_stack: """ if not target_elbv2.attributes_outputs: target_elbv2.init_outputs() target_elbv2.generate_outputs() add_outputs(elbv2_stack.stack_template, target_elbv2.outputs) lb_zone_id = target_elbv2.attributes_outputs[LB_DNS_ZONE_ID] lb_dns_name = target_elbv2.attributes_outputs[LB_DNS_NAME] add_parameters( route53_stack.stack_template, [lb_zone_id["ImportParameter"], lb_dns_name["ImportParameter"]], ) route53_stack.Parameters.update({ lb_zone_id["ImportParameter"].title: lb_zone_id["ImportValue"], lb_dns_name["ImportParameter"].title: lb_dns_name["ImportValue"], }) elbv2_alias = AliasTarget( HostedZoneId=Ref(lb_zone_id["ImportParameter"]), DNSName=Ref(lb_dns_name["ImportParameter"]), ) record_props = { "AliasTarget": elbv2_alias, "Region": Ref(AWS_REGION), "Type": "A", "Name": name, } if not keyisset("SetIdentifier", record_props): record_props["SetIdentifier"] = Ref(AWS_STACK_NAME) if route53_zone.cfn_resource: zone_id_attribute = GetAtt(route53_zone.cfn_resource, PUBLIC_DNS_ZONE_ID.return_value) record_props["HostedZoneId"] = zone_id_attribute elif route53_zone.mappings: zone_id_attribute = route53_zone.attributes_outputs[PUBLIC_DNS_ZONE_ID] record_props["HostedZoneId"] = zone_id_attribute["ImportValue"] cfn_resource = RecordSetType( f"elbv2{target_elbv2.logical_name}Route53{record_props['Type']}{NONALPHANUM.sub('', record_props['Name'])}"[: 128], **record_props, ) if cfn_resource.title not in route53_stack.stack_template.resources: route53_stack.stack_template.add_resource(cfn_resource) if elbv2_stack.title not in route53_stack.DependsOn: route53_stack.DependsOn.append(elbv2_stack.title)
def add_composite_alarms(template: Template, new_alarms: list[Alarm]) -> None: for alarm in new_alarms: if not alarm.cfn_resource and ((alarm.parameters and keyisset( "CompositeExpression", alarm.parameters)) or alarm.properties): alarm.is_composite = True create_composite_alarm(alarm, new_alarms) add_resource(template, alarm.cfn_resource) alarm.init_outputs() alarm.generate_outputs() add_outputs(template, alarm.outputs)
def __init__(self, name: str, settings: ComposeXSettings, **kwargs): stack_template = build_template("Root stack for IAM Roles") add_parameters(stack_template, [CLUSTER_NAME]) super().__init__(name, stack_template, **kwargs) exec_role_managed_policy = add_ecs_execution_role_managed_policy(stack_template) self.Parameters.update( {CLUSTER_NAME.title: settings.ecs_cluster.cluster_identifier} ) new_roles = import_family_roles(settings, exec_role_managed_policy) for role in new_roles: self.stack_template.add_resource(role.cfn_resource) role.stack = self if not role.attributes_outputs: role.generate_outputs() add_outputs(stack_template, role.outputs)
def create_new_domains(new_domains, stack): """ Function to create the new CFN Template for the OS Domains to create :param list[ecs_composex.opensearch.opensearch_stack.OpenSearchDomain] new_domains: :param ecs_composex.common.stacks.ComposeXStack stack: """ for domain in new_domains: domain.set_override_subnets() props = import_record_properties(domain.properties, opensearchservice.Domain) if keyisset("VPCOptions", props) or domain.subnets_override: add_new_security_group(domain, props, stack) if domain.parameters: apply_domain_parameters(domain, stack, props) if keyisset("AdvancedSecurityOptions", props): correcting_required_settings(domain, props) validate_instance_types(domain, props) domain.cfn_resource = opensearchservice.Domain(domain.logical_name, **props) domain.init_outputs() stack.stack_template.add_resource(domain.cfn_resource) domain.generate_outputs() if domain.security_group: domain.add_new_output_attribute( OS_DOMAIN_SG, ( f"{domain.logical_name}{OS_DOMAIN_SG.return_value}", domain.security_group, GetAtt, OS_DOMAIN_SG.return_value, ), ) domain.add_new_output_attribute( OS_DOMAIN_PORT, ( f"{domain.logical_name}{OS_DOMAIN_PORT.title}", OS_DOMAIN_PORT.Default, OS_DOMAIN_PORT.Default, False, ), ) add_parameters(stack.stack_template, [OS_DOMAIN_PORT]) add_outputs(stack.stack_template, domain.outputs)
def create_new_default_vpc(self, title, vpc_module, settings): """ In case no x-vpc was specified but the deployment settings require a new VPC, allows for an easy way to set one. """ self.vpc_resource = Vpc( name="vpc", definition={"Properties": { VPC_CIDR.title: Vpc.default_ipv4_cidr }}, module=vpc_module, settings=settings, ) template = init_vpc_template() self.vpc_resource.create_vpc(template, settings) self.is_void = False self.vpc_resource.init_outputs() super().__init__(title, stack_template=template) self.vpc_resource.generate_outputs() add_outputs(template, self.vpc_resource.outputs)
def __init__(self, title, settings: ComposeXSettings, module: XResourceModule, **kwargs): self.is_void = True self.vpc_resource = None if not keyisset(module.res_key, settings.compose_content): LOG.warning(f"{module.res_key} - not defined. Assuming no VPC") self.is_void = True else: self.vpc_resource = Vpc("vpc", settings.compose_content[module.res_key], module, settings) if self.vpc_resource.lookup: self.vpc_resource.lookup_vpc(settings) elif self.vpc_resource.properties: template = init_vpc_template() self.vpc_resource.create_vpc(template, settings) self.is_void = False self.vpc_resource.init_outputs() super().__init__(title, stack_template=template, **kwargs) self.vpc_resource.generate_outputs() add_outputs(template, self.vpc_resource.outputs)
def handle_services_association(resource, res_root_stack, settings): """ Function to handle association of listeners and targets to the LB :param ecs_composex.elbv2.elbv2_stack.Elbv2 resource: :param ecs_composex.common.settings.ComposeXSettings settings: :param ecs_composex.common.stacks.ComposeXStack res_root_stack: :return: """ template = res_root_stack.stack_template resource.set_listeners(template) resource.associate_to_template(template) add_outputs(template, resource.outputs) identified = [] for target in resource.families_targets: if target[1].launch_type == "EXTERNAL": LOG.error( f"x-elbv2.{resource.name} - Target family {target[0].name} uses EXTERNAL launch type. Ignoring" ) continue tgt_arn = define_service_target_group_definition( resource, target[0], target[1], target[2], res_root_stack) for service in resource.services: target_name = f"{target[0].name}:{target[1].name}" if target_name == service["name"]: service["target_arn"] = tgt_arn identified.append(True) if not identified: LOG.error( f"{resource.module.res_key}.{resource.name} - No services found as targets. Skipping association" ) return for listener in resource.listeners: listener.map_lb_services_to_listener_targets(resource) for listener in resource.listeners: listener.handle_certificates(settings, res_root_stack) listener.handle_cognito_pools(settings, res_root_stack) listener.define_default_actions(template)
def retrieve_services(settings: ComposeXSettings, services: dict, x_stack: ComposeXStack) -> list[tuple]: """ Function to :param ecs_composex.common.settings.ComposeXSettings settings: :param dict services: :param ecs_composex.common.stacks.ComposeXStack x_stack: :return: """ services_params = [] families_original_names = [f.name for f in settings.families.values()] for name, service_def in services.items(): if name not in families_original_names: LOG.warn(f"Service family {name} is not defined. Skipping") continue family = get_family_from_name(settings, name) if family is None: LOG.warn( f"Could not identify the {name} family in {families_original_names}" ) continue s_param = Parameter(f"{family.stack.title}{SERVICE_T}Name", Type="String") if SERVICE_T not in family.template.outputs: add_outputs( family.template, [Output(s_param.title, Value=GetAtt(SERVICE_T, "Name"))], ) x_stack.Parameters.update({ s_param.title: GetAtt(family.stack.title, f"Outputs.{s_param.title}") }) services_params.append((family.stack.title, s_param)) add_parameters(x_stack.stack_template, [value[1] for value in services_params]) return services_params
def define_service_target_group( resource, family, service, resources_root_stack, target_definition, ): """ Function to create the elbv2 target group :param ecs_composex.elbv2.elbv2_stack.Elbv2 resource: the ELBv2 to attach to :param ecs_composex.common.compose_services.ComposeService service: the service target :param ecs_composex.ecs.ecs_family.ComposeFamily family: the family owning the service :param ecs_composex.common.stacks.ComposeXStack resources_root_stack: :param dict target_definition: the Service definition :return: the target group :rtype: troposphere.elasticloadbalancingv2.TargetGroup """ props = {} set_healthcheck_definition(props, target_definition) props["Port"] = target_definition["port"] props["Protocol"] = (props["HealthCheckProtocol"] if not keyisset("protocol", target_definition) else target_definition["protocol"]) fix_nlb_settings(props) props["TargetType"] = "ip" import_target_group_attributes(props, target_definition, resource, service) validate_props_and_service_definition(props, service) target_group_name = f"Tgt{resource.logical_name}{family.logical_name}{service.logical_name}{props['Port']}" target_group = ComposeTargetGroup( target_group_name, elbv2=resource, family=family, stack=resource.stack, VpcId=Ref(VPC_ID), **props, ) if target_group.title not in resources_root_stack.stack_template.resources: resources_root_stack.stack_template.add_resource(target_group) else: target_group = resources_root_stack.stack_template.resources[ target_group.title] target_group.init_outputs() target_group.generate_outputs() add_outputs(resources_root_stack.stack_template, target_group.outputs) if target_group not in family.target_groups: family.target_groups.append(target_group) tgt_parameter = target_group.attributes_outputs[TGT_GROUP_ARN][ "ImportParameter"] add_parameters(family.template, [tgt_parameter]) family.stack.Parameters.update({ tgt_parameter.title: target_group.attributes_outputs[TGT_GROUP_ARN]["ImportValue"], }) service_lb = EcsLb( ContainerPort=props["Port"], ContainerName=service.name, TargetGroupArn=Ref(tgt_parameter), ) family.ecs_service.lbs.append(service_lb) add_parameters(family.template, [ELB_GRACE_PERIOD]) family.ecs_service.ecs_service.HealthCheckGracePeriodSeconds = Ref( ELB_GRACE_PERIOD) handle_sg_lb_ingress_to_service(resource, family, resources_root_stack) return target_group