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", )
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)
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
def set_health_check_codes(self, codes): self.health_check_matcher = Matcher(HttpCode=codes)
def __init__(self): super(TargetPath, self).__init__() self.health_check_matcher = Matcher(HttpCode="200-399")
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
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)) ])
], 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))
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)