k3sm licensed apache-2.0 (DCO)

Reaching a service from the host

Publish a workload on a NodePort and reach it from the Mac itself, and understand which interfaces that port answers on.


Ten minutes. Take the Deployment from the previous tutorial, answer on a fixed port of your Mac, and get a clear picture of who else can reach it.

NodePort on k3sm behaves the way it does upstream, which has consequences on a personal Mac.

1. The Service#

Start from deployments and services with hello-http running, then apply a NodePort in front of it:

apiVersion: v1
kind: Service
metadata:
  name: hello-http-nodeport
  namespace: default
  labels:
    app: hello-http
spec:
  type: NodePort
  selector:
    app: hello-http
  ports:
    - name: http
      port: 80
      targetPort: http
      nodePort: 30080
      protocol: TCP

This is examples/service-nodeport.yaml.

k3sm kubectl apply -f service-nodeport.yaml
k3sm kubectl get svc hello-http-nodeport

The nodePort is pinned here so the port is predictable. In real use, omit it and let the API server allocate one; a pinned port is one more thing to collide with.

2. Reach It#

curl http://127.0.0.1:30080/

The workload answers with hello, through the userspace proxy, from a process on your own machine.

3. Which Addresses Answer#

  • The listener is bound to the wildcard, *:30000-32767. Every interface on the Mac answers, loopback and your LAN address alike. There is no host-only mode, so a NodePort Service on your laptop is published to whatever network the laptop is on. Treat it accordingly on a café network.
  • The node port is a wildcard listener, and Go sets no SO_REUSEPORT. It must not collide with another NodePort Service or an unrelated app already listening on your Mac; the loser of a collision fails to bind. Pods running a pulled image get separate per-IP port spaces for ordinary ports (≥1024, TCP and UDP), so such a Pod’s wildcard :8080 is rewritten onto its own address. An image: native host binary gets no rewrite. The hello-http workload here is one of those, so its containerPort shares the host’s one port space and is a collision candidate too.
  • k3sm reserves the NodePort range and 10250 for its own wildcard listeners. A type: LoadBalancer Service that claims one of those is rejected at apply time, with the port named in the message. Plain NodePort Services like this one are unaffected, because the API server allocates their node port out of that very range.
  • UDP node ports are unimplemented, so a node port answers TCP only.

If you use type: LoadBalancer instead, read the address section of limitations first. The bind address and the advertised EXTERNAL-IP differ. The port answers on every interface, while the advertised address is a loopback-scoped alias that a LAN client has no route to. Dial the Mac’s LAN address and the Service port; a curl at the advertised EXTERNAL-IP from another machine hangs until it times out.

4. Clean Up#

k3sm kubectl delete -f service-nodeport.yaml

Next#

  • storage gives a workload a volume that outlives it.
  • troubleshooting covers a Service that does not answer, and where the log for it lives.