Kubernetes Service Types — Comprehensive Guide

13 min

Project projet11-kubernetes-services · In-depth reference document.

This document goes much further than the solution: it details all Service types, the internal notions (kube-proxy, Endpoints, EndpointSlices, DNS), the important YAML fields, traffic policies, session affinity, multi-port, classic pitfalls, and good practices.

Table of contents

  1. Reminder: the role of a Service
  2. Anatomy of a Service (all fields)
  3. Type 1 — ClusterIP
  4. Type 2 — NodePort
  5. Type 3 — LoadBalancer
  6. Type 4 — ExternalName
  7. Headless Service (no ClusterIP)
  8. Service without a selector (manual Endpoints)
  9. port vs targetPort vs nodePort
  10. Multi-port and named ports
  11. How it works under the hood: kube-proxy
  12. Endpoints and EndpointSlices
  13. Service DNS (CoreDNS)
  14. Traffic policies (externalTrafficPolicy / internalTrafficPolicy)
  15. Session affinity
  16. Protocols: TCP, UDP, SCTP, appProtocol
  17. Service vs Ingress vs Gateway API
  18. Types recap table
  19. Classic pitfalls and troubleshooting
  20. Good practices
  21. Mini-exercises

1. Reminder: the role of a Service

A Pod is ephemeral: it can be recreated at any time, with a new IP. You therefore cannot rely on a Pod IP to communicate.

A Service is a stable abstraction that:

  • provides a permanent network identity (a virtual IP and/or a DNS name);
  • selects a set of Pods via their labels;
  • distributes traffic (load balancing) across those Pods;
  • updates automatically when Pods appear or disappear.

Core idea: the Service does not “contain” the Pods. It finds them continuously thanks to the label selector, and maintains the list of their addresses in the Endpoints.


2. Anatomy of a Service (all fields)

yaml
apiVersion: v1
kind: Service
metadata:
  name: mon-service
  labels:
    app: demo
  annotations: {}               # metadata (often used by cloud LoadBalancers)
spec:
  type: ClusterIP               # ClusterIP | NodePort | LoadBalancer | ExternalName
  selector:                     # which Pods this Service targets (by labels)
    app: demo
  ports:
    - name: http                # port name (useful if several ports)
      protocol: TCP             # TCP (default) | UDP | SCTP
      port: 80                  # Service port (what clients see)
      targetPort: 5000          # container port (or container port name)
      nodePort: 30080           # (NodePort/LoadBalancer) port opened on the node
  clusterIP: 10.96.0.10         # (optional) fixed IP ; "None" = headless
  sessionAffinity: None         # None | ClientIP
  externalTrafficPolicy: Cluster  # Cluster | Local (NodePort/LoadBalancer)
  internalTrafficPolicy: Cluster  # Cluster | Local
  ipFamilyPolicy: SingleStack   # SingleStack | PreferDualStack | RequireDualStack
  externalIPs: []               # external IPs routed to this Service (advanced)

Each field is detailed below. You can create a minimal Service in 8 lines; all other fields have reasonable default values.


3. Type 1 — ClusterIP

The default type. Assigns an internal virtual IP (in the Service CIDR range, e.g. 10.96.0.0/12), reachable only from inside the cluster.

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-clusterip
spec:
  type: ClusterIP
  selector:
    app: demo-back
  ports:
    - port: 80
      targetPort: 5000

Characteristics:

  • Unreachable from the outside (no EXTERNAL-IP).
  • Basis of internal communication (frontend → backend, app → database, microservices among themselves).
  • Reachable by DNS name: http://demo-clusterip (see §13).

When to use it: for everything that stays in the cluster. It is the most common type.


4. Type 2 — NodePort

Does everything ClusterIP does (it gets an internal IP), plus: it opens a static port on every node of the cluster (default range 30000–32767).

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-nodeport
spec:
  type: NodePort
  selector:
    app: demo-back
  ports:
    - port: 80          # Service port (internal)
      targetPort: 5000  # container port
      nodePort: 30082   # port opened on EVERY node

Access: http://<any-node-ip>:30082 (with Docker Desktop: http://localhost:30082).

Important points:

  • If you do not specify nodePort, Kubernetes picks one in the range.
  • The same port is opened on all nodes (thanks to the routing mesh / kube-proxy), even those that host no Pod of the Service.
  • Not elegant for production (non-standard ports, manual management), but perfect in dev/local and often the brick under a LoadBalancer.

5. Type 3 — LoadBalancer

Does everything NodePort does, plus: it asks the infrastructure (the cloud) to provision an external load balancer with a public IP.

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-lb
spec:
  type: LoadBalancer
  selector:
    app: demo-back
  ports:
    - port: 8090
      targetPort: 5000

Depending on the environment:

EnvironmentBehavior
AWS / GCP / AzureCreates a real managed LB (ELB/NLB, GCP LB…) and fills EXTERNAL-IP
Docker DesktopEXTERNAL-IP = localhosthttp://localhost:8090
minikubeminikube tunnel provides the external IP
kind / bare-metalStays <pending> without a controller such as MetalLB

Full chain: LoadBalancer → NodePort → ClusterIP → Endpoints → Pods.

Annotations (provider-specific) drive the LB, e.g. on AWS:

yaml
metadata:
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
    service.beta.kubernetes.io/aws-load-balancer-internal: "true"

One LoadBalancer per service is expensive in the cloud. In production, we often prefer a single entry point (Ingress/Gateway) in front of several services (see §17).


6. Type 4 — ExternalName

Special case: no selector, no Pod, no IP. It simply creates a DNS alias (CNAME record) to an external name.

yaml
apiVersion: v1
kind: Service
metadata:
  name: base-externe
spec:
  type: ExternalName
  externalName: db.exemple.com     # Pods that call "base-externe" are redirected here

Use: point a stable internal name (base-externe) to a service outside the cluster (a managed database, a third-party API). If the address changes, you change one place.

Limit: this is pure DNS, with no load balancing or port control. Not suitable if the external service expects a particular HTTP Host.


7. Headless Service (no ClusterIP)

By setting clusterIP: None, you get a Service without a virtual IP. DNS then returns directly the IPs of all Pods (a list of A records), instead of a single IP.

yaml
apiVersion: v1
kind: Service
metadata:
  name: demo-headless
spec:
  clusterIP: None        # <-- headless
  selector:
    app: demo-back
  ports:
    - port: 80
      targetPort: 5000

What it is for:

  • When the client wants to see each Pod individually (no centralized LB).
  • Indispensable for StatefulSets: each Pod gets a stable DNS name (pod-0.demo-headless, pod-1.demo-headless…), useful for replicated databases (Cassandra, Kafka, etc.).
Normal ClusterIPHeadless (clusterIP: None)
Virtual IPYes (one)No
DNS answer1 IP (the Service’s)N IPs (the Pods’)
DistributionBy kube-proxyThe client’s job
Use caseStateless web/APIReplicated databases, StatefulSet

8. Service without a selector (manual Endpoints)

A Service can have no selector. In that case, Kubernetes does not fill the Endpoints by itself: you define them by hand. Handy to expose an external resource under a stable internal IP.

yaml
apiVersion: v1
kind: Service
metadata:
  name: api-legacy
spec:
  ports:
    - port: 80
      targetPort: 8080
---
apiVersion: v1
kind: Endpoints           # (or EndpointSlice, more modern)
metadata:
  name: api-legacy         # SAME name as the Service
subsets:
  - addresses:
      - ip: 192.168.1.50   # external server
    ports:
      - port: 8080

Difference with ExternalName: here we route by IP (with possible load balancing over several IPs), not by DNS CNAME.


9. port vs targetPort vs nodePort

This is the source of confusion. Three different ports, three roles:

FieldWhereMeaning
portOn the ServiceThe port clients use to reach the Service
targetPortOn the containerThe port where the application actually listens in the Pod
nodePortOn the node(NodePort/LoadBalancer) the port opened on the machine

Example read aloud: “clients hit port 80 of the Service, which forwards to port 5000 of the container; in NodePort, you also enter through port 30082 of the machine”.

targetPort can reference a port name defined on the container (see §10), which avoids hard-coding the number.


10. Multi-port and named ports

A Service can expose several ports (e.g. HTTP + metrics). In that case, each entry must have a name.

yaml
spec:
  selector:
    app: demo
  ports:
    - name: http
      port: 80
      targetPort: web          # references a NAMED container port
    - name: metrics
      port: 9090
      targetPort: 9090

On the container side, we name the ports:

yaml
containers:
  - name: app
    ports:
      - name: web              # <-- reused by targetPort: web
        containerPort: 5000
      - name: metrics
        containerPort: 9090

Advantage of named ports: if the container port changes, you have nothing to change in the Service.


11. How it works under the hood: kube-proxy

The Service is an abstract object: it is not a process that receives traffic. The magic is done by kube-proxy, a component present on every node, which programs the kernel network rules to redirect “Service IP:port” to “Pod IP:port”.

kube-proxy modes:

ModePrincipleNotes
iptables (default)iptables rules, random Pod selectionSimple, robust, very widespread
IPVSKernel hash table, real LB algorithms (rr, lc, sh…)More performant on large clusters
nftablesSuccessor of iptablesMore recent

Practical consequences:

  • iptables distribution is random (not a true ordered round-robin).
  • kube-proxy does not see the HTTP layer: it is L3/L4 (IP/port). For HTTP routing (by path, by host), you need an Ingress (§17).

12. Endpoints and EndpointSlices

The Service ↔ Pods link is materialized by objects:

  • Endpoints (historical): a single object listing all IP:port of ready Pods.
  • EndpointSlices (modern, recommended): the list is split into slices (max ~100 endpoints each) → much better scalability on large services.
bash
kubectl get endpoints demo-clusterip
kubectl get endpointslices -l kubernetes.io/service-name=demo-clusterip

Who updates the list? The endpoint controller: as soon as a Pod becomes Ready (readinessProbe OK) and matches the selector, its IP enters; if it falls, it leaves.

A not Ready Pod is removed from the Endpoints → it receives no traffic. That is why the readinessProbe is essential: it controls who is “in” the Service. (Exception: publishNotReadyAddresses: true also publishes not-ready Pods — specific headless usage.)


13. Service DNS (CoreDNS)

Kubernetes runs CoreDNS. Each Service receives a deterministic DNS name:

<service>                                   # same namespace
<service>.<namespace>                        # other namespace
<service>.<namespace>.svc.cluster.local      # full FQDN

Example from a Pod:

bash
curl http://demo-clusterip                       # same namespace
curl http://demo-clusterip.default               # explicit
curl http://demo-clusterip.default.svc.cluster.local

Records produced:

  • Normal Service → one A record to the ClusterIP.
  • Headless Service → several A records, one per Pod.
  • Named ports → SRV records: _http._tcp.demo-clusterip….
  • ExternalNameCNAME to the target.

Historically, Kubernetes also injected environment variables (DEMO_CLUSTERIP_SERVICE_HOST, ..._PORT) into Pods created after the Service. DNS remains the recommended method (works regardless of creation order).


14. Traffic policies

externalTrafficPolicy (incoming external traffic, NodePort/LoadBalancer)

ValueEffectTrade-off
Cluster (default)Traffic can be redirected to another node to reach a PodGood distribution, but the client source IP is masked (SNAT) and one extra network hop
LocalServes only Pods on the node that receives the packetPreserves the client source IP, no hop; but imbalance if Pods are poorly distributed

internalTrafficPolicy (internal traffic, between Pods)

ValueEffect
Cluster (default)Routes to any Pod of the Service
LocalRoutes only to Pods on the same node (useful for latency / locality)

externalTrafficPolicy: Local is the key setting when you need to know the real client IP (logs, security, geolocation).


15. Session affinity

By default, each request can go to any Pod. To “stick” a client to the same Pod:

yaml
spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800     # 3 h
  • None (default): distribution on every request.
  • ClientIP: all requests from the same IP go to the same Pod (basic L4 sticky sessions).

For finer HTTP sessions (by cookie), we rather use an Ingress (L7).


16. Protocols: TCP, UDP, SCTP, appProtocol

  • protocol: TCP (default), UDP (DNS, games, streaming), SCTP (telecom).
  • You can mix several protocols on the same Service (distinct ports).
  • appProtocol (indicative) specifies the application protocol (http, https, grpc) for tools/LBs.
yaml
ports:
  - name: dns-udp
    port: 53
    protocol: UDP
    targetPort: 53
  - name: dns-tcp
    port: 53
    protocol: TCP
    targetPort: 53

17. Service vs Ingress vs Gateway API

A Service works at L4 (IP/port). It does not know how to route by URL, by hostname, or handle TLS. For that:

ObjectLayerRole
Service (ClusterIP/NodePort/LB)L3/L4Stable address + simple LB to Pods
IngressL7 (HTTP/HTTPS)Routing by host and path, TLS, a single entry point for several services
Gateway APIL7 (successor of Ingress)More expressive, role separation, multi-protocol

Typical production model: a single LoadBalancer → Ingress → several internal ClusterIP. You save expensive LBs and centralize TLS/routing.


18. Types recap table

TypeInternal IPExternal accessDNSSelectorUse case
ClusterIPYesNo1 A (ClusterIP)YesInternal communication (the most common)
NodePortYesNode port1 AYesDev/local, brick of an LB
LoadBalancerYesPublic IP1 AYesPublic cloud service
ExternalNameNo— (CNAME)CNAMENoAlias to an external service
Headless (clusterIP: None)NoNoN A (Pods)YesStatefulSet, replicated databases
Without selectorYesdepends on type1 ANoManual Endpoints (external resource)

19. Classic pitfalls and troubleshooting

SymptomFrequent causeSolution
Service does not answerSelector ≠ labels of the PodsAlign spec.selector and the Pod template labels
Empty EndpointsNo Ready Pod or no matching Podkubectl get endpoints <svc> ; check readinessProbe and labels
Connection refused internallyWrong targetPorttargetPort = real container port
EXTERNAL-IP stays <pending>No LB controller (kind/bare-metal)Docker Desktop OK ; otherwise MetalLB / port-forward
Client source IP maskedexternalTrafficPolicy: ClusterSwitch to Local
NodePort unreachablePort out of range / busyUse 30000–32767, change nodePort
DNS does not resolveWrong namespace / CoreDNS downTest the FQDN ; kubectl -n kube-system get pods (coredns)

Diagnostic commands:

bash
kubectl get svc <nom> -o wide
kubectl describe svc <nom>
kubectl get endpoints <nom>
kubectl get endpointslices -l kubernetes.io/service-name=<nom>
kubectl run test --rm -it --image=busybox:1.36 -- sh   # nslookup <svc>, wget -qO- http://<svc>

20. Good practices

  • By default, ClusterIP. Expose outside only what must be.
  • A single LoadBalancer + Ingress in front of several ClusterIP (cost + centralized TLS).
  • Name your ports (multi-port, and targetPort by name → decoupling).
  • Take care of readinessProbes: they decide who is in the Endpoints.
  • Label/selector consistency: that is error #1. Keep stable labels (app, tier, version).
  • Use externalTrafficPolicy: Local when the real client IP matters.
  • Prefer EndpointSlices (enabled by default on recent versions) for scalability.
  • Never hard-code a Pod IP: use the Service DNS name.

21. Mini-exercises

Exercise 1 — Turn a NodePort into a ClusterIP

Remove type: NodePort (or set ClusterIP), re-apply, then prove it is no longer reachable from the host but is reachable by name from a Pod (curl http://demo-nodeport... renamed). Observe kubectl get svc: no more nodePort column.

Exercise 2 — Break then repair the Endpoints

Change the Service selector to app: inexistant, re-apply, and see kubectl get endpoints empty + service unreachable. Put app: demo-back back: the Endpoints come back.

Exercise 3 — Headless Service

Create a Service with clusterIP: None, then from a Pod: nslookup demo-headless. You must see several IPs (one per Pod) instead of a single one.

Exercise 4 — Multi-port

Add a named metrics port (9090) on the container and the Service. Check with kubectl describe svc that both ports appear, and that targetPort really references the port name.

Exercise 5 — ExternalName

Create an ExternalName Service to example.com. From a Pod: nslookup mon-alias must return a CNAME to example.com.


Back to the project solution · Basic concepts: 01-CONCEPTS-SERVICES.md · Commands: 02-COMMANDES.md.


Course created by Dr. Haythem REHOUMA — Development and Deployment of Data Solutions