The AWS application load balancer is a key infrastructure component for providing access to your application to the Internet, or even for use within your VPC between components.
In this example we’ll create a VPC, then within deploy 2 private and 2 public subnets, we’ll then attach an Internet Gateway, a NAT gateway (to allow the private subnet instances to reach the Internet) and finally deploy two EC2 instances that sit behind the Application Load Balancer and have the example application published.
You can find the AWS CloudFormation template here:
Let’s step over some of the key parts, but broken down. First, the VPC and subnets:
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
EnableDnsSupport: true
EnableDnsHostnames: true
CidrBlock: 192.168.0.0/16
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-VPC"
# Public and Private Subnets
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: eu-west-2a
CidrBlock: 192.168.0.0/24
MapPublicIpOnLaunch: true # Makes any instance launched within get a public IP address assigned, note this is different from the elastic IP.
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-PublicSubnet-1"
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: eu-west-2b
CidrBlock: 192.168.100.0/24
MapPublicIpOnLaunch: true # Makes any instance launched within get a public IP address assigned, note this is different from the elastic IP.
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-PublicSubnet-2"
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: eu-west-2a
CidrBlock: 192.168.1.0/24
MapPublicIpOnLaunch: false # Makes any instance launched within get a public IP address assigned, note this is different from the elastic IP.
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-PrivateSubnet-1"
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: eu-west-2b
CidrBlock: 192.168.101.0/24
MapPublicIpOnLaunch: false # Makes any instance launched within get a public IP address assigned, note this is different from the elastic IP.
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-PrivateSubnet-2"
All quite straight forward, we create two private and two public subnets, notice the “MapPublicIPOnLaunch” attribute on the public subnet, this is set to “true” to ensure anything connected is given a globally routable IPv4 address at creation.
Next we deploy the Internet Gateway, we attach it to the VPC with a VPCGatewayAttachment, simple enough. The next parts are separate from this, the Elastic IP address is a static IP address created to be assigned to the NAT Gateway. A NAT Gateway you ask, well that is used to provide Internet access to any instances on the private subnets. By default only a public subnet gets Internet access, but in many cases you’ll want your instances on the private subnet to get access to the Internet if not just for updates, note though this access is one way, i.e. egress/outbound only.
# Internet Gateway and Attachment to VPC
InternetGateway:
Type: AWS::EC2::InternetGateway
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# Elastic IP Address for NAT Gateway
NatGWPublicIP:
Type: AWS::EC2::EIP
DependsOn: VPC
Properties:
Domain: vpc
# NAT Gateway (to provide egress only Internet access from Private Subnets)
NatGateway:
Type: AWS::EC2::NatGateway
DependsOn: NatGWPublicIP
Properties:
SubnetId: !Ref PublicSubnet1
AllocationId: !GetAtt NatGWPublicIP.AllocationId
Good stuff, now let’s look at the routing tables. We have a Public Route Table which is used by the public subnets, and a Private Route Table that is used by the private subnets. These Route Tables tell instances where they need to go to reach other resources and/or the Internet. They are very similar, the only real difference in this example is the “GatewayId” and “NatGatewayId” between the public and private route tables.
# Public Routing
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PublicRoute:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
RouteTableId: !Ref PublicRouteTable
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet1
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet2
# Private Routing
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PrivateRoute:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet1
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet2
Now we are getting to the juicy bits, the Application Load Balancer (excuse the ELB name, that was left in by accident). So we create a Security Group that defines what traffic is allowed into the ALB, in this case we are allowing port 80 (HTTP) from anywhere (0.0.0.0/0). Then we define the Application Load Balancer itself, and bind it to the two Public Subnets we created, i.e. there will be an endpoint in each of these which gets an IPv4 address (essentially) so it can be reached, and finally binds itself to the Security Group we just created.
# ELB Security Group allowing Port 80 from anywhere
ELBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: 'Allow inbound HTTP access to Application Load Balancer'
VpcId:
Ref: VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
ApplicationLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing # or internal
Subnets:
- Ref: PublicSubnet1
- Ref: PublicSubnet2
SecurityGroups:
- Ref: ELBSecurityGroup
Okay, now we need to create a Listener, the ALB is essentially a placeholder for other components, in this case the Listener and Target Group. The Listener is the externally facing part of the ALB, its what listens for client connections (in this example out on the Internet) and in this example port 80 (HTTP). The other part is the Target Group, the target group is to what the Listener will send the traffic, in our case we want it to go to our two EC2 instances (which we will create in a moment).
EC2TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 30
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 15
HealthyThresholdCount: 5
Matcher:
HttpCode: '200'
Name: EC2TargetGroup
Port: 80
Protocol: HTTP
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: '20'
Targets:
- Id: !Ref EC2Instance1
- Id: !Ref EC2Instance2
UnhealthyThresholdCount: 3
VpcId:
Ref: VPC
Tags:
- Key: Name
Value: EC2TargetGroup
- Key: Port
Value: 80
Okay, now we define our two EC2 instances, along with a Security Group which restricts what can reach these instances. They’ll be deployed into the private subnet so they are not directly reachable from the Internet anyway, but in this case we add them to a security group which only allows traffic from the ALB Security Group we created a few steps back.
# eC2 Instance and security group
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: 'Allow inbound HTTP access to EC2 instance.'
VpcId:
Ref: VPC
EC2SecurityGroupIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: "Rule to allow connections to EC2 instance from the ELBSecurityGroup"
GroupId: !Ref EC2SecurityGroup
SourceSecurityGroupId: !Ref ELBSecurityGroup
FromPort: 80
IpProtocol: tcp
ToPort: 80
EC2Instance1:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-028a5cd4ffd2ee495 # This specifies a particular AMI Linux 2 image, youd probably want it to select a suitable from a pre-created list for each region.
InstanceType: t2.medium
IamInstanceProfile: !Ref Ec2SsmInstanceProfile
SubnetId: !Ref PrivateSubnet1
SecurityGroupIds:
- !Ref EC2SecurityGroup
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-EC2Instance-1"
UserData:
Fn::Base64: !Sub |
#!/bin/bash
echo "---- UserData Start ----"
apt update
apt install -y apache2
echo "Hello World from $(hostname -f)" > /var/www/html/index.html
echo "---- UserData Complete ----"
EC2Instance2:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-028a5cd4ffd2ee495 # This specifies a particular AMI Linux 2 image, youd probably want it to select a suitable from a pre-created list for each region.
InstanceType: t2.medium
IamInstanceProfile: !Ref Ec2SsmInstanceProfile
SubnetId: !Ref PrivateSubnet2
SecurityGroupIds:
- !Ref EC2SecurityGroup
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-EC2Instance-2"
UserData:
Fn::Base64: !Sub |
#!/bin/bash
echo "---- UserData Start ----"
apt update
apt install -y apache2
echo "Hello World from $(hostname -f)" > /var/www/html/index.html
echo "---- UserData Complete ----"
# The Outputs are bits of information that the Cloudformation template will output once it completes running.
# For example if you are creating EC2 instances, this can output the IP addresses, auto-generated name and so on.
Outputs:
VpcId:
Description: The VPC ID
Value: !Ref VPC
ALBHostName:
Description: 'Application Load Balancer Hostname'
Value:
!GetAtt ApplicationLoadBalancer.DNSName
ApplicationLoadBalancer:
Description: 'Application Load Balancer'
Value:
Ref: ApplicationLoadBalancer
And finally lets look at the SSM Role, what this does is provide a role that the EC2 instances are part of which you can use access the EC2 instance console without use of SSH.
# Declare Role
# We declare the role, it will create it with an autogenerated name.
Ec2SsmIamRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${AWS::StackName}-Ec2SsmIamRole"
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ec2.amazonaws.com]
Action: [sts:AssumeRole]
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Tags:
- Key: Name
Value: !Sub "${AWS::StackName}-Ec2SsmIamRole"
Ec2SsmInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub "${AWS::StackName}-Ec2SsmIamRole-InstanceProfileName"
Path: /
Roles:
- !Ref Ec2SsmIamRole
That’s it! Now, I stepped through this config bit by bit, but in reality, you’d deploy the template in one fell swoop, with a command like:
aws cloudformation create-stack --template-body file://alb-v2.yaml --stack-name albtest --capabilities CAPABILITY_NAMED_IAM
In this case the stack-name is the name of the CloudFormation Stack that will be created into which your resources will go.
Conclusion
Hopefully this has given you an overview of deploying a simple Application Load Balancer deployment, you can add a listener for HTTPS which can use a certificate to secure connections from clients, you can also make the HTTP listener redirect to HTTPS automatically. There are also a number of other tweaks that you can make to change the behaviour of the load balancer, for example Sticky Sessions.
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-elb.html