P02 — operator-facing documentation (REQ-091, REQ-092, REQ-093; gate C-22). docs/cli.md (REQ-091): - Full CLI command/flag reference: every command/subcommand with synopsis, flag tables (name/type/default/description), one-line examples. - Global flags (--json, --system, --config, --no-deprecation-warnings). - Output modes (text/json/watch), env vars, exit codes. - Deprecated surface callout boxes (daemon, cert, node-join-mTLS, HCL jobspec) pointing to v0.11 removal. docs/jobspec.md (REQ-092): - Markdown frontmatter schema reference: all top-level keys, block reference (runtime/ports/env-secrets/volumes/restart/update/service/ health/lifecycle/constraints/affinity/tasks), kinds matrix (Job/Service/DaemonSet required vs allowed), CEL subset grammar, body byte-exact preservation (R-015), deprecated HCL callout. docs/ingress.md (REQ-093): - Traefik ingress reference: service->Traefik mapping (D-175), R-007 socket-vs-TCP-bind semantics, generated YAML shape (routers/services/ healthCheck), atomic reload (C-10), drain (weight:0), TLS (certResolver, trust domain, step-ca), worked-example pointer to examples/full-stack/, v0.11 forward limitations. All factual claims grounded in live codebase (gate C-22). Cross-links verified to resolve. ---ci--- project: orca phase: 2 milestone: v0.10 status: execute ---/ci---
6.7 KiB
Orca Ingress & Traefik Guide
This document explains how Orca configures ingress via Traefik dynamic configuration. It covers the service→Traefik mapping, the R-007 socket-vs-TCP-bind model, atomic reload, drain, TLS, and a worked example.
Canonical path (v0.9): Orca generates Traefik dynamic configuration files via the
TraefikEmitter. Thekind: Serviceworkload implies a Traefik route. The emitter renders one YAML file per Service; Traefik watches the dynamic config directory and reloads atomically on change.
The model
A kind: Service jobspec implies a Traefik route (D-175). Job
and DaemonSet do not carry a Traefik route by default — a
service: block on a Job is rejected by the validator.
When orca job run submits a kind: Service workload, the
TraefikEmitter renders a Traefik dynamic config file at:
/etc/traefik/dynamic/orca-<service-name>.yaml
This file contains:
- One router (
orca-<name>) with aPathPrefixrule and TLS config. - One service (
orca-<name>) as aloadBalancerwith one server per port, pointing at the workload's Unix socket (or TCP port). - A healthCheck stanza when the
health:block is present.
Traefik watches /etc/traefik/dynamic/ via fsnotify and reloads
whenever a file changes. Orca writes config atomically (write-tmp +
rename) so Traefik sees a single IN_MOVED_TO event and never observes
a half-written file.
R-007: socket vs TCP bind
Orca workloads bind to a Unix socket by default, not a TCP port. This is the R-007 security model: loopback-only by default, no network exposure.
Default: Unix socket
When service.bind is empty (default), the workload binds a Unix
socket at:
/run/orca/alloc-<alloc-id>/port-<port-name>.sock
systemd creates /run/orca/alloc-<alloc-id>/ via
RuntimeDirectory=orca/alloc-<alloc-id> (mode 0750, owned by
orca:orca). The Traefik backend server URL is:
servers:
- url: "unix:///run/orca/alloc-<alloc-id>/port-<port-name>.sock"
TCP opt-in: service.bind: 127.0.0.1
When service.bind: 127.0.0.1 is set, the workload binds a TCP port
directly (loopback only). The emitter adds an ExecStartPre marker to
the systemd unit so the bind mode is visible:
ExecStartPre=/bin/echo orca: bind 127.0.0.1 port <name> (tcp, R-007 opt-in)
service.bind must be a valid IP address. Empty (socket default) or
127.0.0.1 (TCP opt-in) are the documented values; any other valid IP
is accepted but the bind happens in the process, not the emitter.
Generated Traefik YAML
For a Service named web with port http:
http:
routers:
orca-web:
rule: PathPrefix("/web")
service: orca-web
tls:
certResolver: orca
domains:
- main: "cluster.orca.local"
services:
orca-web:
loadBalancer:
servers:
- url: "unix:///run/orca/alloc-<alloc-id>/port-http.sock"
healthCheck:
path: /healthz
interval: 5s
timeout: 1s
- One router per Service, named
orca-<service-name>. - Router rule:
PathPrefix("/<service-name>"). - TLS:
certResolver: orca, trust domaincluster.orca.local(placeholder; step-ca provisioner overrides in v0.11). - One service per Service, named
orca-<service-name>. - One server per port, URL is
unix://<socket-path>. healthCheckstanza present whenhealth:block is set (required for Service). Path is/healthz; interval and timeout come from thehealth:block.
Atomic reload (gate C-10)
Orca writes Traefik config atomically to avoid Traefik observing a half-written file:
- Write to
<path>.tmpviaWriteFileIdempotent(write + fsync). mv -f <path>.tmp <path>(atomic POSIX rename).
Traefik's fsnotify watcher sees a single IN_MOVED_TO event and
reloads. If the new config is malformed, Traefik logs an error and
holds last-good config — the cluster keeps serving traffic on the
previous config.
Drain
RenderDrain produces the same Traefik YAML with weight: 0 on every
server in the load balancer:
servers:
- url: "unix:///run/orca/alloc-<alloc-id>/port-http.sock"
weight: 0
Traefik stops sending traffic to the drained backend. The workload keeps running; drain is reversible (re-submit the normal config to restore traffic).
TLS
- certResolver:
orca(references the Traefik ACME/step-ca certificate resolver configured in Traefik's static config). - Trust domain:
cluster.orca.local(placeholder in v0.9; step-ca provisioner in v0.11 overrides with the real cluster trust domain). - SPIFFE SVIDs: workload identity via SPIFFE SVIDs minted at submit time via step-ca (v0.11-P01.5, gate C-08). The SVID is a URI SAN in the workload's X.509 cert.
Health checks
The health: block (required for Service) maps to the Traefik
healthCheck stanza:
health:
check_type: http
interval: 5s
timeout: 1s
unhealthy_threshold: 2
→
healthCheck:
path: /healthz
interval: 5s
timeout: 1s
Traefik polls each backend's /healthz at the configured interval. An
unhealthy backend is removed from the load balancer pool until it
passes the health check again.
Worked example
See examples/full-stack/ for a complete multi-service stack with ingress configured:
web-app.md— frontend Service (socket bind, PathPrefix route)api.md— backend API Service (TCP opt-in,127.0.0.1bind)examples/full-stack/rendered/traefik-dynamic-web-app.yaml— the Traefik config Orca generates
v0.11 forward (limitations)
The following are not yet implemented in v0.9 and will land in v0.11:
service.host/service.route_id: stored on theServiceBlockbut not yet consumed by theTraefikEmitter. The router rule is hardcodedPathPrefix("/<name>"). Custom host-based routing lands in v0.11.- Socket activation: real socket-activation (socket unit files, fd
passing) lands in v0.11-P08. The current emitter renders the
RuntimeDirectory+ socket path comments but does not create socket units. - Transactional update execution: the
update:block's rolling/ canary/blue-green plan is computed by the emitter but not yet executed transactionally. Transactional execution lands in v0.11-P10. - SPIFFE SVID minting: workload identity via step-ca SVIDs lands in v0.11-P01.5 (gate C-08).
- Secrets in env:
env: { KEY: { from: "secret:..." } }resolution toEnvironmentFile=/LoadCredential=lands in v0.11-P03.
See also
- docs/cli.md — CLI reference
- docs/jobspec.md — Jobspec reference (
service:,health:,ports:blocks) - examples/full-stack/ — Full-stack example with ingress