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.