Script the CVM deploy, and write down what a cluster move costs - #101
Draft
HONGJICAI wants to merge 5 commits into
Draft
Script the CVM deploy, and write down what a cluster move costs#101HONGJICAI wants to merge 5 commits into
HONGJICAI wants to merge 5 commits into
Conversation
The release workflow produced the attested manifest and switch.sh moved traffic between two CVMs; in between sat a dashboard form. That form is not a convenience gap — allowed_envs and the public_logs / public_sysinfo / public_tcbinfo / secure_time flags all land in the measured app-compose, so every field in it is an unrecorded input to app_id, and two blue/green sides can only be compared if they were filled in identically. deploy.sh is that record. It takes a release's digest-pinned docker-compose.release.yml (checked against the sha256 the release publishes, and refused outright if any gateway-image line is still on a tag — the pin covers cvm-identity too, which runs as root with the guest-agent socket), hands the CLI a mode-600 env file rather than an argv full of secrets, reads back the two DNS records whose absence fails silently, and waits on /readyz by app-id before anything points at the new CVM. It never moves traffic: that stays switch.sh's separate decision. Also corrects the CLI: `phala cvm create` is gone in favour of `phala deploy`, and --vcpu/--memory in favour of --instance-type. The new README section is the other half. Moving GATEWAY_DOMAIN reads like a one-line edit and is not: the new cluster's OS image needs an osimages.json entry or pcverify FAILS for every user; its mr_config_id must still be the V1 layout or compose_hash cannot be read out of a quote at all; app_id must still be compose_hash's leading 20 bytes or the record model and the standby probe both stop meaning what they say. All three are verification inputs, so they are settled on a throwaway CVM before a cutover, not after. And because the serving alias names the cluster and both sides share it, blue and green cannot straddle two clusters — a cluster move is a cutover with a DNS-shaped gap, not a zero-downtime flip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012sACS7WuB37x2tUMfsxvXi
dstack's app-compose carries a `name` field and app_id is that manifest's hash,
so the CVM name given at create time is measured — `pcverify` prints it as
`app name`, and the live deployment's is `0g-pc-gateway-a-1`. It reads like a
dashboard label and is not one: changing it is a different app_id, i.e. a new
CVM and a re-audit, never a rename.
So deploy.sh now names a CVM 0g-pc-gateway-<ZG_PROM_ENV>-<side> rather than
0g-pc-gateway-<side>. Both halves earn their place in a measured field: without
the env, a staging CVM and a mainnet one on the same build would hash to one
app_id, and nothing else in the manifest separates them — DOMAIN and the router
URL are ${…} references whose values are never measured. ZG_PROM_ENV already
carries exactly this distinction, so there is one place to state it.
And a name collision is now refused. The name is also the CLI's handle
(`--cvm-id` accepts one), so a second CVM under a live name leaves
`phala cvms delete --cvm-id …` addressing whichever the API picks — the failure
you least want to discover with a rollback in hand. --allow-duplicate-name is
the deliberate override, which is also how a replica of one side gets deployed.
blue-green.md said the image digest was the only side-distinguishing field in
the measurement. That was true of the compose TEXT and false of the manifest
app_id actually hashes: with per-side names the two sides differ by name even
before the image does, so "rehearse with two different builds" was never the
requirement it claimed to be. The scaling section gains the other half of the
same fact — replicas must share a name or they are not replicas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sACS7WuB37x2tUMfsxvXi
The two environments deploy.env feeds were separated in one direction only.
Toward the CVM it was already an allowlist — build_env_file writes ten names
and nothing else can reach a container, which is right, because the compose
interpolates a fixed set of ${…} references and allowed_envs is measured.
Toward child processes it was `export` everything, inherited from switch.sh.
That is fine for switch.sh, whose children are curl and openssl. It is not fine
here: our child is the phala CLI, which has an env contract of its own. So the
Cloudflare token and the Prometheus password were sitting in the environment of
every process this script starts, and a `DEBUG=1` in deploy.env — which reads
like it configures the script — would have reached the CLI, where
DEBUG=phala::api-client prints every HTTP request body. PRIVATE_KEY and
ETH_RPC_URL are the same shape of trap in the other direction.
Values are now plain shell variables, and exactly four names a child genuinely
needs (PHALA_CLOUD_API_KEY, PHALA_CLOUD_API_PREFIX, PRIVATE_KEY, ETH_RPC_URL)
are exported.
Also warns on the two things a config file cannot do but looks like it can: a
ZG_GATEWAY_* the compose does not reference (every other gateway setting is
literal, measured text — changing one is an app_id change, not a variable), and
DEBUG, which is now deliberately not passed on. Both would otherwise be
discovered by deploying and wondering why nothing changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sACS7WuB37x2tUMfsxvXi
`--cvm-id`'s help says it takes "UUID, app_id, instance_id, or name", and this
script believed it. The CLI passes that value straight into the `cvms/{id}` API
path and resolves no names itself; its SDK schemas accept only id, uuid, app_id
and instance_id. So name resolution, if it happens at all, is the API's and
undocumented — not something to build on.
The place it would have hurt is the collision check. A name that never resolves
answers "not found" every time, so the guard would have passed silently on
exactly the case it exists to catch, and the first symptom would be two CVMs
answering to one name with a rollback in hand.
Names are now resolved against `phala cvms list` — matched on the list's own
`name` field, with the platform id read back out — and every later call uses
that id. The deploy output prints it beside app_id, and the docs no longer tell
anyone to `phala cvms delete --cvm-id <name>`.
The measured-name half of the story is unaffected: that one rests on dstack's
AppCompose carrying a `name` field and on the live deployment's own pcverify
output (`app name 0g-pc-gateway-a-1`), not on anything the CLI claims. A name
is measured AND not an identifier — the two facts are independent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sACS7WuB37x2tUMfsxvXi
Eight things, in rough order of how much they would have cost:
A list that could not be READ was indistinguishable from a name that is free.
`cvm_ids_by_name` now dies on a failed `phala cvms list` instead of returning
nothing, because "the API did not answer" silently becoming "that measured name
is available" is the exact hole the list-based lookup exists to close.
Under --allow-duplicate-name the resolver returned the FIRST match, which is
usually the CVM that was already there — so the app_id fallback could have
probed the old healthy CVM and pronounced the new one ready, and the failure
message would have named the live replica's id to delete. An ambiguous name now
resolves to nothing, and the caller says which case it is in.
The five measured app-compose booleans were compared against literal `true`
with nothing rejecting a near miss: PUBLIC_TCBINFO=True sent
--no-public-tcbinfo while the plan printed `True`, turning off the guest-agent
route pcverify reads app-compose through — a silent app_id change that fails
code identity for every user. They are now validated to exactly true/false.
Two commands were documented in forms that always die: `verify` without
--release/--compose (it has to know which manifest to compare byte-for-byte)
and `status` without --side (that is half the CVM's name). The post-deploy
banner now prints `verify` with the flag THIS run used — the resolved release
tag, not `latest`, which by then may be a different release.
RELEASE_TAG was assigned inside a command substitution, so the plan never named
the release `--release latest` had resolved to. It travels through a file now.
`grep -n` prefixed line numbers before the ^-anchored gateway pattern filtered
them, so the "other unpinned images" warning re-listed the gateway lines it had
just approved. Filter first, number after.
And two empty-array expansions lacked the `${arr[@]+…}` guard that line 769
already had — `set -u` on bash < 4.4, which macOS still ships and this script
re-execs into.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012sACS7WuB37x2tUMfsxvXi
HONGJICAI
marked this pull request as draft
August 31, 2026 13:27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two thirds of a release were already scripted and one third was not.
release.ymlproduces the attested artifact,switch.shmoves traffic between two CVMs — and in between sat a dashboard form. That form is not a convenience gap:allowed_envs, the CVM name, and thepublic_logs/public_sysinfo/public_tcbinfo/secure_timeflags all land in the measured app-compose, so every field in it is an unrecorded input toapp_id, and two blue/green sides can only be compared if they were filled in identically.deploy/phala/deploy.shis that record, plus a README section on what movingGATEWAY_DOMAINactually costs.deploy.sh
docker-compose.release.yml, checked against the sha256 the release publishes (the API's assetdigest, falling back to the notes), and refuses any gateway-image line still on a tag — includingcvm-identity, which runs as root with the guest-agent socket mounted, and which must carry the same digest asgateway.-e KEY=VALUE: an argv is world-readable inpsfor the life of the call and four of these values are secrets./readyzby app-id (<app_id>-443s.<base>), so the new CVM is verified before anything points at it. A cold side has to finish its first warmer sweep, so that window is minutes.public_logsandpublic_sysinfooff (this CVM handles sealed prompts, and anything a container logs is disclosed),public_tcbinfoon (pcverify fetchesapp-compose.jsonthrough it), never--dev-os(it wants an SSH key, i.e. interactive root inside the enclave).switch.sh's separate decision.It also corrects the CLI:
phala cvm createis gone in favour ofphala deploy, and--vcpu/--memoryin favour of--instance-type.INSTANCE_TYPEis deliberately left with no default, becauseGOMEMLIMIT=24GiBin the compose was derived by hand from a 32 GiB CVM and nothing in the manifest can check the shape.Two facts about the CVM name, which are independent
It is measured. dstack's
AppComposecarries anamefield andapp_idis that manifest's hash, so the name is insideapp_id— the live deployment's own pcverify output printsapp name 0g-pc-gateway-a-1. It is not a label to be tidied later: a new name is a newapp_id, i.e. a new CVM and a re-audit. So the default is0g-pc-gateway-<ZG_PROM_ENV>-<side>; without the env, a staging CVM and a mainnet one on the same build would hash to oneapp_id, sinceDOMAINand the router URL are${…}references whose values are never measured.This corrects
blue-green.md, which said the image digest was the only side-distinguishing field in the measurement. That was true of the compose text and false of the manifestapp_idhashes.It is not an identifier.
--cvm-id's help says it takes "UUID, app_id, instance_id, or name", but the CLI passes the value straight into thecvms/{id}path and its SDK schemas accept only id / uuid / app_id / instance_id — any name resolution is the API's, undocumented, and unverified. So names are resolved againstphala cvms listand every later call uses the platform id, which the deploy output prints besideapp_id.The env separation
deploy.envfeeds two different environments and they no longer bleed into each other:${…}references andallowed_envsis measured;exporteverything (inherited fromswitch.sh). That is fine forcurlandopenssl; it is not fine for the phala CLI, which has an env contract of its own. The Cloudflare token and the Prometheus password sat in the environment of every child, and aDEBUG=1indeploy.envwould have reached the CLI, whereDEBUG=phala::api-clientprints every HTTP request body.Now four names a child genuinely needs are exported and the rest are plain shell variables. The script also warns on the two things a config file looks like it can do and cannot: a
ZG_GATEWAY_*the compose does not reference (every other gateway setting is literal, measured text) andDEBUG.Moving to another dstack cluster (README)
Changing
GATEWAY_DOMAINreads like a one-line edit. Three of the things it moves are verification inputs, so they get settled on a throwaway CVM before a cutover — the failure otherwise lands on users runningpcverify, not on us:osimages.jsonentry, or the os-image check fails for every user (that entry is Pin dstack-0.5.9 in the OS-image allowlist #100 fordstack-0.5.9);mr_config_idmust still be the V1 layout, orcompose_hashcannot be read out of a quote at all — a code change, not a redeploy;app_idmust still becompose_hash's leading 20 bytes, or the record model and the standby probe stop meaning what they say.Plus: the SNI allowlist is per-cluster and fails as a bare TLS error with DNS and certificates looking perfect; the serving alias names the cluster and both sides share it, so blue and green cannot straddle two clusters and a cluster move is a cutover with a DNS-shaped gap; certificates start over from an empty volume against a per-hostname rate limit; and every enforced check is fail-closed, so a blocked egress host looks exactly like a CVM that never becomes ready.
Self-review
A review of the branch found eight things, all fixed in the last commit — the ones worth naming here:
phala cvms listwas indistinguishable from a free name, so an API blip would have waved a duplicate of a measured name through the collision guard;--allow-duplicate-namethe resolver returned the first match — usually the CVM already there — so the fallback could have probed the old healthy CVM and pronounced the new one ready;truewith nothing rejecting a near miss:PUBLIC_TCBINFO=Truesent--no-public-tcbinfowhile the plan printedTrue;verifyandstatuswere documented in forms that always die (the first needs to know which manifest to compare byte-for-byte, the second needs--side).Every path was exercised against a stub CLI: preflight, dry-run, deploy, the not-ready failure (exit 1, and it says the CVM is still running), name collision, ambiguous name, list failure, bad boolean, floating tag, wrong-side issuance, cluster mismatch, missing config.
pcverify's flag combination used byverifywas confirmed to be accepted (exit 1 on an unreachable host, not exit 2 for a caller mistake).Not exercised against the real platform. Nobody has run this against Phala Cloud yet, so two things want checking on the first real run: where
phala deploy --jsonputsapp_id(the script scans for any 40-hex value and falls back tocvms get), and whetherINSTANCE_TYPEon the new cluster can actually give the 32 GiB the compose'sGOMEMLIMITassumes.Generated by Claude Code