# 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`. The `kind: Service` > workload 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-.yaml ``` This file contains: - One **router** (`orca-`) with a `PathPrefix` rule and TLS config. - One **service** (`orca-`) as a `loadBalancer` with 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-/port-.sock ``` systemd creates `/run/orca/alloc-/` via `RuntimeDirectory=orca/alloc-` (mode 0750, owned by `orca:orca`). The Traefik backend server URL is: ```yaml servers: - url: "unix:///run/orca/alloc-/port-.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: ```ini ExecStartPre=/bin/echo orca: bind 127.0.0.1 port (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`: ```yaml 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-/port-http.sock" healthCheck: path: /healthz interval: 5s timeout: 1s ``` - One router per Service, named `orca-`. - Router rule: `PathPrefix("/")`. - TLS: `certResolver: orca`, trust domain `cluster.orca.local` (placeholder; step-ca provisioner overrides in v0.11). - One service per Service, named `orca-`. - One server per port, URL is `unix://`. - `healthCheck` stanza present when `health:` block is set (required for Service). Path is `/healthz`; interval and timeout come from the `health:` block. ## Atomic reload (gate C-10) Orca writes Traefik config atomically to avoid Traefik observing a half-written file: 1. Write to `.tmp` via `WriteFileIdempotent` (write + fsync). 2. `mv -f .tmp ` (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: ```yaml servers: - url: "unix:///run/orca/alloc-/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: ```yaml health: check_type: http interval: 5s timeout: 1s unhealthy_threshold: 2 ``` → ```yaml 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/](../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.1` bind) - `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 the `ServiceBlock` but not yet consumed by the `TraefikEmitter`. The router rule is hardcoded `PathPrefix("/")`. 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 to `EnvironmentFile=`/`LoadCredential=` lands in v0.11-P03. ## See also - [docs/cli.md](cli.md) — CLI reference - [docs/jobspec.md](jobspec.md) — Jobspec reference (`service:`, `health:`, `ports:` blocks) - [examples/full-stack/](../examples/full-stack/) — Full-stack example with ingress