Skip to content

[Feature] Support per-pod FQDNs & inject to Ray node ip - #5191

Open
machichima wants to merge 7 commits into
ray-project:masterfrom
machichima:fqdn-hostname
Open

[Feature] Support per-pod FQDNs & inject to Ray node ip#5191
machichima wants to merge 7 commits into
ray-project:masterfrom
machichima:fqdn-hostname

Conversation

@machichima

@machichima machichima commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Why are these changes needed?

Ray pods are only addressable by pod IP, which changes on every recreation. Also when using mTLS feature, when scale up a worker, we will need to update the certificate to include the worker pod IP. This PR adds spec.podFQDN to RayCluster, giving each pod a resolvable DNS name: <hostname>.<cluster>-headless.<ns>.svc.<cluster-domain>, and optionally inject this to Ray node's --node-ip-address.

Two modes:

  • DNSOnly (default): create per-pod DNS records only (pod hostname/subdomain + the headless worker Service). Ray keeps registering with pod IPs; FQDNs are for external consumers.
  • RegisterAsNodeAddress: additionally inject the FQDN as --node-ip-address, so Ray nodes register and address each other by DNS name. This enables static DNS-SAN certificates for mTLS instead of re-issuing certs on every pod IP change.

Notes:

  • Make the field is immutable: existing pods are not updated in place, so mutation would leave nodes registered under mixed identities. For using RayCluster recreate upgrade strategy, it will only delete pods only, which will leave a orphan service. We can consider making it mutable with recreate upgrade strategy.
  • Skips pods that already have a subdomain (e.g. TPU multi-host webhook).

Manual test:

TODO left:

  • Add validation
  • Add tests
  • Support with mTLS

Related issue number

Related to: #5000 and #5048

Labels

  • If this PR has user-facing changes that require documentation updates at release time, I have added the doc-updates-required label.
  • If this PR contains breaking changes, I have added the breaking-change label.

Checks

  • I've made sure the tests are passing.
  • Testing Strategy
    • Unit tests
    • Manual tests
    • This PR is not tested :(

Manual test instructions

@machichima

machichima commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

Test RegisterAsNodeAddress mode on kind cluster

  • All tests ensures Ray nodes register with FQDN instead of pod IP

Test 1: Check head <-> worker & worker <-> worker connection

❯ kubectl apply -f rayjob-hostname-verification.yaml
rayjob.ray.io/rayjob-hostname-verification created
configmap/rayjob-hostname-verification-code unchanged

# Get raycluster name of the rayjob
❯ C=$(kubectl get rayjob rayjob-hostname-verification -o jsonpath='{.status.rayClusterName}')

# Get the hostname and subdomain of the pod
❯ kubectl get pod -l ray.io/node-type=worker,ray.io/cluster=$C \
    -o custom-columns=NAME:.metadata.name,HOSTNAME:.spec.hostname,SUBDOMAIN:.spec.subdomain
NAME                                                          HOSTNAME                                                      SUBDOMAIN
rayjob-hostname-verification-h8w8r-small-group-worker-wcwzk   rayjob-hostname-verification-h8w8r-small-group-worker-zwqhd   rayjob-hostname-verification-h8w8r-headless
rayjob-hostname-verification-h8w8r-small-group-worker-x6jr7   rayjob-hostname-verification-h8w8r-small-group-worker-nxwjs   rayjob-hostname-verification-h8w8r-headless

# Ensure we can resolve worker's FQDN from head pod
❯ W=$(kubectl get pod -l ray.io/node-type=worker,ray.io/cluster=$C -o jsonpath='{.items[0].metadata.name}')
  H=$(kubectl get pod $W -o jsonpath='{.spec.hostname}')
  HP=$(kubectl get pod -l ray.io/node-type=head,ray.io/cluster=$C -o jsonpath='{.items[0].metadata.name}')
  kubectl exec $HP -- getent hosts $H.$C-headless.default.svc.cluster.local
10.244.0.24     rayjob-hostname-verification-h8w8r-small-group-worker-zwqhd.rayjob-hostname-verification-h8w8r-headless.default.svc.cluster.local

# Check Ray registers node with FQDN rather than IP
❯  kubectl exec $HP -- python -c "import ray; ray.init(address='auto'); print([n['NodeManagerAddress'] for n in ray.nodes()])"
2026-08-22 07:31:41,585 INFO worker.py:1696 -- Using address 127.0.0.1:6379 set in the environment variable RAY_ADDRESS
2026-08-22 07:31:41,587 INFO worker.py:1837 -- Connecting to existing Ray cluster at address: 10.244.0.23:6379...
2026-08-22 07:31:41,590 INFO worker.py:2014 -- Connected to Ray cluster. View the dashboard at rayjob-hostname-verification-h8w8r-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2062: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
['rayjob-hostname-verification-h8w8r-small-group-worker-zwqhd.rayjob-hostname-verification-h8w8r-headless.default.svc.cluster.local', 'rayjob-hostname-verification-h8w8r-head-svc.default.svc.cluster.local', 'rayjob-hostname-verification-h8w8r-small-group-worker-nxwjs.rayjob-hostname-verification-h8w8r-headless.default.svc.cluster.local']

# Ensure following connections work
# 1. Head -> worker
# 2. worker _> worker
# 3. actor bi-directional connection
❯ kubectl logs job/rayjob-hostname-verification
2026-08-22 07:31:21,995 INFO cli.py:41 -- Job submission server address: http://rayjob-hostname-verification-h8w8r-head-svc.default.svc.cluster.local:8265
2026-08-22 07:31:22,373 SUCC cli.py:65 -- ---------------------------------------------------------------
2026-08-22 07:31:22,373 SUCC cli.py:66 -- Job 'rayjob-hostname-verification-fmhmn' submitted successfully
2026-08-22 07:31:22,373 SUCC cli.py:67 -- ---------------------------------------------------------------
2026-08-22 07:31:22,373 INFO cli.py:291 -- Next steps
2026-08-22 07:31:22,373 INFO cli.py:292 -- Query the logs of the job:
2026-08-22 07:31:22,373 INFO cli.py:294 -- ray job logs rayjob-hostname-verification-fmhmn
2026-08-22 07:31:22,373 INFO cli.py:296 -- Query the status of the job:
2026-08-22 07:31:22,373 INFO cli.py:298 -- ray job status rayjob-hostname-verification-fmhmn
2026-08-22 07:31:22,373 INFO cli.py:300 -- Request the job to be stopped:
2026-08-22 07:31:22,373 INFO cli.py:302 -- ray job stop rayjob-hostname-verification-fmhmn
2026-08-22 07:31:23,141 INFO cli.py:41 -- Job submission server address: http://rayjob-hostname-verification-h8w8r-head-svc.default.svc.cluster.local:8265
2026-08-22 07:31:22,163 INFO job_manager.py:568 -- Runtime env is setting up.
Running entrypoint for job rayjob-hostname-verification-fmhmn: python /home/ray/samples/worker_to_worker_tls_test.py
2026-08-22 07:31:22,930 INFO worker.py:1696 -- Using address rayjob-hostname-verification-h8w8r-head-svc.default.svc.cluster.local:6379 set in the environment variable RAY_ADDRESS
2026-08-22 07:31:22,932 INFO worker.py:1837 -- Connecting to existing Ray cluster at address: rayjob-hostname-verification-h8w8r-head-svc.default.svc.cluster.local:6379...
2026-08-22 07:31:22,938 INFO worker.py:2014 -- Connected to Ray cluster. View the dashboard at rayjob-hostname-verification-h8w8r-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2062: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
Dashboard: rayjob-hostname-verification-h8w8r-head-svc.default.svc.cluster.local:8265
Driver: {'node_id': '8bfad57bee4da3168de7b5ea417e194161b0a505d8249f49ef737b8c', 'hostname': 'rayjob-hostname-verification-h8w8r-head-f78rf', 'pid': 1080}
Using worker_a=2d45fa7aac7bd399295ef95585adb74a52d93532128e828fac3947f8
Using worker_b=f7723d75d827740d2181d9c1fea0dfa6f084da0cfac62cf25f19a66b
Head -> Worker -> Head: {'tag': 'head-to-worker', 'node_id': '2d45fa7aac7bd399295ef95585adb74a52d93532128e828fac3947f8', 'hostname': 'rayjob-hostname-verification-h8w8r-small-group-worker-zwqhd', 'pid': 246}
Same node as driver? False
Pinned to worker_a? True
Worker -> Worker chained: {'chain_node': {'node_id': '2d45fa7aac7bd399295ef95585adb74a52d93532128e828fac3947f8', 'hostname': 'rayjob-hostname-verification-h8w8r-small-group-worker-zwqhd', 'pid': 246}, 'double_node': {'value': 42, 'node_id': 'f7723d75d827740d2181d9c1fea0dfa6f084da0cfac62cf25f19a66b', 'hostname': 'rayjob-hostname-verification-h8w8r-small-group-worker-nxwjs', 'pid': 246}}
chain() on worker_a? True
double() on worker_b? True
chain() and double() same node? False
Actor A: {'name': 'A', 'node_id': '2d45fa7aac7bd399295ef95585adb74a52d93532128e828fac3947f8', 'hostname': 'rayjob-hostname-verification-h8w8r-small-group-worker-zwqhd', 'pid': 299}
Actor B: {'name': 'B', 'node_id': 'f7723d75d827740d2181d9c1fea0dfa6f084da0cfac62cf25f19a66b', 'hostname': 'rayjob-hostname-verification-h8w8r-small-group-worker-nxwjs', 'pid': 299}
A pinned to worker_a? True
B pinned to worker_b? True
A and B same node? False
A->B: {'sender': {'name': 'A', 'node_id': '2d45fa7aac7bd399295ef95585adb74a52d93532128e828fac3947f8', 'hostname': 'rayjob-hostname-verification-h8w8r-small-group-worker-zwqhd', 'pid': 299}, 'response': {'receiver': 'B', 'receiver_node': 'f7723d75d827740d2181d9c1fea0dfa6f084da0cfac62cf25f19a66b', 'receiver_host': 'rayjob-hostname-verification-h8w8r-small-group-worker-nxwjs', 'receiver_pid': 299, 'msg': 'hello', 'sender': 'A'}}
B->A: {'sender': {'name': 'B', 'node_id': 'f7723d75d827740d2181d9c1fea0dfa6f084da0cfac62cf25f19a66b', 'hostname': 'rayjob-hostname-verification-h8w8r-small-group-worker-nxwjs', 'pid': 299}, 'response': {'receiver': 'A', 'receiver_node': '2d45fa7aac7bd399295ef95585adb74a52d93532128e828fac3947f8', 'receiver_host': 'rayjob-hostname-verification-h8w8r-small-group-worker-zwqhd', 'receiver_pid': 299, 'msg': 'world', 'sender': 'B'}}
A send() and B greet() same node? False
B send() and A greet() same node? False
ALL OK
2026-08-22 07:31:28,207 SUCC cli.py:65 -- --------------------------------------------------
2026-08-22 07:31:28,207 SUCC cli.py:66 -- Job 'rayjob-hostname-verification-fmhmn' succeeded
2026-08-22 07:31:28,207 SUCC cli.py:67 -- --------------------------------------------------

Test 2: Check autoscaler v2 works

  • Result: Can scale up / down successfully

  • Add podFQDN in ray-operator/config/samples/ray-cluster.autoscaler-v2.yaml

spec:
  podFQDN:
    mode: RegisterAsNodeAddress
# Update ray-cluster.autoscaler-v2.yaml to enable using hostname
❯ kubectl apply -f ray-operator/config/samples/ray-cluster.autoscaler-v2.yaml
raycluster.ray.io/raycluster-autoscaler created
configmap/ray-example unchanged

# Record head pod name to use later
❯ HP=$(kubectl get pod -l ray.io/node-type=head,ray.io/cluster=raycluster-autoscaler -o jsonpath='{.items[0].metadata.name}')

# Ensure head pod starts
❯ k get pods
NAME                                READY   STATUS    RESTARTS   AGE
kuberay-operator-548665bc96-6wmnt   1/1     Running   0          12h
raycluster-autoscaler-head-kjjvd    1/2     Running   0          7s

# Trigger scale up from 0 to 2
❯ kubectl exec $HP -c ray-head -- python /home/ray/samples/detached_actor.py a1
  kubectl exec $HP -c ray-head -- python /home/ray/samples/detached_actor.py a2
2026-08-22 18:56:32,088 INFO worker.py:1696 -- Using address 127.0.0.1:6379 set in the environment variable RAY_ADDRESS
2026-08-22 18:56:32,090 INFO worker.py:1837 -- Connecting to existing Ray cluster at address: 10.244.0.31:6379...
2026-08-22 18:56:32,095 INFO worker.py:2014 -- Connected to Ray cluster. View the dashboard at raycluster-autoscaler-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2062: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
2026-08-22 18:56:33,408 INFO worker.py:1696 -- Using address 127.0.0.1:6379 set in the environment variable RAY_ADDRESS
2026-08-22 18:56:33,410 INFO worker.py:1837 -- Connecting to existing Ray cluster at address: 10.244.0.31:6379...
2026-08-22 18:56:33,414 INFO worker.py:2014 -- Connected to Ray cluster. View the dashboard at raycluster-autoscaler-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2062: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(

# scale up worker pods
❯ kubectl get pod -l ray.io/node-type=worker,ray.io/cluster=raycluster-autoscaler
NAME                                             READY   STATUS    RESTARTS   AGE
raycluster-autoscaler-small-group-worker-47qwg   0/1     Running   0          19s
raycluster-autoscaler-small-group-worker-t275t   0/1     Running   0          19s

# worker pods have hostname and subdomain
❯ kubectl get pod -l ray.io/node-type=worker,ray.io/cluster=raycluster-autoscaler \
    -o custom-columns=NAME:.metadata.name,HOSTNAME:.spec.hostname,SUBDOMAIN:.spec.subdomain
NAME                                             HOSTNAME                                         SUBDOMAIN
raycluster-autoscaler-small-group-worker-47qwg   raycluster-autoscaler-small-group-worker-d4zmd   raycluster-autoscaler-headless
raycluster-autoscaler-small-group-worker-t275t   raycluster-autoscaler-small-group-worker-qzzzh   raycluster-autoscaler-headless

# Check address for the registered nodes
❯ kubectl exec $HP -c ray-head -- python -c "import ray; ray.init(address='auto'); print([n['NodeManagerAddress'] for n in ray.nodes()])"
2026-08-22 18:57:41,325 INFO worker.py:1696 -- Using address 127.0.0.1:6379 set in the environment variable RAY_ADDRESS
2026-08-22 18:57:41,327 INFO worker.py:1837 -- Connecting to existing Ray cluster at address: 10.244.0.31:6379...
2026-08-22 18:57:41,331 INFO worker.py:2014 -- Connected to Ray cluster. View the dashboard at raycluster-autoscaler-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2062: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
['raycluster-autoscaler-head-svc.default.svc.cluster.local', 'raycluster-autoscaler-small-group-worker-d4zmd.raycluster-autoscaler-headless.default.svc.cluster.local', 'raycluster-autoscaler-small-group-worker-qzzzh.raycluster-autoscaler-headless.default.svc.cluster.local']

# Check autoscaler log, we can see scale up request
❯ kubectl logs $HP -c autoscaler | grep "Submitting a scale request"
2026-08-22 18:56:35,770 - INFO - Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 2}), workers_to_delete=defaultdict(<class 'list'>, {}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())
2026-08-22 18:56:35,770 INFO cloud_provider.py:358 -- Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 2}), workers_to_delete=defaultdict(<class 'list'>, {}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())

# Trigger scale down
❯ kubectl exec $HP -c ray-head -- python /home/ray/samples/terminate_detached_actor.py a1
  kubectl exec $HP -c ray-head -- python /home/ray/samples/terminate_detached_actor.py a2
2026-08-22 18:58:05,906 INFO worker.py:1696 -- Using address 127.0.0.1:6379 set in the environment variable RAY_ADDRESS
2026-08-22 18:58:05,908 INFO worker.py:1837 -- Connecting to existing Ray cluster at address: 10.244.0.31:6379...
2026-08-22 18:58:05,911 INFO worker.py:2014 -- Connected to Ray cluster. View the dashboard at raycluster-autoscaler-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2062: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
2026-08-22 18:58:06,712 INFO worker.py:1696 -- Using address 127.0.0.1:6379 set in the environment variable RAY_ADDRESS
2026-08-22 18:58:06,714 INFO worker.py:1837 -- Connecting to existing Ray cluster at address: 10.244.0.31:6379...
2026-08-22 18:58:06,718 INFO worker.py:2014 -- Connected to Ray cluster. View the dashboard at raycluster-autoscaler-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2062: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(

# Check autoscaler log, we can see new scale down request
❯ kubectl logs $HP -c autoscaler | grep "Submitting a scale request"
2026-08-22 18:56:35,770 - INFO - Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 2}), workers_to_delete=defaultdict(<class 'list'>, {}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())
2026-08-22 18:56:35,770 INFO cloud_provider.py:358 -- Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 2}), workers_to_delete=defaultdict(<class 'list'>, {}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())
2026-08-22 18:59:12,530 - INFO - Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 0}), workers_to_delete=defaultdict(<class 'list'>, {'small-group': [CloudInstance(cloud_instance_id='raycluster-autoscaler-small-group-worker-t275t', node_type='small-group', node_kind=2, is_running=True, request_id=None), CloudInstance(cloud_instance_id='raycluster-autoscaler-small-group-worker-47qwg', node_type='small-group', node_kind=2, is_running=True, request_id=None)]}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())
2026-08-22 18:59:12,530 INFO cloud_provider.py:358 -- Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 0}), workers_to_delete=defaultdict(<class 'list'>, {'small-group': [CloudInstance(cloud_instance_id='raycluster-autoscaler-small-group-worker-t275t', node_type='small-group', node_kind=2, is_running=True, request_id=None), CloudInstance(cloud_instance_id='raycluster-autoscaler-small-group-worker-47qwg', node_type='small-group', node_kind=2, is_running=True, request_id=None)]}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())

# worker pods being removed
❯ k get pods
NAME                                READY   STATUS    RESTARTS   AGE
kuberay-operator-548665bc96-6wmnt   1/1     Running   0          12h
raycluster-autoscaler-head-kjjvd    2/2     Running   0          3m26s

Test 3: with autoscaler v1

  • Result: autoscaler v1 joins pods to Ray nodes by IP string, which never matches an FQDN. Scale-up still fires, but idle scale-down never triggers

  • Add podFQDN in ray-operator/config/samples/ray-cluster.autoscaler.yaml and set autoscaler version to v1

spec:
  podFQDN:
    mode: RegisterAsNodeAddress
  ...
  autoscalerOptions:
    version: v1
    ...
❯ k apply -f ray-operator/config/samples/ray-cluster.autoscaler.yaml
raycluster.ray.io/raycluster-autoscaler created
configmap/ray-example created

# Ensure head pod starts
❯ k get pods
NAME                                READY   STATUS    RESTARTS   AGE
kuberay-operator-548665bc96-6wmnt   1/1     Running   0          13h
raycluster-autoscaler-head-5fpc7    1/2     Running   0          8s


❯ HP=$(kubectl get pod -l ray.io/node-type=head,ray.io/cluster=raycluster-autoscaler -o jsonpath='{.items[0].metadata.name}')

# Scale up 1 pod
❯ kubectl exec $HP -c ray-head -- python /home/ray/samples/detached_actor.py a1
2026-08-22 20:16:41,245 INFO worker.py:1696 -- Using address 127.0.0.1:6379 set in the environment variable RAY_ADDRESS
2026-08-22 20:16:41,246 INFO worker.py:1837 -- Connecting to existing Ray cluster at address: 10.244.0.38:6379...
2026-08-22 20:16:41,255 INFO worker.py:2014 -- Connected to Ray cluster. View the dashboard at raycluster-autoscaler-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2062: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(

# new worker pod created
❯ k get pods
NAME                                             READY   STATUS    RESTARTS   AGE
kuberay-operator-548665bc96-6wmnt                1/1     Running   0          13h
raycluster-autoscaler-head-5fpc7                 2/2     Running   0          75s
raycluster-autoscaler-small-group-worker-xpxrb   1/1     Running   0          46s

# Check the version of autoscaler
❯ kubectl get raycluster raycluster-autoscaler -o jsonpath='{.spec.autoscalerOptions.version}'
v1%

# Check the autoscaler log, we can see:
# Removed 2 stale ip mappings: {'raycluster-autoscaler-small-group-worker-vw8ll.raycluster-autoscaler-headless.default.svc.cluster.local', 'raycluster-autoscaler-head-svc.default.svc.cluster.local'} not in {'10.244.0.38', '10.244.0.39'}
# Proof that autoscaler v1 does not match FQDN
❯ kubectl logs $HP -c autoscaler --tail=10
Pending Demands:
 (no resource demands)
2026-08-22 20:17:58,089 - INFO - LoadMetrics: Removed ip: raycluster-autoscaler-small-group-worker-vw8ll.raycluster-autoscaler-headless.default.svc.cluster.local.
2026-08-22 20:17:58,089 INFO load_metrics.py:153 -- LoadMetrics: Removed ip: raycluster-autoscaler-small-group-worker-vw8ll.raycluster-autoscaler-headless.default.svc.cluster.local.
2026-08-22 20:17:58,089 - INFO - LoadMetrics: Removed ip: raycluster-autoscaler-head-svc.default.svc.cluster.local.
2026-08-22 20:17:58,089 INFO load_metrics.py:153 -- LoadMetrics: Removed ip: raycluster-autoscaler-head-svc.default.svc.cluster.local.
2026-08-22 20:17:58,089 - INFO - LoadMetrics: Removed 2 stale ip mappings: {'raycluster-autoscaler-small-group-worker-vw8ll.raycluster-autoscaler-headless.default.svc.cluster.local', 'raycluster-autoscaler-head-svc.default.svc.cluster.local'} not in {'10.244.0.38', '10.244.0.39'}
2026-08-22 20:17:58,089 INFO load_metrics.py:156 -- LoadMetrics: Removed 2 stale ip mappings: {'raycluster-autoscaler-small-group-worker-vw8ll.raycluster-autoscaler-headless.default.svc.cluster.local', 'raycluster-autoscaler-head-svc.default.svc.cluster.local'} not in {'10.244.0.38', '10.244.0.39'}
2026-08-22 20:17:58,089 - INFO - The autoscaler took 0.016 seconds to complete the update iteration.
2026-08-22 20:17:58,089 INFO autoscaler.py:463 -- The autoscaler took 0.016 seconds to complete the update iteration.

# Trigger scale down
❯ kubectl exec $HP -c ray-head -- python /home/ray/samples/terminate_detached_actor.py a1
2026-08-22 20:18:05,473 INFO worker.py:1696 -- Using address 127.0.0.1:6379 set in the environment variable RAY_ADDRESS
2026-08-22 20:18:05,474 INFO worker.py:1837 -- Connecting to existing Ray cluster at address: 10.244.0.38:6379...
2026-08-22 20:18:05,479 INFO worker.py:2014 -- Connected to Ray cluster. View the dashboard at raycluster-autoscaler-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2062: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(

# Worker pod is not removed after idle timeout (60 sec)
❯ k get pods
NAME                                             READY   STATUS    RESTARTS   AGE
kuberay-operator-548665bc96-6wmnt                1/1     Running   0          13h
raycluster-autoscaler-head-5fpc7                 2/2     Running   0          115s
raycluster-autoscaler-small-group-worker-xpxrb   1/1     Running   0          86s

❯ k get pods
NAME                                             READY   STATUS    RESTARTS   AGE
kuberay-operator-548665bc96-6wmnt                1/1     Running   0          13h
raycluster-autoscaler-head-5fpc7                 2/2     Running   0          5m51s
raycluster-autoscaler-small-group-worker-xpxrb   1/1     Running   0          5m22s

# Check address for the registered nodes, Ray registers node with FQDN rather than IP
❯ kubectl exec $HP -c ray-head -- python -c \
    "import ray; ray.init(address='auto'); print([n['NodeManagerAddress'] for n in ray.nodes()])"
2026-08-22 20:22:06,392 INFO worker.py:1696 -- Using address 127.0.0.1:6379 set in the environment variable RAY_ADDRESS
2026-08-22 20:22:06,393 INFO worker.py:1837 -- Connecting to existing Ray cluster at address: 10.244.0.38:6379...
2026-08-22 20:22:06,398 INFO worker.py:2014 -- Connected to Ray cluster. View the dashboard at raycluster-autoscaler-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2062: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
['raycluster-autoscaler-small-group-worker-vw8ll.raycluster-autoscaler-headless.default.svc.cluster.local', 'raycluster-autoscaler-head-svc.default.svc.cluster.local']

Signed-off-by: machichima <nary12321@gmail.com>
Signed-off-by: machichima <nary12321@gmail.com>
Signed-off-by: machichima <nary12321@gmail.com>
@chipspeak

Copy link
Copy Markdown
Contributor

Hey @machichima apologies for the delay in looking at this. This is neat but I think it still hits the same race when it comes to mTLS. It'll handle stable identity but I don't think it will stop Ray from resolving to a pod IP on the Python side. RAY_ADDRESS on the head pod defaults to 127.0.0.1, and when Python calls ray.init() or ray job submit, this converts that loopback to the pod IP, which won't be in the cert's DNS SANs. So RayJobs would fail TLS verification even with RegisterAsNodeAddress enabled. Am I right here?

Signed-off-by: machichima <nary12321@gmail.com>
podTemplate := workerSpec.Template
// If the replica of workers is more than 1, `ObjectMeta.Name` may cause name conflict errors.
// Hence, we set `ObjectMeta.Name` to an empty string, and use GenerateName to prevent name conflicts.
podTemplate.ObjectMeta.Name = ""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just move it closer to podTemplate.GenerateName for readability. No behavior change

Signed-off-by: machichima <nary12321@gmail.com>
Signed-off-by: machichima <nary12321@gmail.com>
@machichima

Copy link
Copy Markdown
Collaborator Author

Hey @machichima apologies for the delay in looking at this. This is neat but I think it still hits the same race when it comes to mTLS. It'll handle stable identity but I don't think it will stop Ray from resolving to a pod IP on the Python side. RAY_ADDRESS on the head pod defaults to 127.0.0.1, and when Python calls ray.init() or ray job submit, this converts that loopback to the pod IP, which won't be in the cert's DNS SANs. So RayJobs would fail TLS verification even with RegisterAsNodeAddress enabled. Am I right here?

Yes, I looked into it, I think there are still two places that would need change:

  1. We can set this to fqdnRayIP

    // case 1: head => Use LOCAL_HOST
    // case 2: worker => Use fqdnRayIP (fully qualified domain name)
    ip := utils.LOCAL_HOST

  2. Command to start autoscaler does not take address as flag, so autoscaler will directly use node_ip to perform health-check (here) -> this will need Ray change

    autoscalerStartCmd := "ray kuberay-autoscaler --cluster-name $(RAY_CLUSTER_NAME) --cluster-namespace $(RAY_CLUSTER_NAMESPACE)"

Did you see more places that would still need changes? I'll first change the two I listed above and test the mTLS behavior first

@machichima

machichima commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Test with mTLS

Without Ray code change

Ray autoscaler will use node ip to do health check (here), this requires Ray code change. Tests in this section are done without Ray code change, which head certificate still need it's IP in the certificate hence need the wait-for-tls-ip-san init container.

Basic - without Ray change - check certificate set correctly

Mainly check if we include worker wildcard in the certificate rather than pod IPs, and check:

  1. We CANNOT call ray health-check to head pod from arbitrary pod
  2. We CAN call ray health-check to head pod from worker pod
export CLUSTER=raycluster-mtls
  export HEAD=$(kubectl get pod -l ray.io/cluster=$CLUSTER,ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')
  export HEADSVC=$CLUSTER-head-svc.default.svc.cluster.local
  export WORKER=$(kubectl get pod -l ray.io/cluster=$CLUSTER,ray.io/node-type=worker -o jsonpath='{.items[0].metadata.name}')

# Run a new pod (without certificate), cannot connect to GCS -> ensure mTLS is working
❯ kubectl run tls-probe --rm -it --restart=Never --image=rayproject/ray:2.55.1 -- \
    ray health-check --address $HEADSVC:6379 --skip-version-check
All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
[2026-09-03 04:58:01,506 W 1 1] rpc_client.h:153: Failed to connect to GCS at address raycluster-mtls-head-svc.default.svc.cluster.local:6379 within 5 seconds.
[2026-09-03 04:58:31,568 W 1 1] gcs_client.cc:205: Failed to get cluster ID from GCS server: TimedOut: Timed out while waiting for GCS to become available.

Aborted!
pod "tls-probe" deleted from default namespace
pod default/tls-probe terminated (Error)

# Call from head pod
❯ kubectl exec $HEAD -c ray-head -- ray health-check --address $HEADSVC:6379 --skip-version-check && echo OK
OK

# Call from worker pod
❯ kubectl exec $WORKER -c ray-worker -- ray health-check --address $HEADSVC:6379 --skip-version-check && echo OK

# Get DNS and IP set in the certificate
❯ kubectl get certificate -o custom-columns='NAME:.metadata.name,DNS:.spec.dnsNames,IP:.spec.ipAddresses'
NAME                                 DNS                                                                IP
ray-ca-certificate-raycluster-mtls   <none>                                                             <none>
ray-head-cert-raycluster-mtls        [localhost raycluster-mtls-head-svc.default.svc.cluster.local]     [10.244.0.11 127.0.0.1]
ray-worker-cert-raycluster-mtls      [*.raycluster-mtls-headless.default.svc.cluster.local localhost]   [127.0.0.1]

# Get secret of worker
❯ kubectl get secret ray-worker-secret-$CLUSTER -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -ext subjectAltName
X509v3 Subject Alternative Name: critical
    DNS:*.raycluster-mtls-headless.default.svc.cluster.local, DNS:localhost, IP Address:127.0.0.1

# Run ray.init in head pod and do get_node_ip_address() -> we will get the fqdn of the head pod
❯ kubectl exec $HEAD -c ray-head -- python -c "import ray; ray.init(); print('gcs:', ray.get_runtime_context().gcs_address); print('node_ip:', ray.util.get_node_ip_address())"
2026-09-03 04:59:13,204 INFO worker.py:1672 -- Using address raycluster-mtls-head-svc.default.svc.cluster.local:6379 set in the environment variable RAY_ADDRESS
2026-09-03 04:59:13,210 INFO worker.py:1814 -- Connecting to existing Ray cluster at address: raycluster-mtls-head-svc.default.svc.cluster.local:6379...
2026-09-03 04:59:13,221 INFO worker.py:2003 -- Connected to Ray cluster. View the dashboard at raycluster-mtls-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2051: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
gcs: raycluster-mtls-head-svc.default.svc.cluster.local:6379
node_ip: raycluster-mtls-head-svc.default.svc.cluster.local

# See containers in the pod, head still have wait-for-tls-ip-san init container
❯ kubectl get pod -l ray.io/cluster=raycluster-mtls \
    -o custom-columns='POD:.metadata.name,INIT:.spec.initContainers[*].name,CONTAINERS:.spec.containers[*].name'
POD                                        INIT                  CONTAINERS
raycluster-mtls-head-zsmgx                 wait-for-tls-ip-san   ray-head
raycluster-mtls-small-group-worker-hsslt   wait-gcs-ready        ray-worker

Trigger scaling manually

  • try to patch RayCluster to scale up / down
  • Use ray list nodes in head pod and see if we list nodes with fqdn rather than IP
❯ k apply -f ray-operator/config/samples/ray-cluster.mtls.yaml
raycluster.ray.io/raycluster-mtls created

# Trigger scale up
❯ kubectl patch raycluster $CLUSTER --type json -p '[{"op":"replace","path":"/spec/workerGroupSpecs/0/replicas","value":2},{"op":"replace","path":"/spec/workerGroupSpecs/0/maxReplicas","value":2}]'
raycluster.ray.io/raycluster-mtls patched

❯ kubectl get pod -l ray.io/cluster=$CLUSTER,ray.io/node-type=worker
NAME                                       READY   STATUS    RESTARTS   AGE
raycluster-mtls-small-group-worker-6wjcc   1/1     Running   0          54s
raycluster-mtls-small-group-worker-hsslt   1/1     Running   0          27s

# check pod name = hostname
❯ kubectl get pod -l ray.io/cluster=$CLUSTER,ray.io/node-type=worker -o custom-columns='NAME:.metadata.name,HOSTNAME:.spec.hostname,SUBDOMAIN:.spec.subdomain'
NAME                                       HOSTNAME                                   SUBDOMAIN
raycluster-mtls-small-group-worker-6wjcc   raycluster-mtls-small-group-worker-6wjcc   raycluster-mtls-headless
raycluster-mtls-small-group-worker-hsslt   raycluster-mtls-small-group-worker-hsslt   raycluster-mtls-headless

# List nodes, we can see two worker nodes
❯ kubectl exec $HEAD -c ray-head -- ray list nodes --format json | jq -r '.[] | [.node_ip, .state, (if .is_head_node then "head" else "worker" end)] | @tsv'
raycluster-mtls-small-group-worker-6wjcc.raycluster-mtls-headless.default.svc.cluster.local     ALIVE   worker
raycluster-mtls-head-svc.default.svc.cluster.local      ALIVE   head
raycluster-mtls-small-group-worker-hsslt.raycluster-mtls-headless.default.svc.cluster.local     ALIVE   worker

# Trigger scale down
❯ kubectl patch raycluster $CLUSTER --type json -p '[{"op":"replace","path":"/spec/workerGroupSpecs/0/replicas","value":1}]'
raycluster.ray.io/raycluster-mtls patched

# One worker pod being removed
❯ kubectl get pod -l ray.io/cluster=$CLUSTER
NAME                                       READY   STATUS    RESTARTS   AGE
raycluster-mtls-head-zsmgx                 1/1     Running   0          3m14s
raycluster-mtls-small-group-worker-hsslt   1/1     Running   0          2m47s

# One worker becomes dead
❯ kubectl exec $HEAD -c ray-head -- ray list nodes --format json | jq -r '.[] | [.node_ip, .state, (if .is_head_node then "head" else "worker" end)] | @tsv'
raycluster-mtls-small-group-worker-6wjcc.raycluster-mtls-headless.default.svc.cluster.local     DEAD    worker
raycluster-mtls-head-svc.default.svc.cluster.local      ALIVE   head
raycluster-mtls-small-group-worker-hsslt.raycluster-mtls-headless.default.svc.cluster.local     ALIVE   worker

Autoscaling

Update ray-operator/config/samples/ray-cluster.mtls.yaml to enable autoscaler v2 and add config map with script for scale up / down

  • Can scale up / down successfully
❯ kubectl apply -f ray-operator/config/samples/ray-cluster.mtls.yaml
raycluster.ray.io/raycluster-mtls created
configmap/ray-example created

❯ kubectl get pods -l ray.io/cluster=raycluster-mtls
NAME                         READY   STATUS    RESTARTS   AGE
raycluster-mtls-head-5v6js   2/2     Running   0          54s

❯ export CLUSTER=raycluster-mtls
  export HEAD=$(kubectl get pod -l ray.io/cluster=$CLUSTER,ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')

❯ kubectl logs $HEAD -c autoscaler | grep -E "The Ray head is ready|Starting the autoscaler|error|TLS" | head
2026-09-03 05:37:57,356 INFO run_autoscaler.py:65 -- The Ray head is ready. Starting the autoscaler.

# Scale up
❯ kubectl exec $HEAD -c ray-head -- python /home/ray/samples/detached_actor.py a1
  kubectl exec $HEAD -c ray-head -- python /home/ray/samples/detached_actor.py a2
2026-09-03 05:39:12,578 INFO worker.py:1672 -- Using address raycluster-mtls-head-svc.default.svc.cluster.local:6379 set in the environment variable RAY_ADDRESS
2026-09-03 05:39:12,584 INFO worker.py:1814 -- Connecting to existing Ray cluster at address: raycluster-mtls-head-svc.default.svc.cluster.local:6379...
2026-09-03 05:39:12,593 INFO worker.py:2003 -- Connected to Ray cluster. View the dashboard at raycluster-mtls-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2051: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
2026-09-03 05:39:13,885 INFO worker.py:1672 -- Using address raycluster-mtls-head-svc.default.svc.cluster.local:6379 set in the environment variable RAY_ADDRESS
2026-09-03 05:39:13,890 INFO worker.py:1814 -- Connecting to existing Ray cluster at address: raycluster-mtls-head-svc.default.svc.cluster.local:6379...
2026-09-03 05:39:13,898 INFO worker.py:2003 -- Connected to Ray cluster. View the dashboard at raycluster-mtls-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2051: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
(autoscaler +0s) Tip: use `ray status` to view detailed cluster status. To disable these messages, set RAY_SCHEDULER_EVENTS=0.
(autoscaler +0s) Adding 1 node(s) of type small-group.
(autoscaler +0s) Resized to 1 CPUs.

# See new worker pods
❯ kubectl get pod -l ray.io/cluster=$CLUSTER,ray.io/node-type=worker
NAME                                       READY   STATUS     RESTARTS   AGE
raycluster-mtls-small-group-worker-c7bxl   0/1     Init:0/1   0          6s
raycluster-mtls-small-group-worker-xrjwm   0/1     Running    0          11s

# Check their name = hostname
❯ kubectl get pod -l ray.io/cluster=$CLUSTER,ray.io/node-type=worker -o custom-columns='NAME:.metadata.name,HOSTNAME:.spec.hostname,SUBDOMAIN:.spec.subdomain'
NAME                                       HOSTNAME                                   SUBDOMAIN
raycluster-mtls-small-group-worker-c7bxl   raycluster-mtls-small-group-worker-c7bxl   raycluster-mtls-headless
raycluster-mtls-small-group-worker-xrjwm   raycluster-mtls-small-group-worker-xrjwm   raycluster-mtls-headless

# Check ray list node shows fqdn
❯ kubectl exec $HEAD -c ray-head -- ray list nodes --format json | jq -r '.[] | [.node_ip, .state] | @tsv'
raycluster-mtls-head-svc.default.svc.cluster.local      ALIVE
raycluster-mtls-small-group-worker-c7bxl.raycluster-mtls-headless.default.svc.cluster.local     ALIVE
raycluster-mtls-small-group-worker-xrjwm.raycluster-mtls-headless.default.svc.cluster.local     ALIVE


❯ kubectl logs $HEAD -c autoscaler | grep "Submitting a scale request"
2026-09-03 05:39:14,442 - INFO - Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 1}), workers_to_delete=defaultdict(<class 'list'>, {}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())
2026-09-03 05:39:14,442 INFO cloud_provider.py:365 -- Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 1}), workers_to_delete=defaultdict(<class 'list'>, {}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())
2026-09-03 05:39:19,513 - INFO - Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 2}), workers_to_delete=defaultdict(<class 'list'>, {}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())
2026-09-03 05:39:19,513 INFO cloud_provider.py:365 -- Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 2}), workers_to_delete=defaultdict(<class 'list'>, {}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())

# Scale down
❯ kubectl exec $HEAD -c ray-head -- python /home/ray/samples/terminate_detached_actor.py a1
  kubectl exec $HEAD -c ray-head -- python /home/ray/samples/terminate_detached_actor.py a2
2026-09-03 05:40:44,050 INFO worker.py:1672 -- Using address raycluster-mtls-head-svc.default.svc.cluster.local:6379 set in the environment variable RAY_ADDRESS
2026-09-03 05:40:44,056 INFO worker.py:1814 -- Connecting to existing Ray cluster at address: raycluster-mtls-head-svc.default.svc.cluster.local:6379...
2026-09-03 05:40:44,064 INFO worker.py:2003 -- Connected to Ray cluster. View the dashboard at raycluster-mtls-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2051: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
2026-09-03 05:40:45,199 INFO worker.py:1672 -- Using address raycluster-mtls-head-svc.default.svc.cluster.local:6379 set in the environment variable RAY_ADDRESS
2026-09-03 05:40:45,205 INFO worker.py:1814 -- Connecting to existing Ray cluster at address: raycluster-mtls-head-svc.default.svc.cluster.local:6379...
2026-09-03 05:40:45,213 INFO worker.py:2003 -- Connected to Ray cluster. View the dashboard at raycluster-mtls-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2051: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(

# Ensure we scale down
❯ kubectl get pod -l ray.io/cluster=$CLUSTER
NAME                         READY   STATUS    RESTARTS   AGE
raycluster-mtls-head-5v6js   2/2     Running   0          4m14s

❯ kubectl logs $HEAD -c autoscaler | grep "Submitting a scale request" | tail -1
2026-09-03 05:41:51,214 INFO cloud_provider.py:365 -- Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 0}), workers_to_delete=defaultdict(<class 'list'>, {'small-group': [CloudInstance(cloud_instance_id='raycluster-mtls-small-group-worker-xrjwm', node_type='small-group', node_kind=2, is_running=True, request_id=None), CloudInstance(cloud_instance_id='raycluster-mtls-small-group-worker-c7bxl', node_type='small-group', node_kind=2, is_running=True, request_id=None)]}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())

Signed-off-by: machichima <nary12321@gmail.com>
@machichima

machichima commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Autoscaling v2 with Ray code change

Update Ray Image with my python change

  • As I only made python change, I can directly copy the modified python file into the ray image
git diff master -- python/ray > autoscaler.patch

# create docker file
cat > Dockerfile <<'EOF'
FROM rayproject/ray:nightly-py311-cpu
COPY autoscaler.patch /tmp/autoscaler.patch
RUN cd $(python -c "import ray,os; print(os.path.dirname(os.path.dirname(ray.__file__)))") \
 && patch -p2 < /tmp/autoscaler.patch
EOF

# Build and load to kind
docker build -t ray-autoscaler-hostname:dev .
kind load docker-image ray-autoscaler-hostname:dev --name <your-kind-cluster>

Update YAML

Update ray-cluster.mtls.yaml YAML and add:

  autoscalerOptions:
    version: v2
    idleTimeoutSeconds: 60
    # TODO(local test only, remove before commit): Ray image with `ray kuberay-autoscaler --gcs-address`.
    image: ray-autoscaler-hostname:dev
    imagePullPolicy: IfNotPresent

Result

❯ kubectl apply -f ray-operator/config/samples/ray-cluster.mtls.yaml
raycluster.ray.io/raycluster-mtls created
configmap/ray-example unchanged

❯ kubectl get pods -l ray.io/cluster=raycluster-mtls
NAME                         READY   STATUS    RESTARTS   AGE
raycluster-mtls-head-5pl62   1/2     Running   0          8s

❯ export HEAD=$(kubectl get pod -l ray.io/cluster=raycluster-mtls,ray.io/node-type=head -o jsonpath='{.items[0].metadata.name}')

# ** Ensure there's no init container for head pod
❯ kubectl get pod $HEAD -o jsonpath='init={.spec.initContainers[*].name}{"\n"}'
init=

# Head pod certificate IP only include localhost
❯ kubectl get certificate ray-head-cert-raycluster-mtls -o jsonpath='{.spec.ipAddresses}{"\n"}'
["127.0.0.1"]

# Ful certificates
❯ kubectl get certificate -o custom-columns='NAME:.metadata.name,DNS:.spec.dnsNames,IP:.spec.ipAddresses'
NAME                                 DNS                                                                IP
ray-ca-certificate-raycluster-mtls   <none>                                                             <none>
ray-head-cert-raycluster-mtls        [localhost raycluster-mtls-head-svc.default.svc.cluster.local]     [127.0.0.1]
ray-worker-cert-raycluster-mtls      [*.raycluster-mtls-headless.default.svc.cluster.local localhost]   [127.0.0.1]

# Check the autoscaler command include --gcs-address
❯ kubectl exec $HEAD -c autoscaler -- sh -c 'tr "\0" " " < /proc/1/cmdline; echo' | grep -o -- '--gcs-address=[^ ]*'
--gcs-address=raycluster-mtls-head-svc.default.svc.cluster.local:6379


❯  kubectl logs $HEAD -c autoscaler | grep -E "The Ray head is ready|rror" | head -3
2026-09-03 06:51:44,646 INFO run_autoscaler.py:89 -- The Ray head is ready. Starting the autoscaler.

# Scale up
❯ kubectl exec $HEAD -c ray-head -- python /home/ray/samples/detached_actor.py a1
  kubectl exec $HEAD -c ray-head -- python /home/ray/samples/detached_actor.py a2
2026-09-03 06:54:15,209 INFO worker.py:1672 -- Using address raycluster-mtls-head-svc.default.svc.cluster.local:6379 set in the environment variable RAY_ADDRESS
2026-09-03 06:54:15,215 INFO worker.py:1814 -- Connecting to existing Ray cluster at address: raycluster-mtls-head-svc.default.svc.cluster.local:6379...
2026-09-03 06:54:15,225 INFO worker.py:2003 -- Connected to Ray cluster. View the dashboard at raycluster-mtls-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2051: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
2026-09-03 06:54:16,546 INFO worker.py:1672 -- Using address raycluster-mtls-head-svc.default.svc.cluster.local:6379 set in the environment variable RAY_ADDRESS
2026-09-03 06:54:16,552 INFO worker.py:1814 -- Connecting to existing Ray cluster at address: raycluster-mtls-head-svc.default.svc.cluster.local:6379...
2026-09-03 06:54:16,561 INFO worker.py:2003 -- Connected to Ray cluster. View the dashboard at raycluster-mtls-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2051: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(

# Ensure new worker pods being created
❯ kubectl get pod -l ray.io/cluster=$CLUSTER,ray.io/node-type=worker
NAME                                       READY   STATUS    RESTARTS   AGE
raycluster-mtls-small-group-worker-5wlkg   0/1     Running   0          15s
raycluster-mtls-small-group-worker-ntr49   0/1     Running   0          15s

# Check pod name = hostname
❯ kubectl get pod -l ray.io/cluster=$CLUSTER,ray.io/node-type=worker -o custom-columns='NAME:.metadata.name,HOSTNAME:.spec.hostname,SUBDOMAIN:.spec.subdomain'
NAME                                       HOSTNAME                                   SUBDOMAIN
raycluster-mtls-small-group-worker-5wlkg   raycluster-mtls-small-group-worker-5wlkg   raycluster-mtls-headless
raycluster-mtls-small-group-worker-ntr49   raycluster-mtls-small-group-worker-ntr49   raycluster-mtls-headless

# ray list nodes will use hostname as node ip
❯ kubectl exec $HEAD -c ray-head -- ray list nodes --format json | jq -r '.[] | [.node_ip, .state] | @tsv'
raycluster-mtls-small-group-worker-ntr49.raycluster-mtls-headless.default.svc.cluster.local     ALIVE
raycluster-mtls-head-svc.default.svc.cluster.local      ALIVE
raycluster-mtls-small-group-worker-5wlkg.raycluster-mtls-headless.default.svc.cluster.local     ALIVE

❯ kubectl logs $HEAD -c autoscaler | grep "Submitting a scale request"
2026-09-03 06:54:20,577 INFO cloud_provider.py:443 -- Submitting a scale request: KubeRayProvider.ScaleRequest(desired_num_workers=defaultdict(<class 'int'>, {'small-group': 2}), workers_to_delete=defaultdict(<class 'list'>, {}), worker_groups_without_pending_deletes=set(), worker_groups_with_pending_deletes=set())

# Scale down
❯ kubectl exec $HEAD -c ray-head -- python /home/ray/samples/terminate_detached_actor.py a1
  kubectl exec $HEAD -c ray-head -- python /home/ray/samples/terminate_detached_actor.py a2
2026-09-03 06:55:33,611 INFO worker.py:1672 -- Using address raycluster-mtls-head-svc.default.svc.cluster.local:6379 set in the environment variable RAY_ADDRESS
2026-09-03 06:55:33,617 INFO worker.py:1814 -- Connecting to existing Ray cluster at address: raycluster-mtls-head-svc.default.svc.cluster.local:6379...
2026-09-03 06:55:33,626 INFO worker.py:2003 -- Connected to Ray cluster. View the dashboard at raycluster-mtls-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2051: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(
2026-09-03 06:55:34,729 INFO worker.py:1672 -- Using address raycluster-mtls-head-svc.default.svc.cluster.local:6379 set in the environment variable RAY_ADDRESS
2026-09-03 06:55:34,736 INFO worker.py:1814 -- Connecting to existing Ray cluster at address: raycluster-mtls-head-svc.default.svc.cluster.local:6379...
2026-09-03 06:55:34,745 INFO worker.py:2003 -- Connected to Ray cluster. View the dashboard at raycluster-mtls-head-svc.default.svc.cluster.local:8265
/home/ray/anaconda3/lib/python3.10/site-packages/ray/_private/worker.py:2051: FutureWarning: Tip: In future versions of Ray, Ray will no longer override accelerator visible devices env var if num_gpus=0 or num_gpus=None (default). To enable this behavior and turn off this error message, set RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0
  warnings.warn(

# Ensure worker pods being terminated
❯ kubectl get pod -l ray.io/cluster=$CLUSTER
NAME                         READY   STATUS    RESTARTS   AGE
raycluster-mtls-head-5pl62   2/2     Running   0          9m37s

@machichima

machichima commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Quick Summary

  1. Latest commit 2fff093 is based on Ray change [Core] add --gcs-address flag for kuberay_autoscaler ray#65894 -> Will need to wait until Ray's PR merge and release
  2. Test with mTLS
    1. Without Ray change, head certificate still needs IP, and head pod still need wait-for-tls-ip-san init container (link)
    2. With Ray change, head cert do not need IP and head pod do NOT need init container (link)

cc @chipspeak @rueian

@machichima
machichima marked this pull request as ready for review September 4, 2026 02:10

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 2fff093. Configure here.

workerSpec.RayStartParams["node-ip-address"] = fmt.Sprintf("%s.%s.%s.svc.%s",
podTemplate.Name, podTemplate.Spec.Subdomain, instance.Namespace, utils.GetClusterDomainName())
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preset subdomain breaks mTLS identity

Medium Severity

When a worker template already has subdomain set, the new FQDN path is skipped entirely, including --node-ip-address injection. Those workers still register with pod IPs, but this change also removes IP SANs and wait-for-tls-ip-san. mTLS handshakes then fail for TPU multi-host and any other preset-subdomain group.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2fff093. Configure here.

podTemplate.Name = podName + rand.String(5) // podName ends with "-", <= 63 chars total
podTemplate.GenerateName = ""
podTemplate.Spec.Hostname = podTemplate.Name
podTemplate.Spec.Subdomain = instance.Name + utils.DashSymbol + utils.HeadlessServiceSuffix

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Headless subdomain exceeds DNS limit

Medium Severity

Worker subdomain is set to {cluster.Name}-headless with no truncation. Service names and pod subdomains must be valid DNS labels (≤63 characters). A cluster name of 55+ characters yields an invalid subdomain, so Kubernetes rejects the pods (and the headless Service). The new long-name test only checks the pod name length.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2fff093. Configure here.

ip := utils.LOCAL_HOST
if utils.EnvVarExists(utils.RAY_USE_TLS, container.Env) {
ip = fqdnRayIP
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TLS env check is too broad

Medium Severity

Head RAY_ADDRESS switches from localhost to the head Service FQDN whenever RAY_USE_TLS is present on the container, not when KubeRay mTLS is enabled. Users who already set RAY_USE_TLS for Ray’s native file-based TLS will get a new head address, which can break ray.init() if their certificates only cover loopback or the pod IP.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2fff093. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants