Operator steps in order: prepare/uncordon titan-24, merge, run one validation build (both arch leaves + promoted index), apply the titan-22 affinity flip, verify placement. Documents the correction that worker membership is reconciled by the Flux node-prefer-noschedule CronJob, not Ansible, and lists what was validated locally vs what only a real Jenkins build can prove. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
8.4 KiB
hermes-agent multi-arch rollout runbook
Goal: build the hermes-agent container image for both linux/arm64 and
linux/amd64 so the hermes-agent workload can eventually run on the amd64
node titan-22, while the arm64 pi fleet remains a valid fallback.
This branch (feature/hermes-agent-multiarch) contains everything needed. The
steps below are the operator procedure, in order. Do not reorder: the
titan-22 runtime flip is deliberately the last step and only after a validated
multi-arch image exists.
What changed on the branch (for reviewers)
| Area | File | Change |
|---|---|---|
| Base image | dockerfiles/Dockerfile.hermes-agent |
FROM repointed from the arm64-only leaf sha256:47d4bd4c… to the upstream multi-arch OCI index sha256:9c841866021c54c4596849f6135717e8a4d52ba510b7f52c50aef1de1a283973 (tag v2026.7.7.2, revision 9de9c25f…). Same reviewed upstream version; arm64 leaf is byte-identical to the old pin. |
| Pipeline | ci/Jenkinsfile.hermes-agent-image |
arm64 kaniko leg now pushes …-build-<N>-arm64; new amd64 kaniko leg on a titan-24-pinned, resource-capped pod pushes …-build-<N>-amd64; new Combine multi-arch index stage; evidence archive expanded. |
| Combiner | ci/scripts/hermes_multiarch_combine.py |
New fail-closed pure-python step that verifies both leaves and publishes/re-verifies a Docker manifest list at the arch-less …-build-<N> tag, emitting the index digest in the existing digest-file/image-file format. |
| Promotion | ci/scripts/hermes_oci_promote.py |
Unchanged. Docker manifest lists are already inside its MANIFEST_TYPES allow-list, so it promotes the multi-arch INDEX to …-release with no edit. |
| Release scripts | ci/scripts/hermes_image_release.py |
Unchanged. The combiner hands it the arch-less tag + index digest, exactly the shape it already validates/renders. |
| Worker join | infrastructure/core/node-prefer-noschedule-cronjob.yaml |
clear_worker titan-24 amd64 + a soft PreferNoSchedule guard taint protecting the co-hosted Sui validator. |
| Runtime flip (LAST) | services/hermes/agent-deployment.yaml |
Adds titan-22 to the allowed set with a soft preference + media-taint toleration, keeping the arm64 fleet as fallback. Prepared as a separate commit — apply only after validation. |
Correction to the original assumption
Worker/arch/taint labels in this cluster are not managed by Ansible. They are
reconciled every minute by the Flux-managed node-prefer-noschedule CronJob
(kubectl label/kubectl taint). hosts/ Ansible is a thin skeleton and
titan-24 is already present there. Therefore the "worker-join" change lives in
that CronJob, and it is applied by Flux at merge, not by a hardware playbook
run. The only genuine hardware/operator action is ensuring titan-24 is
uncordoned (it is on the Ananke recovery uncordon denylist).
Step (a) — Prepare titan-24 as a schedulable amd64 worker
The worker label + guard taint ship in this branch's CronJob change and land automatically once Flux reconciles after merge (step b). Before the first amd64 build, the operator must make sure titan-24 can actually accept the pinned pod:
# On a control-plane host / with cluster admin kubeconfig:
kubectl get node titan-24 -o wide # confirm Ready, amd64
kubectl uncordon titan-24 # recovery keeps it cordoned by default
# The Sui validator runs as docker-compose at /opt/sui-validator (outside k8s);
# it is protected by the build pod's tight resource caps, not by k8s priority.
After merge (step b), confirm the CronJob applied the labels/taint:
kubectl get node titan-24 --show-labels | tr ',' '\n' | grep -E 'worker|hardware'
# expect: node-role.kubernetes.io/worker=true and hardware=amd64
kubectl describe node titan-24 | grep -i taint
# expect: atlas.bstein.dev/sui-validator=true:PreferNoSchedule
If titan-24 must NOT be labeled yet, hold the merge; the labels are inert until the pod is scheduled and the guard taint only softly repels routine pods.
Step (b) — Merge the branch to main
Merge feature/hermes-agent-multiarch to main without the final titan-22
flip commit if you want to stage the runtime change separately (see step d). The
Dockerfile, Jenkinsfile, combiner, tests, and CronJob are safe to merge: the
arm64 build path is unchanged and the amd64 leg + combine only add new,
fail-closed artifacts. Flux then applies the CronJob (titan-24 becomes a
worker).
Step (c) — Trigger ONE validation build
Trigger the existing manual hermes-agent-image job exactly as today (the same
PUBLISH_IMAGE=true, EXPECTED_SOURCE_REVISION=<full reviewed sha on main>,
CONFIRM_PUBLISH=PUBLISH HERMES AGENT gate). Nothing about the trigger changes.
Success looks like:
- arm64 leg builds on an rpi5 pod and pushes
…-build-<N>-arm64. - amd64 leg builds on titan-24 and pushes
…-build-<N>-amd64. - Combine multi-arch index prints
"result": "published"with anindex_digest, and the registry resolves…-build-<N>to a Docker manifest list referencing exactly the two leaves. - Render / Verify-and-archive / Publish stages behave exactly as before but on
the index digest; the
…-build-<N>-releasetag is the multi-arch index. - Archived evidence now includes both per-arch digests/images plus the index digest/image.
Independent confirmation the release is multi-arch:
skopeo inspect --raw docker://registry.bstein.dev/bstein/hermes-agent:git-<sha>-build-<N>-release \
| jq -r '.mediaType, (.manifests[] | "\(.platform.os)/\(.platform.architecture) \(.digest)")'
# expect: application/vnd.docker.distribution.manifest.list.v2+json
# linux/amd64 sha256:…
# linux/arm64 sha256:…
Flux will roll the hermes-agent deployment to the new index digest. It still
runs on arm64 (unchanged affinity), and arm64 nodes keep pulling the arm64 leaf
— so this step is safe even though the runtime is still arm64-only.
Step (d) — Apply the titan-22 runtime affinity flip
Only after step (c) proves the multi-arch image. Apply the final, separately
messaged commit APPLY ONLY AFTER multi-arch image validated: allow hermes-agent on titan-22 (it edits services/hermes/agent-deployment.yaml and the matching
layout test). It:
- adds a second
nodeSelectorTermmatching amd64titan-22(worker) — the arm64 fleet term is untouched, so the pi fleet remains a valid fallback; - adds a soft
preferredDuringSchedulingnudge toward titan-22; - adds a toleration for titan-22's
atlas.bstein.dev/media-primary=true:PreferNoScheduletaint. This does not change jellyfin's own scheduling or priority — a toleration only lets hermes-agent also consider titan-22 as spare capacity.
Merge that commit; Flux recreates the pod (deployment strategy is Recreate).
Step (e) — Verify the worker lands correctly
kubectl -n hermes get pod -l app=hermes-agent -o wide
# It may still land on an rpi5 (fallback is intact and pis are preferred-eligible);
# it is now ALSO permitted on titan-22. To confirm titan-22 can host it, cordon
# the pi fleet briefly or scale/drain, or simply confirm the pod tolerates the
# media taint and schedules when pis are unavailable.
kubectl -n hermes describe pod -l app=hermes-agent | grep -iE 'node:|toleration'
To roll back the runtime flip, revert the step-(d) commit; the arm64-only affinity returns and the multi-arch image keeps working on arm64.
What is validated locally vs what only a real build proves
Validated locally on this branch:
- The base index digest resolves to both
linux/arm64(the old pin) andlinux/amd64, same revision9de9c25f…(skopeo inspect --raw). hermes_multiarch_combine.pyend-to-end unit tests (full hash + arch-config verification chain, idempotency, and every fail-closed branch).- The updated + new image-builder tests,
hermes_oci_promote, and the automation tests pass. kustomize build services/hermesandkustomize build infrastructure/coresucceed.
Only a real Jenkins build can prove:
- Kaniko auto-selects the correct native leaf from the index on each arch, and the amd64 heredoc-compat replay produces a working amd64 image.
- The amd64 pod actually schedules on titan-24 within its resource cap without starving the Sui validator.
- Harbor accepts the manually-assembled manifest list PUT and immutability applies to the index tag.
- Cross-pod
stash/unstashof the amd64 leaf evidence behaves as expected.