Kubernetes minimo viabile

La traduzione dell'articolo è stata preparata in vista dell'inizio del corso «Pratiche e strumenti DevOps».

Kubernetes minimo viabile

Se stai leggendo questo, probabilmente hai sentito parlare di Kubernetes (e se non lo hai fatto, come sei arrivato qui?). Ma cosa rappresenta davvero Kubernetes? Questo è “Orchestrazione di container a livello industriale”? Или «Sistema Operativo Cloud-Native»? Что вообще это значит?

A essere sincero, non sono sicuro al 100%. Ma penso sia interessante esplorare gli interni e vedere cosa succede realmente in Kubernetes sotto i suoi molteplici strati di astrazione. Quindi, per curiosità, vediamo come appare realmente un minimo “cluster Kubernetes”. (Questo sarà molto più semplice di Kubernetes The Hard Way.)

Suppongo che tu abbia conoscenze di base su Kubernetes, Linux e container. Tutto ciò di cui parleremo qui è destinato solo a scopi di esplorazione/studio, non eseguire nulla di tutto ciò in produzione!

Panoramica

Kubernetes contiene molti componenti. Secondo Wikipedia, l'architettura appare come segue:

Kubernetes minimo viabile

Qui sono mostrati almeno otto componenti, ma la maggior parte di essi la ignoreremo. Voglio affermare che la cosa minima che si può giustamente definire Kubernetes consiste in tre componenti principali:

  • kubelet
  • kube-apiserver (che dipende da etcd — il suo database)
  • ambiente di esecuzione dei container (in questo caso Docker)

Vediamo cosa dice la documentazione riguardo ciascuno di essi (rus., eng.). Prima di tutto kubelet:

L'agente in esecuzione su ogni nodo nel cluster. Si assicura che i container siano avviati nel pod.

Sembra abbastanza semplice. E per quanto riguarda l'ambiente di esecuzione dei container (container runtime)?

L'ambiente di esecuzione dei container è un programma progettato per eseguire i container.

Molto informativo. Ma se conosci Docker, dovresti avere un'idea generale di cosa fa. (I dettagli della suddivisione delle responsabilità tra l'ambiente di esecuzione dei container e kubelet sono in realtà abbastanza sottili e qui non entrerò nei dettagli.)

E API server?

Il server API è un componente del pannello di controllo di Kubernetes, che rappresenta l'API di Kubernetes. L'API server è la parte client del pannello di controllo di Kubernetes

Chiunque abbia mai fatto qualcosa con Kubernetes ha dovuto interagire con l'API, sia direttamente che tramite kubectl. È il cuore di ciò che rende Kubernetes quello che è — il cervello che trasforma le montagne di YAML, che tutti conosciamo e amiamo (?), in un'infrastruttura funzionante. È evidente che l'API debba essere presente nella nostra configurazione minima.

Requisiti preliminari

  • Una macchina Linux virtuale o fisica con accesso root (sto usando Ubuntu 18.04 su una macchina virtuale).
  • E questo è tutto!

Installazione noiosa

Sulla macchina che utilizzeremo, è necessario installare Docker. (Non intendo descrivere in dettaglio come funziona Docker e i container; se siete interessati, ci sono articoli fantastici). Installiamolo semplicemente con apt:

$ sudo apt install docker.io
$ sudo systemctl start docker

Dopo questo, dobbiamo ottenere i binari di Kubernetes. In realtà, per avviare il nostro “cluster”, abbiamo bisogno solo di kubelet, poiché per avviare gli altri componenti del server potremo usare kubelet. Per interagire con il nostro cluster dopo che sarà attivo, utilizzeremo anche kubectl.

$ curl -L https://dl.k8s.io/v1.18.5/kubernetes-server-linux-amd64.tar.gz > server.tar.gz
$ tar xzvf server.tar.gz
$ cp kubernetes/server/bin/kubelet .
$ cp kubernetes/server/bin/kubectl .
$ ./kubelet --version
Kubernetes v1.18.5

Cosa succede se proviamo semplicemente a eseguire kubelet?

$ ./kubelet
F0609 04:03:29.105194    4583 server.go:254] mkdir /var/lib/kubelet: permesso negato

kubelet deve essere eseguito come root. Abbastanza logico, dato che deve gestire tutto il nodo. Diamo un'occhiata alle sue opzioni:

$ ./kubelet -h

$ ./kubelet -h | wc -l
284

Wow, quante opzioni! Fortunatamente, ci serviranno solo un paio di esse. Ecco uno dei parametri che ci interessa:

--pod-manifest-path string

Il percorso della cartella contenente i file per i pod statici, o il percorso di un file con la descrizione dei pod statici. I file che iniziano con un punto vengono ignorati. (OBSOLETO: questo parametro dovrebbe essere impostato nel file di configurazione passato a Kubelet tramite l'opzione —config. Per ulteriori informazioni vedere. kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file .)

Questo parametro ci consente di eseguire pod statici — pod che non sono gestiti tramite l'API di Kubernetes. I pod statici sono usati raramente, ma sono molto utili per alzare rapidamente un cluster, ed è proprio ciò di cui abbiamo bisogno. Ignoreremo questo forte avviso (ancora una volta, non eseguitelo in produzione!) e vediamo se possiamo avviare il pod.

Prima creiamo una directory per i pod statici e avviamo kubelet:

$ mkdir pods
$ sudo ./kubelet --pod-manifest-path=pods

Poi, in un'altra finestra del terminale/tmux/altrove, creeremo il manifesto del pod:

$ cat < pods/hello.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hello
spec:
  containers:
  - image: busybox
    name: hello
    command: ["echo", "hello world!"]
EOF

kubelet inizia a scrivere alcuni avvisi e sembra che non stia succedendo nulla. Ma non è così! Vediamo Docker:

$ sudo docker ps -a
CONTAINER ID        IMAGE                  COMMAND                 CREATED             STATUS                      PORTS               NAMES
8c8a35e26663        busybox                "echo 'hello world!'"   36 seconds ago      Exited (0) 36 seconds ago                       k8s_hello_hello-mink8s_default_ab61ef0307c6e0dee2ab05dc1ff94812_4
68f670c3c85f        k8s.gcr.io/pause:3.2   "pause"                2 minutes ago       Up 2 minutes                                    k8s_POD_hello-mink8s_default_ab61ef0307c6e0dee2ab05dc1ff94812_0
$ sudo docker logs k8s_hello_hello-mink8s_default_ab61ef0307c6e0dee2ab05dc1ff94812_4
hello world!

kubelet ha letto il manifesto del pod e ha dato a Docker l'istruzione di avviare un paio di contenitori secondo la nostra specifica. (Se sei curioso di sapere di più sul contenitore "pause", questa è una manomissione di Kubernetes — per dettagli, vedi in questo blog.) Kubelet avvierà il nostro contenitore busybox con il comando specificato e continuerà a riavviarlo all'infinito fino a quando il pod statico non sarà rimosso.

Complimenti. Abbiamo appena ideato uno dei modi più complicati per visualizzare del testo nel terminale!

Avviamo etcd

Il nostro obiettivo finale è avviare l'API di Kubernetes, ma per farlo dobbiamo prima avviare sono stati resi. Diamo inizio a un cluster etcd minimo, posizionando la sua configurazione nella directory pods (ad esempio, pods/etcd.yaml):

apiVersion: v1
kind: Pod
metadata:
  name: etcd
  namespace: kube-system
spec:
  containers:
  - name: etcd
    command:
    - etcd
    - --data-dir=/var/lib/etcd
    image: k8s.gcr.io/etcd:3.4.3-0
    volumeMounts:
    - mountPath: /var/lib/etcd
      name: etcd-data
  hostNetwork: true
  volumes:
  - hostPath:
      path: /var/lib/etcd
      type: DirectoryOrCreate
    name: etcd-data

Se hai mai lavorato con Kubernetes, file YAML come questi ti saranno familiari. Qui ci sono solo due punti da notare:

Abbiamo montato la cartella dell'host /var/lib/etcd in pod, per garantire che i dati etcd vengano conservati dopo un riavvio (se non lo fai, lo stato del cluster verrà cancellato ad ogni riavvio del pod, il che non è buono nemmeno per un'installazione minima di Kubernetes).

Abbiamo installato hostNetwork: true. Questo parametro, com'è ovvio, configura etcd per utilizzare la rete host anziché la rete interna del pod (questo faciliterà al server API la ricerca del cluster etcd).

Un semplice test mostra che etcd è effettivamente in esecuzione su localhost e sta salvando i dati su disco:

$ curl localhost:2379/version
{"etcdserver":"3.4.3","etcdcluster":"3.4.0"}
$ sudo tree /var/lib/etcd/
/var/lib/etcd/
└── member
    ├── snap
    │   └── db
    └── wal
        ├── 0.tmp
        └── 0000000000000000-0000000000000000.wal

Avvio del server API

Avviare il server API Kubernetes è ancora più semplice. L'unico parametro da fornire è --etcd-servers, fa ciò che ti aspetti:

apiVersion: v1
kind: Pod
metadata:
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - name: kube-apiserver
    command:
    - kube-apiserver
    - --etcd-servers=http://127.0.0.1:2379
    image: k8s.gcr.io/kube-apiserver:v1.18.5
  hostNetwork: true

Posiziona questo file YAML nella directory pods, e il server API si avvierà. Un controllo con curl mostra che l'API di Kubernetes ascolta sulla porta 8080 con accesso completamente aperto — non è richiesta autenticazione!

$ curl localhost:8080/healthz
ok
$ curl localhost:8080/api/v1/pods
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "selfLink": "/api/v1/pods",
    "resourceVersion": "59"
  },
  "items": []
}

(Ancora una volta, non eseguire questo in produzione! Sono rimasto un po' sorpreso che la configurazione predefinita fosse così insicura. Ma suppongo che sia stato fatto per facilitare lo sviluppo e il testing.)

E, sorpresa piacevole, kubectl funziona out of the box senza ulteriori configurazioni!

$ ./kubectl version
Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.5", GitCommit:"e6503f8d8f769ace2f338794c914a96fc335df0f", GitTreeState:"clean", BuildDate:"2020-06-26T03:47:41Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.5", GitCommit:"e6503f8d8f769ace2f338794c914a96fc335df0f", GitTreeState:"clean", BuildDate:"2020-06-26T03:39:24Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
$ ./kubectl get pod
No resources found in default namespace.

Problema

Ma se si scava un po' più a fondo, sembra che ci sia qualcosa che non va:

$ ./kubectl get pod -n kube-system
No resources found in kube-system namespace.

I pod statici che abbiamo creato sono scomparsi! In realtà, il nostro nodo kubelet non viene nemmeno rilevato:

$ ./kubectl get nodes
No resources found in default namespace.

Qual è il problema? Se ricordate, qualche paragrafo fa abbiamo avviato kubelet con un insieme di parametri della riga di comando estremamente semplici, quindi kubelet non sa come collegarsi al server API e comunicargli il suo stato. Consultando la documentazione, troviamo il flag corrispondente:

--kubeconfig string

Percorso del file kubeconfig, in cui è specificato come connettersi al server API. La presenza di --kubeconfig attiva la modalità API server, l'assenza di --kubeconfig attiva la modalità autonoma.

Per tutto questo tempo, senza saperlo, abbiamo eseguito kubelet in modalità ‘autonoma’. (Se fossimo stati pignoli, si potrebbe considerare la modalità autonoma di kubelet come ‘Kubernetes minimamente vitale’, ma sarebbe stata molto noiosa.) Per far funzionare una configurazione ‘reale’, dobbiamo passare il file kubeconfig a kubelet, affinché sappia come comunicare con l'API server. Fortunatamente, è piuttosto semplice (poiché non abbiamo problemi di autenticazione o certificati):

apiVersion: v1
kind: Config
clusters:
- cluster:
    server: http://127.0.0.1:8080
  name: mink8s
contexts:
- context:
    cluster: mink8s
  name: mink8s
current-context: mink8s

Salvatelo come kubeconfig.yaml, terminate il processo kubelet e riavviate con i parametri necessari:

$ sudo ./kubelet --pod-manifest-path=pods --kubeconfig=kubeconfig.yaml

(Tra l'altro, se provate a contattare l'API tramite curl, quando kubelet non è in esecuzione, scoprirete che funziona ancora! Kubelet non è il ‘genitore’ dei suoi pod, come Docker, è più simile a un “demone di gestione”. I contenitori gestiti da kubelet continueranno a funzionare finché kubelet non li fermerà.)

Dopo pochi minuti kubectl dovrebbe mostrarci i pod e i nodi, come ci aspettiamo:

$ ./kubectl get pods -A
NAMESPACE     NAME                    READY   STATUS             RESTARTS   AGE
default       hello-mink8s            0/1     CrashLoopBackOff   261        21h
kube-system   etcd-mink8s             1/1     Running            0          21h
kube-system   kube-apiserver-mink8s   1/1     Running            0          21h
$ ./kubectl get nodes -owide
NAME     STATUS   ROLES    AGE   VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION       CONTAINER-RUNTIME
mink8s   Ready       21h   v1.18.5   10.70.10.228           Ubuntu 18.04.4 LTS   4.15.0-109-generic   docker://19.3.6

Facciamoci congratulazioni questa volta, sul serio (so che già lo abbiamo fatto) — abbiamo creato un ‘cluster’ Kubernetes minimale, funzionante con un'API completa!

Avviamo il pod

Ora vediamo di cosa è capace l'API. Cominciamo con un pod nginx:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx

Qui riceveremo un errore piuttosto interessante:

$ ./kubectl apply -f nginx.yaml
Erro dal server (Forbidden): erro durante la creazione di "nginx.yaml": i pod "nginx" sono
proibiti: errore nella ricerca dell'account di servizio default/default: serviceaccount
"default" non trovato
$ ./kubectl get serviceaccounts
Nessuna risorsa trovata nello spazio dei nomi predefinito.

Qui possiamo vedere quanto sia terribilmente incompleta la nostra ambientazione Kubernetes: non abbiamo account di servizio. Proviamo ancora una volta, creando manualmente un account di servizio e vediamo cosa succede:

$ cat <<EOS | ./kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: default
EOS
serviceaccount/default creato
$ ./kubectl apply -f nginx.yaml
Errore dal server (ServerTimeout): errore durante la creazione di "nginx.yaml": Nessun token API
trovato per l'account di servizio "default", riprovare dopo che il token è stato
creato automaticamente e aggiunto all'account di servizio

Anche quando abbiamo creato manualmente un account di servizio, il token di autenticazione non viene creato. Continuando a sperimentare con il nostro "cluster" minimalista, scopriremo che la maggior parte delle cose utili che di solito si verificano automaticamente saranno assenti. Il server API Kubernetes è piuttosto minimalista, gran parte delle complesse configurazioni automatiche avviene in vari controller e job in background che non sono ancora in esecuzione.

Possiamo aggirare questo problema impostando l'opzione automountServiceAccountToken per l'account di servizio (dal momento che tanto non dovremo usarlo):

$ cat <<EOS | ./kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: default
automountServiceAccountToken: false
EOS
serviceaccount/default configurato
$ ./kubectl apply -f nginx.yaml
pod/nginx creato
$ ./kubectl get pods
NOME    PRONTO   STATO    RIAVVII   ETÀ
nginx   0/1     In attesa   0          13m

Finalmente, il pod è apparso! Ma in realtà non si avvierà, poiché non abbiamo un pianificatore (scheduler) — un altro componente importante di Kubernetes. Ancora una volta, vediamo che l'API Kubernetes è sorprendentemente "stupida": quando si crea un pod nell'API, lo registra, ma non cerca di scoprire su quale nodo eseguirlo.

In realtà, per avviare un pod, non è necessario un pianificatore. È possibile aggiungere manualmente un nodo nel manifesto sotto il parametro nodeName:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
  nodeName: mink8s

(Sostituisci mink8s con il nome del nodo.) Dopo delete e apply vediamo che nginx è partito e ascolta l'indirizzo IP interno:

$ .\/kubectl delete pod nginx
pod "nginx" eliminato
$ .\/kubectl apply -f nginx.yaml
pod/nginx creato
$ .\/kubectl get pods -owide
NOME    PRONTO   STATO    RIAVVII   ETÀ   IP           NODO     NOME NODI NOMINATI   PORTI DI PRONTO
nginx   1\/1     In esecuzione   0          30s   172.17.0.2   mink8s   <none>           <none>
$ curl -s 172.17.0.2 | head -4
<!DOCTYPE html>
<html>
<head>
<title>Benvenuto su nginx!</title>

Per assicurarci che la rete tra i pod funzioni correttamente, possiamo eseguire curl da un altro pod:

$ cat &lt;&lt;EOS | ./kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: curl
spec:
  containers:
  - image: curlimages/curl
    name: curl
    command: ["curl", "172.17.0.2"]
  nodeName: mink8s
EOS
pod/curl created
$ ./kubectl logs curl | head -6
  % Total    % Ricevuto % Xferd  Velocità Media   Tempo    Tempo     Tempo  Corrente
                                 Dload  Upload   Totale   Speso    Rimanente  Velocità
<!DOCTYPE html>
<html>
<head>
<title>Benvenuto su nginx!</title>

È piuttosto interessante esplorare questo ambiente e vedere cosa funziona e cosa no. Ho scoperto che ConfigMap e Secret funzionano come ci si aspetterebbe, mentre Service e Deployment no.

Successo!

Questo post sta diventando lungo, quindi annuncerò la vittoria e affermerò che questa è una configurazione valida, che può essere chiamata “Kubernetes”. In sintesi: quattro file binari, cinque parametri della riga di comando e “solo” 45 righe YAML (non così tante secondo gli standard di Kubernetes) e abbiamo molte cose funzionanti:

  • I Pod vengono gestiti tramite l'API Kubernetes standard (con alcuni hack)
  • È possibile caricare immagini di container pubbliche e gestirle
  • I Pod rimangono attivi e vengono riavviati automaticamente
  • La rete tra i pod all'interno di un singolo nodo funziona piuttosto bene
  • ConfigMap, Secret e il più semplice montaggio degli storage funzionano come previsto

Ma gran parte di ciò che rende Kubernetes davvero utile è ancora assente, per esempio:

  • Scheduler dei pod
  • Autenticazione / autorizzazione
  • Più nodi
  • Rete dei servizi
  • DNS interno del cluster
  • Controller per account di servizio, deployment, integrazione con fornitori di cloud e la maggior parte degli altri “vantaggi” che porta Kubernetes

Quindi, cosa abbiamo realmente ottenuto? L’API Kubernetes, di per sé, è in realtà solo una piattaforma per l'automazione dei container. Non fa molto — questo è compito per vari controller e operatori che utilizzano l'API — ma fornisce un ambiente coerente per l'automazione.

Scopri di più sul corso nel webinar gratuito.

Leggi di più:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster