How To Assign Static Ip to AWS Load Balancers

Guillaume Vincent
Guillaume Vincent
The cover image of the article showing a balancer to illustrate the role of a load-balancer
Photo by Piret Ilver on Unsplash
Table of Contents
Table of Contents

The good load-balancer to choose and the way to do it with Terraform

Recently I have confronted myself with the need of using a static IP with load-balancers. The reason was to be able to whitelist IP addresses in firewalls. By making some researches, I have discovered that ELBv1 does not support this feature. ELBs manage IP addresses behind the scene you have no control over it.

The first step was to migrate to a network load-balancer (NLB). In the first section, we’re going to inspect its characteristics. Then we put in place a small Terraform project to assigned reserved public IP to an NLB.

Network Load Balancer

At first view, the NLB looks like ELBv1 and offers a single endpoint to the clients. But, it uses target group and listener concepts to manage backend and frontend :

  • Listeners wait for connection requests from clients following the defined protocol and port. It forwards the client requests to a configured target group.
  • Target groups request the targets that can be EC2 instances for example. A target can belong to several target groups. When defining a target you have to configure the protocol and the destination port. The target group also manages the health check of targets.

The AWS documentation lists the benefits of using an NLB. You can check it out here. NLB focuses on network level and has some limitations :

  • You cannot attach security groups to it. If you want to do it, you can attach them to the autoscaling group used by your target.
  • The attached security groups expect only CIDR blocks
  • Your security groups need to allow the NLB network interfaces for the health check.

Terraform

Data sources

The data sources are all in the same file and execute these actions :

  • Retrieve availability zones of the current region
  • Retrieve Ubuntu 20.04 AMI
  • Prepare shell script to install Apache server

The content of the shell script :

#!/bin/bash
sudo apt-get update -y
sudo apt-get install -y apache2
sudo systemctl start apache2
sudo systemctl enable apache2
echo "<h1>Deployed via Terraform</h1>" | sudo tee /var/www/html/index.html

Variables

Redundant variables are centralized into variables.tf. We’ll use one availability zone from the previous data source:

variable "tags" {
  type    = map(string)
  default = {
	  Environment = "dev"
	  Project     = "nlb-with-static-ip"
  }
}

variable "webserver_port" {
  type    = number
  default = 80
}

locals {
  availability_zone = element(data.aws_availability_zones.this.names, 0)
  name_prefix       = "${var.tags["Environment"]}_${var.tags["Project"]}"
}
variables.tf

Network

In network.tf we define a VPC, a public subnet, an internet gateway, and a route table:

resource "aws_vpc" "this" {
  cidr_block = "10.0.0.0/16"
  tags       = merge(
  {
	Name = "${local.name_prefix}_vpc"
  }, var.tags)
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
  tags              = merge(
  {
	Name = "${local.name_prefix}_igw"
  }, var.tags)
}

resource "aws_subnet" "public" {
  vpc_id            = aws_vpc.this.id
  cidr_block        = "10.0.0.0/24"
  availability_zone = local.availability_zone
  tags              = merge(
  {
	Name = "${local.name_prefix}_public_subnet"
  }, var.tags)
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.this.id

  route {
	cidr_block = "0.0.0.0/0"
	gateway_id = aws_internet_gateway.this.id
  }

  tags              = merge(
  {
	Name = "${local.name_prefix}_public_rt"
  }, var.tags)
}

resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}
network.tf

Security group

security-groups.tf defines a security group with a rule that authorizes HTTP traffic from everywhere. There will be no issue with the health check of the target group.

resource "aws_security_group" "public" {
  name        = "Allow Public Access"
  description = "Allow Public Access"
  vpc_id      = aws_vpc.this.id

  ingress {
	description = "HTTP from everywhere"
	from_port   = 80
	to_port     = 80
	protocol    = "tcp"
	cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
	from_port   = 0
	to_port     = 0
	protocol    = "-1"
	cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
	Name = "allow_public"
  }
}
security-groups.tf

Launch configuration

We define a launch configuration that :

  • Uses the Ubuntu AMI from the previous data source
  • Embeds the security group to expose HTTP traffic. As mentioned before this is not possible to add it directly to the NLB resource.
  • Handle user-data to install and configure the Apache webserver at boot
  • Have a create_before_destroy flag enabled in the lifecycle block to avoid conflicts in case of launch-configuration recreation
resource "aws_launch_configuration" "webserver" {
  name_prefix                 = "${local.name_prefix}_webserver"
  image_id                    = data.aws_ami.ubuntu.image_id
  instance_type               = "t2.micro"
  security_groups             = [
	aws_security_group.public.id]
  user_data                   = data.template_cloudinit_config.this.rendered

  lifecycle {
	create_before_destroy = true
  }
}
launch-configuration.tf

Network load balancer

The AWS EIP is reserved before the creation of the NLB because it has an implicit dependency. The EIP is mapped to the public subnet located in us-east-1a. You can map as many subnet and EIP you want using a dynamic block and iterate over the values.

A target group is configured to look for TCP/80 inside the VPC. It has also a health check to ensure all is fine. A listener is configured to forward incoming traffic from the NLB on TCP/80 to the target group :

resource "aws_eip" "nlb" {
  tags = merge(
  {
	Name = "${local.name_prefix}_nlb_eip"
  }, var.tags)
}

resource "aws_lb" "this" {
  name               = "${replace(local.name_prefix, "_", "-")}-nlb"
  internal           = false
  load_balancer_type = "network"

  subnet_mapping {
	subnet_id     = aws_subnet.public.id
	allocation_id = aws_eip.nlb.id
  }

  tags = merge(
  {
	Name = "${local.name_prefix}_nlb"
  }, var.tags
  )
}

resource "aws_lb_target_group" "http" {
  name        = "${replace(local.name_prefix, "_", "-")}-http"
  vpc_id      = aws_vpc.this.id
  port        = var.webserver_port
  protocol    = "TCP"
  target_type = "instance"

  health_check {
	enabled  = true
	interval = 30
	port     = var.webserver_port
  }

  tags = var.tags
}

resource "aws_alb_listener" "http" {
  load_balancer_arn = aws_lb.this.arn
  port              = var.webserver_port
  protocol          = "TCP"

  default_action {
	type             = "forward"
	target_group_arn = aws_lb_target_group.http.arn
  }
}
lb.tf

Autoscaling group

The security group is registered inside the autoscaling group and uses the launch-configuration to initiate EC2 instance :

resource "aws_autoscaling_group" "webserver" {
  launch_configuration = aws_launch_configuration.webserver.id
  max_size             = 1
  min_size             = 1

  # Target groups need to be registered in autoscaling group
  target_group_arns = [
  	aws_lb_target_group.http.arn
  ]

  vpc_zone_identifier = [
	aws_subnet.public.id
  ]

  tags = concat(
  [
	{
	  key                 = "Name"
	  value               = aws_launch_configuration.webserver.name
	  propagate_at_launch = true
	},
	{
	  key                 = "Environment"
	  value               = var.tags["Environment"]
	  propagate_at_launch = true
	},
	{
	  key                 = "Project"
	  value               = var.tags["Project"]
	  propagate_at_launch = true
	}
  ])
}
autoscaling-groups.tf

Validation

Deploy the stack

Init and deploy the stack :

$ terraform init
$ terraform apply

Check AWS web console

In β€œEC2 > Network & Security > Elastic IPs” we can see the public IP allocated to us-east-1 :

The Elastic IP (EIP) associated with the network load-balancer
The Elastic IP (EIP) associated with the network load-balancer

In β€œEC2 > Load-Balancing > Load-Balancers” we can ensure the NLB with the previous EIP mapped on the public subnet in us-east-1a availability zone :

The network load-balancer in the AWS web console
The network load-balancer in the AWS web console

We open a browser page to http://3.217.174.254 and we have the page configure in the user-data :

The web server page accessible using the public EIP
The web server page accessible using the public EIP

Resources

What is a Network Load Balancer? - Elastic Load Balancing
Automatically distribute incoming traffic across multiple targets using a Network Load Balancer.
Using static IP addresses for Application Load Balancers | Amazon Web Services
Update: You can use AWS Global Accelerator to get static IP addresses that act as a fixed entry point to your application endpoints in a single or multiple AWS Regions, such as your Application Load Balancers, Network Load Balancers or Amazon EC2 instances. These IP addresses are announced from mult…


Great! Next, complete checkout for full access to Getbetterdevops
Welcome back! You've successfully signed in
You've successfully subscribed to Getbetterdevops
Success! Your account is fully activated, you now have access to all content
Success! Your billing info has been updated
Your billing was not updated