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.
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:
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.
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.
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.
apiVersion: v1
kind: Service
metadata:
name: demo-clusterip
spec:
type: ClusterIP
selector:
app: demo-back
ports:
- port: 80
targetPort: 5000Characteristics:
EXTERNAL-IP).http://demo-clusterip (see §13).When to use it: for everything that stays in the cluster. It is the most common type.
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).
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 nodeAccess: http://<any-node-ip>:30082 (with Docker Desktop: http://localhost:30082).
Important points:
nodePort, Kubernetes picks one in the range.Does everything NodePort does, plus: it asks the infrastructure (the cloud) to provision an external load balancer with a public IP.
apiVersion: v1
kind: Service
metadata:
name: demo-lb
spec:
type: LoadBalancer
selector:
app: demo-back
ports:
- port: 8090
targetPort: 5000Depending on the environment:
| Environment | Behavior |
|---|---|
| AWS / GCP / Azure | Creates a real managed LB (ELB/NLB, GCP LB…) and fills EXTERNAL-IP |
| Docker Desktop | EXTERNAL-IP = localhost → http://localhost:8090 |
| minikube | minikube tunnel provides the external IP |
| kind / bare-metal | Stays <pending> without a controller such as MetalLB |
Full chain: LoadBalancer → NodePort → ClusterIP → Endpoints → Pods.
Annotations (provider-specific) drive the LB, e.g. on AWS:
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).
Special case: no selector, no Pod, no IP. It simply creates a DNS alias (CNAME record) to an external name.
apiVersion: v1
kind: Service
metadata:
name: base-externe
spec:
type: ExternalName
externalName: db.exemple.com # Pods that call "base-externe" are redirected hereUse: 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.
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.
apiVersion: v1
kind: Service
metadata:
name: demo-headless
spec:
clusterIP: None # <-- headless
selector:
app: demo-back
ports:
- port: 80
targetPort: 5000What it is for:
pod-0.demo-headless, pod-1.demo-headless…), useful for replicated databases (Cassandra, Kafka, etc.).| Normal ClusterIP | Headless (clusterIP: None) | |
|---|---|---|
| Virtual IP | Yes (one) | No |
| DNS answer | 1 IP (the Service’s) | N IPs (the Pods’) |
| Distribution | By kube-proxy | The client’s job |
| Use case | Stateless web/API | Replicated databases, StatefulSet |
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.
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: 8080Difference with ExternalName: here we route by IP (with possible load balancing over several IPs), not by DNS CNAME.
This is the source of confusion. Three different ports, three roles:
| Field | Where | Meaning |
|---|---|---|
port | On the Service | The port clients use to reach the Service |
targetPort | On the container | The port where the application actually listens in the Pod |
nodePort | On 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”.
targetPortcan reference a port name defined on the container (see §10), which avoids hard-coding the number.
A Service can expose several ports (e.g. HTTP + metrics). In that case, each entry must have a name.
spec:
selector:
app: demo
ports:
- name: http
port: 80
targetPort: web # references a NAMED container port
- name: metrics
port: 9090
targetPort: 9090On the container side, we name the ports:
containers:
- name: app
ports:
- name: web # <-- reused by targetPort: web
containerPort: 5000
- name: metrics
containerPort: 9090Advantage of named ports: if the container port changes, you have nothing to change in the Service.
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:
| Mode | Principle | Notes |
|---|---|---|
| iptables (default) | iptables rules, random Pod selection | Simple, robust, very widespread |
| IPVS | Kernel hash table, real LB algorithms (rr, lc, sh…) | More performant on large clusters |
| nftables | Successor of iptables | More recent |
Practical consequences:
The Service ↔ Pods link is materialized by objects:
IP:port of ready Pods.kubectl get endpoints demo-clusterip
kubectl get endpointslices -l kubernetes.io/service-name=demo-clusteripWho 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: truealso publishes not-ready Pods — specific headless usage.)
Kubernetes runs CoreDNS. Each Service receives a deterministic DNS name:
<service> # same namespace
<service>.<namespace> # other namespace
<service>.<namespace>.svc.cluster.local # full FQDNExample from a Pod:
curl http://demo-clusterip # same namespace
curl http://demo-clusterip.default # explicit
curl http://demo-clusterip.default.svc.cluster.localRecords produced:
_http._tcp.demo-clusterip….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).
| Value | Effect | Trade-off |
|---|---|---|
| Cluster (default) | Traffic can be redirected to another node to reach a Pod | Good distribution, but the client source IP is masked (SNAT) and one extra network hop |
| Local | Serves only Pods on the node that receives the packet | Preserves the client source IP, no hop; but imbalance if Pods are poorly distributed |
| Value | Effect |
|---|---|
| Cluster (default) | Routes to any Pod of the Service |
| Local | Routes only to Pods on the same node (useful for latency / locality) |
externalTrafficPolicy: Localis the key setting when you need to know the real client IP (logs, security, geolocation).
By default, each request can go to any Pod. To “stick” a client to the same Pod:
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800 # 3 hNone (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).
protocol: TCP (default), UDP (DNS, games, streaming), SCTP (telecom).appProtocol (indicative) specifies the application protocol (http, https, grpc) for tools/LBs.ports:
- name: dns-udp
port: 53
protocol: UDP
targetPort: 53
- name: dns-tcp
port: 53
protocol: TCP
targetPort: 53A Service works at L4 (IP/port). It does not know how to route by URL, by hostname, or handle TLS. For that:
| Object | Layer | Role |
|---|---|---|
| Service (ClusterIP/NodePort/LB) | L3/L4 | Stable address + simple LB to Pods |
| Ingress | L7 (HTTP/HTTPS) | Routing by host and path, TLS, a single entry point for several services |
| Gateway API | L7 (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.
| Type | Internal IP | External access | DNS | Selector | Use case |
|---|---|---|---|---|---|
| ClusterIP | Yes | No | 1 A (ClusterIP) | Yes | Internal communication (the most common) |
| NodePort | Yes | Node port | 1 A | Yes | Dev/local, brick of an LB |
| LoadBalancer | Yes | Public IP | 1 A | Yes | Public cloud service |
| ExternalName | No | — (CNAME) | CNAME | No | Alias to an external service |
Headless (clusterIP: None) | No | No | N A (Pods) | Yes | StatefulSet, replicated databases |
| Without selector | Yes | depends on type | 1 A | No | Manual Endpoints (external resource) |
| Symptom | Frequent cause | Solution |
|---|---|---|
| Service does not answer | Selector ≠ labels of the Pods | Align spec.selector and the Pod template labels |
| Empty Endpoints | No Ready Pod or no matching Pod | kubectl get endpoints <svc> ; check readinessProbe and labels |
| Connection refused internally | Wrong targetPort | targetPort = real container port |
EXTERNAL-IP stays <pending> | No LB controller (kind/bare-metal) | Docker Desktop OK ; otherwise MetalLB / port-forward |
| Client source IP masked | externalTrafficPolicy: Cluster | Switch to Local |
| NodePort unreachable | Port out of range / busy | Use 30000–32767, change nodePort |
| DNS does not resolve | Wrong namespace / CoreDNS down | Test the FQDN ; kubectl -n kube-system get pods (coredns) |
Diagnostic commands:
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>targetPort by name → decoupling).app, tier, version).externalTrafficPolicy: Local when the real client IP matters.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.
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.
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.
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.
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