# One hostname, two backends. # # The API is routed STRAIGHT AT ITS SERVICE rather than through the web container's nginx, which is # what that nginx does on Compose. Two reasons: it removes a hop from every API request and every # websocket frame, and it makes the web pod a pure static server again — no dependency on resolving # the API, nothing to keep in sync between an ingress and a config file. # # The consequence has to be applied, not just noted: there is now exactly ONE proxy rewriting # X-Forwarded-For, so `TRUST_PROXY_HOPS` is 1. It is 2 in the compose files, and leaving it there # would make `req.ip` a value the caller chooses. See 02-config.yaml. # # Set `externalTrafficPolicy: Local` on the ingress controller's own Service as well, or the node's # SNAT will make every request in the world appear to come from a handful of addresses — which # defeats every per-address limit in the application at once. apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: jarvis namespace: jarvis annotations: # An agent is idle between operations and the server pings every 30s, so this only has to # outlast a quiet period comfortably. Too short and a healthy fleet reconnects all day. nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" # Matches BODY_LIMIT in apps/api/src/main.ts. A document may hold 400,000 characters, which is # over a megabyte once accented text is UTF-8 encoded, and a diagram export posts back the SVG # the browser rendered. Below this they are rejected as a bare 413 with no message. nginx.ingress.kubernetes.io/proxy-body-size: "4m" # # NO affinity annotation, and none should be added. The reflex is understandable — there are # websockets here — but stickiness cannot solve the problem it would be hired for: the hard case # is co-locating an operator's browser with an AGENT's websocket, which arrives from a different # network at a different time and whose id is not even known at the browser's handshake. No # ingress annotation can express that; it is handled in the application, over Redis. # # And there is no polling to make sticky: both the client and the server pin # transports: ["websocket"], so one logical socket is one TCP connection. spec: ingressClassName: nginx tls: - hosts: ["jarvis.example.com"] secretName: jarvis-tls rules: - host: jarvis.example.com http: paths: # Longest prefix first for readers; the controller sorts by specificity itself. # # The agent websocket has its own rule even though it sits under /api/, mirroring the # split in apps/web/nginx.conf: it is the one path where an idle connection legitimately # stays open for an hour between frames. - path: /api/agents/ws pathType: Prefix backend: service: name: jarvis-api port: name: http - path: /socket.io/ pathType: Prefix backend: service: name: jarvis-api port: name: http - path: /api/ pathType: Prefix backend: service: name: jarvis-api port: name: http # Everything else is the SPA, whose nginx answers index.html for any path it does not # have a file for. - path: / pathType: Prefix backend: service: name: jarvis-web port: name: http