March 2026 • Infrastructure & Networking • 6 min read

Architecting Resilient Connectivity for Ray Clusters on Kubernetes: A Troubleshooting Guide

Executive Summary

In distributed computing, Ray has become the standard for scaling AI and Python workloads. However, exposing a Ray Cluster (Dashboard, Client, and Serve) via an NGINX Ingress Controller often presents "Heisenbugs"—errors that appear and disappear after cluster restarts.

This technical report documents a real-world scenario where a Ray Ingress setup failed due to security-hardened webhooks, missing Layer 4 mappings, and Node IP drift. It provides a "Best Practice" blueprint for a production-ready, static-entry configuration.

1. The Challenge: "It worked yesterday, but not today."

A common symptom in Kubernetes is a configuration that works during the initial session but fails after a node reboot or pod reschedule. In this case, three distinct layers failed:

  • The Security Layer: Modern NGINX controllers (v1.10+) block configuration-snippet annotations to prevent code injection.
  • The Protocol Layer: Ray requires both HTTP (Dashboard/Serve) and raw TCP (Ray Client/GCS) traffic. Standard Ingress only handles HTTP.
  • The Infrastructure Layer: Hardcoded externalIPs in the Service became invalid as the underlying Node IPs changed during cluster lifecycle events.

2. Technical Deep-Dive: Root Cause Analysis

A. The Admission Webhook Conflict

When trying to fix header issues, the use of nginx.ingress.kubernetes.io/configuration-snippet triggered a BadRequest.

Error: admission webhook "validate.nginx.ingress.kubernetes.io" denied the request: risky annotation.

Cause: Security hardening in NGINX Ingress blocks custom snippets by default.

B. Missing Endpoints for TCP

The NGINX logs showed: Service does not have any active Endpoint for TCP port 9000.

Cause: While the Ingress handled HTTP traffic on port 80, the NGINX controller was not configured to "listen" or "route" the specific Ray TCP ports (10001, 9000, 6379) via its internal ConfigMap.

C. The Static IP Problem

By using a list of multiple externalIPs, the architecture was fragile. If the node hosting the Ingress Controller moved, the traffic entry point broke.

3. The Best-Practice Solution: "Pinned Gateway" Architecture

To resolve this, we implemented a Pinned Gateway Architecture. This ensures the Ingress Controller always lands on a specific, reliable node (IP .33) and utilizes the Host Network for maximum stability.

Step 1: Node Labeling (Persistence)

First, we mark our target gateway node to ensure Kubernetes always schedules the Ingress Controller there.

Bash
kubectl label node <your-node-name> ingress-ready=true

Step 2: Protocol Bridging (TCP ConfigMap)

We create a dedicated mapping for Ray's internal TCP protocols. This allows NGINX to handle non-HTTP traffic.

YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: tcp-services
  namespace: ingress-nginx
data:
  "10001": "raycluster/ray-cluster-kuberay-head-svc:10001"
  "9000": "raycluster/ray-cluster-kuberay-head-svc:9000"
  "6379": "raycluster/ray-cluster-kuberay-head-svc:6379"

Step 3: Hardened Ingress Controller Deployment

We update the Ingress Controller to use hostNetwork and nodeSelector. This "pins" the controller to IP 192.168.1.33.

Terminal
spec:
  template:
    spec:
      hostNetwork: true            # Use the physical Node IP directly
      dnsPolicy: ClusterFirstWithHostNet
      nodeSelector:
        ingress-ready: "true"      # Only run on our labeled .33 node
      containers:
      - args:
        - /nginx-ingress-controller
        - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
        # ... other args

Step 4: Clean, Snippet-Free Ingress Spec

By using pathType: ImplementationSpecific and the rewrite-target annotation, we avoid using "risky" snippets while maintaining full functionality for the Dashboard and Ray Serve.

YAML
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ray-head-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /ray(/|$)(.*)
        backend:
          service:
            name: ray-cluster-kuberay-head-svc
            port: { number: 8265 }

4. Operational Best Practices

To prevent future "Day 2" failures, follow these rules:

  • Prefer ingressClassName over Annotations: Modern K8s clusters require the spec.ingressClassName field for proper routing.
  • Trailing Slashes Matter: When using rewrite-target, always access the dashboard via http://<IP>/ray/. Missing the final / will cause CSS/JS assets to fail to load.
  • Avoid Hardcoded External IPs: Instead of listing IPs in the Service, use nodeSelector and hostNetwork for bare-metal clusters, or a proper LoadBalancer (like MetalLB) for virtual clusters.
  • Security Over Snippets: If you need custom Nginx headers, enable them globally in the Controller ConfigMap rather than locally in the Ingress object to pass security webhooks.

5. Conclusion

Connectivity issues in Kubernetes are rarely about a single broken line of code; they are usually about the interaction between security webhooks, network protocols, and scheduling logic. By pinning the Ingress Controller and properly mapping TCP services, we transform a fragile setup into a robust, production-ready AI gateway.

Tools Used: Kubernetes, KubeRay, NGINX Ingress, YAML Engineering.

This documentation is part of my DevOps Portfolio, showcasing my ability to resolve complex cloud-native networking challenges.

Mars 2026 • Infrastructure & Réseaux • 6 min de lecture

Architecturer une Connectivité Résiliente pour les Clusters Ray sur Kubernetes : Guide de Dépannage

Résumé Exécutif

Dans l'informatique distribuée, Ray est devenu la norme pour faire évoluer les charges de travail IA et Python. Cependant, exposer un Cluster Ray (Dashboard, Client et Serve) via un contrôleur d'ingress NGINX présente souvent des "Heisenbugs"—des erreurs qui apparaissent et disparaissent après le redémarrage du cluster.

Ce rapport technique documente un scénario réel où une configuration Ray Ingress a échoué en raison de webhooks de sécurité renforcés, de mappages de couche 4 manquants et de la dérive des adresses IP des nœuds. Il fournit un modèle de "Meilleure Pratique" pour une configuration d'entrée statique prête pour la production.

1. Le Défi : "Ça marchait hier, mais pas aujourd'hui."

Un symptôme courant dans Kubernetes est une configuration qui fonctionne lors de la session initiale mais échoue après le redémarrage d'un nœud ou la reprogrammation d'un pod. Dans ce cas, trois couches distinctes ont échoué :

  • La Couche Sécurité : Les contrôleurs NGINX modernes (v1.10+) bloquent les annotations configuration-snippet pour empêcher l'injection de code.
  • La Couche Protocole : Ray nécessite la gestion du trafic HTTP (Dashboard/Serve) ET TCP brut (Ray Client/GCS). L'Ingress standard ne gère que le HTTP.
  • La Couche Infrastructure : Les adresses externalIPs codées en dur dans le Service devenaient invalides lorsque les adresses IP des nœuds sous-jacents changeaient.

2. Analyse Technique : Causes Profondes

A. Le Conflit du Webhook d'Admission

En essayant de corriger les problèmes d'en-tête, l'utilisation de nginx.ingress.kubernetes.io/configuration-snippet a déclenché un BadRequest.

Erreur : admission webhook "validate.nginx.ingress.kubernetes.io" denied the request: risky annotation.

Cause : Le renforcement de la sécurité dans NGINX Ingress bloque les extraits personnalisés par défaut.

B. Points de Terminaison Manquants pour TCP

Les journaux NGINX montraient : Service does not have any active Endpoint for TCP port 9000.

Cause : Bien que l'Ingress gérait le trafic HTTP sur le port 80, le contrôleur NGINX n'était pas configuré pour router les ports TCP spécifiques de Ray (10001, 9000, 6379) via son ConfigMap interne.

C. Le Problème de l'IP Statique

En utilisant une liste de plusieurs externalIPs, l'architecture était fragile. Si le nœud hébergeant le contrôleur Ingress se déplaçait, le point d'entrée du trafic se cassait.

3. La Solution : Architecture de "Passerelle Épinglée"

Pour résoudre ce problème, nous avons implémenté une architecture de passerelle épinglée. Cela garantit que le contrôleur Ingress atterrit toujours sur un nœud spécifique et fiable (IP .33) et utilise le réseau hôte (Host Network) pour une stabilité maximale.

Étape 1 : Étiquetage des Nœuds (Persistance)

Tout d'abord, nous marquons notre nœud cible pour nous assurer que Kubernetes y planifie toujours le contrôleur Ingress.

Bash
kubectl label node <votre-nom-de-noeud> ingress-ready=true

Étape 2 : Pontage de Protocole (TCP ConfigMap)

Nous créons un mappage dédié pour les protocoles TCP internes de Ray. Cela permet à NGINX de gérer le trafic non HTTP.

YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: tcp-services
  namespace: ingress-nginx
data:
  "10001": "raycluster/ray-cluster-kuberay-head-svc:10001"
  "9000": "raycluster/ray-cluster-kuberay-head-svc:9000"
  "6379": "raycluster/ray-cluster-kuberay-head-svc:6379"

Étape 3 : Déploiement Renforcé du Contrôleur

Nous mettons à jour le contrôleur Ingress pour utiliser hostNetwork et nodeSelector. Cela "épingle" le contrôleur à l'IP 192.168.1.33.

Terminal
spec:
  template:
    spec:
      hostNetwork: true            # Utilise directement l'IP physique du noeud
      dnsPolicy: ClusterFirstWithHostNet
      nodeSelector:
        ingress-ready: "true"      # Exécute uniquement sur notre noeud labellisé
      containers:
      - args:
        - /nginx-ingress-controller
        - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services

Étape 4 : Spécification Ingress Propre

En utilisant pathType: ImplementationSpecific et l'annotation rewrite-target, nous évitons d'utiliser des scripts "risqués" tout en conservant toutes les fonctionnalités pour le Dashboard et Ray Serve.

YAML
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ray-head-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /ray(/|$)(.*)
        backend:
          service:
            name: ray-cluster-kuberay-head-svc
            port: { number: 8265 }

4. Bonnes Pratiques Opérationnelles

  • Préférez ingressClassName aux Annotations : Les clusters K8s modernes nécessitent le champ spec.ingressClassName pour un routage approprié.
  • Les Barres Obliques Finales Comptent : Lors de l'utilisation de rewrite-target, accédez toujours au dashboard via http://<IP>/ray/. S'il manque le / final, les actifs CSS/JS ne se chargeront pas.
  • Évitez les IP Externes Codées en Dur : Utilisez nodeSelector et hostNetwork pour les clusters bare-metal, ou un LoadBalancer approprié (comme MetalLB).
  • Sécurité avant les Scripts : Si vous avez besoin d'en-têtes Nginx personnalisés, activez-les globalement dans le ConfigMap du contrôleur.

5. Conclusion

Les problèmes de connectivité dans Kubernetes concernent rarement une seule ligne de code cassée ; ils concernent généralement l'interaction entre les webhooks de sécurité, les protocoles réseau et la logique de planification. En épinglant le contrôleur Ingress et en mappant correctement les services TCP, nous transformons une configuration fragile en une passerelle IA robuste et prête pour la production.

Outils : Kubernetes, KubeRay, NGINX Ingress, YAML Engineering.