# AWS with Terraform Tutorial: AWS Load Balancers (16)

> Use Terraform to create an Application Load Balancer with a target group, listeners and HTTPS in front of an Auto Scaling group on AWS.

- Source: https://www.itwonderlab.com/aws-terraform-tutorial-aws-load-balancers/
- Published: 2026-10-05
- Updated: 2026-10-05
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## How to create AWS Application Load Balancers with Terraform

**Using the Terraform `aws_lb`, `aws_lb_target_group` and `aws_lb_listener` resource blocks to distribute the traffic between the instances of an Auto Scaling group.**

Welcome to our tutorial series about [Terraform or OpenTofu on AWS](https://www.itwonderlab.com/tag/aws-terraform-tutorial/). The [previous section](https://www.itwonderlab.com/aws-terraform-tutorial-aws-auto-scaling/) created a group of front-end instances whose IP addresses change all the time. In this section an Application Load Balancer becomes the single, stable entry point: it receives the traffic from the Internet, sends it only to healthy instances and, with a certificate, serves it over HTTPS.

![AWS with Terraform: The Essential Guide, 21 sections. Select a section to open its tutorial.](https://www.itwonderlab.com/media/tutorials/AWS-Terraform-Essentials/ITWL-Tutorials-AWS-Terraform-Essentials-Steps.svg)

### Prerequisites

Read the previous sections of the tutorial, listed in the [series index](#series) at the end of this page. This section builds on:

- [AWS Auto Scaling](https://www.itwonderlab.com/aws-terraform-tutorial-aws-auto-scaling/): the group `ditwl-asg-front-end`,
- [AWS Security Groups](https://www.itwonderlab.com/aws-terraform-tutorial-aws-security-groups/): the group `ditwl-sg-front-end`,
- [AWS Route 53 (DNS)](https://www.itwonderlab.com/aws-with-terraform-tutorial-aws-route-53/): the public zone `ditwl-r53-public` (only for the DNS name and HTTPS).

### AWS Load Balancers

Elastic Load Balancing offers several types of load balancers:

| Type | Layer | Use it for |
|---|---|---|
| **Application Load Balancer (ALB)** | 7 (HTTP/HTTPS) | Web sites and APIs: routing by host name, path or header, redirects, HTTPS termination |
| **Network Load Balancer (NLB)** | 4 (TCP/UDP/TLS) | Very high performance, static IP addresses, non-HTTP protocols |
| **Gateway Load Balancer (GWLB)** | 3 | Firewalls and traffic inspection appliances |

This tutorial uses an **Application Load Balancer**. It has four parts:

- the **load balancer** (`aws_lb`), placed in the public subnets of at least two Availability Zones,
- a **security group** that controls who can reach it,
- a **target group** (`aws_lb_target_group`): the list of instances that receive the traffic and the health check used to know which are healthy,
- a **listener** (`aws_lb_listener`): the port and protocol the load balancer listens on and what it does with the requests.

### Definition of an Application Load Balancer with Terraform

#### Security groups

The load balancer accepts HTTP from the Internet. The front-end instances now accept HTTP **only from the load balancer**, which means nobody can reach them directly. Remove the rule `ditwl-sr-internet-to-front-end-http` created in the Security Groups section and add:

```hcl title="terraform-aws-tutorial.tf"
# Security Group for the load balancer
resource "aws_security_group" "ditwl-sg-alb-front-end" {
  name        = "ditwl-sg-alb-front-end"
  vpc_id      = aws_vpc.ditlw-vpc.id
  description = "Load balancer of the front-end servers"
}

# Allow access from the Internet to port 80 HTTP in the load balancer
resource "aws_security_group_rule" "ditwl-sr-internet-to-alb-http" {
  security_group_id = aws_security_group.ditwl-sg-alb-front-end.id
  type              = "ingress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"] # Internet
  description       = "Allow access from the Internet to port 80 in the load balancer"
}

# Allow the load balancer to reach port 80 in the front-end servers
resource "aws_security_group_rule" "ditwl-sr-alb-to-front-end-egress" {
  security_group_id        = aws_security_group.ditwl-sg-alb-front-end.id
  type                     = "egress"
  from_port                = 80
  to_port                  = 80
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.ditwl-sg-front-end.id
  description              = "Allow traffic from the load balancer to the front-end servers"
}

# Allow access from the load balancer to port 80 in the front-end servers
resource "aws_security_group_rule" "ditwl-sr-alb-to-front-end-http" {
  security_group_id        = aws_security_group.ditwl-sg-front-end.id
  type                     = "ingress"
  from_port                = 80
  to_port                  = 80
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.ditwl-sg-alb-front-end.id
  description              = "Allow access from the load balancer to port 80 in the front-end"
}
```

Referencing a security group instead of an IP range is a best practice: the rule keeps working when the load balancer changes its addresses.

#### Load balancer, target group and listener

```hcl title="terraform-aws-tutorial.tf"
# Application Load Balancer in the two public subnets
resource "aws_lb" "ditwl-alb-front-end" {
  name                       = "ditwl-alb-front-end"
  load_balancer_type         = "application"
  internal                   = false
  security_groups            = [aws_security_group.ditwl-sg-alb-front-end.id]
  subnets                    = [aws_subnet.ditwl-sn-za-pro-pub-00.id, aws_subnet.ditwl-sn-zb-pro-pub-04.id]
  drop_invalid_header_fields = true
}

# Target group: the front-end instances and how to check that they are healthy
resource "aws_lb_target_group" "ditwl-tg-front-end" {
  name                 = "ditwl-tg-front-end"
  port                 = 80
  protocol             = "HTTP"
  vpc_id               = aws_vpc.ditlw-vpc.id
  deregistration_delay = 30 # seconds to finish the requests in progress before removing an instance

  health_check {
    path                = "/"
    matcher             = "200"
    interval            = 15
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 3
  }
}

# Listener: HTTP on port 80 forwards the requests to the target group
resource "aws_lb_listener" "ditwl-lbl-front-end-http" {
  load_balancer_arn = aws_lb.ditwl-alb-front-end.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.ditwl-tg-front-end.arn
  }
}

output "ditwl-alb-front-end-dns" {
  value = aws_lb.ditwl-alb-front-end.dns_name
}
```

#### Connect the Auto Scaling group to the target group

Modify the Auto Scaling group from the previous section. Two arguments change: the group registers its instances in the target group, and it uses the load balancer health check, so an instance that does not answer is replaced and not only the ones that are stopped.

```hcl title="terraform-aws-tutorial.tf"
resource "aws_autoscaling_group" "ditwl-asg-front-end" {
  # ... the rest of the arguments do not change ...
  target_group_arns = [aws_lb_target_group.ditwl-tg-front-end.arn]
  health_check_type = "ELB"
}
```

For a production environment, move the instances to the private subnets (`vpc_zone_identifier = [aws_subnet.ditwl-sn-za-pro-pri-02.id, aws_subnet.ditwl-sn-zb-pro-pri-06.id]`). Only the load balancer is then exposed to the Internet, and the instances use the NAT Gateways to download packages.

#### A DNS name for the load balancer

An **alias record** in the public zone points `www` to the load balancer. Unlike a CNAME, it is free and can also be used for the zone apex.

```hcl title="terraform-aws-tutorial.tf"
resource "aws_route53_record" "ditwl-r53-public-www" {
  zone_id = aws_route53_zone.ditwl-r53-public.zone_id
  name    = "www.${aws_route53_zone.ditwl-r53-public.name}"
  type    = "A"

  alias {
    name                   = aws_lb.ditwl-alb-front-end.dns_name
    zone_id                = aws_lb.ditwl-alb-front-end.zone_id
    evaluate_target_health = true
  }
}
```

### HTTPS with AWS Certificate Manager

AWS Certificate Manager (ACM) issues free public certificates for the load balancer. The certificate is validated with a DNS record, which Terraform creates in the public zone, so the zone must be delegated and working.

```hcl title="terraform-aws-tutorial.tf"
# Certificate for www.demo.itwonderlab.com
resource "aws_acm_certificate" "ditwl-acm-www" {
  domain_name       = "www.${aws_route53_zone.ditwl-r53-public.name}"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

# DNS records that prove that we own the domain
resource "aws_route53_record" "ditwl-r53-acm-www-validation" {
  for_each = {
    for o in aws_acm_certificate.ditwl-acm-www.domain_validation_options : o.domain_name => {
      name   = o.resource_record_name
      record = o.resource_record_value
      type   = o.resource_record_type
    }
  }

  allow_overwrite = true
  zone_id         = aws_route53_zone.ditwl-r53-public.zone_id
  name            = each.value.name
  type            = each.value.type
  records         = [each.value.record]
  ttl             = 60
}

# Wait until the certificate is issued
resource "aws_acm_certificate_validation" "ditwl-acm-www" {
  certificate_arn         = aws_acm_certificate.ditwl-acm-www.arn
  validation_record_fqdns = [for r in aws_route53_record.ditwl-r53-acm-www-validation : r.fqdn]
}

# HTTPS listener
resource "aws_lb_listener" "ditwl-lbl-front-end-https" {
  load_balancer_arn = aws_lb.ditwl-alb-front-end.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate_validation.ditwl-acm-www.certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.ditwl-tg-front-end.arn
  }
}
```

Then allow port 443 in `ditwl-sg-alb-front-end` (a second `aws_security_group_rule` like `ditwl-sr-internet-to-alb-http` with port 443), and change the HTTP listener to redirect to HTTPS:

```hcl title="terraform-aws-tutorial.tf"
resource "aws_lb_listener" "ditwl-lbl-front-end-http" {
  load_balancer_arn = aws_lb.ditwl-alb-front-end.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}
```

### Run the Terraform Plan

```shell
$ tofu plan
$ tofu apply
```

The ALB takes a few minutes to be active and the instances need to pass the health checks before they receive traffic. Then test it:

```shell
$ tofu output ditwl-alb-front-end-dns
$ curl http://<alb-dns-name>/
front-end ip-172-21-1-35
$ curl http://<alb-dns-name>/
front-end ip-172-21-5-120
```

Each request can be answered by a different instance: the response shows the name of the instance that served it. Check the status of the targets in the AWS console (EC2 → Target Groups → `ditwl-tg-front-end` → Targets) or with `aws elbv2 describe-target-health`.

To destroy the infrastructure and avoid charges:

```shell
$ tofu destroy
```

### AWS Load Balancers Cost

An Application Load Balancer is billed **per hour** while it exists plus per **Load Balancer Capacity Unit (LCU)** consumed (new connections, active connections, processed bytes and rule evaluations). A small test environment costs a few cents a day, but a forgotten load balancer is a monthly charge, so destroy it after the tests. See the [Elastic Load Balancing pricing](https://aws.amazon.com/elasticloadbalancing/pricing/). ACM public certificates are free.

### Common Questions About AWS Load Balancers

#### Should I use an Application or a Network Load Balancer?

Use an ALB for HTTP and HTTPS applications: it understands the requests and can route by host name or path. Use an NLB when you need static IP addresses, very low latency or protocols that are not HTTP.

#### How do I send different paths to different target groups?

Add `aws_lb_listener_rule` resources to a listener with a `condition` on the path (`/api/*`) or the host name and an `action` that forwards to another target group.

#### Why does the target group show unhealthy instances?

Check that the security group of the instances allows the traffic from the load balancer, that the application listens on the target group port and that the `health_check` path returns the code in `matcher`. The `health_check_grace_period` of the Auto Scaling group must be long enough for the instance to boot.

#### Can I terminate HTTPS in the instances instead?

You can, with an NLB in TLS passthrough mode, but terminating HTTPS in the ALB is simpler: ACM renews the certificates automatically and the instances do not handle certificates.

### Next Steps

So far Terraform and AWS were used together to create the infrastructure. The next section shows how to use [Terraform, AWS and Ansible together](https://www.itwonderlab.com/terraform-aws-ansible/) to configure the servers.
