Networking & ingress
Every fleet has two listeners with two very different audiences: the public Worker listener (:8080), which your users reach through ingress, and the internal listener (:8081), which nothing outside the fleet and the operator may ever reach. The operator wires both correctly.
Ingress modes
How spec.hostnames get routed is an operator-level choice
(--ingress-mode, or operator.ingressMode in Helm):
| Mode | Mechanism | When to use it |
|---|---|---|
httproute (default) | One Gateway API HTTPRoute per app, carrying every hostname in spec.hostnames, attached to a shared platform Gateway by parentRefs (--gateway-name/--gateway-namespace, default edge/infra). Cross-namespace attachment is authorized by that Gateway's allowedRoutes; TLS is the Gateway's own. The drain-503 retry needs experimental-channel CRDs — see below. | Clusters that already run Gateway API; Istio's implementation is the one this was built against. |
virtualservice | Classic Istio VirtualServices bound to pre-existing networking.istio.io Gateways (--istio-gateways, comma-separated namespace/name). | Clusters whose edge is an existing istio-ingressgateway. |
ingress | Plain networking.k8s.io/v1 Ingress; --ingress-class selects the class, and --cluster-issuer makes each app's Ingress request its own cert-manager TLS certificate. Drain-retry and WebSocket policies are expressed as ingress-nginx annotations (ignored by other controllers). | ingress-nginx, Traefik, cloud ingress controllers. |
none | No routes are created; hostnames has no effect. | Internal-only platforms, or ingress managed entirely outside the operator. |
When Gateway API CRDs are missing in httproute mode, or a route errors, the
WorkerApp reports IngressReady: False and hostnames are not routed;
the fleet itself keeps running.
Internal consumers: no ingress at all
Internal-only apps need no hostnames. Consumers in the cluster reach the serving Service
directly at <app>-celld.<namespace>.svc:8080. The
spec.service block shapes that Service:
service:
type: LoadBalancer # ClusterIP (default) | LoadBalancer | NodePort
annotations: # cloud LB configuration, e.g. an internal/private LB
service.beta.kubernetes.io/aws-load-balancer-internal: "true"
Drain-aware edge policy
A draining celld node answers new requests with 503 and a closed connection, expecting
the client to retry against a healthy node. At the platform edge, the gateway is that
client: the operator configures explicit retry-on-503 on every route
it emits (Istio's default retryOn does not cover 503 responses), which makes
drains, and therefore rollouts, invisible to end users.
One caveat in httproute mode: HTTPRouteRule.retry exists only in
the Gateway API experimental channel. Standard-channel CRDs accept the
route and silently drop the field, so the operator checks the stored object and reports
IngressReady: True with reason RouteReconciledRetryDropped and
the message drain 503s are not retried at the gateway. If you see that reason,
either install the experimental channel or expect clients to see 503s during rollouts.
Related wiring the operator takes care of:
- Readiness is celld's own
/__celld/health; a draining pod flips to 503 and leaves the EndpointSlice. - The headless internal Service sets
publishNotReadyAddresses, so peers can still reach a draining pod to receive its cells. - Liveness is TCP only, because an HTTP liveness probe on the health path would kill nodes mid-handoff.
The WebSocket profile
spec.websockets: true switches the app's routes and scaling to a
socket-friendly profile:
- Generous timeouts so quiet, hibernated sockets are not severed by the
proxy — but only in the modes that can express them:
httproutedisables the route's request timeout, andingresssetsproxy-read-timeout/proxy-send-timeoutto 3600s. Invirtualservicemode the operator emits no timeout override, so Istio's default route timeout still applies — set it on your ownVirtualServiceor mesh defaults if you serve long-lived sockets that way. - Conservative scale-down: a removed pod closes its sockets, so the stabilization window stretches to 30 minutes (from 10). See autoscaling policy.
Note there is no session affinity: the operator emits no DestinationRule and
sets no sticky-session annotation. It does not need one — celld's signed peer tunnel
serves any cell from any node — but if you want requests to land on a cell's owner for
latency reasons, that is your own edge configuration to add.
Keeping proxies happy without waking cells: have clients ping
periodically and answer with setWebSocketAutoResponse: celld replies to a
matched message without waking the hibernated cell, and the proxy's idle clock resets.
Where TLS lives
celld terminates no TLS anywhere. Public TLS belongs to the Gateway (or Ingress + cert-manager). The pod network should be encrypted (Istio ambient mode, or a CNI with WireGuard) because celld's peer protocol relies on network confidentiality for its payloads (its HMAC auth protects integrity, not secrecy).
The internal listener's protections are covered in Security.