Distributed setup
Use ADR-0051 and
its implementation/evidence for scope.
The personal TARGET=dev-NAME workflow remains in development.
Commands with ENV do not load the checkout's personal .env. Explicit personal
TARGET or SSH selections still fail before access to an installation. Ordinary
dev-box commands retain their saved settings, runner bindings and private state.
Inputs on one administration machine
Run administration through an SSH runner that holds the environment's private inputs and state, for example a small VM inside the site:
The first command records the runner for that ENV; later commands reuse it, and
another runner or local execution is refused until its private
.local/deployments/<ENV>/runner.json is deliberately removed. The workstation
sends only the committed revision; untracked and ignored files never travel. The
runner account must be unprivileged and have git, python3, uv, kubectl,
helm and SSH access to the inventory hosts; commands check their tools first. Its output is also kept in
~/.local/share/pharos-deployments/<ENV>/delivery.log, so an operation continues
after the SSH session ends. INVENTORY, KUBECONFIG, STATE_DIR and
PHAROS_PRIVATE_ROOT then name files on the runner. Without DEPLOY_RUNNER and a
recorded binding, the workstation is the administration machine.
Keep native inventory and its group_vars under
~/.config/pharos/environments/<ENV>/inventory/hosts.yml, or select another
private file with INVENTORY. Directories use 0700 and private files 0600.
Keep vault unlock credentials in the operator's secret facility. Never commit
connection credentials, private keys or administrator kubeconfig.
The native groups are kube-master, kube-master-primary, kube-node, etcd
and k8s_cluster; equivalent modern Kubespray names are supported. Conflicting
aliases fail. Switches and other non-cluster groups remain valid. Per-host
storage_devices preserve existing references for observation; fresh installation
requires explicit /dev/disk/by-id/ devices. Label three selected storage/monitor
hosts with storage-node: "True" and storage-control: "True"; selected AMD64
service hosts use pharos.platform/services: "true".
Administrator kubeconfig defaults to <private-root>/<ENV>/admin/kubeconfig.
KUBECONFIG selects another private file. PHAROS_PRIVATE_ROOT changes the common
private root; STATE_DIR selects a private record directory. These are paths,
never credential values. No private-state symlink or laptop-to-runner copy is
required. Set pharos_api_access: ssh on the primary only when API access needs
its native SSH binding; otherwise direct access is used.
A complete site also needs a private external-AAI JSON file named by the selected
primary host's pharos_external_aai_file. The administrative path must be mode
0600 and bind pharos-control-plane, pharos-tools and pharos-gitlab (and pharos-open-webui when the site serves chat sign-in), with the
issuer and an explicit public CA certificate. Do not put credentials in inventory,
committed policy, logs or the checkout. The plan records only the canonical input
fingerprint; delivery refuses missing or changed input before opening the cluster.
Commands
renew-tokens renews the platform's own GitLab and Waldur tokens (Source
Control's and the compatibility service's) without a delivery, under the
environment lock. It reads the expiry the Secrets record and renews only a token
in its last week, and finishes a rotation an earlier run left half done. Every
workload that reads a renewed Secret is rolled, and a replaced GitLab token is
revoked only after all of them run Ready on the new token and the provider accepts
it; until then the rotation stays marked for the next run. It fails when GitLab or
Waldur is not Synced and Healthy. Run it daily on the runner with the user timer in
devops/platform/systemd/. The timer runs the revision and runner modules the last
successful deploy or reconcile pinned, under the runner's checkout lock, so it
needs one successful delivery first:
hpc-site-central provisions the platform half of the environment's HPC Site (its
Waldur offering, the Site agent's account and the Site API bearers). It runs where
the private inputs are, on the runner when one is set or recorded, under the same
environment lock, and keeps its files in that machine's
~/.config/pharos/environments/<ENV>/hpc-site. The offering stays Draft unless
OFFERING_STATE=Active is given.
The plan writes private evidence on the administration machine only. Offline mode performs no SSH/API
queries and is accepted only for a plan. Commit tracked source before mutation;
the command freezes that revision for the whole operation. ansible-apply installs
one whole fresh topology, followed by selected foundation layers. It refuses
unrecorded existing Kubernetes/etcd state and never wipes devices. A pending
operation may be retried with its unchanged inputs; inspect its private stage log
when it fails. On a ready foundation, ansible-apply also reapplies changed
storage/mesh/Argo policy, but refuses node, group, Kubernetes version or CNI
changes. The environment lock prevents overlapping operations.
If Kubernetes itself stopped after occupying the hosts but before recording its
cluster identity, inspect that installation and restore its verified private
access/identity first. The command refuses to adopt an unanchored partial cluster.
Kubespray uses its own pinned tools under devops/kubespray; the host/foundation
tools use devops with the collections pinned in devops/ansible/requirements.yml,
installed privately under ~/.cache/pharos; operator collections are not used. Do not override Kubespray's version assertion or downgrade the
personal workflow's Ansible. State/source caches remain private on the administration machine.
icaruscloudlab remains foundation-only until it has approved public origins and
external identity. A three-node lab with one control plane/etcd member is not HA.
GPU hosts use the explicit host/cluster ownership in the NVIDIA GPU Operator guide. Host driver, toolkit and containerd runtime work is Ansible-only on selected GPU hosts. The operator and its operands are an opt-in Argo component, never part of the dev-box composition.
When delivery is enabled, Argo installs the closed complete-site composition:
Argo CD, routes, shared infrastructure, GitLab, Waldur, LLM, Pharos, the paused
Site Runtime and themelio-compat. The compatibility service is mandatory until
all its routes have moved to the new Themelio. Development AAI, Mailpit, browser
aliases and Waldur seeds are excluded.
KServe and LeaderWorkerSet are included in this composition. Delivery refreshes their bootstrap prerequisites and grants on existing foundations as well as fresh installations. See platform model serving for architecture-specific CPU images and preservation of external prerequisites.
Public routing uses the same catalogue as a dev box, with explicit HTTPS/443 origins and existing certificates. The root receives exact active profiles, catalogue checksum, Ceph certificate trust and built image digests. Unknown or missing Applications, development identity or an incomplete child set fail before the root is applied.
While make deploy runs, the GPU (gpu-dra, gpu-operator), Kubeflow and
llm-inference Applications can read Unknown or Missing ("namespace … is not
managed") for a while. That is expected: the Argo step writes the cluster views
for the platform's own namespaces only, and delivery widens them for the selected
compositions after the image builds, before the root names those Applications
(platform_deploy/delivery.py, share_namespaces). Do not patch the views by
hand. That step clears the state; the readiness gate at the end must still report
every Application Synced and Healthy at the delivered revision.
Qualification creates an owned namespace and checks cross-node storage, actual network denial and image distribution. Success deletes it with its exact UID; failure retains it and the private report for inspection. The registry probe builds one real platform image without starting its application or giving it credentials, then pulls that digest on all three nodes.
GitLab LFS capacity
The distributed GitLab profile keeps three idle webservice/Workhorse replicas and allows its HPA to reach 18 under load. It uses 900-second Rails waits, a 3600-second Workhorse proxy-header timeout and 16 concurrent Git LFS transfers. Pinned GitLab 19.4.2 reached 154 MiB/s at 18 replicas with four clients; eight clients reached 213 MiB/s, or 99% of the matched direct-RGW ceiling. A four-client 1 GiB sequential test reached 252 MiB/s upload and 292 MiB/s download.
The 19.5.1 images remain pinned to amd64 pending equivalent ARM64 qualification.
The preceding 19.4.2 images failed at startup on the cluster's ARM64 nodes with
64 KiB pages (jemalloc: Unsupported system page size), so the distributed
overlay pins every GitLab component to kubernetes.io/arch=amd64.
The Site Runtime preparation image pins checksum-verified Git LFS 3.8.1 rather
than the Bookworm package. The matched direct-download client test measured an
11% median improvement over 3.6.1, while hashing and disk writes were ruled out
as the remaining bottleneck. The previous immutable batch-image digest is the
rollback; replacing the download with apt-get install git-lfs restores the
distro client for a subsequent build.
Keep LFS downloads proxied until a public TLS, presigning and NetworkPolicy
contract exists; the object endpoint is currently internal HTTP. GitLab excludes
LFS objects from attachment and push-size limits, and chart 10.5.1 has no
per-object LFS cap.
Existing site
These existing-foundation operations read identities/readiness and capture a
private review. They do not invoke Kubespray, discover/wipe disks or replace
application writers. Optional private pharos_expected_cluster_uid and
pharos_expected_ceph_fsid bindings reject a different site. Follow the
cutover runbook after 0058 supplies
the complete identity/application composition and a maintenance window.
cutover-plan writes a read-only maintenance review and preserves the foundation
record. After approving that private capture and retiring the reviewed writers,
make foundation-adopt ENV=icarus-cloud ... rechecks the source/input/topology
fingerprints and live Kubernetes, node and Ceph identities before recording the
existing foundation. The Kubernetes API and every kubelet must match the pinned
release. Follow the cutover runbook
for the exact sequence and repeat adoption after a policy change. Kubespray
foundations retain their installer-owned record.
Approved origins use HTTPS/443 with a distinct hostname per service; certificates
stay under their existing renewal owner. The Pharos browser origin uses one
platform.<domain> name for the web application and its same-origin API, native
/auth/oidc/callback and realtime routes; a separate direct API hostname is not
selected by implication. The native Site API uses the distinct site.<domain>
host, with only /v1/site/commands/ and /v1/site/ssh-access/ forwarded to the
HTTPS site-api Service. It performs scoped bearer authentication and does not
use browser OIDC or register a Site API redirect URI. The gateway verifies the
backend certificate for site-api.pharos.svc with the internal Site API trust.
Every distributed installation declares a kubeflow and an llm-chat origin. The
retained web's sidebar links to both, and to the pharos origin's Support view,
so delivery stops when either origin is missing. A new kubeflow origin also needs
its https://<kubeflow-origin>/oauth2/callback registered at the AAI, as
the Kubeflow identity decision
describes.
Icarus-cloud currently selects the canonical *.staging.pharosfactory.ai
application names, while Argo, storage and the AAI issuer retain separately
sourced bindings. Danias owns DNS changes for *.pharosfactory.ai; DNS and
certificate existence remain qualification gates. The Site Agent and installer
use these public base origins without appending /v1/site/:
Management routes keep the same restrictions.
Choose one certificate owner before installing the public edge. With the default
edge_certificate_mode: existing, an external issuer such as the CSLab inventory's
Certbot layout renews the certificates; the role verifies trust, hostname, expiry
and key match for edge_certificate_files and writes no port-80 listener. With
edge_certificate_mode: certbot the role owns the names instead: it installs
/etc/nginx/conf.d/pharos-platform-http.conf, which answers
/.well-known/acme-challenge/ from edge_acme_webroot and redirects every other
request to HTTPS, requests one lineage per approved host with
certbot certonly --webroot, then installs the 443 configuration. Set
edge_acme_email and review with edge_acme_staging: true before a real site
switches its names. A renewal hook (nginx -t && systemctl reload nginx) is
recorded in each lineage, and the run checks it with certbot renew --dry-run --run-deploy-hooks, because a plain dry run runs neither the recorded directory
nor the deploy hooks.
edge_acme_server selects an alternative ACME directory, which is how a
laboratory's own test CA is exercised without a public name.
make deploy installs the edge itself, right before it applies the root: it
writes the edge's environment from the policy's origins (exactly the edge
schema's keys) to the private state directory and runs edge-distributed.yml on
the inventory's platform-edge hosts. A certificate for a newly selected origin is
issued before the HTTPS configuration names it. The certificate owner above stays
the private inventory's choice, and the delivery run skips the certbot renew
dry-run probe; run the playbook with edge_acme_renewal_check: true to check
renewal on purpose.
The challenge listener must be the only server on the host that declares
listen 80 default_server, and the 443 file must be the only
listen 443 ssl default_server; retire a legacy virtual host in the same step
rather than alongside it, because nginx -t rejects the duplicate. The role
retires the nginx package's own welcome vhost, whose symlink also claims port 80,
and fails on any other occupant instead of removing it. A listener that fails that
check is rolled back before the run stops: the previous file is restored, a first
installation is removed and the retired welcome symlink is put back, so the host
keeps a configuration every later nginx -t, reload and restart accepts. A pending
reload survives a failed reload or an interruption after validation. Retrying with
an unchanged listener completes that reload before requesting certificates.
Outbound mail is one smtp block in the environment policy: the relay host,
port, TLS mode, the EHLO name the relay will see, the sender, the public API
address a member's client uses and the address blocks the sending pods may reach.
An enabled block must have all of them, including at least one address block; a
malformed one is refused before any site uses it. Sending workloads reach that relay directly over
STARTTLS and verify the relay's own certificate, so the address the relay permits
is the one it observes. The blocks are declared rather than resolved while
rendering, because a policy cannot name a host and a DNS answer would not be
deterministic: smtp.egress takes exact networks, refuses a default route, and
the delivery renders one owned smtp-egress NetworkPolicy in the pharos
namespace only while smtp.enabled is true, withdrawing it otherwise. The base
manifests stay site-neutral, so each environment declares its own relay.
The delivery renders the block into Metallo's Deployment beside the workloads it
places, as LLM_ACCESS_SMTP_HOST, _PORT, _TLS, _FROM and _EHLO plus
LLM_ACCESS_PUBLIC_API_URL, and renders nothing while smtp.enabled is false.
The SMTP public_api origin must match edge.origins.llm-api; delivery appends /v1
for the client base shown in personal-key emails. The workload therefore reads one
reviewed source: it keeps mailing off without a host
and refuses to start without a sender and the public API address. The EHLO name
matters because a container's own name is not what the relay knows the site by. Waldur and Zammad take the same
values when their profiles are enabled: Zammad's desk sends its outgoing mail to
that relay, with the policy's TLS mode and certificate verification, in place of
the Mailpit sink it bundles, and only while smtp.enabled is true. A password,
if one is ever needed,
follows the external-identity pattern: a private file with mode 0600 rendered to
a per-namespace Secret.
Waldur receives the relay, sender, STARTTLS or implicit TLS mode, and EHLO name
through its chart's mail values. It uses Django's standard certificate
verification. Its API, worker and scheduler restart when those values change.
Disabling the SMTP policy removes the relay from the generated Waldur values.
Waldur's namespace currently restricts ingress only; the pharos SMTP egress
policy above applies to Metallo.
Project invitations use the HTML and plain-text templates in
gitops/manifests/waldur/chart/files/invitations.
The generator inserts this environment's Themelio /dashboard URL. The recipient
signs in with the invited address and accepts the pending invitation there.
Other invitation scopes retain Waldur's own acceptance link. The post-sync
bootstrap applies the two overrides through waldur override_templates on every
sync, so existing installations receive template changes. Other stored templates
are preserved. Template customisation does not enable a disabled notification.
To activate this on an existing site:
- Include the generator and chart changes in the site's reviewed deployment
revision and deliver it through the normal environment workflow. The SMTP
policy already enabled in Icarus Cloud needs no additional policy change.
Wait for Waldur's API, worker and scheduler rollouts and the post-sync
pharos-bootstrapJob to succeed. - With the site's administrator kubeconfig, check the worker's effective
EMAIL_HOST,EMAIL_PORT,EMAIL_USE_TLS,EMAIL_USE_SSL,DEFAULT_FROM_EMAILanddjango.core.mail.utils.DNS_NAME.get_fqdn(). Confirmusers.invitation_createdis enabled under Waldur's Notifications administration and that the two stored invitation templates contain the current dashboard URL. - Create one project invitation to an explicitly approved test recipient. Confirm receipt of both HTML and plain-text content, the sender, expiry, dashboard link and acceptance after AAI sign-in. Check the worker and relay logs if delivery fails. A successful TCP or STARTTLS handshake establishes connectivity; mailbox receipt and project membership establish the journey.
- After delivery is qualified, review affected invitations and resend those still pending. Replace expired invitations through the normal invitation workflow. SMTP activation affects Waldur notification mail generally, so review its existing pending work before rollout.
The separate TLS client (the smtp-proxy role) is retained for a host-local
sender only; it was never wired to the policy and is not part of this path. It
validates upstream TLS, delays relay DNS until connection time and never shares
an Nginx startup dependency.
Test the edge configuration without sending mail:
Page source
Source snapshot · Revision f928834c8.