Put an HTTPS Application Load Balancer in front of a service
Create an internet-facing Application Load Balancer in TypeScript (AWS SDK for JavaScript v3) — public subnets, security groups chained to the targets, a target group with the right target type and health check, an HTTPS listener with an ACM certificate, an HTTP-to-HTTPS redirect, and DNS — in the order that avoids each known failure.
markdownguidev4B4MM-3VY5stage: buildtrust: officialservice: elbinterface: sdkinterface: clicreate application load balancerCreateLoadBalancerCommandCreateTargetGroupCommandCreateListenerCommandtarget type ipHTTPS listenerACM certificateSslPolicyredirect HTTP to HTTPSinternet-facingpublic subnetssecurity group chainingCanonicalHostedZoneId
An internet-facing ALB goes in public subnets in at least two AZs, with its own security group open on 443. The targets' security group admits only the ALB's group. The listener uses an ACM certificate that is issued (not Pending Validation) and covers the domain clients will use, never the load balancer's default …amazonaws.com DNS name.
Before you start
- A certificate for your domain in ACM, in the ALB's Region (certificates can't be copied between Regions; request one per Region,
reference/acm/certificates.md), with status Issued. A certificate still inPending Validationcan't be used: finish DNS validation first. - Two public subnets in different AZs, each routed
0.0.0.0/0 → igw. An internet-facing load balancer in private subnets never answers. - Security groups:
alb-sgallows 443 (and 80, for the redirect) from clients, or from CloudFront's prefix list if CloudFront is in front.app-sgallows the target port fromalb-sg, which covers the health-check port too unless you use a different one (guides/vpc/build-private-network.md).
Create it (TypeScript, AWS SDK for JavaScript v3)
import {
ElasticLoadBalancingV2Client,
CreateLoadBalancerCommand,
CreateTargetGroupCommand,
CreateListenerCommand,
} from "@aws-sdk/client-elastic-load-balancing-v2";
const elb = new ElasticLoadBalancingV2Client({ region: "<region>" });
const lb = await elb.send(new CreateLoadBalancerCommand({
Name: "<app>-alb",
Type: "application",
Scheme: "internet-facing",
Subnets: ["<public subnet a>", "<public subnet b>"],
SecurityGroups: ["<alb sg>"],
}));
const lbArn = lb.LoadBalancers?.[0]?.LoadBalancerArn!;
const tg = await elb.send(new CreateTargetGroupCommand({
Name: "<app>-tg",
Protocol: "HTTP",
Port: 8080,
VpcId: "<vpc>",
TargetType: "ip", // "instance" for EC2, "ip" for Fargate/awsvpc, "lambda"
HealthCheckPath: "/healthz",
Matcher: { HttpCode: "200-399" },
}));
const tgArn = tg.TargetGroups?.[0]?.TargetGroupArn!;
await elb.send(new CreateListenerCommand({
LoadBalancerArn: lbArn,
Protocol: "HTTPS",
Port: 443,
Certificates: [{ CertificateArn: "<ACM certificate ARN, same Region as the ALB>" }],
SslPolicy: "ELBSecurityPolicy-TLS13-1-0-2021-06",
DefaultActions: [{ Type: "forward", TargetGroupArn: tgArn }],
}));
await elb.send(new CreateListenerCommand({
LoadBalancerArn: lbArn,
Protocol: "HTTP",
Port: 80,
DefaultActions: [{ Type: "redirect", RedirectConfig: { Protocol: "HTTPS", Port: "443", StatusCode: "HTTP_301" } }],
}));
console.log(lb.LoadBalancers?.[0]?.DNSName, lb.LoadBalancers?.[0]?.CanonicalHostedZoneId);
The parameter names are checked against the SDK's types. The CLI equivalents are aws elbv2 create-load-balancer, create-target-group and create-listener, with the same fields in kebab-case. The security-policy name is one the troubleshooting page lists as a backend policy for TLS 1.3 listeners. Choose the front-end policy your clients need (UNVERIFIED that this name is accepted as a listener policy; it is valid in the SDK's string type, which is unconstrained).
Choices that matter
TargetType:instanceregisters EC2 instance IDs;ipis required for Fargate and anyawsvpctask (patterns/alb-to-ecs-fargate.md);lambdafor functions, where 1 MB request and response bodies apply.- Health check: point at a cheap path that returns 2xx without authentication or host-based routing. The check's
Hostheader is the target's private IP. Defaults and ranges are inreference/elb/health-checks-and-timeouts.md. - Idle timeout is 60 s by default. Make the application's keep-alive timeout longer, or you get intermittent 502s.
Register targets
aws elbv2 register-targets --target-group-arn <tg> --targets Id=<instance-id-or-ip>,Port=8080
aws elbv2 describe-target-health --target-group-arn <tg>
A target is healthy after it passes one check, and is then marked healthy after 5 consecutive passes (defaults). Targets in an AZ that the load balancer doesn't have enabled show Target.NotInUse. ECS services and Auto Scaling groups register targets for you.
Point DNS at it
Create a Route 53 alias A record (and AAAA for dual-stack) whose target is the ALB's DNSName and CanonicalHostedZoneId (guides/route53/point-domain-at-aws.md). An alias works at the zone apex, follows the ALB's changing IPs, and costs nothing per query. Don't hard-code the ALB's IP addresses.
Protect it
deletion_protection.enabled=truefor production.- If CloudFront is in front, either restrict
alb-sgto the CloudFront origin-facing prefix list, or make the ALB internal and use a VPC origin (patterns/cloudfront-to-alb.md).
Verify
curl -sv https://<your-domain>/healthz
| Result | Next |
|---|---|
| Connection timeout | Subnets not public, alb-sg closed, or VPC Block Public Access (guides/vpc/diagnose-connection-timeout.md) |
NET::ERR_CERT_COMMON_NAME_INVALID | The domain isn't on the certificate, or you used the ALB's default DNS name |
| 503 | No healthy or registered targets |
| 502 / 504 | guides/elb/fix-alb-5xx.md |