Creating an Elastic Load Balancer

Introduction

Using Python to use the CDK to create an Application Load Balancer.

The Load Balancer will have an Auto Scaling Group as the target.

The EC2 instance (webserver) inside the Auto Scaling Group will have httpd installed, and serve a static index page.

This allows the Load Balancer to use a health check to see whether the page returns a 200 (OK) status.

The Load Balancer will expose a public dns name, this allows us to access the webserver.

Port 80 on the webserver will only be accessible via the Load Balancer so no direct access via the EC2 instance IP-Address.

2 stacks were created for this example.

1. NetworkStack

This stack defined the following components:

  • VPC
  • Public Subnets
  • Routing table
  • Internet gateway

2. ELBStack

This stack define the following components:

  • Auto Scaling Group with a single EC2 instance
  • Application Elastic Load Balancer

app.py

#!/usr/bin/env python3

from aws_cdk import core
from NetworkStack import NetworkStack
from ELBStack import ELBStack

# define custom properties
props = {
    'namespace':'elb',
    'vpc_name':'devops',
    'ec2_instance_name':'webserver',
    'wan_ip':'your_ip_address',
    'ec2_instance_type' : 't2.micro',
    'key_pair':'ec2-key-pair',
}

# create handle to the AWS account and region
env = core.Environment(account="your-aws-account-id",region="your-region")

app = core.App()

# calls the Network stack
network_stack = NetworkStack(app,f"{props['namespace']}-network",props,env=env)

# calls the Elastic Load Balancer stack, passing in additional properties defined inside the Network stack
elb_stack = ELBStack(app,f"{props['namespace']}-app",network_stack.output_props,env=env)
elb_stack.add_dependency(network_stack)

app.synth()

NetworkStack.py

from aws_cdk import (
    core,
    aws_ec2 as ec2
)

class NetworkStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, props, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # The code that defines your stack goes here

        # Create VPC 

        vpc = ec2.CfnVPC(
            self,
            "VPC",
            enable_dns_hostnames=True,
            enable_dns_support=True,
            cidr_block="10.0.0.0/16"

        )

        vpc.tags.set_tag(key="Name",value=props['vpc_name'])

        # Create Routing table for public subnet
        route_table_public = ec2.CfnRouteTable(
            self,
            "RtbPublic",
            vpc_id=vpc.ref
        )

        route_table_public.tags.set_tag(key="Name",value="EC2 Public Routing Table")

        # Create public subnet 1

        public_subnet1 = ec2.CfnSubnet(
            self,
            "PublicSubnet1",
            cidr_block="10.0.0.0/24",
            vpc_id=vpc.ref,
            map_public_ip_on_launch=True,
            availability_zone="eu-west-1a"
        )

        # Create public subnet 2 ( we need 2 subnets in different AZs to use for the load balancer )

        public_subnet2 = ec2.CfnSubnet(
            self,
            "PublicSubnet2",
            cidr_block="10.0.1.0/24",
            vpc_id=vpc.ref,
            map_public_ip_on_launch=True,
            availability_zone="eu-west-1b"
        )

        public_subnet1.tags.set_tag(key="Name",value="devops-public-subnet-1")
        public_subnet2.tags.set_tag(key="Name",value="devops-public-subnet-2")
        

        # Create internet gw

        inet_gateway = ec2.CfnInternetGateway(
            self,
            "DevOpsIgw",
            tags=[core.CfnTag(key="Name",value="devops-igw")]
        )

        # Attach gw to the VPC

        ec2.CfnVPCGatewayAttachment(
            self,
            "IgwAttachment",
            vpc_id=vpc.ref,
            internet_gateway_id=inet_gateway.ref
        )

        # Add gw to route to routing table

        ec2.CfnRoute(
            self,
            "RouteInetGateway",
            route_table_id=route_table_public.ref,
            destination_cidr_block="0.0.0.0/0",
            gateway_id=inet_gateway.ref

        )

       
        # Routing table association with public subnet
        ec2.CfnSubnetRouteTableAssociation(
            self,
            "RtbAssocPublic1",
            route_table_id=route_table_public.ref,
            subnet_id=public_subnet1.ref
        )

        # Routing table association with public subnet
        ec2.CfnSubnetRouteTableAssociation(
            self,
            "RtbAssocPublic2",
            route_table_id=route_table_public.ref,
            subnet_id=public_subnet2.ref
        )

        # The VPC id is needed inside the ELBStack, so copy this into the props
        self.output_props = props.copy()
        self.output_props['vpc_id']  = vpc.ref

    # make the new props available
    @property
    def outputs(self):
        return self.output_props

ELBStack.py

from aws_cdk import (
    core,
    aws_elasticloadbalancingv2 as elbv2,
    aws_elasticloadbalancingv2_targets as target,
    aws_ec2 as ec2,
    aws_autoscaling as autoscaling,
    aws_iam as iam
)

class ELBStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, props, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # The code that defines your stack goes here

         # define an EC2 security group

        sg_ec2 = ec2.CfnSecurityGroup(
            self,
            "ec2-sec-group",
            group_description="EC2 Security Group",
            vpc_id=props['vpc_id']
            
        )

        sg_ec2.tags.set_tag(key="Name",value="sg-devops-ec2")

        # define a security group rule to only allow ssh from your IP-Address
        ec2.CfnSecurityGroupIngress(
            self,
            "sg-ssh-ingress",
            ip_protocol="tcp",
            cidr_ip=props['wan_ip']+"/32",
            from_port=22,
            to_port=22,
            group_id=sg_ec2.ref
        )

        # define machine image ami

        amzn_linux_ami = ec2.MachineImage.latest_amazon_linux(
            edition=ec2.AmazonLinuxEdition.STANDARD,
            generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
            virtualization=ec2.AmazonLinuxVirt.HVM,
            storage=ec2.AmazonLinuxStorage.GENERAL_PURPOSE
        )

        # define the SSM role to attach to the EC2 instance

        ssm_role = iam.Role(
            self,
            "SSMRole",
            assumed_by=iam.ServicePrincipal(service="ssm.amazonaws.com"),
            managed_policies=[
                iam.ManagedPolicy.from_aws_managed_policy_name("AmazonSSMManagedInstanceCore")
            ]
        )


        # create autoscaling group, which only has 1 instance for testing

        asg = autoscaling.AutoScalingGroup(
            self,
            "AutoScalingGroup",
            instance_type=ec2.InstanceType(props['ec2_instance_type']),
            machine_image=amzn_linux_ami,
            associate_public_ip_address=True,
            update_type=autoscaling.UpdateType.REPLACING_UPDATE,
            desired_capacity=1,
            min_capacity=1,
            max_capacity=1,
            vpc=ec2.Vpc.from_lookup(self,"VPCLookup1",vpc_name=props['vpc_name']),
            vpc_subnets={'subnet_type':ec2.SubnetType.PUBLIC},
            security_group=ec2.SecurityGroup.from_security_group_id(self,"LookupSG",security_group_id=sg_ec2.ref),
            key_name=props['key_pair'],
            role=ssm_role,
            auto_scaling_group_name="asg-devops"
        )

        # Add user data script to install httpd, create an index page to serve and start the service

        asg.add_user_data("sudo yum -y update; sudo yum install httpd -y; sudo echo '<h1> This is a test page </h1>' > /var/www/html/index.html ; sudo systemctl start httpd; sudo systemctl enable httpd")

        # Create Application Loadbalancer

        lb = elbv2.ApplicationLoadBalancer(
            self,
            "ApplicationLoadbalancer",
            internet_facing=True,
            load_balancer_name="elb-apache",
            vpc=ec2.Vpc.from_lookup(self,"VPCLookup2",vpc_name=props['vpc_name']),
            
        )

        # Add a listener for HTTP
        listener = lb.add_listener("Listener",port=80)        
        listener.add_targets("Target", port=80, targets=[asg])

Extra notes

I ran into an issue setting up the Load Balancer listener target.

listener.add_targets("Target", port=80, targets=[asg])

I tried to look up an existing Auto Scaling Group and use this for targets=[asg]

However, this caused an error so I had to define the ASG inside the same stack, which you can see in the above code example.

Last updated on 21 Aug 2020
Published on 21 Aug 2020