Les Pods sont éphémères : ils peuvent être créés, détruits, et leurs adresses IP changent. Comment fournir un point d'accès stable à un ensemble de Pods fournissant une fonctionnalité donnée (ex: un serveur web) ? C'est le rôle des Services.
Un Service Kubernetes est une abstraction qui définit un ensemble logique de Pods (généralement déterminés par un selector
basé sur les labels) et une politique pour y accéder. Ils fournissent un moyen d'accéder à l'application sans tenir compte des Pods individuels (IPs, nombre...).
Les Services sont découplés des Pods via les Endpoints. Lorsqu'un Service est créé et qu'il cible des Pods via un selector
, Kubernetes crée aussi un objet Endpoints
(ou EndpointSlice
dans les versions plus récentes) qui contient la liste dynamique des adresses IP et ports des Pods qui correspondent au selector
du Service et qui sont prêts (Ready
).
Historiquement, pour chaque Service, Kubernetes créait un unique objet Endpoints
qui listait toutes les adresses IP et les ports des Pods correspondants.
EndpointSlice : Une Approche Plus Scalable
Avec l'augmentation de la taille des clusters et du nombre de Pods par Service, l'objet unique Endpoints
pouvait devenir très volumineux. Les mises à jour de cet unique objet devenaient coûteuses en termes de performance et de trafic réseau pour les composants (comme kube-proxy
) qui les surveillent.
Pour résoudre ce problème, l'objet EndpointSlice
a été introduit (et est activé par défaut dans les versions récentes de Kubernetes, généralement à partir de la 1.19+).
Les EndpointSlice
offrent une alternative plus scalable et extensible :
Endpoints
par Service, Kubernetes crée plusieurs objets EndpointSlice
. Chaque EndpointSlice
contient un sous-ensemble des endpoints du Service (par défaut, jusqu'à 100 endpoints par slice, mais cela est configurable).EndpointSlice
concernées doivent être mises à jour, et non un unique et potentiellement énorme objet Endpoints
.kube-proxy
.EndpointSlice
sont plus efficaces.Principales Différences entre Endpoints
et EndpointSlice
:
Caractéristique | Endpoints (Ancien) |
EndpointSlice (Nouveau/Préféré) |
---|---|---|
Nombre d'objets | Un seul objet par Service | Plusieurs objets par Service |
Taille max. | Peut devenir très grand, limitant la scalabilité | Chaque slice a une taille limitée (ex: 100 endpoints) |
Mises à jour | L'objet entier est mis à jour | Seules les slices affectées sont mises à jour |
Performance | Moins performant pour les services à grande échelle | Plus performant, surtout à grande échelle |
Support Topologie | Limité (via annotations) | Support natif et plus riche des hints de topologie |
Exemple Conceptuel :
Si un Service mon-service
a 500 Pods backend :
Endpoints
: Un seul objet Endpoints
nommé mon-service
contenant 500 adresses.EndpointSlice
: Cinq objets EndpointSlice
(par exemple, mon-service-abc1
, mon-service-def2
, etc.), chacun contenant 100 adresses. Si un Pod change, seule une de ces cinq slices est modifiée.Bien que kube-proxy
puisse toujours lire les objets Endpoints
pour la rétrocompatibilité, il privilégiera EndpointSlice
si disponible. En tant qu'utilisateur, vous interagirez le plus souvent avec l'objet Service
lui-même, et Kubernetes gère la création et la mise à jour des Endpoints
ou EndpointSlice
en arrière-plan.
Vous pouvez lister les EndpointSlices avec :
kubectl get endpointslice -n <namespace>
# ou pour un service spécifique (en utilisant les labels)
kubectl get endpointslice -l kubernetes.io/service-name=nom-du-service -n <namespace>
L'objet EndpointSlice
est géré par le EndpointSliceReconciler
au sein du kube-controller-manager
.
Le composant kube-proxy
, qui s'exécute sur chaque nœud, surveille l'API Server pour les modifications des Services et des Endpoints (ou EndpointSlices). Il utilise ces informations pour configurer des règles de réseau (par exemple, en utilisant iptables, IPVS) sur chaque nœud. Ces règles interceptent le trafic destiné à l'IP virtuelle du Service (ClusterIP) et le redirigent vers l'un des Pods backend sains.
Chaque Service a un spec.type
qui définit comment il est exposé. Il existe plusieurs types :
ClusterIP
(Défaut)NodePort
LoadBalancer
ExternalName
C'est le type de Service par défaut.
ClusterIP
).Exemple :
Pour tester, créons d'abord un déploiement Nginx :
# deployment-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-svc-example
spec:
replicas: 3
selector:
matchLabels:
app: svc-example
template:
metadata:
labels:
app: svc-example
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
Appliquez ce déploiement :
kubectl apply -f deployment-nginx.yaml
Maintenant, créons le Service de type ClusterIP
:
# service-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-clusterip
spec:
type: ClusterIP # Peut être omis car c'est le défaut
selector:
app: svc-example # Sélectionne les Pods avec le label app=svc-example
ports:
- protocol: TCP
port: 80 # Port exposé par le Service (sur sa ClusterIP)
targetPort: 80 # Port sur lequel les Pods écoutent (containerPort du Pod Nginx)
Appliquez le Service :
kubectl apply -f service-clusterip.yaml
Vérification :
$ kubectl get svc svc-clusterip
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
svc-clusterip ClusterIP 10.111.131.195 <none> 80/TCP 5s
$ kubectl get endpoints svc-clusterip
NAME ENDPOINTS AGE
svc-clusterip 192.168.126.54:80,192.168.126.55:80,192.168.194.121:80 15s
# Les IPs des Endpoints sont celles des Pods Nginx
Pour tester l'accès, depuis un autre Pod dans le cluster (par exemple, un Pod BusyBox) :
# Lancer un pod de test temporaire
kubectl run tmp-busybox --image=busybox:1.36 --rm -it -- sh
# Depuis le shell du pod busybox:
/ # wget -qO- svc-clusterip
# Ou avec l'IP du service: / # wget -qO- 10.111.131.195
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
/ # exit
NodePort
).ClusterIP
est automatiquement créé en interne. Le trafic arrivant sur le <IP-du-Noeud>:<NodePort>
est routé vers cette ClusterIP
, puis vers les Pods.NodePort
est choisi dans une plage configurée (par défaut 30000-32767). Si non spécifié, un port aléatoire dans cette plage est assigné.Exemple : (en réutilisant le déploiement deployment-svc-example
)
# service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-nodeport
spec:
type: NodePort
selector:
app: svc-example # Cible les mêmes Pods Nginx
ports:
- protocol: TCP
port: 80 # Port interne (sur la ClusterIP du service)
targetPort: 80 # Port des Pods Nginx
nodePort: 30080 # Port externe sur chaque nœud (doit être dans la plage autorisée)
# Si omis, un port aléatoire est choisi.
Appliquez le Service :
kubectl apply -f service-nodeport.yaml
Vérification :
$ kubectl get svc svc-nodeport
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
svc-nodeport NodePort 10.107.27.120 <none> 80:30080/TCP 64s
Vous pouvez maintenant accéder à Nginx via <IP-de-nimporte-quel-Noeud>:30080
depuis l'extérieur du cluster (assurez-vous que les pare-feux autorisent le trafic sur ce port).
NodePort
et ClusterIP
sont automatiquement créés en arrière-plan. Le fournisseur de cloud configure son load balancer externe pour router le trafic vers les NodePort
des nœuds du cluster.Exemple :
# service-loadbalancer.yaml
apiVersion: v1
kind: Service
metadata:
name: svc-loadbalancer
spec:
type: LoadBalancer
selector:
app: svc-example # Cible les Pods Nginx
ports:
- protocol: TCP
port: 80 # Port exposé par le Load Balancer externe
targetPort: 80 # Port des Pods Nginx
Appliquez le Service :
kubectl apply -f service-loadbalancer.yaml
Vérification :
$ kubectl get svc svc-loadbalancer
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
svc-loadbalancer LoadBalancer 10.98.100.20 35.23.xx.yy 80:3xxxx/TCP 2m
# L'EXTERNAL-IP peut prendre quelques minutes à apparaître.
Une fois l'EXTERNAL-IP
attribuée, le service Nginx est accessible via cette IP sur le port 80.
spec.externalName
) en retournant un enregistrement CNAME
au client DNS interne au cluster.ExternalName
sans reconfigurer les applications.Exemple :
# service-externalname.yaml
apiVersion: v1
kind: Service
metadata:
name: bdd-externe
namespace: production
spec:
type: ExternalName
externalName: ma-base-de-donnees.mon-fournisseur-cloud.com
Lorsqu'un Pod dans le namespace production
tente de résoudre bdd-externe
(ou bdd-externe.production.svc.cluster.local
), le DNS du cluster retournera un CNAME vers ma-base-de-donnees.mon-fournisseur-cloud.com
.
Comment un Pod découvre-t-il l'adresse IP d'un Service ? Kubernetes fournit un service DNS interne au cluster.
Ce DNS est généralement implémenté par CoreDNS (dans les installations récentes), s'exécutant sous forme de Pods dans le namespace kube-system
. Chaque Kubelet configure les Pods pour qu'ils utilisent ce service DNS interne pour la résolution de noms.
Kubernetes crée automatiquement des enregistrements DNS pour les Services :
A
(ou AAAA
pour IPv6) pour <nom-du-service>.<nom-du-namespace>.svc.cluster.local
. Cet enregistrement pointe vers l'adresse IP virtuelle (ClusterIP) du Service.
svc-clusterip.default.svc.cluster.local
<nom-du-service>
.
default
, svc-clusterip
résoudra l'IP du service.SRV
sont aussi créés pour les ports nommés du Service.C'est principalement via les enregistrements DNS des Services que les applications découvrent et communiquent avec d'autres applications dans le cluster. Utiliser le nom DNS d'un Service est préférable à utiliser directement l'IP d'un Pod (car les Pods sont éphémères et leurs IPs peuvent changer) ou même l'IP d'un Service (car le nom est plus stable et abstrait).
Vérification du DNS depuis un Pod :
# Lancer un pod de test temporaire
kubectl run dns-test --image=busybox:1.36 --rm -it -- sh
# Depuis le shell du pod busybox:
# Vérifier la résolution du service Kubernetes API (qui est aussi un service)
/ # nslookup kubernetes.default
Server: 10.96.0.10 # IP du service DNS (CoreDNS)
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: kubernetes.default
Address 1: 10.96.0.1 # IP du service kubernetes (API Server)
# Vérifier la résolution de notre service Nginx (si dans le namespace 'default')
/ # nslookup svc-clusterip
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: svc-clusterip
Address 1: 10.111.131.195 # L'IP de svc-clusterip
/ # exit
Alors que les Services de type NodePort
ou LoadBalancer
exposent directement un service sur un port ou une IP externe, ils fonctionnent principalement au niveau 4 (TCP/UDP). Pour une gestion plus fine du trafic HTTP/HTTPS (niveau 7), comme le routage basé sur l'hôte ou le chemin (path), la terminaison TLS (SSL), ou le load balancing basé sur HTTP, on utilise les Ingress.
Un objet Ingress définit des règles pour router le trafic externe (principalement HTTP/S) vers des Services internes au cluster.
Cependant, l'objet Ingress seul ne fait rien. Il faut un Ingress Controller qui tourne dans le cluster pour lire les objets Ingress et appliquer concrètement les règles de routage définies. L'Ingress Controller est généralement un Pod (ou un ensemble de Pods) qui exécute un reverse proxy / load balancer (ex: Nginx, Traefik, HAProxy, Contour) exposé à l'extérieur (souvent via un Service de type LoadBalancer
ou NodePort
). Plusieurs Ingress Controllers existent, et leur déploiement n'est pas inclus par défaut dans tous les clusters Kubernetes.
Exemple d'un objet Ingress simple :
Supposons que nous ayons deux Services (service-web
sur le port 80, service-api
sur le port 8080) que nous voulons exposer.
# ingress-example.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mon-ingress
annotations:
# Les annotations sont souvent utilisées pour configurer l'Ingress Controller spécifique
# Exemple pour Nginx Ingress Controller:
nginx.ingress.kubernetes.io/rewrite-target: /
# kubernetes.io/ingress.class: "nginx" # Ancienne méthode pour spécifier la classe
spec:
ingressClassName: nginx # Important de spécifier quel Ingress Controller utiliser (si plusieurs sont présents ou requis par le controller)
rules:
- host: monapp.example.com # Règle pour cet hôte
http:
paths:
- path: /web # Trafic pour /web
pathType: Prefix # Type de correspondance (Prefix ou Exact)
backend:
service:
name: service-web # Route vers ce Service
port:
number: 80 # Sur ce port du Service
- path: /api
pathType: Prefix
backend:
service:
name: service-api
port:
number: 8080
# On peut aussi définir un backend par défaut si aucune règle ne correspond
defaultBackend:
service:
name: service-default-page # Un service qui sert une page 404 ou d'accueil
port:
number: 80
Dans cet exemple :
monapp.example.com/web/*
sera routé vers service-web
sur le port 80.monapp.example.com/api/*
sera routé vers service-api
sur le port 8080.monapp.example.com
(ou sans Host correspondant si l'Ingress Controller le permet) ira vers service-default-page
.Terminaison TLS :
L'Ingress peut aussi gérer la terminaison TLS (HTTPS). Il faut pour cela :
kubernetes.io/tls
contenant le certificat TLS (tls.crt
) et la clé privée (tls.key
).kubectl create secret tls mon-tls-secret --cert=path/to/tls.crt --key=path/to/tls.key
# ingress-tls-example.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mon-ingress-tls
spec:
ingressClassName: nginx
tls:
- hosts:
- monapp.secure.example.com
secretName: mon-tls-secret # Nom du Secret Kubernetes contenant le cert/clé
rules:
- host: monapp.secure.example.com
http:
paths:
- path: /secure-data
pathType: Prefix
backend:
service:
name: service-secure-api
port:
number: 443 # Le service backend peut écouter sur HTTP ou HTTPS
L'Ingress Controller (ex: Nginx) utilisera alors le certificat de mon-tls-secret
pour servir le trafic HTTPS pour monapp.secure.example.com
, puis communiquera avec le backend service-secure-api
(potentiellement en HTTP clair, sauf configuration contraire).
Named Port dans l'Ingress Backend :
Si votre Service définit des ports nommés, vous pouvez les utiliser dans la configuration backend de l'Ingress.
Service avec port nommé :
apiVersion: v1
kind: Service
metadata:
name: svc-app-avec-port-nomme
spec:
selector:
app: mon-app
ports:
- name: http-admin # Nom du port
protocol: TCP
port: 8080
targetPort: 80
Ingress utilisant le port nommé :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-avec-port-nomme
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /admin-stuff
pathType: Prefix
backend:
service:
name: svc-app-avec-port-nomme
port:
name: http-admin # Utilise le nom du port ici
Ceci est utile si le numéro de port du service change, tant que le nom reste le même, l'Ingress n'a pas besoin d'être mis à jour.