Example AWS Application Load Balancer (ALB)

AWS Cloud

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:

https://github.com/tristanhself/general/blob/67ce1f2922ad6893ab0cf6941f346b231fec36e3/aws/alb/alb-v2.yaml

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

Image Attribution

Leave a Reply

Your email address will not be published. Required fields are marked *