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

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

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 in Pending Validation can'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-sg allows 443 (and 80, for the redirect) from clients, or from CloudFront's prefix list if CloudFront is in front. app-sg allows the target port from alb-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: instance registers EC2 instance IDs; ip is required for Fargate and any awsvpc task (patterns/alb-to-ecs-fargate.md); lambda for 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 Host header is the target's private IP. Defaults and ranges are in reference/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=true for production.
  • If CloudFront is in front, either restrict alb-sg to 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
ResultNext
Connection timeoutSubnets not public, alb-sg closed, or VPC Block Public Access (guides/vpc/diagnose-connection-timeout.md)
NET::ERR_CERT_COMMON_NAME_INVALIDThe domain isn't on the certificate, or you used the ALB's default DNS name
503No healthy or registered targets
502 / 504guides/elb/fix-alb-5xx.md

Read this document as markdown — the same bytes an agent receives.