AWS with Terraform Tutorial: AWS Load Balancers (16)
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. The previous section 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.
Prerequisites #
Read the previous sections of the tutorial, listed in the series index at the end of this page. This section builds on:
- AWS Auto Scaling: the group
ditwl-asg-front-end, - AWS Security Groups: the group
ditwl-sg-front-end, - AWS Route 53 (DNS): 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:
# 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 #
# 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.
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.
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.
# 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:
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 #
$ 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:
$ 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:
$ 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. 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 to configure the servers.