Ravindra BagaleCourses & study guides मराठी

Chapter 62: Kubernetes Services, Networking and Load Balancing: CNI, ClusterIP, NodePort, LoadBalancer, DNS and NetworkPolicy

62.3 NodePort Services: opening a port on every node, and when not to

Why: the problem

Suppose a small team at Nykaa runs a staging cluster on three EC2 instances with kubeadm and no cloud load balancer. QA testers need to open the staging storefront from their laptops. ClusterIP is internal only, so it will not work. The quickest way out is a NodePort Service. A NodePort Service is a ClusterIP Service plus one extra thing: a port between 30000 and 32767 opened on every node. Traffic that reaches any node's IP on that port is forwarded to the Service and then to one of its pods, even if no pod runs on that node. You can let Kubernetes pick the port or set it yourself. NodePort is simple, but it has real limits. Clients must know node IPs, which change when nodes are replaced. The port range is odd for users. Every node opens the port, which widens the attack surface. And by default the extra hop to another node hides the client's IP, unless you set externalTrafficPolicy: Local. That is why production apps use LoadBalancer or Ingress, which use NodePorts underneath. In this lesson we expose the Nykaa storefront with a fixed NodePort reachable from the laptop through kind's port mapping, prove that every node answers, compare Cluster and Local traffic policies, and see the port range error.

What: every term in plain English

  • NodePort — A Service type that opens the same port on every node.
  • nodePort range — 30000-32767 by default (kube-apiserver --service-node-port-range).
  • port / targetPort / nodePort — Service port, pod port, and node port.
  • externalTrafficPolicy: Cluster — Default. Any node forwards to any pod; client IP is replaced by a node IP (SNAT).
  • externalTrafficPolicy: Local — Only nodes with a local pod answer, and the client IP is kept.
  • SNAT — Source NAT: the packet's source IP is changed to the node's IP.
  • Health check node port — With Local, a port that load balancers query to find nodes with pods.
  • kind extraPortMappings — Maps a port on your laptop to a port on a kind node container.
  • Security group — On AWS, the firewall that must allow the NodePort.
  • Ingress and LoadBalancer — Production front doors that build on NodePort.
Deploy the storefront with 2 replicas. Create a NodePort Service with nodePort 30080. Curl from the laptop and from each node IP. Read the client IP in the logs. Switch to externalTrafficPolicy: Local. Try a port outside the range. Clean up.
NodePort: same high port on every node; Local keeps the client IP

How: step by step

  1. Deploy the storefront with 2 replicas. On the ch62 cluster.
  2. Create a NodePort Service with nodePort 30080. Matches the kind port mapping.
  3. Curl from the laptop and from each node IP. All nodes answer.
  4. Read the client IP in the logs. Cluster policy hides it.
  5. Switch to externalTrafficPolicy: Local. Only nodes with pods answer; real IP kept.
  6. Try a port outside the range. Validation error.
  7. Clean up. Keep the cluster for 62.4.

Example 1: Expose the storefront on port 30080 and open it from the laptop

Goal. Expose the storefront on port 30080 and open it from the laptop.

store.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: store
  namespace: net
spec:
  replicas: 2
  selector:
    matchLabels:
      app: store
  template:
    metadata:
      labels:
        app: store
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - name: http
              containerPort: 80
          resources:
            requests: {cpu: 20m, memory: 32Mi}
            limits: {memory: 64Mi}
---
apiVersion: v1
kind: Service
metadata:
  name: store
  namespace: net
spec:
  type: NodePort
  selector:
    app: store
  ports:
    - name: http
      port: 80
      targetPort: http
      nodePort: 30080

What each line does

  • type: NodePort — ClusterIP plus a port on every node.
  • nodePort: 30080 — Fixed so it matches kind's extraPortMappings from 62.1.
  • port: 80 — Inside the cluster, store:80 still works.
  • localhost:30080 — kind maps it to the control-plane node container.
  • No pod on control-plane — The node still forwards the request to a worker pod.

Green output

$ kubectl apply -f store.yaml
$ kubectl get svc -n net store
NAME    TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)
store   NodePort   10.110.42.18    <none>        80:30080/TCP
$ curl -s localhost:30080 | grep -o '<title>.*</title>'
<title>Welcome to nginx!</title>
$ kubectl get pods -n net -l app=store -o wide
NAME                     READY   STATUS    NODE
store-6f7d9c8b4-2kq9x    1/1     Running   ch62-worker
store-6f7d9c8b4-p8z4m    1/1     Running   ch62-worker2

Expected result. The laptop reached the app through the control-plane node, which has no store pod. kube-proxy forwarded it to a worker.

Example 2: Prove every node answers, and see the client IP is lost

Goal. Prove every node answers, and see the client IP is lost.

every-node.sh

for n in ch62-control-plane ch62-worker ch62-worker2; do
  ip=$(docker inspect -f '{{.NetworkSettings.Networks.kind.IPAddress}}' $n)
  printf '%s %s ' $n $ip
  docker run --rm --network kind curlimages/curl:8.10.1 -s -o /dev/null -w '%{http_code}\n' http://$ip:30080
done

What each line does

  • docker inspect — Gets each kind node's IP on the kind Docker network.
  • docker run --network kind — A client outside the cluster but on the same network, like a tester on the office LAN.
  • 200 from all three — NodePort is open on every node.
  • Logs — nginx sees a node IP, not the client IP.
  • Why — With Cluster policy the forwarding node SNATs the packet.

Green output

$ bash every-node.sh
ch62-control-plane 172.18.0.2 200
ch62-worker 172.18.0.3 200
ch62-worker2 172.18.0.4 200
$ docker run --rm --network kind curlimages/curl:8.10.1 -s -o /dev/null http://172.18.0.2:30080
$ kubectl logs -n net -l app=store --tail=1 --prefix
[pod/store-6f7d9c8b4-2kq9x/nginx] 192.168.66.0 - - [25/Oct/2026:03:05:12 +0000] "GET / HTTP/1.1" 200 615

Expected result. All nodes answered 200. nginx logged 192.168.66.0, an address on the control-plane node, not the client's 172.18.0.x address.

Example 3: Keep the client IP with externalTrafficPolicy: Local

Goal. Keep the client IP with externalTrafficPolicy: Local.

store-local.yaml

apiVersion: v1
kind: Service
metadata:
  name: store
  namespace: net
spec:
  type: NodePort
  externalTrafficPolicy: Local
  selector:
    app: store
  ports:
    - name: http
      port: 80
      targetPort: http
      nodePort: 30080

What each line does

  • externalTrafficPolicy: Local — A node sends traffic only to its own pods.
  • Control plane — Has no store pod, so it now drops NodePort traffic.
  • Workers — Answer and keep the real client IP.
  • healthCheckNodePort — Kubernetes adds one so a load balancer can skip empty nodes.
  • Trade-off — Load can be uneven if pods are not spread evenly.

Green output

$ kubectl apply -f store-local.yaml
$ kubectl get svc -n net store -o jsonpath='{.spec.healthCheckNodePort}{"\n"}'
31544
$ bash every-node.sh
ch62-control-plane 172.18.0.2 000
ch62-worker 172.18.0.3 200
ch62-worker2 172.18.0.4 200
$ docker run --rm --network kind curlimages/curl:8.10.1 -s -o /dev/null http://172.18.0.3:30080
$ kubectl logs -n net -l app=store --tail=1 --prefix | grep 172.18
[pod/store-6f7d9c8b4-2kq9x/nginx] 172.18.0.5 - - [25/Oct/2026:03:08:40 +0000] "GET / HTTP/1.1" 200 615

Expected result. Now the control plane does not answer (000, a timeout), and nginx logs the real client 172.18.0.5. Note localhost:30080 now fails too, because kind maps it to the control-plane node.

Every command, step by step (kind lab with kubectl)

  1. Deploy store

    kubectl apply -f store.yaml
    kubectl rollout status deploy/store -n net
    

    What it does: Two pods.

    deployment "store" successfully rolled out
    
  2. Read the ports

    kubectl get svc -n net store
    

    What it does: 80:30080 means port 80, node port 30080.

    store   NodePort   10.110.42.18   <none>   80:30080/TCP
    
  3. Open from the laptop

    curl -sI localhost:30080 | head -1
    

    What it does: Through kind's port mapping.

    HTTP/1.1 200 OK
    
  4. Still works as ClusterIP

    kubectl exec -n net curl -- curl -sI store | head -1
    

    What it does: Inside the cluster.

    HTTP/1.1 200 OK
    
  5. Find node IPs

    kubectl get nodes -o wide | awk '{print $1, $6}'
    

    What it does: INTERNAL-IP column.

    NAME INTERNAL-IP
    ch62-control-plane 172.18.0.2
    ch62-worker 172.18.0.3
    ch62-worker2 172.18.0.4
    
  6. Hit every node

    bash every-node.sh
    

    What it does: All 200.

    ch62-control-plane 172.18.0.2 200
    
  7. See the kube-proxy rule

    docker exec ch62-worker iptables -t nat -S KUBE-NODEPORTS | grep 30080
    

    What it does: kube-proxy added a rule for 30080 on every node.

    -A KUBE-NODEPORTS -p tcp -m comment --comment "net/store:http" -m tcp --dport 30080 -j KUBE-EXT-...
    
  8. Let Kubernetes choose a port

    kubectl expose deploy store -n net --name store-auto --type NodePort --port 80
    kubectl get svc -n net store-auto
    

    What it does: A random port in range.

    store-auto   NodePort   10.97.5.201   <none>   80:31927/TCP
    
  9. Try a bad port

    kubectl create service nodeport bad -n net --tcp=80:80 --node-port=8080
    

    What it does: Out of range.

    The Service "bad" is invalid: spec.ports[0].nodePort: Invalid value: 8080: provided port is not in the valid range. The range of valid ports is 30000-32767
    
  10. Switch to Local

    kubectl apply -f store-local.yaml
    bash every-node.sh
    

    What it does: Only nodes with pods answer.

    ch62-control-plane 172.18.0.2 000
    ch62-worker 172.18.0.3 200
    
  11. Clean up

    kubectl delete svc -n net store-auto
    kubectl apply -f store.yaml
    

    What it does: Back to Cluster policy. Keep store for 62.4.

    service/store configured
    

When it is red 1

provided port is not in the valid range. The range of valid ports is 30000-32767

Cause. nodePort outside the range.

Fix, step by step

  1. Use 30000-32767 or let Kubernetes choose.

When it is red 2

spec.ports[0].nodePort: Invalid value: 30080: provided port is already allocated

Cause. Another Service already uses that node port.

Fix, step by step

  1. kubectl get svc -A | grep 30080 and pick another.

When it is red 3

curl: (28) Connection timed out (from the internet to an EC2 node)

Cause. The security group or NACL does not allow the port.

Fix, step by step

  1. Open the port to the right source range only.

When it is red 4

Some nodes do not answer the NodePort

Cause. externalTrafficPolicy: Local and no pod on those nodes.

Fix, step by step

  1. Expected; use a load balancer health check or spread pods.

Practice tasks

Easy Task 1: Expose

Why: Create.

What to do: Create the store NodePort.

How:
  1. 30080.
Solution
kubectl get svc -n net store

Easy Task 2: Laptop

Why: Test.

What to do: Open it from the laptop.

How:
  1. kind mapping.
Solution
curl -s localhost:30080

Easy Task 3: Nodes

Why: Test.

What to do: Hit every node IP.

How:
  1. every-node.sh.
Solution
bash every-node.sh

Medium Task 4: Rule

Why: Inspect.

What to do: Find the iptables rule.

How:
  1. KUBE-NODEPORTS.
Solution
docker exec ch62-worker iptables -t nat -S KUBE-NODEPORTS

Medium Task 5: Auto

Why: Port.

What to do: Let Kubernetes pick a port.

How:
  1. kubectl expose.
Solution
kubectl get svc -n net store-auto

Medium Task 6: Range

Why: Break.

What to do: Try port 8080.

How:
  1. Error.
Solution
kubectl create service nodeport bad -n net --tcp=80:80 --node-port=8080

Medium Task 7: Client IP

Why: Compare.

What to do: Read logs under Cluster and Local.

How:
  1. --prefix.
Solution
kubectl logs -n net -l app=store --tail=1 --prefix

Medium Task 8: Health

Why: Read.

What to do: Find healthCheckNodePort.

How:
  1. jsonpath.
Solution
kubectl get svc -n net store -o yaml

Hard Task 9: Spread

Why: Main task.

What to do: Spread 3 replicas and use Local.

How:
  1. topologySpreadConstraints.
Solution
kubectl get pods -n net -o wide

Hard Task 10: Explain

Why: Interview.

What to do: Explain why NodePort is not used directly in production.

How:
  1. IPs, ports, security.
Solution
kubectl explain service.spec.externalTrafficPolicy

Homework: try at home

For Nykaa staging, expose the storefront with NodePort 30080 and externalTrafficPolicy Local, spread 3 replicas with a topology spread constraint so every worker has a pod, and prove that every worker answers and logs the real client IP.

Recap

  • NodePort opens one port (30000-32767) on every node.
  • Any node forwards to a pod; Cluster policy hides the client IP.
  • externalTrafficPolicy: Local keeps the client IP but only nodes with pods answer.
  • LoadBalancer and Ingress use NodePorts underneath.
  • Next: LoadBalancer Services with MetalLB and on AWS.

FAQ

Q. How does a tester reach a NodePort on a private EC2 node?

Through a VPN or bastion into the VPC, then node-private-IP:30080. Do not give nodes public IPs just for testing.

Q. What if a node is replaced by the autoscaler?

Its IP changes, so any bookmark or DNS record that used it breaks. That is a main reason to put a load balancer in front.

Q. Does kube-proxy need to run for NodePort to work?

Yes, in iptables or IPVS mode kube-proxy writes the KUBE-NODEPORTS rules. With a kube-proxy replacement CNI, the CNI does it instead.

Q. Can I limit NodePort to some node addresses?

Yes, kube-proxy's nodePortAddresses setting restricts which node IPs listen, for example only the private interface.

Q. Can I use port 80 as a NodePort?

Not by default. You can change the apiserver range, but that is rarely a good idea.

Q. Is NodePort fine for a demo on EC2?

Yes, with a security group open only to your own IP.

Q. Does NodePort work with UDP?

Yes, set protocol: UDP.

Q. Which IP do clients use?

Any node's IP; for public access the node needs a public IP.

Q. Does Local policy affect ClusterIP traffic?

No, that is internalTrafficPolicy, a separate field.

Q. Is hostPort the same?

No. hostPort opens a port only on the node where that pod runs.

Interview questions

  1. What is a NodePort Service?

    Model answer: A Service that exposes the same high port on every node and forwards to its pods.

  2. What is externalTrafficPolicy?

    Model answer: Cluster forwards anywhere and SNATs; Local keeps client IP and only uses local pods.

  3. NodePort vs hostPort?

    Model answer: NodePort is on all nodes via kube-proxy; hostPort is only the node running that pod.

  4. Why not use NodePort in production?

    Model answer: Changing node IPs, unusual ports, larger attack surface, no TLS or health-aware routing.

  5. How does a LoadBalancer use NodePort?

    Model answer: The cloud load balancer sends traffic to node ports on the instances (in instance mode).

  6. How would you expose a staging app on EC2 without a cloud load balancer?

    Model answer: A NodePort Service with a fixed port, a security group open only to the office or VPN range, and externalTrafficPolicy Local if the app needs real client IPs. Move to a LoadBalancer or Ingress before production.

Quick quiz

Q1. What is the default NodePort range?

30000-32767.

Q2. Do nodes without pods answer with Cluster policy?

Yes, they forward.

Q3. What does Local keep?

The client's source IP.

Q4. Does a NodePort Service also have a ClusterIP?

Yes.

Q5. Why is NodePort rare in production?

Node IPs change, odd ports, every node exposed.

Samajla ka?