Repository navigation
Conversation
The ALB target group health check used /health, which answers HTTP 200 even while a task is still initializing, so the ALB routed production traffic to cold Fargate tasks. Point it at /ready (503 until this task has finished database initialization, added in LeanerCloud#646) and keep the ECS container health check, which is liveness, on /health. ECS also replaces tasks the target group reports unhealthy, so add a health_check_grace_period_seconds (default 300s) to the ECS service; without it a task still connecting to the database or running migrations would be killed before it could become ready. The deploy smoke gate drops the /api/auth/check-admin warm-up and the /health body parse and runs scripts/wait-for-healthy.sh against /ready, mirroring deploy-azure.yml. Closes LeanerCloud#647 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 8 billable files and costs up to $2.00. Or wait 38 minutes for your next included review. View limit details
Comment |
cristim
left a comment
There was a problem hiding this comment.
Independent review of b775945: draft: blocks ready
Scope: .github/workflows/deploy-aws-fargate.yml, terraform/environments/aws/compute.tf, terraform/modules/compute/aws/fargate/{main,variables}.tf, terraform/modules/compute/aws/fargate_readiness_test.go. The PR body is treated as a claim; everything below was checked against code at this SHA.
Findings
-
BLOCKING: the deploy readiness gate passes vacuously when HTTPS is enabled.
.github/workflows/deploy-aws-fargate.yml:~295-319(both new steps) callscripts/wait-for-healthy.shwithAPP_URL=http://<alb dns>(the URL is built in the "Get ALB URL" step fromfargate_alb_dns_name, terraform/environments/aws/outputs.tf:121). Whenenable_httpsis true (terraform/environments/aws/compute.tf:193, any deployment with frontend domains plus a zone), the port 80 listener returns a 301 redirect (terraform/modules/compute/aws/fargate/main.tf:501-513). The script usescurl --fail-with-bodywithout-L(scripts/wait-for-healthy.sh:50), which accepts any status below 400, so a 301 counts as "ready". Reproduced: a local server answering 301 on every path makesHEALTH_REQUIRED_STREAK=3 bash scripts/wait-for-healthy.sh http://127.0.0.1:<port>print "Service is ready." and exit 0. The replaced gate failed closed in that configuration (a 301 has an empty body, sojq -e '.status == "healthy"'failed). Net effect: for HTTPS deployments the PR turns a loud-failing gate into one that can never fail, which is the opposite of the issue's intent. Azure is unaffected because its URL is https. Suggested fix: make the script accept only 2xx (for example-o /dev/null -w '%{http_code}'and a^2check, keeping the body for the failure message) and add a 301 case to scripts/test-wait-for-healthy.sh; or have the workflow poll the https URL. A workflow-level static test asserting the gate cannot pass on a redirect would be a good regression test. -
NON-BLOCKING (owner decision, already disclosed in the PR body): ALB health on
/readyalso drives ECS task replacement./readyreturns 503 whendb.HealthCheckorauthService.Pingfails at runtime (internal/server/health.go:96-115, 66-82; main.tf:468-480). With the module defaults (interval 30, unhealthy_threshold 3, variables.tf:162-184), a DB outage or Multi-AZ failover longer than about 90s makes ECS stop and replace every task; the ALB fail-open only helps until the targets are actually stopped. Previously the service kept answering (SPA, /health, non-DB routes) and recovered as soon as the DB did; now recovery needs full task start plus init (up to the 300s grace). I would raise the environment'sunhealthy_threshold(or document the choice in docs/DEPLOYMENT.md) before ready, but this is a product call, not a defect in the diff. -
NON-BLOCKING: the test-deployment step budget (60s sleep plus 10 x 10s = about 160s) is shorter than the grace period/init budget (about 135s DB connect retries plus migrations, per the PR text). Previously the warm-up request could not shorten that either, so this is no regression, but a slow first boot can fail the fast gate where the old loop would equally fail. Consider
HEALTH_MAX_ATTEMPTSof 30 for the first gate. -
NON-BLOCKING:
terraform/environments/aws/outputs.tf:250and docs/DEPLOYMENT.md still tell operators to curl/health. The PR lists this as out of scope; fine.
No other defects survived refutation.
What I attacked
- Source of truth for /ready and /health: internal/server/http.go:31-37 registers both on the mux; handlers in internal/server/health.go:30-57 need no auth and no request-triggered DB init. /ready is 200 or 503, so matcher "200" is right.
startBackgroundInit(http.go:116, started at http.go:183) drives init without traffic, so a cold task does become ready on its own; the migrations "pending" state is the only gate and a failed migration stays ready (health.go:66-82), so no never-healthy trap. Probes are cheap (5s context timeout vs the target group's 5s timeout, variables.tf:168). - Container healthCheck (main.tf:681) stays on /health with startPeriod 60, correct as liveness. ECS service has a
load_balancerblock (main.tf:714-718) so the grace period is valid, and the deployment circuit breaker (main.tf:733-736) is unchanged. - Callers: grepped all .tf/.yml/.sh/.md for
health_check_path, the fargate module and the new variable. The only module caller is terraform/environments/aws/compute.tf:154; no other env, profile or tfvars sets the variable, and the new variable has a default, so nothing else breaks. CloudFormation has no ECS/ALB resources. terraform fmt -check -recursiveon the module and environment: clean.terraform init -backend=falseplusterraform validateon both: valid (TF_DATA_DIR in scratch, lock files untouched).actionlinton the workflow: clean. Nothing planned or applied.- Tests:
go test -run TestFargate ./terraform/modules/compute/awspasses at the SHA. Fail-before: restoring origin/main's four changed files makes TestFargateALBHealthCheckUsesReadiness, TestFargateServiceHasHealthCheckGracePeriod and TestFargateDeploySmokeGateUsesReadiness fail (the liveness test is a guard, passes both ways). Mutation probes, each reverted at once: matcher "200" -> "200-499" fails the ALB test; grace default 300 -> 0 fails the grace test. Note these static tests cannot catch finding 1; it is a behavior of the shared script. - Scope/size: 212 additions (under the 400 cap) across 5 files; main.tf was already over 500 lines (1296) before this PR, +13 here.
- Labels: PR carries effort/m, impact/many, priority/p2, severity/medium, triaged, type/bug, urgency/this-quarter; identical to #647. OK.
- CodeRabbit: only "Review skipped: draft pull request"; no threads, nothing to resolve. I did not ping it.
Gate status (origin/main Review gate)
- Independent adversarial review naming the SHA: this comment, but verdict is "draft: blocks ready".
- Actionable findings resolved: no, finding 1 open.
- CI on the SHA: both runs ("CI - Build & Test" run 38965748718, "pre-commit" 38965748768) were queued/pending when I checked, all checks pending, so there is no green CI yet (a fork run may need maintainer approval). CodeRabbit is optional per the gate.
- Local verification on macOS: static only (terraform fmt/validate, actionlint, Go static tests, wait-for-healthy.sh redirect repro). No real-account or live ALB verification exists, and none was done by me.
- Verdict, SHA and evidence recorded on the PR: this comment.
Blocks ready
- Finding 1 (gate accepts a 301 redirect, passes vacuously on HTTPS deployments).
- CI green on the final SHA.
- Owner acknowledgement of finding 2 (outage-time task churn), or a higher unhealthy_threshold.
…ed URL Review of LeanerCloud#1025 found the Fargate smoke gate could pass vacuously: with HTTPS enabled the port 80 listener answers 301 on every path, and scripts/wait-for-healthy.sh (curl --fail-with-body, no -L) counted any status below 400 as ready. - wait-for-healthy.sh: only 2xx counts as ready. A 3xx is not ready and is logged with its redirect target (%{redirect_url}); redirects are not followed. Non-2xx bodies are still printed. Tests add 301 stub cases and a real-curl 301 server case (the reviewer's repro), which fail against the old script. - New fargate_app_url output: https on the api-<env> alias (covered by the listener's wildcard cert) when HTTPS is enabled, otherwise http on the ALB DNS name. test-deployment gates on it instead of the bare ALB DNS name. - Target group unhealthy_threshold default 3 -> 6 (about 180s of failing /ready at the 30s interval before ECS replaces a task), so a short database blip or Multi-AZ failover does not cycle every task. - First gate budget 10 -> 30 attempts (300s), covering the ECS grace period. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review. New head: 1. BLOCKING: the gate passed vacuously on a 301. Fixed in two places.
2. Task churn during a database outage. I took the smallest safe change and raised the target group's 3. First gate budget. Raised from 10 to 30 × 10s (300s), so it covers the 300s grace period. The workflow test pins this: attempts × interval must be at least the grace period. 4. Docs/outputs still say curl Evidence (static and offline only; nothing planned, applied or deployed):
🤖 Generated with Claude Code |
cristim
left a comment
There was a problem hiding this comment.
Independent delta review of 14e957f (previous: b775945): no actionable findings
OLD is an ancestor of NEW (one commit on top, no rebase). I reviewed git diff OLD NEW as new code and re-checked the full PR diff against origin/main (merge-tree: no conflicts). The author's reply is a claim; everything below was run.
Previous findings
- BLOCKING, gate passed on a 301: RESOLVED.
- Reproduction: a local server answering 301 on every path,
HEALTH_REQUIRED_STREAK=3 HEALTH_MAX_ATTEMPTS=3. OLD script (b775945): "Readiness check 3: ready (3/3) / Service is ready.", exit 0 (wrong). NEW script (scripts/wait-for-healthy.sh:53-90): every attempt "not ready yet ... HTTP 301 redirect to https://app.example.invalid/ready", ends with::error::Service not ready 3x ..., exit 1. - Regression cases: scripts/test-wait-for-healthy.sh with the OLD script swapped in fails 4 cases (301 forever, 301 mid-streak, 503 status/body, and "real curl against a 301 /ready" using a real python server); with the NEW script all 23 pass, including real curl 200, 503 and 301.
- Script logic: curl has no -f now,
-w '%{http_code} %{redirect_url}'is judged in acase; only2??counts,3??and others reset the streak, a curl transport error resets it and reports stderr. The temp dir is cleaned by trap. No other caller breaks: Azure uses an https URL and 2xx. - Root cause also addressed: the workflow now gates on a new
fargate_app_urloutput (terraform/environments/aws/outputs.tf:131-144), exported asapp_url(deploy-aws-fargate.yml:227-233, 273-294). It is https on theapi-<env>alias exactly whenapi_https_url != nulland the alias exists. I checked the sources: the alias is created whensubdomain_zone_name != ""(dns_records.tf:39-40) andenable_httpsrequires a non-empty zone (compute.tf:193), so the alias always exists when HTTPS is on; the listener certificate is*.<zone>(acm.tf:7), which coversapi-<env>.<zone>(one label). Otherwise it falls back tohttp://<ALB DNS>, which forwards (main.tf:501-513). The "Get app URL" step fails loudly if the output is empty or has no scheme.
- Reproduction: a local server answering 301 on every path,
- NON-BLOCKING, fast-gate budget vs init time: RESOLVED. Fast gate is now 30 x 10s = 300s (deploy-aws-fargate.yml:~317), at least the 300s grace period; a Go test pins attempts x interval >= grace default.
- NON-BLOCKING, ECS replacement on DB outage: PARTIALLY RESOLVED, and acceptable.
unhealthy_thresholddefault is now 6 (variables.tf:180-184): about 180s of continuous /ready 503 before the ALB marks a target unhealthy, which covers a typical 60-120s Multi-AZ failover. It still cycles tasks on an outage longer than about 3 minutes; that is the inherent cost of readiness-driven admission on ECS and is disclosed in the PR body. 6 is within the ALB limit of 10 and no other module/env overrides the variable (grepped). No action required. - NON-BLOCKING, /health hints in docs/outputs: unchanged, out of scope as stated.
New code attacked
- fargate_readiness_test.go new tests:
TestFargateALBToleratesShortDatabaseBlip, the app_url wiring assertions andTestFargateAppURLOutputFollowsListener. Probe 1: unhealthy_threshold default 6 -> 3 fails the blip test. Probe 2: restoring OLD deploy-aws-fargate.yml and outputs.tf fails the smoke-gate test and the app_url output test. Both reverted at once.go test ./terraform/modules/compute/awspasses at the SHA. terraform fmt -check -recursive(module + env): clean.terraform init -backend=false+validateon both: valid.actionlint: clean.scripts/test-wait-for-healthy.sh: passed=23 failed=0. Nothing planned or applied.- Edge:
terraform output -raw fargate_app_urlis null for non-fargate platforms, but this workflow only deploys fargate and a null output fails the "Get app URL" step loudly rather than passing. - Labels still match #647. CodeRabbit: only the "draft not reviewed" notice, no threads; not pinged.
Nits (NON-BLOCKING)
- PR additions are now 469 (cap guideline 400); mostly tests and test scaffolding for the script. Not worth splitting at this point.
Gate status (origin/main Review gate)
- Independent review naming the SHA: this comment.
- Actionable findings: none open.
- CI on 14e957f: "CI - Build & Test" (run 38967021268) queued, "pre-commit" (38967021277) in progress at the time I checked, 14 checks pending, 7 passing, 1 skipping; no action_required runs. Not green yet, and I did not wait.
- Local verification (macOS): static plus script reproduction with a local 301/503/200 server and real curl; no live ALB or real-account verification. It is still a draft.
Blocks ready
- Green CI on this SHA, and taking the PR out of draft. Nothing else.
Root cause
PR #646 (#488) added
/ready, which answers 503 until the process has finished initialization, and moved every Azure traffic-admission check to it. AWS Fargate was left on the old contract: the ALB target group health check still used/health./healthanswers HTTP 200 even while a task is still initializing ("degraded"), so the ALB marked cold tasks healthy and sent production traffic to them during rollouts and scale-out.The deploy smoke gate in
deploy-aws-fargate.ymlalso still used the pre-#488 pattern. It sent a warm-up request to/api/auth/check-admin, then parsed the/healthbody. The warm-up only initializes whichever task the ALB happens to pick.Fix
terraform/modules/compute/aws/fargate):health_check_pathnow defaults to/ready, andterraform/environments/aws/compute.tfpasses/ready. The matcher stays"200", so a 503 counts as not ready.health_check_grace_period_seconds(default 300, validated as a whole number ≥ 0) so a task still connecting to the database or running migrations isn't killed while it initializes. During the grace period the ALB still keeps the unready task out of rotation.unhealthy_thresholddefault goes from 3 to 6./readyalso answers 503 while the database is unreachable, and ECS replaces any target the ALB marks unhealthy. At the 30s interval, ECS now starts replacing a task after about 180s of continuous 503s, not 90s, so a routine RDS Multi-AZ failover (typically 60–120s) doesn't cycle every task. Recovery after the database returns ishealthy_threshold2 × 30s, which is unchanged.healthCheckstays on/health. It is liveness (failing it replaces the task), so it must not depend on the database.scripts/wait-for-healthy.sh): only a 2xx response counts as ready. A 3xx is not ready, and the log line names the redirect target (%{redirect_url}). Redirects are deliberately not followed. For other non-2xx responses, the status and body are printed. Before this change, any status below 400 passed, so a 301 counted as ready.fargate_app_urloutput interraform/environments/aws/outputs.tf. When the module's HTTPS listener exists (api_https_url != null) it ishttps://api-<env>.<subdomain_zone_name>, otherwisehttp://<ALB DNS>. Theapi-<env>alias (dns_records.tf) points at the same ALB, and the listener's wildcard certificate covers it. None of the existing outputs gives a URL that works here:fargate_alb_dns_name/api_urlget a 301 when HTTPS is on, andapi_https_url(https://<ALB DNS>) fails TLS verification against that certificate. The deploy job exports it asapp_url, andtest-deploymentgates on it.scripts/wait-for-healthy.shon/ready, mirroringdeploy-azure.yml. There are two steps: a fast-fail readiness step (streak 1, 30 × 10s, which covers the 300s grace period) and the rollout-race smoke step (streak 3 within 30 × 5s). The warm-up request is gone.health_check.path(traffic admission)/health(200 while degraded)/ready(503 until initialized)200/ 30s / 2unhealthy_thresholdhealth_check_grace_period_secondshealthCheck(liveness)curl -f localhost:8080/health, startPeriod 60http://<ALB DNS>(301 when HTTPS is on)fargate_app_url(https alias when HTTPS is on)deploy-aws-fargate.ymltest gate/health+ jq, 10 × 10swait-for-healthy.shon/ready, 30 × 10sdeploy-aws-fargate.ymlsmoke step/healthbody parsewait-for-healthy.sh, 3 consecutive ready within 30 attemptswait-for-healthy.shverdictCloudFormation parity:
cloudformation/stacks/CUDlydefines no ECS, Fargate or target group resources.Other callers of
wait-for-healthy.sh:deploy-azure.yml(https Container App URL) and the GCP caller in #1024 (https Cloud Runservice_url). Both hit the app directly over https and get a 2xx when ready, so 2xx-only changes nothing for them.Tests
terraform/modules/compute/aws/fargate_readiness_test.gohas six static Go tests over the TF, workflow and output files:/readywith matcher200;/health;unhealthy_threshold × intervalis at least 150s, the threshold is at most 10, and the env doesn't override them lower;wait-for-healthy.shonapp_urlwith no warm-up, no/health, nohttp://prefixing, and a fast-gate budget at least as long as the grace period;fargate_app_urlswitches to the https alias exactly when the HTTPS listener exists.scripts/test-wait-for-healthy.sh: all 19 existing cases still pass, plus 4 new ones (23/23):curlagainst a local server answering 301 on every path (the reviewer's repro) now fails.Against the old script, those 4 new cases fail and the 301 server case passes vacuously (exit 0).
Mutation probes: 25 of 25 killed. They cover the target group, variables, env override, workflow URL/budget/warm-up/
/health, the output's https switch, and in the script: accepting 3xx, accepting any status,-L, dropping the redirect message, and dropping the body.Verification
Static and offline checks only. Nothing was planned, applied or deployed against any account.
terraform fmt -check -recursive+terraform init -backend=false(temporary data dir, removed afterwards) +validateonterraform/modules/compute/aws/fargateandterraform/environments/aws: clean and valid. Lock files unchanged.go test ./terraform/modules/compute/aws(withAWS_CONFIG_FILE/AWS_SHARED_CREDENTIALS_FILE=/dev/null),go vet, golangci-lint v2.10.1: ok, 0 issues. No non-test Go files changed.actionlint(all workflows): clean. zizmor 1.29.0--offline --persona=pedantic --min-severity=medium .github/workflows/(as in CI): no findings.git inittemp repo with an isolated HOME: clean.scripts/test-git-secrets-allowlist.sh: 42 PASS, 0 FAIL.Operators: re-apply the AWS Fargate environment. This is an in-place update of
aws_lb_target_group.mainandaws_ecs_service.mainplus the new output. For HTTPS deployments the gate now resolvesapi-<env>.<subdomain_zone_name>, so the subdomain zone must be delegated. If it isn't, the gate fails loudly with a DNS error rather than passing.Behaviour change for the owner to acknowledge
On ECS, failing the ALB health check also triggers task replacement, and
/readyanswers 503 when the database is unreachable at runtime. The chosen mitigation isunhealthy_threshold6: a database outage still cycles tasks, but only after about 180s of continuous 503s (about 90s before this change). The trade-off is that a task whose own database connection breaks stays in rotation up to about 180s. A dead process is still caught sooner by the container liveness check, and a stopped task is deregistered right away. When every target is unhealthy the ALB fails open. Flagged for owner acknowledgement.Out of scope
/readyroute.terraform/modules/deployment-checksand the GCP uptime monitor (fix(infra): deployment checks and GCP uptime monitor gate on tautological /health 200 #649), and GCP probes (fix(gcp): exclude uninitialized replicas from GKE and Cloud Run readiness #648).curl .../healthhint inquick_start_commands(terraform/environments/aws/outputs.tf) anddocs/DEPLOYMENT.md. That output is shared with Lambda, which has no/ready, so it isn't a one-line fix.Closes #647
🤖 Generated with Claude Code