AWS Application Load Balancer Setup (ALB): HTTP to HTTPS

This guide walks you through setting up an AWS Application Load Balancer that forwards HTTP traffic to EC2 instances, then upgrades to HTTPS using a free ACM certificate with an automatic HTTP-to-HTTPS redirect. By the end, you will have a production-ready load balancer setup that most AWS web applications rely on as their public entry point.

Simple explanation

An Application Load Balancer sits in front of your EC2 instances and accepts incoming web requests so your instances do not have to be exposed directly to the internet. Instead of users hitting one EC2 instance directly, they hit the ALB, and the ALB decides which healthy instance should handle each request.

📞

The receptionist analogy: Think of the ALB as a receptionist at a busy office. Callers (users) contact the receptionist (ALB) rather than dialing each employee (EC2 instance) directly. The receptionist checks who is available, routes the call to the right person, and if someone is out sick (instance fails), automatically stops sending calls to that person until they are back.

The ALB also handles your HTTPS certificate. It accepts encrypted traffic from browsers, decrypts it, and forwards plain HTTP to your instances internally. Your application code never has to deal with TLS. The ALB handles it at the edge so each instance sees ordinary HTTP requests regardless of whether the user connected over HTTP or HTTPS.

How it works

Traffic flows through several layers before reaching your application. Each layer has a specific job:

Request flow

Internet → ALB (public subnets, ports 80/443) → Listener → Listener rules → Target group → EC2 instances (can be private subnets)

  • Listener: the front door of the ALB. A listener is bound to a port and protocol (for example, HTTP on port 80). Every ALB needs at least one listener. When you add HTTPS, you add a second listener on port 443.
  • Listener rules: conditions that decide what happens to each request. The default rule forwards everything to a target group. You can add path-based or host-based rules later to route different requests to different backends.
  • Target group: a collection of backend resources (EC2 instances in this guide) that will receive requests. The target group also runs health checks to know which instances are available. Only healthy instances receive traffic.
  • Health check: a periodic HTTP request the ALB sends to each target on a defined path (for example, /health). If the instance responds with HTTP 200, it is considered healthy. If it fails enough consecutive checks, it is removed from rotation until it recovers.

The ALB itself lives in public subnets so it can receive traffic from the internet. Your EC2 instances do not need to be publicly reachable. They can sit in private subnets and receive traffic only from the ALB. This is one of the main reasons to use a load balancer instead of exposing instances directly. See Subnets in AWS for how public and private subnets work.

What you are building

After completing this guide you will have created the following resources in AWS:

  • ALB security group: allows inbound TCP on ports 80 and 443 from anywhere (0.0.0.0/0). This is intentional for a public-facing load balancer.
  • EC2 instance security group: allows inbound TCP on port 80 only from the ALB security group. Instances are not reachable directly from the internet.
  • Target group: contains your EC2 instances and runs health checks against /health every 30 seconds.
  • Application Load Balancer: internet-facing, deployed across two public subnets in different Availability Zones for high availability.
  • HTTP listener (port 80): initially forwards to the target group, then modified to redirect all traffic to HTTPS.
  • HTTPS listener (port 443): terminates TLS using an ACM certificate and forwards decrypted traffic to the target group.
  • Route 53 alias record: maps your domain name to the ALB’s DNS name.

What you need before starting

  • A VPC with at least two public subnets in different Availability Zones. The ALB requires subnets in at least two AZs.
  • At least one EC2 instance running a web server, with a port your application listens on (this guide uses port 80).
  • AWS CLI installed and configured with IAM permissions for ec2, elbv2, acm, and route53.
  • A domain name managed in Route 53 (required for the HTTPS section). If you have not set this up yet, see Route 53 Setup.
Gather your IDs before you start

You will need your VPC ID and subnet IDs in several commands. Run aws ec2 describe-subnets and aws ec2 describe-vpcs to find them, or look them up in the AWS Console under VPC → Subnets. Having them ready avoids interruption mid-setup.

When to use this

This setup is the right choice when:

  • You are running a web application or REST API on EC2 and need a stable public entry point.
  • You want HTTPS without managing certificates on each instance.
  • You want instances to be unreachable directly from the internet, accessible only through the ALB.
  • You plan to scale horizontally (add more instances) and want traffic distributed automatically.
  • You want automatic failover if one of your instances goes down.

This is not the right setup if your traffic is non-HTTP (use a Network Load Balancer for TCP/UDP), if you only have one instance and no plans to scale (a simpler Elastic IP may be sufficient), or if all traffic is internal between services (use an internal load balancer instead).

Step 1: Create security groups

You need two security groups: one for the ALB and one for your EC2 instances. They work together. The ALB security group accepts traffic from the internet, and the EC2 security group accepts traffic only from the ALB security group. This keeps your instances unreachable directly from outside, even if they have public IP addresses.

Create the ALB security group first, then use its ID when writing the EC2 rule. Referencing the security group ID directly (rather than a CIDR block) is a core AWS pattern. If you later swap out the ALB’s security group, the EC2 rule automatically stops accepting traffic from the old group.

# Security group for the ALB — allows internet traffic on ports 80 and 443
aws ec2 create-security-group \
  --group-name alb-sg \
  --description "Security group for ALB" \
  --vpc-id vpc-0123456789abcdef0

ALB_SG_ID=$(aws ec2 describe-security-groups \
  --filters Name=group-name,Values=alb-sg \
  --query 'SecurityGroups[0].GroupId' --output text)

aws ec2 authorize-security-group-ingress \
  --group-id $ALB_SG_ID \
  --protocol tcp --port 80 --cidr 0.0.0.0/0

aws ec2 authorize-security-group-ingress \
  --group-id $ALB_SG_ID \
  --protocol tcp --port 443 --cidr 0.0.0.0/0

# Security group for EC2 instances — allows traffic from the ALB security group only
aws ec2 create-security-group \
  --group-name web-instance-sg \
  --description "Security group for web instances behind ALB" \
  --vpc-id vpc-0123456789abcdef0

WEB_SG_ID=$(aws ec2 describe-security-groups \
  --filters Name=group-name,Values=web-instance-sg \
  --query 'SecurityGroups[0].GroupId' --output text)

# Allow inbound from the ALB security group on port 80 only
aws ec2 authorize-security-group-ingress \
  --group-id $WEB_SG_ID \
  --protocol tcp --port 80 \
  --source-group $ALB_SG_ID
Attach the security group to your instances

Creating web-instance-sg does nothing unless it is actually attached to your EC2 instances. Either assign it at launch time, or modify the instance now via the Console or CLI. Skipping this step is the single most common reason health checks fail on the first attempt.

Success looks like: both commands return without error, and echo $ALB_SG_ID and echo $WEB_SG_ID each print an sg- prefixed ID.

Step 2: Create the target group

A target group is the list of backends the ALB can send traffic to. It is also where you configure the health check: the periodic request the ALB sends to each instance to confirm it is alive and ready.

🏥

The triage nurse analogy: A health check works like a triage nurse who checks in on each member of the team every 30 seconds. If a team member does not respond twice in a row, they are pulled from the rotation. Once they respond correctly two times in a row, they are added back. The ALB never sends a patient to someone who is not responding.

The health check path /health must exist on your web server and return HTTP 200. If your application does not have a dedicated health endpoint, you can use / temporarily. A proper health endpoint that verifies your application is actually working (not just that the web server is running) is better practice. Register your EC2 instance IDs at the bottom, substituting real IDs from your account.

# Create a target group on port 80
aws elbv2 create-target-group \
  --name web-targets \
  --protocol HTTP \
  --port 80 \
  --vpc-id vpc-0123456789abcdef0 \
  --target-type instance \
  --health-check-path /health \
  --health-check-interval-seconds 30 \
  --healthy-threshold-count 2 \
  --unhealthy-threshold-count 3 \
  --matcher HttpCode=200

TG_ARN=$(aws elbv2 describe-target-groups \
  --names web-targets \
  --query 'TargetGroups[0].TargetGroupArn' --output text)

# Register your EC2 instances with the target group
aws elbv2 register-targets \
  --target-group-arn $TG_ARN \
  --targets Id=i-0123456789abcdef0 Id=i-0987654321fedcba0

The —healthy-threshold-count 2 setting means an instance must pass two consecutive health checks (2 x 30 seconds = 60 seconds minimum) before it is considered healthy. During this window, the instance will not receive any traffic even if the ALB is already active. This is normal. Do not immediately assume something is broken if targets show “initial” or “unhealthy” right after creation.

Step 3: Create the load balancer

This command creates the actual ALB resource. The two most important flags are —scheme internet-facing (makes the ALB reachable from the public internet) and —subnets (which must be public subnets with a route to an internet gateway). Provide subnet IDs from two different Availability Zones. The ALB requires at least two AZs for high availability.

Why public subnets? The ALB nodes need to receive packets from internet users. If you place the ALB in a private subnet, it has no path to receive those packets, even if the scheme is set to “internet-facing.” The subnets are where the ALB nodes themselves live. Your EC2 instances’ subnets are separate and can be private. See Private vs Public IP Addresses for how this maps to actual network reachability.

# Create the ALB in two public subnets across different AZs
aws elbv2 create-load-balancer \
  --name web-alb \
  --type application \
  --scheme internet-facing \
  --subnets subnet-public-1a subnet-public-1b \
  --security-groups $ALB_SG_ID

ALB_ARN=$(aws elbv2 describe-load-balancers \
  --names web-alb \
  --query 'LoadBalancers[0].LoadBalancerArn' --output text)

ALB_DNS=$(aws elbv2 describe-load-balancers \
  --names web-alb \
  --query 'LoadBalancers[0].DNSName' --output text)

echo "ALB DNS name: $ALB_DNS"

The $ALB_DNS value is your ALB’s DNS name, something like web-alb-0123456789.us-east-1.elb.amazonaws.com. You will use this to test the setup and later to create a Route 53 alias record. The ALB typically takes 1 to 3 minutes to reach the “active” state.

Step 4: Create the listener on port 80

A listener is the rule that tells the ALB what to do when it receives a connection on a specific port. Here you create the HTTP listener on port 80. The default action is forward: send all incoming requests to the target group you created in Step 2.

Later, once HTTPS is working, you will modify this listener to redirect HTTP traffic to HTTPS instead of forwarding it. For now, forward is correct. It lets you verify the basic setup before adding the certificate layer.

# Create a listener that forwards all HTTP traffic to the target group
aws elbv2 create-listener \
  --load-balancer-arn $ALB_ARN \
  --protocol HTTP \
  --port 80 \
  --default-actions Type=forward,TargetGroupArn=$TG_ARN

echo "Listener created. ALB DNS: $ALB_DNS"
echo "Wait 1-3 minutes for the ALB to become active, then test:"
echo "curl http://$ALB_DNS/"

Step 5: Verify health checks and test

Before adding HTTPS, confirm the basic HTTP path works end-to-end. Check that your instances are showing as healthy in the target group, then make a real HTTP request to the ALB DNS name.

Wait before panicking

If targets show “unhealthy” or “initial,” wait at least 60 seconds first. Health checks run every 30 seconds and two must pass before a target is marked healthy. If they are still unhealthy after two full minutes, that is worth investigating. The most common cause is the security group rule from Step 1 not being applied to the instances.

# Check the health of registered targets
aws elbv2 describe-target-health \
  --target-group-arn $TG_ARN \
  --query 'TargetHealthDescriptions[*].[Target.Id,TargetHealth.State,TargetHealth.Description]' \
  --output table

# Test the load balancer
curl -I http://$ALB_DNS/

A healthy target shows healthy in the State column. A successful curl response shows HTTP/1.1 200 OK (or whatever status code your application returns). If targets are healthy but curl fails, check that the ALB security group allows inbound port 80 from 0.0.0.0/0.

Add HTTPS with ACM

Once HTTP is working, adding HTTPS requires three things: an ACM certificate for your domain, a new HTTPS listener on port 443, and updating the port 80 listener to redirect to HTTPS instead of forward.

First, request and validate an ACM certificate. See SSL Certificates with ACM for the full process. DNS validation is usually the simplest method and completes in minutes.

ACM certificates are region-specific

The ACM certificate must be in the same AWS region as your ALB. If your ALB is in us-east-1, request the certificate in us-east-1. It will not appear in the certificate list if it was created in a different region. CloudFront is the exception: it always requires certificates in us-east-1 regardless of distribution location. Do not confuse the two.

Once your certificate is validated and shows status “Issued” in ACM, run the following commands:

# Get the ACM certificate ARN
CERT_ARN=$(aws acm list-certificates \
  --query 'CertificateSummaryList[?DomainName==`app.example.com`].CertificateArn' \
  --output text)

# Create HTTPS listener using the ACM certificate
aws elbv2 create-listener \
  --load-balancer-arn $ALB_ARN \
  --protocol HTTPS \
  --port 443 \
  --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
  --certificates CertificateArn=$CERT_ARN \
  --default-actions Type=forward,TargetGroupArn=$TG_ARN

# Get the ARN of the HTTP listener you created in Step 4
HTTP_LISTENER_ARN=$(aws elbv2 describe-listeners \
  --load-balancer-arn $ALB_ARN \
  --query 'Listeners[?Port==`80`].ListenerArn' --output text)

# Modify the port 80 listener: redirect HTTP to HTTPS with a 301
aws elbv2 modify-listener \
  --listener-arn $HTTP_LISTENER_ARN \
  --default-actions Type=redirect,RedirectConfig='{Protocol=HTTPS,Port=443,StatusCode=HTTP_301}'

After running these commands, traffic to http://app.example.com automatically redirects to https://app.example.com with a 301 (permanent redirect). Browsers cache this redirect, so future visits go straight to HTTPS without an extra round-trip. Verify by running curl -I http://$ALB_DNS/ and checking for HTTP/1.1 301 Moved Permanently with a Location: https://… header in the response.

The TLS policy ELBSecurityPolicy-TLS13-1-2-2021-06 supports TLS 1.2 and 1.3 and is a safe default for most applications. It blocks TLS 1.0 and 1.1, which have known vulnerabilities.

Point your domain to the ALB

ALBs do not have static IP addresses. AWS changes the underlying IPs when it scales load balancer nodes, which means you cannot put an ALB IP address in a regular DNS A record. It will break without warning.

📬

The forwarding address analogy: An ALB’s DNS name is like a postal forwarding address. It does not matter if the physical destination changes, mail sent to the forwarding address always finds its way there. A Route 53 alias record works the same way: you point it at the forwarding address (the ALB DNS name) and Route 53 takes care of resolving it to whatever IPs are current.

An alias record is a Route 53-specific feature that points to an AWS resource by its DNS name. Route 53 resolves it to the current IPs automatically, at no extra charge per query. It also works on root domains (like example.com), where a standard CNAME is not allowed by DNS specifications.

The HostedZoneId inside the AliasTarget block is the hosted zone ID of the ALB itself, not your domain’s hosted zone ID. This value is region-specific. For us-east-1, it is Z35SXDOTRQ7X7K. For more on Route 53 hosted zones, see Route 53 Setup.

# Create a Route 53 alias record pointing to the ALB
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890ABCDE \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "app.example.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "web-alb-0123456789.us-east-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'

Replace Z1234567890ABCDE with your Route 53 hosted zone ID (found under Route 53 → Hosted zones), and replace the DNSName with the value of $ALB_DNS. DNS changes propagate in seconds within Route 53 but may take a few minutes to clear downstream resolver caches.

Common beginner mistakes

  1. Placing the ALB in private subnets for an internet-facing load balancer. An internet-facing ALB must be in public subnets: subnets with a route table entry pointing to an internet gateway. Placing it in private subnets means the load balancer cannot receive packets from the internet, even though the scheme is set to “internet-facing.” Your EC2 instances can still be in private subnets.

  2. Forgetting to update the EC2 security group. This is the most common reason health checks fail. After creating the ALB security group and attaching it to the ALB, you must add an inbound rule to the EC2 instances’ security group allowing port 80 from the ALB security group. Without this rule, health check requests are dropped silently and every target shows as unhealthy.

  3. Requesting the ACM certificate in the wrong region. ACM certificates are region-scoped. If your ALB is in us-east-1, the certificate must also be in us-east-1. It will not appear in the certificate list if it was created in a different region.

  4. Using a hardcoded IP address or CNAME instead of a Route 53 alias record. ALB IP addresses change. A hardcoded A record will eventually point nowhere. A CNAME on a root domain is invalid per DNS spec and will not work. Use a Route 53 alias record for all ALB-pointing DNS entries.

  5. Testing too soon after creating the ALB. ALBs take 1 to 3 minutes to provision, and health checks need at least 60 seconds (2 checks at 30-second intervals) to pass. Targets showing “initial” or “unhealthy” in the first two minutes is normal. Wait before concluding something is misconfigured.

  6. Health check path returning non-200. If your health check is set to path /health but that path returns 404, 302, or 500, the instance will be marked unhealthy. Make sure the health check path is a real endpoint on your application that returns exactly HTTP 200.

Troubleshooting

Most ALB problems fall into one of two categories: targets are unhealthy, or the ALB is reachable but requests return errors. For broader network diagnosis, see Troubleshooting Network Issues.

Targets show as unhealthy:

  • Check that the EC2 instances’ security group has an inbound rule allowing port 80 from the ALB security group, not from a CIDR range. It must reference the security group ID directly.
  • SSH into the instance and test the health check path locally: curl http://localhost/health. If this fails, the application is not running or the path is wrong.
  • Check the health check settings in the target group. If your application takes more than the default timeout (5 seconds) to respond, increase the timeout.
  • Enable ALB access logs or use VPC Flow Logs to confirm packets are actually reaching the instances.

ALB is reachable but returns 502 or 504:

  • 502 Bad Gateway: the ALB reached the target but got an invalid response. The application may be crashing or returning a non-HTTP response on certain requests.
  • 504 Gateway Timeout: the ALB reached the target but it did not respond within the idle timeout (default 60 seconds). Check for slow database queries or blocking operations in your application.
HTTPS certificate warning in the browser

If the browser shows a certificate warning after adding HTTPS, check two things: (1) the ACM certificate’s domain does not match the hostname you are accessing — the certificate must cover the exact domain or a wildcard that matches it; (2) the certificate status in ACM may still show “Pending validation” — confirm the DNS validation record was added correctly to Route 53 and wait for ACM to confirm.

Internet-facing ALB vs Internal Load Balancer

The setup in this guide creates an internet-facing ALB. The choice between internet-facing and internal comes down to one question: who is making the requests?

  • Internet-facing ALB: used when the caller is a user on the public internet. The ALB gets a public DNS name and its nodes have public IP addresses. This is the right choice for any public website, API, or application that users access from a browser or external client.
  • Internal ALB: used when the caller is another service inside your VPC or a connected network. The ALB gets a private DNS name that is only resolvable inside the VPC. This is the right choice for microservices talking to each other, backend APIs called only by other AWS services, or services accessed over a VPN or Direct Connect. For details, see Internal Load Balancers.

Both types use the same ALB features: target groups, health checks, listener rules, and TLS termination. The only difference is whether the ALB nodes get public or private IPs and how the DNS name resolves.

Typical real-world pattern

Applications with a public frontend and a private API layer typically use one internet-facing ALB for the frontend and one internal ALB for service-to-service calls. The setup steps in this guide apply to both. Only the —scheme flag and subnet selection change.

Frequently asked questions

Why do my health checks fail even though my application is running?

Three causes cover the vast majority of cases: (1) the EC2 instances security group does not allow inbound traffic from the ALB security group on the health check port — this is the most common cause; (2) the health check path returns a non-2xx status code, such as a 404 or 500; (3) the health check timeout is shorter than your application startup time. Check security group rules first, then the health check path, then application logs.

Can I use one ALB for both HTTP (port 80) and HTTPS (port 443)?

Yes, and you should. Create two listeners on the same ALB: one on port 80 and one on port 443. Configure the port 80 listener with a redirect action that sends all HTTP traffic to HTTPS with a 301 status code. The port 443 listener handles the actual traffic with a TLS certificate attached. You do not need a second load balancer for HTTPS.

How long does it take for a new ALB to become available?

The ALB itself takes 1 to 3 minutes to provision. Health checks need to pass before targets are marked healthy and receive real traffic. With the defaults used in this guide (2 healthy thresholds at 30-second intervals), allow at least 60 seconds after the ALB shows as active before expecting your targets to serve traffic.

Do I need to open port 443 on my EC2 instances security group?

No. TLS termination happens at the ALB. The ALB decrypts HTTPS traffic and forwards plain HTTP to your instances on port 80. Your EC2 instances only need to accept traffic on whatever port your application listens on (usually port 80) from the ALB security group. Port 443 on EC2 is only needed if you configure end-to-end encryption, which is not covered in this basic setup.

Why use a Route 53 alias record instead of a regular CNAME or A record for the ALB?

ALB DNS names do not have stable IP addresses. The IPs behind an ALB change as AWS scales the load balancer nodes, so you cannot hardcode them in an A record. A CNAME works technically but costs money for each DNS lookup and cannot be used on a root domain (like example.com). A Route 53 alias record is free for AWS resources, resolves automatically to the current ALB IPs, and works on both root and subdomain records.

Last verified: 21 April 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.