Minimo Kubernetes utilizzabile

La traduzione dell'articolo è stata preparata alla vigilia dell'inizio del corso "Pratiche e strumenti DevOps".

Minimo Kubernetes utilizzabile

Se stai leggendo questo articolo, probabilmente hai sentito qualcosa su Kubernetes (e in caso contrario, come sei finito qui?) Ma cos'è esattamente Kubernetes? Questo “Orchestrazioni di contenitori di tipo industriale”? o "Sistema operativo cloud-nativo"? Cosa significa questo?

Ad essere onesti, non ne sono sicuro al 100%. Ma penso che sia interessante scavare all'interno e vedere cosa sta realmente accadendo in Kubernetes sotto i suoi numerosi strati di astrazioni. Quindi, solo per divertimento, diamo un'occhiata a come appare in realtà un "cluster Kubernetes" minimo. (Sarà molto più facile di Kubernetes nel modo più difficile.)

Presumo che tu abbia una conoscenza di base di Kubernetes, Linux e container. Tutto ciò di cui parliamo qui è solo a scopo di ricerca/apprendimento, non metterne nulla in produzione!

panoramica

Kubernetes contiene molti componenti. Secondo wikipedia, l'architettura è simile alla seguente:

Minimo Kubernetes utilizzabile

Qui vengono mostrati almeno otto componenti, ma ne ignoreremo la maggior parte. Ci tengo a precisare che la cosa minima che ragionevolmente può essere chiamata Kubernetes è costituita da tre componenti principali:

  • cubetto
  • kube-apiserver (che dipende da etcd - il suo database)
  • runtime del contenitore (Docker in questo caso)

Vediamo cosa dice la documentazione su ciascuno di essi (Russo., Inglese.). All'inizio cubetto:

Un agente in esecuzione su ogni nodo del cluster. Si assicura che i contenitori siano in esecuzione nel pod.

Sembra abbastanza semplice. Che dire tempi di esecuzione del contenitore (runtime del contenitore)?

Un runtime di contenitore è un programma progettato per eseguire contenitori.

Molto informativo. Ma se hai familiarità con Docker, dovresti avere un'idea generale di cosa fa. (I dettagli della separazione delle responsabilità tra il runtime del contenitore e il kubelet sono in realtà piuttosto sottili e non li approfondirò qui.)

И server API?

API Server è il componente del pannello di controllo Kubernetes che espone l'API Kubernetes. Il server API è il lato client del pannello di controllo Kubernetes

Chiunque abbia mai fatto qualcosa con Kubernetes ha dovuto interagire con l'API direttamente o tramite kubectl. Questo è il cuore di ciò che rende Kubernetes Kubernetes: il cervello che trasforma le montagne di YAML che tutti conosciamo e amiamo (?) in un'infrastruttura funzionante. Sembra ovvio che l'API dovrebbe essere presente nella nostra configurazione minima.

Prerequisiti

  • Macchina virtuale o fisica Linux con accesso root (sto utilizzando Ubuntu 18.04 su una macchina virtuale).
  • Ed è tutto!

Installazione noiosa

Dobbiamo installare Docker sulla macchina che utilizzeremo. (Non entrerò nei dettagli su come funzionano Docker e i contenitori; se sei interessato, c'è articoli meravigliosi). Installiamolo semplicemente con apt:

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

Successivamente, dobbiamo ottenere i binari di Kubernetes. Infatti per il lancio iniziale del nostro “cluster” abbiamo solo bisogno kubelet, poiché per eseguire altri componenti del server possiamo utilizzare kubelet. Per interagire con il nostro cluster dopo che è in esecuzione, 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 scappiamo? kubelet?

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

kubelet deve essere eseguito come root. Abbastanza logico, poiché deve gestire l'intero nodo. Diamo un'occhiata ai suoi parametri:

$ ./kubelet -h
<слишком много строк, чтобы разместить здесь>
$ ./kubelet -h | wc -l
284

Wow, così tante opzioni! Fortunatamente, ce ne servono solo un paio. Ecco uno dei parametri che ci interessano:

--pod-manifest-path string

Percorso della directory contenente i file per i pod statici o percorso di un file che descrive i pod statici. I file che iniziano con punti vengono ignorati. (DEPRECATO: questa opzione deve essere impostata nel file di configurazione passato a Kubelet tramite l'opzione --config. Per ulteriori informazioni, vedere kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file .)

Questa opzione ci permette di correre baccelli statici — pod che non sono gestiti tramite l'API Kubernetes. I pod statici vengono utilizzati raramente, ma sono molto comodi per creare rapidamente un cluster e questo è esattamente ciò di cui abbiamo bisogno. Ignoreremo questo grande avvertimento (di nuovo, non eseguirlo in produzione!) e vedremo se riusciamo a far funzionare il pod.

Per prima cosa creeremo una directory per i pod statici ed eseguiremo kubelet:

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

Quindi, in un altro terminale/finestra tmux/qualunque cosa, creeremo un manifest pod:

$ cat <<EOF > 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 succeda nulla. Ma non è vero! Diamo un'occhiata a 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 Ho letto il manifest del pod e ho dato a Docker il comando di lanciare un paio di container secondo le nostre specifiche. (Se ti stai chiedendo quale sia il contenitore "pausa", è un hack di Kubernetes - vedi questo blog.) Kubelet lancerà il nostro contenitore busybox con il comando specificato e lo riavvierà a tempo indeterminato finché il pod statico non verrà eliminato.

Congratulazioni a te stesso. Abbiamo appena trovato uno dei modi più confusi per inviare testo al terminale!

Avvia ecc

Il nostro obiettivo finale è eseguire l'API Kubernetes, ma per farlo dobbiamo prima eseguirla etcd. Avviamo un cluster etcd minimo inserendo le sue impostazioni 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, questi file YAML dovrebbero esserti familiari. Ci sono solo due punti degni di nota qui:

Abbiamo montato la cartella host /var/lib/etcd nel pod in modo che i dati etcd vengano conservati dopo un riavvio (se ciò non viene fatto, lo stato del cluster verrà cancellato ogni volta che il pod viene riavviato, il che non andrà bene nemmeno per un'installazione minima di Kubernetes).

Abbiamo installato hostNetwork: true. Questa impostazione, non sorprende, configura etcd per utilizzare la rete host invece della rete interna del pod (questo renderà più semplice per il server API trovare il cluster etcd).

Un semplice controllo mostra che etcd è effettivamente in esecuzione su localhost e salva 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

Eseguire un server API Kubernetes è ancora più semplice. L'unico parametro che deve essere passato è --etcd-servers, fa quello 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

Inserisci questo file YAML nella directory podse il server API verrà avviato. Controllo con curl mostra che l'API Kubernetes è in ascolto sulla porta 8080 con accesso completamente aperto: non è richiesta alcuna 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 eseguirlo in produzione! Sono rimasto un po' sorpreso dal fatto che l'impostazione predefinita sia così insicura. Ma immagino che serva a rendere più facili lo sviluppo e il test.)

E, piacevole sorpresa, kubectl funziona immediatamente senza alcuna impostazione aggiuntiva!

$ ./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 scavi un po’ più a fondo, qualcosa sembra andare storto:

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

I pod statici che abbiamo creato sono spariti! In effetti, il nostro nodo kubelet non viene scoperto affatto:

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

Qual è il problema? Se ricordi qualche paragrafo fa, abbiamo avviato kubelet con un set estremamente semplice di parametri della riga di comando, quindi kubelet non sa come contattare il server API e notificargli il suo stato. Dopo aver studiato la documentazione, troviamo il flag corrispondente:

--kubeconfig string

Il percorso del file kubeconfig, che specifica come connettersi al server API. Disponibilità --kubeconfig abilita la modalità server API, no --kubeconfig abilita la modalità offline.

Per tutto questo tempo, senza saperlo, abbiamo eseguito kubelet in “modalità offline”. (Se fossimo pedanti, potremmo pensare a un kubelet autonomo come al "Kubernetes minimo praticabile", ma sarebbe molto noioso). Per far funzionare la configurazione "reale", dobbiamo passare il file kubeconfig a kubelet in modo che sappia come parlare con il server API. Fortunatamente è abbastanza semplice (poiché non abbiamo problemi di autenticazione o certificato):

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

Salva questo come kubeconfig.yaml, interrompi il processo kubelet e riavviare con i parametri necessari:

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

(A proposito, se provi ad accedere all'API tramite curl quando kubelet non è in esecuzione, scoprirai che è ancora in esecuzione! Kubelet non è un "genitore" dei suoi pod come Docker, è più simile a un "controllo daemon." I contenitori gestiti da un kubelet continueranno a funzionare finché il kubelet non li fermerà.)

Ерез несколько минут kubectl dovrebbe mostrarci i pod e i nodi come previsto:

$ ./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    <none>   21h   v1.18.5   10.70.10.228   <none>        Ubuntu 18.04.4 LTS   4.15.0-109-generic   docker://19.3.6

Congratuliamoci davvero con noi stessi questa volta (so che mi sono già congratulato con noi stessi): abbiamo un "cluster" Kubernetes minimo in esecuzione con un'API completamente funzionale!

Lanciamo sotto

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

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

Qui otteniamo un errore piuttosto interessante:

$ ./kubectl apply -f nginx.yaml
Error from server (Forbidden): error when creating "nginx.yaml": pods "nginx" is
forbidden: error looking up service account default/default: serviceaccount
"default" not found
$ ./kubectl get serviceaccounts
No resources found in default namespace.

Qui vediamo quanto sia tristemente incompleto il nostro ambiente Kubernetes: non abbiamo account per i servizi. Riproviamo 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 created
$ ./kubectl apply -f nginx.yaml
Error from server (ServerTimeout): error when creating "nginx.yaml": No API
token found for service account "default", retry after the token is
automatically created and added to the service account

Anche quando creiamo manualmente l'account di servizio, il token di autenticazione non viene generato. Mentre continuiamo a sperimentare con il nostro "cluster" minimalista, scopriremo che mancheranno la maggior parte delle cose utili che di solito accadono automaticamente. Il server API Kubernetes è piuttosto minimalista, con la maggior parte del lavoro pesante e della configurazione automatica che avviene in vari controller e lavori in background che non sono ancora in esecuzione.

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

$ cat <<EOS | ./kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: default
automountServiceAccountToken: false
EOS
serviceaccount/default configured
$ ./kubectl apply -f nginx.yaml
pod/nginx created
$ ./kubectl get pods
NAME    READY   STATUS    RESTARTS   AGE
nginx   0/1     Pending   0          13m

Finalmente è apparso il pod! Ma in realtà non inizierà perché non abbiamo pianificatore (programmatore) è un altro componente importante di Kubernetes. Ancora una volta, vediamo che l'API Kubernetes è sorprendentemente "stupida": quando crei un pod nell'API, lo registra, ma non tenta di capire su quale nodo eseguirlo.

In effetti, non è necessario uno scheduler per eseguire un pod. È possibile aggiungere manualmente un nodo al manifest nel parametro nodeName:

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

(Sostituire mink8s al nome del nodo.) Dopo l'eliminazione e l'applicazione, vediamo che nginx è stato avviato e sta ascoltando l'indirizzo IP interno:

$ ./kubectl delete pod nginx
pod "nginx" deleted
$ ./kubectl apply -f nginx.yaml
pod/nginx created
$ ./kubectl get pods -owide
NAME    READY   STATUS    RESTARTS   AGE   IP           NODE     NOMINATED NODE   READINESS GATES
nginx   1/1     Running   0          30s   172.17.0.2   mink8s   <none>           <none>
$ curl -s 172.17.0.2 | head -4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

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

$ cat <<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    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

È piuttosto interessante scavare in questo ambiente e vedere cosa funziona e cosa no. Ho scoperto che ConfigMap e Secret funzionano come previsto, ma Service e Deployment no.

Успех!

Questo post sta diventando lungo, quindi dichiarerò vittoria e dirò che questa è una configurazione praticabile che può essere chiamata "Kubernetes". Per riassumere: quattro binari, cinque parametri della riga di comando e "solo" 45 righe di YAML (non così tanto per gli standard Kubernetes) e abbiamo alcune cose che funzionano:

  • I pod vengono gestiti utilizzando la normale API Kubernetes (con alcuni hack)
  • Puoi caricare e gestire le immagini del contenitore pubblico
  • I pod rimangono attivi e si riavviano automaticamente
  • La rete tra pod all'interno dello stesso nodo funziona abbastanza bene
  • ConfigMap, il montaggio segreto e semplice dello storage funziona come previsto

Ma manca ancora gran parte di ciò che rende Kubernetes veramente utile, come ad esempio:

  • Programmatore pod
  • Autenticazione/autorizzazione
  • Nodi multipli
  • Rete di servizi
  • DNS interno in cluster
  • Controller per account di servizio, distribuzioni, integrazione con provider cloud e la maggior parte degli altri vantaggi offerti da Kubernetes

Quindi cosa abbiamo effettivamente ottenuto? L'API Kubernetes, in esecuzione da sola, è in realtà solo una piattaforma per automazione dei contenitori. Non fa molto (è un lavoro 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

Aggiungi un commento