TL;DR: A customer portal on AWS "didn't open" even though the app and the load balancer were healthy. Two problems were stacked: the domain's DNS lived in Cloudflare (not Route 53) with no record for the hostname, and the Application Load Balancer was internal, so it only had private IPs. Here's how I found both and fixed them, with every AWS CLI command.
🧩 The problem
A customer-facing portal on a new hostname (let's call it portal.example.com) did not load from the internet. In the AWS console, everything looked green:
- ✅ Application running
- ✅ Targets healthy
- ✅ ALB active
But from outside the office: nothing.
| # | Problem | Why it breaks |
|---|---|---|
| 1 | No DNS record in the authoritative DNS provider (Cloudflare) | The hostname resolves to nothing |
| 2 | The ALB scheme was internal | Internal ALBs get only private IPs, unreachable from the internet |
Let's fix them one at a time.
🔍 Step 1: Find out who is authoritative for the domain
Don't assume Route 53. Ask DNS directly:
dig +short NS example.com
If you see names like *.ns.cloudflare.com, then Cloudflare answers for this domain. Any Route 53 hosted zone for it is ignored by the internet.
Check whether a Route 53 zone exists anyway (a common source of confusion):
aws route53 list-hosted-zones-by-name --dns-name example.com \
--query 'HostedZones[*].[Name,Id,Config.PrivateZone]' --output table
Now check the hostname itself:
dig +short portal.example.com
💡 No answer = no record. That's problem one.
🔍 Step 2: Check if the ALB is internal or internet-facing
aws elbv2 describe-load-balancers \
--query 'LoadBalancers[*].[LoadBalancerName,Scheme,DNSName]' --output table
If Scheme is internal, resolve its DNS name:
dig +short internal-portal-alb-1234567890.REGION.elb.amazonaws.com
You'll get private IPs (10.x, 172.16-31.x or 192.168.x). Nobody outside the VPC or connected networks can reach them. That's problem two.
⚠️ The ALB scheme can't be changed after creation. You need a new, internet-facing ALB.
🌐 Step 3: Confirm you have public subnets
An internet-facing ALB needs at least two public subnets in different Availability Zones. A public subnet has a route to an Internet Gateway:
aws ec2 describe-route-tables \
--filters Name=association.subnet-id,Values=SUBNET_ID \
--query 'RouteTables[*].Routes[?starts_with(GatewayId, `igw-`)]' --output json
An empty result means that subnet is private.
🔐 Step 4: Create a security group for the public ALB
SG_ID=$(aws ec2 create-security-group \
--group-name portal-public-alb-sg \
--description "Public ALB for portal" \
--vpc-id VPC_ID --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $SG_ID \
--protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id $SG_ID \
--protocol tcp --port 80 --cidr 0.0.0.0/0
💡 If Cloudflare proxies the traffic (orange cloud), you can restrict ingress to Cloudflare's published IP ranges instead of
0.0.0.0/0.
Also allow this security group in the targets' security group on the app port.
📜 Step 5: Request an ACM certificate and validate it in Cloudflare
CERT_ARN=$(aws acm request-certificate \
--domain-name portal.example.com \
--validation-method DNS --query CertificateArn --output text)
aws acm describe-certificate --certificate-arn $CERT_ARN \
--query 'Certificate.DomainValidationOptions[0].ResourceRecord'
Create that CNAME in Cloudflare with the proxy off (grey cloud), then wait until it's issued:
aws acm wait certificate-validated --certificate-arn $CERT_ARN
⚙️ Step 6: Create the internet-facing ALB, target group and listeners
Create the ALB:
ALB_ARN=$(aws elbv2 create-load-balancer \
--name portal-public-alb \
--scheme internet-facing --type application \
--subnets PUBLIC_SUBNET_A PUBLIC_SUBNET_B \
--security-groups $SG_ID \
--query 'LoadBalancers[0].LoadBalancerArn' --output text)
Create the target group and register targets:
TG_ARN=$(aws elbv2 create-target-group \
--name portal-tg --protocol HTTP --port 8080 \
--vpc-id VPC_ID --target-type ip \
--health-check-path /health \
--query 'TargetGroups[0].TargetGroupArn' --output text)
aws elbv2 register-targets --target-group-arn $TG_ARN \
--targets Id=TARGET_IP_1 Id=TARGET_IP_2
HTTPS listener + HTTP to HTTPS redirect:
aws elbv2 create-listener --load-balancer-arn $ALB_ARN \
--protocol HTTPS --port 443 --certificates CertificateArn=$CERT_ARN \
--ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
--default-actions Type=forward,TargetGroupArn=$TG_ARN
aws elbv2 create-listener --load-balancer-arn $ALB_ARN \
--protocol HTTP --port 80 \
--default-actions 'Type=redirect,RedirectConfig={Protocol=HTTPS,Port=443,StatusCode=HTTP_301}'
Use the port, target type and health check path your app actually uses. Then check target health:
aws elbv2 describe-target-health --target-group-arn $TG_ARN \
--query 'TargetHealthDescriptions[*].[Target.Id,TargetHealth.State,TargetHealth.Reason]' \
--output table
🌍 Step 7: Add the DNS record in Cloudflare
Get the ALB's DNS name:
aws elbv2 describe-load-balancers --load-balancer-arns $ALB_ARN \
--query 'LoadBalancers[0].DNSName' --output text
In Cloudflare, create a CNAME record:
| Type | Name | Target |
|---|---|---|
| CNAME | portal |
portal-public-alb-xxxx.REGION.elb.amazonaws.com |
⚠️ Never use an A record with the ALB's IPs. They change.
If you turn the Cloudflare proxy on, set SSL/TLS mode to Full (strict), since the ALB has a valid ACM certificate.
✅ Step 8: Test from outside
dig +short portal.example.com
curl -sv https://portal.example.com/health 2>&1 | grep -E "HTTP/|subject:|issuer:"
🛡️ Optional: protect it with AWS WAF
A public endpoint should have a web ACL. Attach an existing one:
aws wafv2 associate-web-acl \
--web-acl-arn WEB_ACL_ARN --resource-arn $ALB_ARN
🎯 Lessons learned
-
Check
dig NSfirst. The authoritative DNS provider is the only one that matters. -
Check the ALB scheme second.
internalmeans private IPs only, and it can't be changed later. - Keep internal and public ALBs separate. Internal for back-office users over VPN, internet-facing for customers, each with its own security group.
- Always CNAME to the ALB DNS name, never to its IPs.
📋 Quick "site won't open" checklist
- [ ] Who is authoritative? (
dig NS) - [ ] Does the record exist there? (
dig +short) - [ ] Is the ALB
internet-facing? - [ ] Are the ALB subnets public?
- [ ] Do the security groups allow 443 in, and ALB to targets?
- [ ] Are targets healthy?
- [ ] Is the certificate issued and attached?
Have you ever spent an hour debugging DNS in the wrong provider? Tell me in the comments. 👇
I write about AWS and DevOps from real production work. Follow me for more hands-on posts.
Top comments (0)