---
cuppy_hub: aws
cuppy_path: guides/elb/create-https-alb.md
cuppy_section: docs
cuppy_doc: B4MM-3VY5
cuppy_doc_uuid: fda6e733-d0ac-41ed-9271-8ed7bc4389df
cuppy_permalink: https://cuppy.io/aws/d/B4MM-3VY5
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/guides/elb/create-https-alb
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: fbe95a2e9dbbc54e8fd8ddc5f0d2379c3415004cadbfb759bafb9c44fee09773
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Put an HTTPS Application Load Balancer in front of a service

**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)

```ts
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
```

| 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` |


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
