Kubernetes Services — NodePort, ClusterIP and LoadBalancer

We will deploy the Docker sample voting app to understand all three service types in one real example from https://github.com/dockersamples/example-voting-app 

The Three Service Types 

ClusterIP

Default service type and Internal only. No external access. 

Used for redis and db — only other pods inside the cluster need to reach them.

spec:
  type: ClusterIP
  selector:
    app: redis
  ports:
    - port: 6379
      targetPort: 6379

Pods find each other by service name — voting-app connects to redis:6379, not an IP. The service name is the DNS hostname inside the cluster.

NodePort

Opens a port on every node in the range 30000-32767. Accessible from outside the cluster via any node IP.

spec:
  type: NodePort
  selector:
    app: voting-app
  ports:
    - port: 80        # service port inside cluster
      targetPort: 80  # container port
      nodePort: 31000 # external port on every node

Hit any node IP on that port:

http://172.16.77.10:31000
http://172.16.77.11:31000
http://172.16.77.12:31000

All three work — the service load balances to the correct pod regardless of which node you hit.

LoadBalancer

Builds on top of NodePort. On k3s, ServiceLB automatically assigns all node IPs. In cloud (AWS/GKE/AKS) it provisions a real external load balancer with a single public IP.

spec:
  type: LoadBalancer
  selector:
    app: voting-app
  ports:
    - port: 5000
      targetPort: 80

k3s creates one svclb pod per node per LoadBalancer service:

kubectl get pods -n kube-system | grep svclb-voting
svclb-voting-app-xxx   1/1   Running   k3s-cp-01
svclb-voting-app-xxx   1/1   Running   k3s-worker-01
svclb-voting-app-xxx   1/1   Running   k3s-worker-02

Three svclb pods = three node IPs assigned to the service.


Port Concepts — NodePort

Three separate port numbers, all can be different:

User hits:     172.16.77.10:31000     ← nodePort (on the node)
                      ↓
Service:       ClusterIP:80           ← port (service's own port)
                      ↓
Pod receives:  10.42.2.x:80          ← targetPort (container port)
ports:
  - targetPort: 80    # 1. what the container listens on
    port: 80          # 2. the service's own port
    nodePort: 31000   # 3. external port on every node (30000-32767)

Deploying the App

Five components, three service types:

ComponentImageService TypePort
voting-appdockersamples/examplevotingapp_vote:beforeLoadBalancer5000→80
result-appdockersamples/examplevotingapp_result:beforeLoadBalancer5001→80
redisredisClusterIP6379
dbpostgres:9.4ClusterIP5432
workerdockersamples/examplevotingapp_worker:latestNone

Worker has no service — it only reads from redis and writes to db, never receives traffic.


Step 1 — Find the Ports

Before writing any manifests, pull the images and inspect them to find the exposed ports:

docker pull dockersamples/examplevotingapp_vote:before
docker inspect dockersamples/examplevotingapp_vote:before | python3 -m json.tool | grep -A5 ExposedPorts
"ExposedPorts": {
    "80/tcp": {}
}
docker inspect redis:latest | python3 -m json.tool | grep -A5 ExposedPorts
"ExposedPorts": {
    "6379/tcp": {}
}
docker inspect postgres:9.4 | python3 -m json.tool | grep -A5 ExposedPorts
"ExposedPorts": {
    "5432/tcp": {}
}


Step 2 — Create Namespace

kubectl create namespace voting
kubectl config set-context --current --namespace=voting

Step 3 — Generate Deployments with dry-run

Use --dry-run=client -o yaml to generate base manifests:

kubectl create deployment redis --image=redis --replicas=1 --dry-run=client -o yaml > redis-deployment.yaml
kubectl create deployment db --image=postgres:9.4 --replicas=1 --dry-run=client -o yaml > db-deployment.yaml
kubectl create deployment voting-app --image=dockersamples/examplevotingapp_vote:before --replicas=1 --dry-run=client -o yaml > voting-app-deployment.yaml
kubectl create deployment result-app --image=dockersamples/examplevotingapp_result:before --replicas=1 --dry-run=client -o yaml > result-app-deployment.yaml
kubectl create deployment worker --image=dockersamples/examplevotingapp_worker:latest --replicas=1 --dry-run=client -o yaml > worker-deployment.yaml

PostgreSQL needs credentials so edit db-deployment.yaml and add env vars under the container:

        env:
          - name: POSTGRES_PASSWORD
            value: "postgres"
          - name: POSTGRES_USER
            value: "postgres"


Step 4 — Generate Services

# ClusterIP — internal only, no external access
kubectl create service clusterip redis --tcp=6379:6379 --dry-run=client -o yaml > redis-service.yaml
kubectl create service clusterip db --tcp=5432:5432 --dry-run=client -o yaml > db-service.yaml

# LoadBalancer — external access, k3s assigns all node IPs
kubectl create service loadbalancer voting-app --tcp=5000:80 --dry-run=client -o yaml > voting-app-service.yaml
kubectl create service loadbalancer result-app --tcp=5001:80 --dry-run=client -o yaml > result-app-service.yaml

Port format is external:container5000:80 means port 5000 on the service, forwarding to port 80 on the pod.

We use ports 5000 and 5001 instead of 80 because Traefik already holds port 80 on all nodes as a host port.


Step 5 — Apply in Order

kubectl apply -f redis-deployment.yaml
kubectl apply -f redis-service.yaml
kubectl apply -f db-deployment.yaml
kubectl apply -f db-service.yaml
kubectl apply -f worker-deployment.yaml
kubectl apply -f voting-app-deployment.yaml
kubectl apply -f voting-app-service.yaml
kubectl apply -f result-app-deployment.yaml
kubectl apply -f result-app-service.yaml

Step 6 — Verify

kubectl get deployments -n voting
NAME         READY   UP-TO-DATE   AVAILABLE
db           1/1     1            1
redis        1/1     1            1
result-app   1/1     1            1
voting-app   1/1     1            1
worker       1/1     1            1
kubectl get svc -n voting
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP                              PORT(S)
db           ClusterIP      10.43.34.195    <none>                                   5432/TCP
redis        ClusterIP      10.43.28.68     <none>                                   6379/TCP
result-app   LoadBalancer   10.43.206.178   172.16.77.10,172.16.77.11,172.16.77.12   5001:30223/TCP
voting-app   LoadBalancer   10.43.229.12    172.16.77.10,172.16.77.11,172.16.77.12   5000:31712/TCP

Access:

  • Voting app: http://172.16.77.10:5000
  • Results: http://172.16.77.10:5001



Service Type Summary

TypeAccessible fromUse case
ClusterIPInside cluster onlyInternal pod-to-pod — redis, db, internal APIs
NodePortNode IPs on high portDev/testing, on-prem without LB controller
LoadBalancerExternal IP/DNSProduction external traffic, user-facing apps

Deleting a Service

Delete just the service without touching the deployment:

kubectl delete svc voting-app -n voting

k3s automatically removes the svclb pods. Reapply when needed:

kubectl apply -f voting-app-service.yaml

(Visited 15 times, 1 visits today)

By Ash Thomas

Ash Thomas is a seasoned IT professional with extensive experience as a technical expert, complemented by a keen interest in blockchain technology.