Ejemplo n.º 1
0
def handle_path_settings(props, path_raw):
    """
    Function to set the path and codes properties

    :param dict props:
    :param str path_raw:
    :return:
    """
    path_re = re.compile(
        r"(/[\S][^:]+.$)|(/[\S]+)(?::)((?:[\d]{1,4},?){1,}.$)|((?:[\d]{1,4},?){1,}.$)"
    )
    groups = path_re.search(path_raw).groups()
    if not groups:
        LOG.debug("No PATH or ReturnCodes set.")
        return
    path = groups[0] or groups[1]
    codes = groups[2] or groups[3]
    if path:
        props["HealthCheckPath"] = path
    if codes:
        props["Matcher"] = Matcher(HttpCode=codes)
    if props["HealthCheckProtocol"] not in ["HTTP", "HTTPS"] and codes:
        raise ValueError(
            groups,
            "Protocol and return codes are only valid for HTTP and HTTPS HealthCheck",
        )
Ejemplo n.º 2
0
def set_healthcheck_definition(props, target_definition):
    """

    :param dict props:
    :param dict target_definition:
    :return:
    """
    healthcheck_props = {
        "HealthCheckEnabled": Ref(AWS_NO_VALUE),
        "HealthCheckIntervalSeconds": Ref(AWS_NO_VALUE),
        "HealthCheckPath": Ref(AWS_NO_VALUE),
        "HealthCheckPort": Ref(AWS_NO_VALUE),
        "HealthCheckProtocol": Ref(AWS_NO_VALUE),
        "HealthCheckTimeoutSeconds": Ref(AWS_NO_VALUE),
        "HealthyThresholdCount": Ref(AWS_NO_VALUE),
    }
    required_mapping = (
        "HealthCheckPort",
        "HealthCheckProtocol",
    )
    required_rex = re.compile(
        r"^([\d]{2,5}):(HTTPS|HTTP|TCP_UDP|TCP|TLS|UDP)$")
    healthcheck_reg = re.compile(
        r"(^(?:[\d]{2,5}):(?:HTTPS|HTTP|TCP_UDP|TCP|TLS|UDP)):?"
        r"((?:[\d]{1}|10):(?:[\d]{1}|10):[\d]{1,3}:[\d]{1,3})?:"
        r"?((?:/[\S][^:]+.$)|(?:/[\S]+)(?::)(?:(?:[\d]{1,4},?){1,}.$)|(?:(?:[\d]{1,4},?){1,}.$))?"
    )
    healthcheck_definition = set_else_none("healthcheck", target_definition)
    if isinstance(healthcheck_definition, str):
        groups = healthcheck_reg.search(healthcheck_definition).groups()
        if not groups[0]:
            raise ValueError(
                "You need to define at least the Protocol and port for healthcheck"
            )
        for count, value in enumerate(required_rex.match(groups[0]).groups()):
            healthcheck_props[required_mapping[count]] = value
        if groups[1]:
            handle_ping_settings(healthcheck_props, groups[1])
        if groups[2]:
            try:
                handle_path_settings(healthcheck_props, groups[2])
            except ValueError:
                LOG.error(target_definition["name"],
                          target_definition["healthcheck"])
                raise
    elif isinstance(healthcheck_definition, dict):
        healthcheck_props.update(healthcheck_definition)
        if keyisset("Matcher", healthcheck_definition):
            healthcheck_props["Matcher"] = Matcher(
                **healthcheck_definition["Matcher"])
    else:
        raise TypeError(
            healthcheck_definition,
            type(healthcheck_definition),
            "must be one of",
            (str, dict),
        )
    props.update(healthcheck_props)
Ejemplo n.º 3
0
 def add_alb_target_group(self):
     '''
     Add load balancer target group to template
     '''
     self.cfn_template.add_resource(TargetGroup(
         title=constants.ALB_TG,
         HealthCheckIntervalSeconds=int('5'),
         HealthCheckPath='/health',
         HealthCheckProtocol='HTTP',
         HealthCheckTimeoutSeconds=int('3'),
         HealthyThresholdCount=int('2'),
         Matcher=Matcher(HttpCode='200'),
         Port=int('8228'),
         Protocol='HTTP',
         UnhealthyThresholdCount=int('5'),
         TargetGroupAttributes=[
             TargetGroupAttribute(
                 Key='deregistration_delay.timeout_seconds',
                 Value='20'
             )
         ],
         VpcId=ImportValue(Sub('${Environment}-${VpcId}'))
     ))
     return self.cfn_template
    def _add_alb(self, cd, service_name, config, launch_type):
        sg_name = 'SG' + self.env + service_name
        svc_alb_sg = SecurityGroup(
            re.sub(r'\W+', '', sg_name),
            GroupName=self.env + '-' + service_name,
            SecurityGroupIngress=self._generate_alb_security_group_ingress(
                config
            ),
            VpcId=Ref(self.vpc),
            GroupDescription=Sub(service_name + "-alb-sg")
        )
        self.template.add_resource(svc_alb_sg)
        alb_name = service_name + pascalcase(self.env)
        if config['http_interface']['internal']:
            alb_subnets = [
                Ref(self.private_subnet1),
                Ref(self.private_subnet2)
            ]
            scheme = "internal"
            alb_name += 'Internal'
            alb_name = alb_name[:32]
            alb = ALBLoadBalancer(
                'ALB' + service_name,
                Subnets=alb_subnets,
                SecurityGroups=[
                    self.alb_security_group,
                    Ref(svc_alb_sg)
                ],
                Name=alb_name,
                Tags=[
                    {'Value': alb_name, 'Key': 'Name'}
                ],
                Scheme=scheme
            )
        else:
            alb_subnets = [
                Ref(self.public_subnet1),
                Ref(self.public_subnet2)
            ]
            alb_name = alb_name[:32]
            alb = ALBLoadBalancer(
                'ALB' + service_name,
                Subnets=alb_subnets,
                SecurityGroups=[
                    self.alb_security_group,
                    Ref(svc_alb_sg)
                ],
                Name=alb_name,
                Tags=[
                    {'Value': alb_name, 'Key': 'Name'}
                ]
            )

        self.template.add_resource(alb)

        target_group_name = "TargetGroup" + service_name
        health_check_path = config['http_interface']['health_check_path'] if 'health_check_path' in config['http_interface'] else "/elb-check"
        if config['http_interface']['internal']:
            target_group_name = target_group_name + 'Internal'

        target_group_config = {}
        if launch_type == self.LAUNCH_TYPE_FARGATE:
            target_group_config['TargetType'] = 'ip'

        service_target_group = TargetGroup(
            target_group_name,
            HealthCheckPath=health_check_path,
            HealthyThresholdCount=2,
            HealthCheckIntervalSeconds=30,
            TargetGroupAttributes=[
                TargetGroupAttribute(
                    Key='deregistration_delay.timeout_seconds',
                    Value='30'
                )
            ],
            VpcId=Ref(self.vpc),
            Protocol="HTTP",
            Matcher=Matcher(HttpCode="200-399"),
            Port=int(config['http_interface']['container_port']),
            HealthCheckTimeoutSeconds=10,
            UnhealthyThresholdCount=3,
            **target_group_config
        )

        self.template.add_resource(service_target_group)
        # Note: This is a ECS Loadbalancer definition. Not an ALB.
        # Defining this causes the target group to add a target to the correct
        # port in correct ECS cluster instance for the service container.
        lb = LoadBalancer(
            ContainerName=cd.Name,
            TargetGroupArn=Ref(service_target_group),
            ContainerPort=int(config['http_interface']['container_port'])
        )
        target_group_action = Action(
            TargetGroupArn=Ref(target_group_name),
            Type="forward"
        )
        service_listener = self._add_service_listener(
            service_name,
            target_group_action,
            alb,
            config['http_interface']['internal']
        )
        self._add_alb_alarms(service_name, alb)
        return alb, lb, service_listener, svc_alb_sg
Ejemplo n.º 5
0
 def set_health_check_codes(self, codes):
     self.health_check_matcher = Matcher(HttpCode=codes)
Ejemplo n.º 6
0
 def __init__(self):
     super(TargetPath, self).__init__()
     self.health_check_matcher = Matcher(HttpCode="200-399")
Ejemplo n.º 7
0
    def to_json(self):
        if self._json is not None:
            return self._json

        # Validity checks
        if len(self.subnet_ids) < 2:
            raise ValidationException(
                "Use .subnet_id() to specify at least two ELB subnets")
        if len(self.cert_ids) < 1:
            raise ValidationException(
                "Use .certificate_id() to specify at least one certificate")
        if not self._ecs_redirect and len(self.default_targets) < 1:
            raise ValidationException(
                "Use .default_target() to specify at least one default target or .ecs_redirect("
                ") to set up a redirect container")
        for (name, tp) in self.target_paths.iteritems():
            if len(set(map(lambda h: h.type, tp.hosts))) != 1:
                raise ValidationException(
                    "Inconsistent target types for %s. All hosts for a given path must have the "
                    "same type (ip or instance)." % name)

        # Build Security Group
        if self._custom_elb_sgs:
            elb_sgs = self._custom_elb_sgs
        else:
            elb_sg = SecurityGroup(
                "ElbSecurityGroup",
                GroupDescription=Sub("${AWS::StackName}-ElbSg"),
                Tags=self.tags_with(Name=Sub("${AWS::StackName}-ElbSg")),
                VpcId=self.vpc_id,
                SecurityGroupEgress=[
                    SecurityGroupRule(CidrIp="0.0.0.0/0", IpProtocol="-1")
                ],
                SecurityGroupIngress=self._sg_rules)
            self.template.add_resource(elb_sg)
            self.template.add_output(
                Output("ElbSecurityGroupOutput",
                       Description="Security group ID assigned to the ELB",
                       Value=Ref(elb_sg),
                       Export=Export(Sub("${AWS::StackName}-ElbSg"))))

            # Build Attachment Security Group
            inst_sg = SecurityGroup(
                "InstanceSecurityGroup",
                GroupDescription=Sub("${AWS::StackName}-InstSg"),
                Tags=self.tags_with(Name=Sub("${AWS::StackName}-InstSg")),
                VpcId=self.vpc_id,
                SecurityGroupEgress=[
                    SecurityGroupRule(CidrIp="0.0.0.0/0", IpProtocol="-1")
                ],
                SecurityGroupIngress=[
                    SecurityGroupRule(IpProtocol="-1",
                                      SourceSecurityGroupId=Ref(elb_sg))
                ])
            self.template.add_resource(inst_sg)
            self.template.add_output(
                Output("InstanceSecurityGroupOutput",
                       Description="Convenience SG to assign to instances",
                       Value=Ref(inst_sg),
                       Export=Export(Sub("${AWS::StackName}-InstSg"))))
            elb_sgs = [Ref("ElbSecurityGroup")]

        # Build ELB
        elb = LoadBalancer("ELB",
                           Name=Ref("AWS::StackName"),
                           SecurityGroups=elb_sgs,
                           Subnets=self.subnet_ids,
                           Tags=self.tags_with(Name=Ref("AWS::StackName")),
                           LoadBalancerAttributes=self.elb_attributes())
        self.template.add_resource(elb)
        self.template.add_output(
            Output("ElbArnOutput",
                   Description="ARN of the ELB",
                   Value=Ref(elb),
                   Export=Export(Sub("${AWS::StackName}-ElbArn"))))
        self.template.add_output(
            Output("ElbDnsOutput",
                   Description="DNS name of the ELB",
                   Value=GetAtt("ELB", "DNSName"),
                   Export=Export(Sub("${AWS::StackName}-ElbDns"))))

        # Build Default Target Group
        if self._ecs_redirect:
            default_tg_protocol = "HTTP"
        else:
            default_tg_protocol = self.default_targets[0].protocol
        default_tg = TargetGroup(
            "DefaultTargetGroup",
            Port=8080,
            Protocol=default_tg_protocol,
            Tags=self.tags_with(Name=Sub("${AWS::StackName}-Default")),
            VpcId=self.vpc_id,
            Targets=list(
                map(lambda h: TargetDescription(Id=h.host, Port=h.port),
                    self.default_targets)),
            HealthyThresholdCount=2,
            Matcher=Matcher(HttpCode="200-399"))
        self.template.add_resource(default_tg)
        self.attach_alarm(default_tg)

        # Build Listener
        self.template.add_resource(
            Listener("HttpsListener",
                     Certificates=list(
                         map(lambda i: Certificate(CertificateArn=i),
                             self.cert_ids)),
                     DefaultActions=[
                         Action(Type="forward",
                                TargetGroupArn=Ref("DefaultTargetGroup"))
                     ],
                     LoadBalancerArn=Ref("ELB"),
                     Port=443,
                     Protocol="HTTPS"))

        # Build HTTP redirect
        if len(self.http_redirect_targets) > 0:
            # Build Redirect Target Group
            http_tg = TargetGroup(
                "RedirectTargetGroup",
                Port=8080,
                Protocol=self.http_redirect_targets[0].protocol,
                Tags=self.tags_with(Name=Sub("${AWS::StackName}-Redirect")),
                VpcId=self.vpc_id,
                Targets=list(
                    map(lambda h: TargetDescription(Id=h.host, Port=h.port),
                        self.http_redirect_targets)),
                HealthyThresholdCount=2,
                Matcher=Matcher(HttpCode="200-399"))
            self.template.add_resource(http_tg)
            self.attach_alarm(http_tg)

        if self._ecs_redirect or len(self.http_redirect_targets) > 0:
            if self._ecs_redirect:
                redirect_tg = "DefaultTargetGroup"
            else:
                redirect_tg = "RedirectTargetGroup"
            # Build Listener
            self.template.add_resource(
                Listener("HttpListener",
                         DefaultActions=[
                             Action(Type="forward",
                                    TargetGroupArn=Ref(redirect_tg))
                         ],
                         LoadBalancerArn=Ref("ELB"),
                         Port=80,
                         Protocol="HTTP"))

        # Build Target Groups & Rules
        for (name, tp) in self.target_paths.iteritems():
            name_an = alpha_numeric_name(name)
            tag_name = taggable_name(name)

            g = TargetGroup(
                "PathTg" + name_an,
                Port=tp.hosts[0].port,
                Protocol=tp.hosts[0].protocol,
                Tags=self.tags_with(Name="%s/%s" % (self.env_name, tag_name),
                                    TargetPath=tag_name),
                Targets=list(map(lambda h: h.to_target_desc(), tp.hosts)),
                VpcId=self.vpc_id,
                HealthCheckPath="/%s" % name,
                HealthyThresholdCount=2,
                Matcher=tp.health_check_matcher)

            # TODO: We should probably explicitly specify this for every TG. Not
            #       doing that now because it will cause lots of updates. Maybe
            #       in 0.4?
            if len(tp.hosts) > 0 and tp.hosts[0].type != "instance":
                g.TargetType = tp.hosts[0].type

            if self.sticky:
                g.TargetGroupAttributes = [
                    TargetGroupAttribute(Key="stickiness.enabled",
                                         Value="true"),
                    TargetGroupAttribute(Key="stickiness.type",
                                         Value="lb_cookie")
                ]
            self.template.add_resource(g)
            self.attach_alarm(g)
            self.template.add_resource(
                ListenerRule(
                    "PathRl" + name_an,
                    Actions=[Action(Type="forward", TargetGroupArn=Ref(g))],
                    Conditions=[
                        Condition(Field="path-pattern",
                                  Values=["/%s/*" % name])
                    ],
                    ListenerArn=Ref("HttpsListener"),
                    Priority=self.priority_hash(name)))
            self.template.add_resource(
                ListenerRule(
                    "PathRln" + name_an,
                    Actions=[Action(Type="forward", TargetGroupArn=Ref(g))],
                    Conditions=[
                        Condition(Field="path-pattern", Values=["/%s" % name])
                    ],
                    ListenerArn=Ref("HttpsListener"),
                    Priority=self.priority_hash(name)))

        # Build Alternate Listeners
        for al in self.alt_listeners:
            tg_name = "AltTg%d" % al.port
            tg_protocol = al.hosts[0].protocol
            tg = TargetGroup(
                tg_name,
                Port=9999,
                Protocol=tg_protocol,
                Tags=self.tags_with(Name=Sub("${AWS::StackName}-%s" %
                                             tg_name)),
                VpcId=self.vpc_id,
                Targets=list(
                    map(lambda h: TargetDescription(Id=h.host, Port=h.port),
                        al.hosts)),
                HealthyThresholdCount=2,
                Matcher=Matcher(HttpCode="200-399"))
            self.template.add_resource(tg)
            self.attach_alarm(tg)

            listener = Listener("AltListener%d" % al.port,
                                DefaultActions=[
                                    Action(Type="forward",
                                           TargetGroupArn=Ref(tg_name))
                                ],
                                LoadBalancerArn=Ref("ELB"),
                                Port=al.port,
                                Protocol=al.protocol)

            if al.protocol == "HTTPS":
                listener.Certificates = list(
                    map(lambda i: Certificate(CertificateArn=i),
                        self.cert_ids))

            self.template.add_resource(listener)

        self._json = self.template.to_json()
        return self._json
Ejemplo n.º 8
0
    Default="3",
)))


template.add_mapping("ECSRegionMap", {
    "us-east-1": {"AMI": "ami-0a6a36557ea3b9859"},
})


# Target group
application_target_group = TargetGroup(
    'ApplicationTargetGroup',
    template=template,
    VpcId=Ref(vpc),
    Matcher=Matcher(
        HttpCode='200-299',
    ),
    Port=8080,
    Protocol='HTTP',
    HealthCheckIntervalSeconds=15,
    HealthCheckPath='/',
    HealthCheckProtocol='HTTP',
    HealthCheckTimeoutSeconds=5,
    HealthyThresholdCount=2,
    UnhealthyThresholdCount=8,
    TargetGroupAttributes=[
        TargetGroupAttribute(
            Key='stickiness.enabled',
            Value='true',
        )
    ],
    def __init__(self,
                 prefix: str,
                 lb_security_groups: List[SecurityGroup],
                 subnets: List[Subnet],
                 vpc: VPC,
                 desired_domain_name: str,
                 healthy_http_codes: Optional[List[int]] = None):
        """
        Constructor.

        :param prefix: A prefix for resource names.
        :param lb_security_groups: Security groups to attach to a loadbalancer. NOTE! when passing loadbalancer
        security groups - make sure the loadbalancer can communicate through ci/cd blue/green deployments
        opened ports. Usually they are 8000 and 44300.
        :param subnets: Subnets in which loadbalancer can exist.
        :param vpc: Virtual private cloud in which target groups and a loadbalancer exist.
        :param desired_domain_name: Domain name for using https.
        :param healthy_http_codes: The deployed instance is constantly pinged to determine if it is available
        (healthy) or not. Specify a list of http codes that your service can return and should be treated as healthy.
        """
        # By default a healthy http code is considered to be 200.
        healthy_http_codes = healthy_http_codes or [200]

        # If your service's task definition uses the awsvpc network mode
        # (which is required for the Fargate launch type), you must choose ip as the target type,
        # not instance, when creating your target groups because
        # tasks that use the awsvpc network mode are associated with an elastic network interface,
        # not an Amazon EC2 instance.
        self.target_type = 'ip'

        # Certificate so a loadbalancer could communicate via HTTPS.
        self.certificate = Certificate(
            prefix + 'FargateEcsCertificate',
            DomainName=desired_domain_name,
            ValidationMethod='DNS',
        )

        # A main target group to which a loadbalancer forwards a HTTP traffic.
        # This is the main group with which our ecs container is associated.
        self.target_group_1_http = TargetGroup(
            prefix + 'FargateEcsTargetGroup1',
            Name=prefix + 'FargateEcsTargetGroup1',
            Matcher=Matcher(
                HttpCode=','.join([str(code) for code in healthy_http_codes])),
            Port=self.TARGET_GROUP_PORT,
            Protocol='HTTP',
            VpcId=Ref(vpc),
            TargetType=self.target_type)

        # Second target group is usd for Blue/Green deployments. A new container (that should be deployed)
        # is associated with the second target group.
        self.target_group_2_http = TargetGroup(
            prefix + 'FargateEcsTargetGroup2',
            Name=prefix + 'FargateEcsTargetGroup2',
            Matcher=Matcher(
                HttpCode=','.join([str(code) for code in healthy_http_codes])),
            Port=self.TARGET_GROUP_PORT,
            Protocol='HTTP',
            VpcId=Ref(vpc),
            TargetType=self.target_type)

        self.load_balancer = LoadBalancer(
            prefix + 'FargateEcsLoadBalancer',
            Subnets=[Ref(sub) for sub in subnets],
            SecurityGroups=[Ref(group) for group in lb_security_groups],
            Name=prefix + 'FargateEcsLoadBalancer',
            Scheme='internet-facing',
        )

        self.load_balancer_output = Output(
            prefix + 'FargateEcsLoadBalancerUrl',
            Description='The endpoint url of a loadbalancer.',
            Value=GetAtt(self.load_balancer, 'DNSName'))

        # Listener that listens to HTTP incoming traffic and redirects to other HTTPS listener.
        self.listener_http_1 = Listener(
            prefix + 'FargateEcsHttpListener1',
            Port=self.LISTENER_HTTP_PORT_1,
            Protocol='HTTP',
            LoadBalancerArn=Ref(self.load_balancer),
            DefaultActions=[
                # Redirect to https.
                Action(Type='redirect',
                       RedirectConfig=RedirectConfig(
                           Host='#{host}',
                           Path='/#{path}',
                           Port=str(self.LISTENER_HTTPS_PORT_1),
                           Query='#{query}',
                           StatusCode='HTTP_301',
                           Protocol='HTTPS'))
            ])

        # Listener that listens to HTTPS traffic and forwards to a target group.
        self.listener_https_1 = Listener(
            prefix + 'FargateEcsHttpsListener1',
            Certificates=[LBCertificate(CertificateArn=Ref(self.certificate))],
            Port=self.LISTENER_HTTPS_PORT_1,
            Protocol='HTTPS',
            LoadBalancerArn=Ref(self.load_balancer),
            DefaultActions=[
                Action(Type='forward',
                       TargetGroupArn=Ref(self.target_group_1_http))
            ])

        # Second listener is usd for Blue/Green deployments (testing new instance). Test HTTP traffic is
        # redirected to test HTTPS traffic.
        self.listener_http_2 = Listener(
            prefix + 'FargateEcsHttpListener2',
            Port=self.LISTENER_HTTP_PORT_2,
            Protocol='HTTP',
            LoadBalancerArn=Ref(self.load_balancer),
            DefaultActions=[
                # Redirect to https.
                Action(Type='redirect',
                       RedirectConfig=RedirectConfig(
                           Host='#{host}',
                           Path='/#{path}',
                           Port=str(self.LISTENER_HTTPS_PORT_2),
                           Query='#{query}',
                           StatusCode='HTTP_301',
                           Protocol='HTTPS'))
            ])

        # Listener that listens to test HTTP traffic and forwards to a secondary target group (new container).
        self.listener_https_2 = Listener(
            prefix + 'FargateEcsHttpsListener2',
            Certificates=[LBCertificate(CertificateArn=Ref(self.certificate))],
            Port=self.LISTENER_HTTPS_PORT_2,
            Protocol='HTTPS',
            LoadBalancerArn=Ref(self.load_balancer),
            DefaultActions=[
                Action(Type='forward',
                       TargetGroupArn=Ref(self.target_group_2_http))
            ])
Ejemplo n.º 10
0
                 ],
                 Scheme='internet-facing',
                 SecurityGroups=[Ref(api_elb_sg)],
                 Subnets=Ref(subnets)))

target_group = template.add_resource(
    TargetGroup(
        'DefaultTargetGroup',
        Name=Join('-', ['api', 'default', Ref(version)]),
        HealthCheckIntervalSeconds=5,
        HealthCheckProtocol='HTTP',
        HealthCheckTimeoutSeconds=2,
        HealthCheckPath='/',
        HealthyThresholdCount=2,
        UnhealthyThresholdCount=3,
        Matcher=Matcher(HttpCode='200'),
        Port=8080,
        Protocol='HTTP',
        TargetGroupAttributes=[
            TargetGroupAttribute(Key='deregistration_delay.timeout_seconds',
                                 Value='120')
        ],
        VpcId=Ref(vpc_id)))

listener = template.add_resource(
    Listener('HttpListener',
             Port=80,
             Protocol='HTTP',
             LoadBalancerArn=Ref(load_balancer),
             DefaultActions=[
                 Action(Type="forward", TargetGroupArn=Ref(target_group))
Ejemplo n.º 11
0
    def test_using_load_balancer(self):
        test_stack_name = "TestALB"
        init_cf_env(test_stack_name)
        ###

        t = Template()
        load_balancer_sg = ts_add_security_group(
            t, name="LoadBalancerSecurityGroup")
        instance_sg = ts_add_security_group(t)

        load_balancer = t.add_resource(
            LoadBalancer(
                "MyLoadBalancer",
                SecurityGroups=[Ref(load_balancer_sg)],
                # The ALB is publicly accessible.
                # (use `internal` instead of `internet-facing` to define a load balancer reachable from private network only)
                Scheme='internet-facing',
                Subnets=[get_subnet(index=0),
                         get_subnet(index=1)
                         ],  # Attaches the ALB to the subnets
                Type='application'))
        target_group = t.add_resource(
            TargetGroup(
                "MyTargetGroup",
                HealthCheckIntervalSeconds=10,
                HealthCheckProtocol='HTTP',
                HealthCheckPath='/index.html',
                HealthCheckTimeoutSeconds=5,
                HealthyThresholdCount=3,
                UnhealthyThresholdCount=2,
                Matcher=Matcher(
                    HttpCode='200-299'
                ),  # If HTTP status code is 2XX, the backend is considered healthy.
                Port=
                80,  # The web server on the EC2 instances listens on port 80.
                Protocol='HTTP',
                VpcId=get_default_vpc(),
            ))
        listener = t.add_resource(
            Listener(
                "MyListener",
                LoadBalancerArn=Ref(load_balancer),
                Port=80,
                Protocol=
                'HTTP',  # The load balancer listens on port 80 for HTTP requests.
                DefaultActions=[
                    Action(
                        Type='forward',
                        # TargetGroupARN is the connection between the ALB and the auto-scaling group
                        TargetGroupArn=Ref(target_group),
                    )
                ]))

        launch_config = t.add_resource(
            LaunchConfiguration(
                "MyLaunchConfiguration",
                ImageId=get_linux2_image_id(),
                InstanceType='m4.xlarge',
                KeyName=KEY,
                SecurityGroups=[Ref(instance_sg)],
                AssociatePublicIpAddress=True,
                InstanceMonitoring=False,
                UserData=Base64(
                    Join('', [
                        '#!/bin/bash -xe\n',
                        '/opt/aws/bin/cfn-init -v --stack ',
                        Ref('AWS::StackName'),
                        '                         --resource MyLaunchConfiguration ',
                        '                         --region ',
                        Ref('AWS::Region'), '\n'
                    ])),
                Metadata=Metadata(
                    Init({
                        'config':
                        InitConfig(
                            packages={'yum': {
                                'httpd': []
                            }},
                            files={
                                '/tmp/config': {
                                    'content':
                                    Join('\n', [
                                        '#!/bin/bash -ex',
                                        'PRIVATE_IP=`curl -s http://169.254.169.254/latest/meta-data/local-ipv4`',
                                        'echo "$PRIVATE_IP" > index.html',
                                    ]),
                                    'mode':
                                    '000500',
                                    'owner':
                                    'root',
                                    'group':
                                    'root',
                                }
                            },
                            commands={
                                '01_config': {
                                    'command': "/tmp/config",
                                    'cwd': '/var/www/html'
                                }
                            },
                            services={
                                'sysvinit': {
                                    'httpd': {
                                        'enabled': True,
                                        'ensureRunning': True
                                    }
                                }
                            })
                    }))))
        auto_scaling_group = t.add_resource(
            AutoScalingGroup(
                "MyAutoScalingGroup",
                LaunchConfigurationName=Ref(launch_config),
                DesiredCapacity=2,
                MinSize=2,
                MaxSize=2,
                VPCZoneIdentifier=[get_subnet(index=0),
                                   get_subnet(index=1)],
                TargetGroupARNs=[
                    Ref(target_group)
                ],  # Registers new EC2 instances with the default target group.
                Tags=[
                    Tag(
                        "Name", test_stack_name, True
                    )  # 'True' means: Attaches the same tags to the virtual machine started by this auto-scaling group
                ]))

        t.add_output([
            Output("URL", Value=Sub('http://${MyLoadBalancer.DNSName}')),
        ])
        dump_template(t, True)
        create_stack(test_stack_name, t)
        outputs = get_stack_outputs(test_stack_name)
        lb_url = get_output_value(outputs, 'URL')
        private_ips = set()
        for i in range(10):
            private_ips.add(run(f'curl {lb_url}', True))
        self.assertEqual(len(private_ips), 2)