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.