Skip to content

On-prem serving scripts (#955 Phase 0) - #1005

Open
tgoodyear wants to merge 8 commits into
mainfrom
p0/serving-scripts
Open

tgoodyear wants to merge 8 commits into
mainfrom
p0/serving-scripts

Conversation

@tgoodyear

@tgoodyear tgoodyear commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Phase 0 of #955: the scripts that will serve usnewsmap.com from an on-prem container behind a Cloudflare Tunnel. Nothing runs them until Phase 2, and nothing here changes how Azure serves. Everything is in the new scripts/onprem/serving/, with a README.

What's in it

  • Units. The Quickwit searcher reads a file:// metastore on local disk, read-only. Two API colors, usnm-api@blue and @green, run the same image as Azure, by digest, under podman. cloudflared runs the usnm-serving tunnel. Caddy sits on loopback in front of the colors. Everything listens on loopback only.
  • gen-env.sh. Writes the API's and the searcher's settings from the sources Bicep reads. --check-app compares the API's settings with the Azure app and fails on any difference it doesn't list. The search-log and OCR-request URLs stay unset until Phase 3.
  • update-api.sh and its timer. The timer runs it 2 minutes after the last run ends. It reads the registry's usnewsmap-api:main manifest with the container's Arc identity and asks GitHub nothing while that image serves. A new image deploys once its build provenance attestation verifies (ci.yml on main; CI attests after the Azure roll, in a Phase 0 API PR). The script then starts the idle color and switches Caddy once /readyz passes. An image whose color doesn't get ready is rejected, and the old color keeps serving.
  • apply-tunnel-ingress.sh and tunnel-ingress.json. The tunnel's ingress, kept in the repository, applied with the Cloudflare API token read from Key Vault.
  • install.sh. Puts it all in place inside the container. It also installs journald retention (30 days), a response cache directory for each color with a 14-day cleanup, and run-cloudflared.sh, which cuts query strings from cloudflared's log lines. Caddy's error log drops the request, so neither logs a search or a visitor's address.

The container, its VLAN and its firewall are in home-infra (tgoodyear/home-infra#59), so no host names or addresses land here.

Testing

  • test_serving.py: 30 tests that run every script against stub tools, including real deploy, rollback, attestation-wait and install runs in a temporary root. CI's run-unit-tests.py picks it up. They pass on macOS (the Linux-only ones skip) and on Debian 12.
  • shellcheck is clean at every severity. systemd-analyze verify passes on the units in Debian 12.
  • Caddy 2.6.2 (Debian 12's version) validates the Caddyfile, a live color switch with caddy reload works, and an upstream failure logs no query string or address. The cloudflared wrapper was run in Debian 12 against sample error lines.
  • gen-env.sh --dry-run --check-app against prod (read-only) matches ca-usnm-prod apart from the listed differences.

Open items for Phase 2

  • How a new index version reaches the running container. The searcher reads one release's file:// metastore, so today a new version means a new config and a searcher restart.
  • api.env sets USNM_TRUST_CF_CONNECTING_IP=true and USNM_READY_REQUIRES_PUBLISHED_INDEXES=true, which come with the API image's attestation from the Phase 0 API PRs. An image from before those PRs ignores both settings and keys rate limits on the loopback peer (USNM_TRUSTED_PROXY_HOPS=0). Until the attestation exists, update-api.sh deploys nothing.
  • Temporary file names in usnm-store that are unique across processes, so both colors could share one cache directory.
  • Searcher upgrades, and monitoring for the container.

Merge only after #283 says the run g publish is done.

🤖 Generated with Claude Code

tgoodyear and others added 3 commits October 22, 2026 21:37
Units, Caddy and cloudflared config, env-file generation and the update
timer for serving from an on-prem container behind a Cloudflare Tunnel,
in scripts/onprem/serving/. Written and tested only; nothing runs them
until Phase 2, and Azure serving is unchanged.

- usnm-searcher.service: the pinned Quickwit binary on a read-only
  file:// metastore, loopback :7280.
- usnm-api@.service: blue (:8081) and green (:8082) colors of the API
  image by digest under podman; Caddy on :8080 forwards to one of them.
- cloudflared-usnm-serving.service and tunnel-ingress.json, applied by
  apply-tunnel-ingress.sh with the Key Vault-held Cloudflare token.
- gen-env.sh: API and searcher settings from the sources Bicep reads,
  with --check-app against the Azure app; search log and OCR requests
  unset until Phase 3.
- update-api.sh and its timer: follow CI's prod deployment (public API,
  conditional requests), verify the image's build attestation, switch
  colors once /readyz passes, reject a commit that doesn't get ready.
- install.sh, journald 30-day retention, 14-day response cache cleanup,
  and test_serving.py (stub tools; deploy and install runs on Linux).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndexes

The settings from the Phase 0 API PRs, per their notes: the API takes the
visitor's address from CF-Connecting-IP when the peer is loopback, and
/readyz fails while the local searcher lacks an index current.json names.
Both are on-prem only, so --check-app expects them there alone. An image
from before those PRs ignores them and falls back to the peer address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 01:43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread scripts/onprem/serving/test_serving.py Fixed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved privacy, shared-cache correctness, and deployment-control issues prevent approval.

7 open issues
What changed in this PR

Adds the Phase 0 scripts for moving serving on-prem behind a Cloudflare Tunnel under #955. The new tooling prepares a local Quickwit searcher and blue/green API deployment without changing current Azure serving.

Changes:

  • Adds service units, loopback proxying, tunnel ingress, and retention settings.
  • Adds configuration generation, installation, and attestation-verified API updates.
  • Documents setup and rollback procedures and adds stub-based tests.
File Description
scripts/​onprem/​serving/​usnm-serving.tmpfiles Defines response-cache cleanup.
scripts/​onprem/​serving/​usnm-serving-update.timer Schedules API update checks.
scripts/​onprem/​serving/​usnm-serving-update.service Runs the updater with host settings.
scripts/​onprem/​serving/​usnm-searcher.service Runs the local Quickwit searcher.
scripts/​onprem/​serving/​usnm-api@.service Defines blue/green API containers.
scripts/​onprem/​serving/​update-api.sh Verifies images and coordinates deployment and rollback.
scripts/​onprem/​serving/​tunnel-ingress.json Routes public hostnames to Caddy.
scripts/​onprem/​serving/​test_serving.py Adds script and configuration tests.
scripts/​onprem/​serving/​README.md Documents setup, operation, and deferred work.
scripts/​onprem/​serving/​journald-usnm-serving.conf Sets journal retention.
scripts/​onprem/​serving/​install.sh Installs and enables serving components.
scripts/​onprem/​serving/​gen-env.sh Generates settings and checks Azure parity.
scripts/​onprem/​serving/​cloudflared-usnm-serving.service Runs the tunnel connector.
scripts/​onprem/​serving/​Caddyfile Proxies requests to the active API color.
scripts/​onprem/​serving/​apply-tunnel-ingress.sh Applies and verifies tunnel configuration.

Review effort: Balanced

Comment thread scripts/onprem/serving/Caddyfile
Comment thread scripts/onprem/serving/cloudflared-usnm-serving.service Outdated
Comment thread scripts/onprem/serving/usnm-api@.service Outdated
Comment thread scripts/onprem/serving/gen-env.sh
Comment thread scripts/onprem/serving/test_serving.py Outdated
Comment thread scripts/onprem/serving/update-api.sh Outdated
Comment thread scripts/onprem/serving/update-api.sh Outdated
tgoodyear and others added 2 commits October 22, 2026 22:26
…ut of logs

Review fixes for #1005:
- update-api.sh polls the registry's usnewsmap-api:main with the Arc
  identity instead of GitHub's deployments API, whose anonymous
  requests count against the 60-an-hour limit even when they come back
  304. A new manifest deploys once its attestation (CI's last publish
  step, after the Azure roll) verifies; the commit comes from the
  attestation and its tag must match. The attestation is asked for each
  run for 30 minutes, then every 10, and the manifest is rejected after
  3 hours. Rejections are by digest.
- --commit creates the hold file also when that commit serves already.
- Caddy's default log drops the request field, which its error log
  fills with the URI and headers when a color can't be reached.
- run-cloudflared.sh cuts query strings from cloudflared's lines: its
  error log carries a failed request's URL.
- Each color gets its own cache directory: LocalStore's temporary names
  use the process and thread id, and both colors are process 1.
- gen-env.sh resolves a relative --out against the caller's directory.
- Tests: no instrumentation-key-shaped fixture, no URL substring check,
  directories skipped in the file scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread scripts/onprem/serving/update-api.sh Outdated
Comment thread scripts/onprem/serving/update-api.sh
Comment thread scripts/onprem/serving/update-api.sh
…ry run or a registry error

Review 2 fixes for #1005:
- --ready-secs defaults to 800: the API's 420 s to load a version plus
  its 360 s readiness cap (containerapp.bicep), so a slow but healthy
  start isn't rejected. A test ties the default to the Bicep values.
- reject() writes nothing on --dry-run; it reports what it would reject.
- A failed lookup of the commit's tag is tried again next run; only the
  registry answering "no such tag" or a different digest rejects.
- The timer, service and README give the deploy time that follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Failed rollback can interrupt serving, while bootstrap and verification infrastructure failures can permanently exclude healthy images.

2 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Persisting transient attestation failures as permanent rejections

scripts/​onprem/​serving/​update-api.sh:254

gh attestation verify can fail for operational reasons, not just an invalid attestation. Even with --bundle, it can retrieve Sigstore trust material over the network unless a custom trusted root is supplied (see the CLI manual). A transient retrieval failure therefore reaches reject() and permanently excludes a valid image from automatic deployment. Preserve the verifier diagnostic and make trust-material retrieval failures retryable, while still requiring successful verification before deployment. Only persist a rejection when invalid attestation data or a policy mismatch is established.

3 resolved since last review

Review effort: Balanced

Comment thread scripts/onprem/serving/update-api.sh Outdated
Comment thread scripts/onprem/serving/update-api.sh Outdated
…ack can't be confirmed

Review 3 fixes for #1005:
- With no serving color, a new color that doesn't get ready isn't
  rejected: nothing shows the host is well, so it's tried again.
- After a failed switch, the new color stops only once Caddy is shown
  to route back to the old one. If that can't be confirmed, both colors
  stay up and the hold file stops the timer until someone checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants