fix(zdeploy): verify Next.js deploys on the server instead of probing the public IP (stops the bogus security-group warning) (#41)

* fix(zdeploy): verify Next.js deploys on the server, not the public IP

The nextjs handler verified by requesting http://<ec2-ip>:<prod-port>/.
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.

* fix(zdeploy): truncate the health-check body in the PASS line

Pointing the nextjs handler at Test-DeployHealth means the body is now
often an HTML page, not a line of JSON, so the PASS line dumped ~10 KB
of markup into the deploy output and buried everything after it.

Match on the full body as before, but print at most 200 characters plus
the byte count. A /health endpoint still prints in full.
This commit is contained in:
kellymichels 2026-08-06 19:01:21 -05:00 committed by GitHub
parent b84d24b3a2
commit c8fcfee76c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 37 additions and 10 deletions

View File

@ -8,7 +8,7 @@ c336a61cb8563736bd6550671d7dc90b14702a43f25a97b1ac15ffd64605fbf6 zbackup_ec2.cm
20e6282901a919aa32ea717c739ba2dc9657426dff860f0e5fce201edefd99da zchecksums.cmd 20e6282901a919aa32ea717c739ba2dc9657426dff860f0e5fce201edefd99da zchecksums.cmd
692b7b0ff9e51e5d9b880358b33c54d2db4f967297a5350e154df1ce6f06ebc6 zchecksums.ps1 692b7b0ff9e51e5d9b880358b33c54d2db4f967297a5350e154df1ce6f06ebc6 zchecksums.ps1
b56085112e7c573926a70a58872163b71416d58ce91ea7114b1f5de5fc96c982 zdeploy.cmd b56085112e7c573926a70a58872163b71416d58ce91ea7114b1f5de5fc96c982 zdeploy.cmd
3352214fac08cd83242680c0e7f60025c8f132bafb510384d7c7a0fe9a4c5094 zdeploy.ps1 c4189cef72368487af4524a00603967d7c8dc03c58ace5d4dd1e264ce1d22bd9 zdeploy.ps1
fa817d3bd7bba98b03b411d3c5a82b210d3cca316ac0c9785afa5b4ac5fe0766 zec2.cmd fa817d3bd7bba98b03b411d3c5a82b210d3cca316ac0c9785afa5b4ac5fe0766 zec2.cmd
5f70432af23df3459a98d9ad6a0da723783f2269dd5f5ab8ecdd6366ebc84942 zec2.ps1 5f70432af23df3459a98d9ad6a0da723783f2269dd5f5ab8ecdd6366ebc84942 zec2.ps1
4df94eda29b6f5b2cb6bfa63e0add4d90ea24d936305f36a69988e8dd5d69154 zec2_rotatekeys.cmd 4df94eda29b6f5b2cb6bfa63e0add4d90ea24d936305f36a69988e8dd5d69154 zec2_rotatekeys.cmd

View File

@ -79,6 +79,12 @@
"path": "/home/YOUR_SSH_USER/stack/nextapp", "path": "/home/YOUR_SSH_USER/stack/nextapp",
"appService": "web" "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" } "deploy": { "zipName": "NextAppDeploy.zip" }
}, },

View File

@ -25,12 +25,15 @@
# python kind: app service "app", db service "db" # python kind: app service "app", db service "db"
# nextjs kind: app service "web", 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 # Projects with a "verify" block are checked ON the server via
# localhost:<port><path> — the only accurate way for stacks that are not # localhost:<port><path> — the only accurate way for stacks that are not
# published through the edge proxy. Projects with only a "domain" fall back # published through the edge proxy. Projects with only a "domain" fall back
# to a Host-header request. Projects with neither are reported as NOT # to a Host-header request. Projects with neither are reported as NOT
# verified rather than passing on the proxy's default vhost. # 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( param(
[Parameter(Position = 0, ValueFromRemainingArguments = $true)] [Parameter(Position = 0, ValueFromRemainingArguments = $true)]
@ -260,11 +263,18 @@ function Test-DeployHealth {
Write-Host "`n--- [$Key] Health check (on server: localhost:$port$path) ---" -ForegroundColor Cyan Write-Host "`n--- [$Key] Health check (on server: localhost:$port$path) ---" -ForegroundColor Cyan
$deadline = (Get-Date).AddSeconds($TimeoutSec) $deadline = (Get-Date).AddSeconds($TimeoutSec)
while ((Get-Date) -lt $deadline) { 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() $body = ($raw | Out-String).Trim()
if ($LASTEXITCODE -eq 0 -and $body) { if ($LASTEXITCODE -eq 0 -and $body) {
if (-not $expect -or $body.Contains($expect)) { if (-not $expect -or $body.Contains($expect)) {
Write-Host " PASS - $body" -ForegroundColor Green # A /health endpoint returns a line of JSON; an app page returns
# kilobytes of HTML. Match on the whole body, but only print
# enough to recognise it — the rest is unreadable in a log.
$shown = if ($body.Length -gt 200) { $body.Substring(0, 200) + "... ($($body.Length) bytes)" } else { $body }
Write-Host " PASS - $shown" -ForegroundColor Green
return $true return $true
} }
Write-Host " Responding but '$expect' not found - waiting..." -ForegroundColor DarkYellow Write-Host " Responding but '$expect' not found - waiting..." -ForegroundColor DarkYellow
@ -539,16 +549,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" 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 Write-Host "`n--- [5] Verifying deployment ---" -ForegroundColor Cyan
if ($Proj.ports -and $Proj.ports.prod) { # Same precedence as the python handler. Do NOT probe http://<ec2-ip>:<prod-port>/
$directUrl = "http://${EC2_IP}:$([int]$Proj.ports.prod)/" # 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) $deadline = (Get-Date).AddSeconds(60)
$verified = $false $verified = $false
while ((Get-Date) -lt $deadline) { while ((Get-Date) -lt $deadline) {
Start-Sleep -Seconds 4 Start-Sleep -Seconds 4
try { try {
$resp = Invoke-WebRequest -Uri $directUrl -TimeoutSec 8 -ErrorAction Stop -UseBasicParsing $resp = Invoke-WebRequest -Uri "http://$EC2_IP/" -Headers $headers -TimeoutSec 8 -ErrorAction Stop -UseBasicParsing
if ($resp.StatusCode -eq 200) { if ($resp.StatusCode -lt 500) {
Write-Host " PASS - app is responding at $directUrl" -ForegroundColor Green Write-Host " PASS - app is responding at https://$($Proj.domain)/" -ForegroundColor Green
$verified = $true $verified = $true
break break
} }
@ -557,8 +574,12 @@ function Invoke-NextDeploy {
} }
} }
if (-not $verified) { 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": <hostPort>, "path": "/health" }' -ForegroundColor Gray
} }
if ($preZipBuild) { if ($preZipBuild) {
# Committed stamp, not +1 — see the note in Wait-VerifyStaticBuild. # Committed stamp, not +1 — see the note in Wait-VerifyStaticBuild.