From 27fad897a0441683fc0bc176027dfc82b29f645b Mon Sep 17 00:00:00 2001 From: KellyMichels Date: Thu, 6 Aug 2026 18:28:08 -0500 Subject: [PATCH] fix(zdeploy): verify Next.js deploys on the server, not the public IP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nextjs handler verified by requesting http://:/. A compose stack behind the edge proxy normally publishes to 127.0.0.1 only, so that request can never be answered and every deploy ended with "is the port open in the security group?" — pointing at a firewall rule for an app that was already serving fine. No security-group change could have made that probe succeed, which is what made the warning actively harmful: it named the one fix guaranteed not to work, and opening the port would have exposed the app directly, bypassing the proxy and TLS. The nextjs handler now uses the precedence the python handler already used: verify.port (curl localhost on the server) -> domain (Host-header check through the edge proxy) -> an honest "NOT verified" instead of a misleading warning. Also follow redirects in the health check. An app whose "/" answers 307 (Next.js -> /login) returns a body of a few bytes that read as "not ready"; -L fetches the page that actually renders. No effect on projects that point verify.path at a plain 200 endpoint such as /health. The example config now documents the verify block on the nextjs project, and CHECKSUMS.txt is regenerated for the changed script. --- CHECKSUMS.txt | 2 +- zconfig.example.json | 6 ++++++ zdeploy.ps1 | 33 +++++++++++++++++++++++++-------- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/CHECKSUMS.txt b/CHECKSUMS.txt index 37e9282..424d7ae 100644 --- a/CHECKSUMS.txt +++ b/CHECKSUMS.txt @@ -8,7 +8,7 @@ c336a61cb8563736bd6550671d7dc90b14702a43f25a97b1ac15ffd64605fbf6 zbackup_ec2.cm 20e6282901a919aa32ea717c739ba2dc9657426dff860f0e5fce201edefd99da zchecksums.cmd 692b7b0ff9e51e5d9b880358b33c54d2db4f967297a5350e154df1ce6f06ebc6 zchecksums.ps1 b56085112e7c573926a70a58872163b71416d58ce91ea7114b1f5de5fc96c982 zdeploy.cmd -3352214fac08cd83242680c0e7f60025c8f132bafb510384d7c7a0fe9a4c5094 zdeploy.ps1 +df80a078ae1869626834245b7a1b9450ddf543fcb7cf78ba5d2dd04ae3bdf577 zdeploy.ps1 fa817d3bd7bba98b03b411d3c5a82b210d3cca316ac0c9785afa5b4ac5fe0766 zec2.cmd 5f70432af23df3459a98d9ad6a0da723783f2269dd5f5ab8ecdd6366ebc84942 zec2.ps1 4df94eda29b6f5b2cb6bfa63e0add4d90ea24d936305f36a69988e8dd5d69154 zec2_rotatekeys.cmd diff --git a/zconfig.example.json b/zconfig.example.json index 682dda9..6ce9850 100644 --- a/zconfig.example.json +++ b/zconfig.example.json @@ -79,6 +79,12 @@ "path": "/home/YOUR_SSH_USER/stack/nextapp", "appService": "web" }, + "verify": { + "_comment": "Optional: after deploy, curl this ON the server (localhost:port+path). Add it when compose publishes the app to 127.0.0.1 only, as it usually does behind the edge proxy — probing the public IP on that port can never answer. Redirects are followed, so a Next.js '/' that 307s to '/login' still verifies. Takes precedence over the domain check.", + "port": 3000, + "path": "/", + "expect": "" + }, "deploy": { "zipName": "NextAppDeploy.zip" } }, diff --git a/zdeploy.ps1 b/zdeploy.ps1 index e1a9481..02698fa 100644 --- a/zdeploy.ps1 +++ b/zdeploy.ps1 @@ -25,12 +25,15 @@ # python kind: app service "app", db service "db" # nextjs kind: app service "web", db service "db" # -# Verification (python kind, when there is no scripts/build_version_tool.py): +# Verification (python kind when there is no scripts/build_version_tool.py, and +# nextjs kind): # Projects with a "verify" block are checked ON the server via # localhost: — the only accurate way for stacks that are not # published through the edge proxy. Projects with only a "domain" fall back # to a Host-header request. Projects with neither are reported as NOT # verified rather than passing on the proxy's default vhost. +# A compose stack usually publishes to 127.0.0.1 only, so probing the public +# IP on the app's port can never answer — give those a "verify" block. # param( [Parameter(Position = 0, ValueFromRemainingArguments = $true)] @@ -260,7 +263,10 @@ function Test-DeployHealth { Write-Host "`n--- [$Key] Health check (on server: localhost:$port$path) ---" -ForegroundColor Cyan $deadline = (Get-Date).AddSeconds($TimeoutSec) while ((Get-Date) -lt $deadline) { - $raw = ssh -o StrictHostKeyChecking=no -i $PEM_KEY $SSH_TARGET "curl -s -m 8 http://localhost:$port$path" + # -L: an app whose "/" redirects (e.g. Next.js "/" -> "/login") answers a + # 307 whose body is a few bytes or empty, which reads as "not ready". + # Follow to the page that actually renders before judging. + $raw = ssh -o StrictHostKeyChecking=no -i $PEM_KEY $SSH_TARGET "curl -sL -m 8 http://localhost:$port$path" $body = ($raw | Out-String).Trim() if ($LASTEXITCODE -eq 0 -and $body) { if (-not $expect -or $body.Contains($expect)) { @@ -539,16 +545,23 @@ function Invoke-NextDeploy { Invoke-Ec2Step "record deploy timestamp; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remotePath/.last_deploy_utc > /dev/null && rm -f $RemoteHome/$zipName" Write-Host "`n--- [5] Verifying deployment ---" -ForegroundColor Cyan - if ($Proj.ports -and $Proj.ports.prod) { - $directUrl = "http://${EC2_IP}:$([int]$Proj.ports.prod)/" + # Same precedence as the python handler. Do NOT probe http://:/ + # here: a compose stack behind the edge proxy usually publishes to + # 127.0.0.1 only, so that probe can never answer and the old "is the port + # open in the security group?" warning sent you chasing a firewall rule + # for an app that was already up. + if ($Proj.verify -and $Proj.verify.port) { + Test-DeployHealth -Key $Key -Proj $Proj -TimeoutSec 60 | Out-Null + } elseif ($Proj.domain) { + $headers = @{ 'Host' = $Proj.domain } $deadline = (Get-Date).AddSeconds(60) $verified = $false while ((Get-Date) -lt $deadline) { Start-Sleep -Seconds 4 try { - $resp = Invoke-WebRequest -Uri $directUrl -TimeoutSec 8 -ErrorAction Stop -UseBasicParsing - if ($resp.StatusCode -eq 200) { - Write-Host " PASS - app is responding at $directUrl" -ForegroundColor Green + $resp = Invoke-WebRequest -Uri "http://$EC2_IP/" -Headers $headers -TimeoutSec 8 -ErrorAction Stop -UseBasicParsing + if ($resp.StatusCode -lt 500) { + Write-Host " PASS - app is responding at https://$($Proj.domain)/" -ForegroundColor Green $verified = $true break } @@ -557,8 +570,12 @@ function Invoke-NextDeploy { } } if (-not $verified) { - Write-Host " WARNING: no response at $directUrl within 60s (is the port open in the security group?)." -ForegroundColor Yellow + Write-Host " WARNING: no response for https://$($Proj.domain)/ within 60s." -ForegroundColor Yellow } + } else { + Write-Host " Deploy finished - NOT verified." -ForegroundColor Yellow + Write-Host " No 'domain' and no 'verify' block in zconfig.json for '$Key'," -ForegroundColor Yellow + Write-Host ' Add to the project: "verify": { "port": , "path": "/health" }' -ForegroundColor Gray } if ($preZipBuild) { # Committed stamp, not +1 — see the note in Wait-VerifyStaticBuild.