Provisioning an Application Load Balancer with Terraform

AWSTerraformLoad BalancingNetworkingInfrastructure as Code

I wrote about Network Load Balancers recently. You get a lot of mileage out of NLB’s, but sometimes you do need Layer 7 features.

One alternative is keeping the NLB and putting a reverse proxy like Traefik behind it. However, a simpler approach can be replacing both with another offering from AWS, the Application Load Balancer (ALB). In this post, I’ll show how to provision ALBs with help of the old trusty Terraform.

Setting up an Application Load Balancer

There are three main components to consider: The load balancer, the listeners, and the target groups. I wrote in detail about this in my previous article, so I won’t belabor too much. Most of the concepts are similar.


Let’s focus on some concrete aspects. For the aws_lb, we use a different load_balancer_type (application).

data "aws_subnet_ids" "this" {
  vpc_id = var.vpc_id

  tags = {
    Tier = "Public"

resource "aws_lb" "this" {
  name               = "basic-load-balancer"
  load_balancer_type = "application"
  subnets            = data.aws_subnet_ids.this.ids

  enable_cross_zone_load_balancing = true

Unlike NLBs, you can’t set static IPs for an ALB.


For an ALB, our listeners have to be either HTTPS or HTTP. We can choose to do SSL termination at this stage. Let’s see an example.

resource "aws_lb_listener" "this" {
  load_balancer_arn = aws_lb.this.arn

  port              = 443
  protocol          = "HTTPS"

  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = aws_acm_certificate.this.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.this.arn

data "aws_acm_certificate" "this" {
  domain = "${var.dns_record_name}.${var.dns_zone_name}"

You can read more about the different SSL Security policies here. I’m referencing an existing certificate for now. We’ll see how to provision it thanks to Route53 later on.

Target groups

Having carried the SSL termination in the step before, our target group is going to work over HTTP.

resource "aws_lb_target_group" "this" {
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  load_balancing_algorithm_type = "least_outstanding_requests"

  stickiness {
    enabled = true
    type    = "lb_cookie"

  health_check {
    healthy_threshold   = 2
    interval            = 30
    protocol            = "HTTP"
    unhealthy_threshold = 2

  depends_on = [

  lifecycle {
    create_before_destroy = true

resource "aws_autoscaling_attachment" "target" {
  autoscaling_group_name = var.autoscaling_group_name
  alb_target_group_arn   = aws_lb_target_group.this.arn

There are some extra features to show:

  • We’re customizing the load balancing algorithm.
  • We’re using sticky sessions through a cookie with the stickiness block.
  • We’ve got a custom health check with the health_check block.

Security Groups

Just like network load balancers, you need to allow traffic to flow through security groups.

resource "aws_security_group" "this" {
  description = "Allow connection between ALB and target"
  vpc_id      = var.vpc_id

resource "aws_security_group_rule" "ingress" {
  for_each = var.ports

  security_group_id =
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  type              = "ingress"
  cidr_blocks       = [""]

Path-based routing

Our load balancer is aware of the path and headers that we’re requesting, and thus we can make routing decisions based on that. We use extra aws_lb_listener_rule resources to enrich the regular listener.

resource "aws_lb_listener_rule" "redirect_based_on_path" {
  listener_arn = aws_lb_listener.this.arn

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.alternative_target.arn

  condition {
    path_pattern {
      values = ["/rest/v2/*"]

If the condition matches (in this case, we’re using a new version of the API in the URL), the default listener will be ignored. Instead, traffic is redirected to a different target group. Similarly, we can make other rules based on a specific header, or a hostname.

Setting up Route53

We’re surely not going to access the load balancer through an autogenerated domain, are we? We want pretty domains, using Route53. First, we set up a CNAME record.

data "aws_route53_zone" "this" {
  name = var.dns_zone_name

resource "aws_route53_record" "this" {
  name = var.dns_record_name
  type = "CNAME"

  records = [

  zone_id = data.aws_route53_zone.this.zone_id
  ttl     = "60"

We’re also setting up a certificate for the domain. It’s validated automatically through a DNS challenge.

resource "aws_acm_certificate" "this" {
  domain_name       = "${var.dns_record_name}.${var.dns_zone_name}"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true

resource "aws_acm_certificate_validation" "this" {
  certificate_arn         = aws_acm_certificate.this.arn
  validation_record_fqdns = [aws_route53_record.web_cert_validation.fqdn]

  lifecycle {
    create_before_destroy = true

resource "aws_route53_record" "web_cert_validation" {
  name = aws_acm_certificate.this.domain_validation_options.0.resource_record_name
  type = aws_acm_certificate.this.domain_validation_options.0.resource_record_type

  records = [aws_acm_certificate.this.domain_validation_options.0.resource_record_value]

  zone_id =
  ttl     = 60

  lifecycle {
    create_before_destroy = true

A word on pricing

As you can see on the pricing page, ALBs have the same fixed costs as NLBs but are more expensive when processing traffic (LCU vs NLCU). The pricing details are a bit arcane. A good rule of thumb is to use an ALB if there is a specific feature that you want to utilize, I guess.


A combination of NLBs and ALBs will cover most if not all your load balancing needs. The sheer amount of features can be overwhelming. I’ve found it easier to start focusing on the basics and discover the vast array of possibilities one by one.