Remarque perev. : L'auteur de l'article, Erkan Erol, un ingénieur de SAP, partage son étude des mécanismes de fonctionnement de la commande kubectl exec
, si familière à tous ceux qui travaillent avec Kubernetes. Il accompagne l'ensemble de l'algorithme avec des listes de code source Kubernetes (et des projets associés), qui vous permettent de comprendre le sujet aussi profondément que nécessaire.
Un vendredi, un collègue est venu me voir et m'a demandé comment exécuter une commande dans le pod à l'aide de
client-go . Je n'ai pas pu lui répondre et j'ai soudain réalisé que je ne savais rien du mécanisme de travail des
kubectl exec
de
kubectl exec
. Oui, j'avais certaines idées sur son appareil, mais je n'étais pas sûr à 100% de leur exactitude et j'ai donc décidé de résoudre ce problème. Après avoir étudié les blogs, la documentation et le code source, j'ai appris beaucoup de nouvelles choses et dans cet article, je veux partager mes découvertes et ma compréhension. Si quelque chose ne va pas, veuillez me contacter sur
Twitter .
La préparation
Pour créer un cluster sur un MacBook, j'ai
cloné ecomm-integration-ballerina / kubernetes-cluster . Il a ensuite corrigé les adresses IP des nœuds dans la configuration de kubelet, car les paramètres par défaut ne permettaient pas à
kubectl exec
d'être
kubectl exec
. Vous pouvez en savoir plus sur la raison principale de cela
ici .
- Toute voiture = mon MacBook
- IP maître = 192.168.205.10
- IP hôte de travail = 192.168.205.11
- Port du serveur API = 6443
Composants

- Processus kubectl exec : lorsque nous exécutons «kubectl exec ...», le processus démarre. Vous pouvez le faire sur n'importe quelle machine ayant accès au serveur API K8s. Remarque trans.: Plus loin dans les listes de consoles, l'auteur utilise le commentaire "n'importe quelle machine", ce qui implique que les commandes suivantes peuvent être exécutées sur toutes ces machines avec accès à Kubernetes.
- serveur api : un composant sur le maître qui donne accès à l'API Kubernetes. Ceci est l'interface du plan de contrôle dans Kubernetes.
- kubelet : un agent qui s'exécute sur chaque nœud du cluster. Il fournit des conteneurs en pod'e.
- runtime conteneur ( runtime conteneur ): logiciel responsable du fonctionnement des conteneurs. Exemples: Docker, CRI-O, containerd ...
- noyau : noyau du système d'exploitation sur le nœud de travail; responsable de la gestion des processus.
- conteneur cible : conteneur qui fait partie d'un pod et fonctionne sur l'un des nœuds de travail.
Ce que j'ai découvert
1. Activité côté client
Créez un pod dans l'espace de noms
default
:
// any machine $ kubectl run exec-test-nginx --image=nginx
Ensuite, nous exécutons la commande exec et attendons 5000 secondes pour d'autres observations:
// any machine $ kubectl exec -it exec-test-nginx-6558988d5-fgxgg -- sh
Le processus kubectl apparaît (avec pid = 8507 dans notre cas):
// any machine $ ps -ef |grep kubectl 501 8507 8409 0 7:19PM ttys000 0:00.13 kubectl exec -it exec-test-nginx-6558988d5-fgxgg -- sh
Si nous vérifions l'activité réseau du processus, nous constatons qu'il a des connexions avec le serveur api (192.168.205.10.6443):
// any machine $ netstat -atnv |grep 8507 tcp4 0 0 192.168.205.1.51673 192.168.205.10.6443 ESTABLISHED 131072 131768 8507 0 0x0102 0x00000020 tcp4 0 0 192.168.205.1.51672 192.168.205.10.6443 ESTABLISHED 131072 131768 8507 0 0x0102 0x00000028
Regardons le code. Kubectl crée une requête POST avec la sous-ressource exec et envoie une requête REST:
req := restClient.Post(). Resource("pods"). Name(pod.Name). Namespace(pod.Namespace). SubResource("exec") req.VersionedParams(&corev1.PodExecOptions{ Container: containerName, Command: p.Command, Stdin: p.Stdin, Stdout: p.Out != nil, Stderr: p.ErrOut != nil, TTY: t.Raw, }, scheme.ParameterCodec) return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)
( kubectl / pkg / cmd / exec / exec.go )
2. Activité sur le côté du nœud maître
On peut également observer la requête côté serveur api:
handler.go:143] kube-apiserver: POST "/api/v1/namespaces/default/pods/exec-test-nginx-6558988d5-fgxgg/exec" satisfied by gorestful with webservice /api/v1 upgradeaware.go:261] Connecting to backend proxy (intercepting redirects) https://192.168.205.11:10250/exec/default/exec-test-nginx-6558988d5-fgxgg/exec-test-nginx?command=sh&input=1&output=1&tty=1 Headers: map[Connection:[Upgrade] Content-Length:[0] Upgrade:[SPDY/3.1] User-Agent:[kubectl/v1.12.10 (darwin/amd64) kubernetes/e3c1340] X-Forwarded-For:[192.168.205.1] X-Stream-Protocol-Version:[v4.channel.k8s.io v3.channel.k8s.io v2.channel.k8s.io channel.k8s.io]]
Notez que la demande HTTP inclut une demande de changement de protocole. SPDY vous permet de multiplexer des flux d'erreur stdin / stdout / stderr / spdy individuels via une seule connexion TCP.Le serveur API reçoit la demande et la convertit en
PodExecOptions
:
( pkg / apis / core / types.go )Pour effectuer les actions requises, api-server doit savoir quel pod il doit contacter:
( pkg / registry / core / pod / strategy.go )Bien sûr, les données des points d'extrémité sont extraites des informations de l'hôte:
nodeName := types.NodeName(pod.Spec.NodeName) if len(nodeName) == 0 {
( pkg / registry / core / pod / strategy.go )Hourra! Kubelet dispose désormais d'un port (
node.Status.DaemonEndpoints.KubeletEndpoint.Port
) auquel le serveur API peut se connecter:
( pkg / kubelet / client / kubelet_client.go )De la documentation de Communication Master-Node> Master to Cluster> apiserver to kubelet :
Ces connexions sont fermées sur le point de terminaison HTTPS de kubelet. Par défaut, apiserver ne vérifie pas le certificat du kubelet, ce qui rend la connexion vulnérable aux «attaques intermédiaires» (MITM) et peu sûre pour travailler sur des réseaux non fiables et / ou publics.
Maintenant, le serveur API connaît le point final et établit une connexion:
( pkg / registry / core / pod / rest / subresources.go )Voyons ce qui se passe sur le nœud maître.
Nous découvrons d'abord l'IP du nœud de travail. Dans notre cas, c'est 192.168.205.11:
// any machine $ kubectl get nodes k8s-node-1 -o wide NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME k8s-node-1 Ready <none> 9h v1.15.3 192.168.205.11 <none> Ubuntu 16.04.6 LTS 4.4.0-159-generic docker://17.3.3
Installez ensuite le port kubelet (10250 dans notre cas):
// any machine $ kubectl get nodes k8s-node-1 -o jsonpath='{.status.daemonEndpoints.kubeletEndpoint}' map[Port:10250]
Il est maintenant temps de vérifier le réseau. Y a-t-il une connexion au nœud de travail (192.168.205.11)? C'est là! Si vous tuez le processus d'
exec
, il disparaîtra, donc je sais que la connexion a été établie par le serveur api à la suite de la commande exécutée.
// master node $ netstat -atn |grep 192.168.205.11 tcp 0 0 192.168.205.10:37870 192.168.205.11:10250 ESTABLISHED …

La connexion entre kubectl et le serveur api est toujours ouverte. De plus, il existe une autre connexion reliant api-server et kubelet.
3. Activité sur le nœud de travail
Maintenant, connectons-nous au nœud de travail et voyons ce qui s'y passe.
Tout d'abord, nous voyons que la connexion avec elle est également établie (deuxième ligne); 192.168.205.10 est l'IP du nœud maître:
// worker node $ netstat -atn |grep 10250 tcp6 0 0 :::10250 :::* LISTEN tcp6 0 0 192.168.205.11:10250 192.168.205.10:37870 ESTABLISHED
Et notre équipe de
sleep
? Hourra, elle est aussi présente!
// worker node $ ps -afx ... 31463 ? Sl 0:00 \_ docker-containerd-shim 7d974065bbb3107074ce31c51f5ef40aea8dcd535ae11a7b8f2dd180b8ed583a /var/run/docker/libcontainerd/7d974065bbb3107074ce31c51 31478 pts/0 Ss 0:00 \_ sh 31485 pts/0 S+ 0:00 \_ sleep 5000 …
Mais attendez: comment le kubelet a-t-il démarré cela? Il y a un démon dans kubelet qui permet d'accéder à l'API via le port pour les requêtes du serveur api:
( pkg / kubelet / server / streaming / server.go )Kubelet calcule le point de terminaison de la réponse pour les demandes d'exécution:
func (s *server) GetExec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) { if err := validateExecRequest(req); err != nil { return nil, err } token, err := s.cache.Insert(req) if err != nil { return nil, err } return &runtimeapi.ExecResponse{ Url: s.buildURL("exec", token), }, nil }
( pkg / kubelet / server / streaming / server.go )Ne confondez pas. Il ne renvoie pas le résultat de la commande, mais le point de terminaison de la communication:
type ExecResponse struct {
( cri-api / pkg / apis / runtime / v1alpha2 / api.pb.go )Kubelet implémente l'interface
RuntimeServiceClient
, qui fait partie de l'interface Container Runtime
(nous avons écrit plus à ce sujet, par exemple, ici - environ Transl.) :
Liste longue de cri-api à kubernetes / kubernetes Il utilise simplement gRPC pour appeler une méthode via l'interface Container Runtime:
type runtimeServiceClient struct { cc *grpc.ClientConn }
( cri-api / pkg / apis / runtime / v1alpha2 / api.pb.go ) func (c *runtimeServiceClient) Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error) { out := new(ExecResponse) err := c.cc.Invoke(ctx, "/runtime.v1alpha2.RuntimeService/Exec", in, out, opts...) if err != nil { return nil, err } return out, nil }
( cri-api / pkg / apis / runtime / v1alpha2 / api.pb.go )Le Container Runtime est responsable de l'implémentation du
RuntimeServiceServer
:
Liste longue de cri-api à kubernetes / kubernetes 
Si c'est le cas, nous devrions voir une connexion entre le kubelet et le runtime du conteneur, non? Voyons ça.
Exécutez cette commande avant et après la commande exec et examinez les différences. Dans mon cas, la différence est la suivante:
// worker node $ ss -a -p |grep kubelet ... u_str ESTAB 0 0 * 157937 * 157387 users:(("kubelet",pid=5714,fd=33)) ...
Hmmm ... Une nouvelle connexion via des sockets unix entre kubelet (pid = 5714) et quelque chose d'inconnu. Qu'est-ce que ça pourrait être? C'est vrai, c'est Docker (pid = 1186)!
// worker node $ ss -a -p |grep 157387 ... u_str ESTAB 0 0 * 157937 * 157387 users:(("kubelet",pid=5714,fd=33)) u_str ESTAB 0 0 /var/run/docker.sock 157387 * 157937 users:(("dockerd",pid=1186,fd=14)) ...
Comme vous vous en souvenez, il s'agit d'un processus de démon docker (pid = 1186) qui exécute notre commande:
// worker node $ ps -afx ... 1186 ? Ssl 0:55 /usr/bin/dockerd -H fd:// 17784 ? Sl 0:00 \_ docker-containerd-shim 53a0a08547b2f95986402d7f3b3e78702516244df049ba6c5aa012e81264aa3c /var/run/docker/libcontainerd/53a0a08547b2f95986402d7f3 17801 pts/2 Ss 0:00 \_ sh 17827 pts/2 S+ 0:00 \_ sleep 5000 ...
4. Activité dans le runtime du conteneur
Examinons le code source du CRI-O pour comprendre ce qui se passe. Dans Docker, la logique est similaire.
Il existe un serveur chargé d'implémenter le
RuntimeServiceServer
:
( cri-o / server / server.go )
( cri-o / erver / container_exec.go )À la fin de la chaîne, le runtime du conteneur exécute une commande sur le nœud de travail:
( cri-o / internal / oci / runtime_oci.go )
Enfin, le noyau exécute les commandes:

Rappels
- API Server peut également initier une connexion à kubelet.
- Les connexions suivantes sont maintenues jusqu'à la fin de la session d'exécution interactive:
- entre kubectl et api-server;
- entre api-server et kubectl;
- entre le kubelet et l'exécution du conteneur.
- Kubectl ou api-server ne peuvent rien exécuter sur les nœuds de production. Kubelet peut démarrer, mais pour ces actions, il interagit également avec le runtime du conteneur.
Les ressources
PS du traducteur
Lisez aussi dans notre blog: