Minimalnie wykonalny Kubernetes

Przekład artykułu przygotowany przed rozpoczęciem kursu „Praktyki i narzędzia DevOps“.

Minimalnie wykonalny Kubernetes

Jeśli to czytasz, prawdopodobnie słyszałeś coś o Kubernetes (a jeśli nie, to jak się tutaj znalazłeś?). Ale czym tak naprawdę jest Kubernetes? To „Orkiestracja kontenerów na poziomie przemysłowym”? Или „Cloud-Native Operating System”? Что вообще это значит?

Szczerze mówiąc, nie jestem tego całkowicie pewien. Ale myślę, że warto zbadać wnętrza i zobaczyć, co tak naprawdę dzieje się w Kubernetes pod jego wieloma warstwami abstrakcji. Z ciekawości, zobaczmy, jak wygląda minimalny „klaster Kubernetes”. (To będzie znacznie prostsze niż Kubernetes The Hard Way.)

Zakładam, że masz podstawową wiedzę o Kubernetes, Linuksie i kontenerach. Wszystko, o czym tutaj będziemy mówić, jest przeznaczone wyłącznie do badań/wniknięcia, nie uruchamiaj nic z tego w produkcji!

Przegląd

Kubernetes zawiera wiele komponentów. Zgodnie z wikipedia, architektura wygląda następująco:

Minimalnie wykonalny Kubernetes

Tutaj pokazano przynajmniej osiem komponentów, ale większość z nich zignorujemy. Chcę stwierdzić, że minimalna rzecz, którą można uzasadnione nazywać Kubernetes, składa się z trzech głównych komponentów:

  • kubelet
  • kube-apiserver (który zależy od etcd — swojej bazy danych)
  • środowisko uruchomieniowe kontenerów (w tym przypadku Docker)

Spójrzmy, co o każdym z nich mówi dokumentacja (rus., angl). Najpierw kubelet:

Agent działający na każdym węźle w klastrze. Monitoruje, aby kontenery były uruchomione w podzie.

Brzmi dosyć prosto. A co z środowiskiem uruchomieniowym kontenerów (container runtime)?

Środowisko uruchomieniowe kontenerów to program przeznaczony do uruchamiania kontenerów.

Bardzo informacyjne. Ale jeśli znasz Dockera, to powinieneś mieć ogólne pojęcie o tym, co on robi. (Szczegóły podziału odpowiedzialności między środowiskiem uruchomieniowym a kubelet są w rzeczywistości dość subtelne i nie będę się w nie zagłębiać.)

I API-server?

Serwer API to komponent panelu zarządzania Kubernetes, który przedstawia API Kubernetes. API-server to część kliencka panelu zarządzania Kubernetes

Każdy, kto kiedykolwiek cokolwiek robił z Kubernetes, musiał wchodzić w interakcję z API albo bezpośrednio, albo przez kubectl. To serce tego, co sprawia, że Kubernetes jest Kubernetes — mózg, który przekształca góry YAML, które wszyscy znamy i kochamy (?), w działającą infrastrukturę. Wydaje się oczywiste, że API powinno być obecne w naszej minimalnej konfiguracji.

Wstępne warunki

  • Wirtualna lub fizyczna maszyna Linux z dostępem root (używam Ubuntu 18.04 na maszynie wirtualnej).
  • I to wszystko!

Nudna instalacja

Na maszynie, którą będziemy używać, należy zainstalować Docker. (Nie zamierzam szczegółowo opisywać, jak działa Docker i kontenery; jeśli chcesz, są świetne artykuły). Po prostu zainstalujmy go przy pomocy apt:

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

Następnie musimy pobrać binaria Kubernetes. W rzeczywistości do początkowego uruchomienia naszego “klastra” potrzebujemy tylko kubelet, ponieważ do uruchomienia innych komponentów serwerowych możemy użyć kubelet. Aby komunikować się z naszym klastrem po jego uruchomieniu, również będziemy korzystać z 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

Co się stanie, jeśli po prostu uruchomimy kubelet?

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

kubelet musi działać z uprawnieniami root. Zasadniczo logiczne, ponieważ musi zarządzać całym węzłem. Zobaczmy jego opcje:

$ ./kubelet -h

$ ./kubelet -h | wc -l
284

O wow, ile opcji! Na szczęście potrzebujemy tylko kilku z nich. Oto jeden z parametrów, który nas interesuje:

--pod-manifest-path string

Ścieżka do katalogu zawierającego pliki dla statycznych podów, lub ścieżka do pliku z opisem statycznych podów. Pliki zaczynające się od kropek są ignorowane. (ZDEPRECJONOWANE: ten parametr należy ustawić w pliku konfiguracyjnym przekazywanym do Kubelet przez opcję —config. Dodatkowe informacje można znaleźć na kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file .)

Ten parametr pozwala nam uruchamiać statyczne pode — pode, które nie są zarządzane przez API Kubernetes. Statyczne pode są rzadko używane, ale są bardzo wygodne do szybkiego uruchomienia klastra, a to dokładnie to, czego potrzebujemy. Zignorujemy to głośne ostrzeżenie (jeszcze raz, nie uruchamiaj tego w produkcji!) i zobaczymy, czy zdołamy uruchomić pod.

Najpierw stworzymy katalog dla statycznych podów i uruchomimy kubelet:

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

Następnie w innym terminalu/oknie tmux/jakimś innym miejscu stworzymy manifest poda:

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

kubelet zaczyna wypisywać różne ostrzeżenia i wydaje się, że nic się nie dzieje. Ale tak nie jest! Przyjrzyjmy się Dockerowi:

$ sudo docker ps -a
IDENTYFIKATOR KONTENERA        OBRAZ                  KOMENDA                 UTWORZONO             STATUS                      PORTY               NAZWY
8c8a35e26663        busybox                "echo 'hello world!'"   36 sekund temu      Zakończono (0) 36 sekund temu                       k8s_hello_hello-mink8s_default_ab61ef0307c6e0dee2ab05dc1ff94812_4
68f670c3c85f        k8s.gcr.io\/pause:3.2   "\/pause"                2 minuty temu       Działa 2 minuty                                    k8s_POD_hello-mink8s_default_ab61ef0307c6e0dee2ab05dc1ff94812_0
$ sudo docker logs k8s_hello_hello-mink8s_default_ab61ef0307c6e0dee2ab05dc1ff94812_4
hello world!

kubelet przeczytałem manifest poda i dałem Dockerowi polecenie uruchomienia dwóch kontenerów zgodnie z naszą specyfikacją. (Jeśli chcesz dowiedzieć się o kontenerze “pause”, to jest to gra w Kubernetes — szczegóły znajdziesz w na tym blogu.) Kubelet uruchomi nasz kontener busybox z podaną komendą i będzie go w nieskończoność restartować, aż statyczny pod zostanie usunięty.

Gratulacje! Właśnie wymyśliliśmy jeden z najciekawszych sposobów na wypisanie tekstu w terminalu!

Uruchamiamy etcd

Naszym ostatecznym celem jest uruchomienie API Kubernetes, ale najpierw musimy uruchomić etcd. Stwórzmy minimalny klaster etcd, umieszczając jego ustawienia w katalogu pods (na przykład, 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

Jeśli kiedykolwiek pracowałeś z Kubernetes, to podobne pliki YAML powinny być Ci znane. Należy zwrócić uwagę na dwa szczegóły:

Zamontowaliśmy folder hosta /var/lib/etcd w podzie, aby dane etcd były zachowywane po restarcie (jeśli tego nie zrobisz, stan klastra będzie zniknął przy każdym restarcie poda, co byłoby niekorzystne nawet dla minimalnej instalacji Kubernetes).

Ustawiliśmy hostNetwork: true. Ta opcja, jak można się domyślić, konfiguruje etcd do korzystania z sieci hosta zamiast wewnętrznej sieci poda (co ułatwi API serwerowi znalezienie klastra etcd).

Prosta kontrola pokazuje, że etcd jest rzeczywiście uruchomione na localhost i zapisuje dane na dysku:

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

Uruchomienie serwera API

Uruchomienie serwera API Kubernetes jest jeszcze prostsze. Jedyny parametr, który należy podać, --etcd-servers, robi dokładnie to, czego się spodziewasz:

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

Umieść ten plik YAML w katalogu pods, a serwer API uruchomi się. Sprawdzenie za pomocą curl pokazuje, że Kubernetes API nasłuchuje na porcie 8080 z całkowitym dostępem — uwierzytelnienie nie jest wymagane!

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

(Znowu, nie uruchamiaj tego w środowisku produkcyjnym! Byłem trochę zdziwiony, że domyślna konfiguracja jest tak niebezpieczna. Ale przypuszczam, że zrobiono to dla ułatwienia rozwoju i testowania.)

I, miła niespodzianka, kubectl działa od razu bez żadnych dodatkowych ustawień!

$ ./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.

Problem

Jednak gdy zagłębimy się nieco głębiej, wydaje się, że coś jest nie tak:

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

Statyczne pody, które stworzyliśmy, zniknęły! W rzeczywistości nasz węzeł kubelet w ogóle nie jest widoczny:

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

W czym więc rzecz? Jeśli pamiętasz, kilka akapitów wcześniej uruchamialiśmy kubelet z niezwykle prostym zestawem parametrów wiersza poleceń, więc kubelet nie wie, jak połączyć się z serwerem API i informować go o swoim stanie. Zgłębiając dokumentację, znajdujemy odpowiedni flag:

--kubeconfig string

Ścieżka do pliku kubeconfig, w którym określono, jak połączyć się z serwerem API. Obecność --kubeconfig włącza tryb API serwera, nieobecność --kubeconfig włącza tryb autonomiczny.

Przez cały ten czas, nie zdając sobie z tego sprawy, uruchamialiśmy kubelet w trybie „autonomicznym”. (Gdybyśmy byli pedantyczni, można by uznać autonomiczny tryb kubelet za „minimalnie działający Kubernetes”, ale to byłoby bardzo nudne). Aby uzyskać „prawdziwą” konfigurację, musimy przekazać plik kubeconfig do kubelet, aby wiedział, jak komunikować się z API-serverem. Na szczęście jest to dość proste (ponieważ nie mamy problemów z uwierzytelnieniem ani certyfikatami):

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

Zapisz to jako kubeconfig.yaml, zabij proces kubelet i uruchom ponownie z wymaganymi parametrami:

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

(A propos, jeśli spróbujesz uzyskać dostęp do API przez curl, gdy kubelet nie działa, odkryjesz, że nadal działa! Kubelet nie jest „rodzicem” swoich podów, podobnie jak Docker, bardziej przypomina „demon zarządzający”. Kontenery zarządzane przez kubelet będą działać, dopóki kubelet ich nie zatrzyma.)

Po kilku minutach kubectl powinien pokazać nam pody i węzły, tak jak się spodziewaliśmy:

$ ./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

Za chwilę gratulujmy sobie naprawdę (wiem, że już gratulowałem) — mamy minimalny „klaster” Kubernetes, działający z pełnoprawnym API!

Uruchamiamy pod

Teraz przyjrzyjmy się, co potrafi API. Zaczniemy od poda nginx:

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

Tutaj napotkamy dość interesujący błąd:

$ ./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.

Tutaj widzimy, jak strasznie niekompletne jest nasze środowisko Kubernetes — nie mamy kont dla usług. Spróbujmy jeszcze raz, tworząc konto usługi ręcznie, i zobaczmy, co się stanie:

$ 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

Nawet kiedy stworzyliśmy konto usługi ręcznie, token uwierzytelniający nie jest tworzony. Kontynuując eksperymenty z naszym minimalistycznym "klastrem", odkryjemy, że wiele użytecznych rzeczy, które zwykle odbywają się automatycznie, będzie brakować. Serwer API Kubernetes jest dość minimalistyczny, a większość ciężkich automatycznych ustawień odbywa się w różnych kontrolerach i zadaniach w tle, które jeszcze nie są wykonywane.

Możemy obejść ten problem, ustawiając opcję automountServiceAccountToken dla konta usługi (ponieważ i tak nie będziemy musieć go używać):

$ 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

W końcu pod się pojawił! Ale w rzeczywistości nie uruchomi się, ponieważ nie mamy harmonogramu (scheduler) – jeszcze jednego ważnego komponentu Kubernetes. Po raz kolejny widzimy, że API Kubernetes na zaskakująco jest „głupie” – kiedy tworzysz pod w API, rejestruje go, ale nie próbuje ustalić, na którym węźle go uruchomić.

Rzeczywiście, aby uruchomić pod, nie potrzebny jest harmonogram. Możesz ręcznie dodać węzeł do manifestu w parametrze nodeName:

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

(Zastąp mink8s nazwa węzła.) Po delete i apply widzimy, że nginx uruchomił się i nasłuchuje na wewnętrzny adres IP:

$ ./kubectl delete pod nginx
pod "nginx" usunięty
$ ./kubectl apply -f nginx.yaml
pod/nginx utworzony
$ ./kubectl get pods -owide
NAZWA   GOTOWE   STATUS   RESTARTY   WIEK   IP           WĘZEŁ     NOMINOWANY WĘZEŁ   BRAMY GOTOWOŚCI
nginx   1/1     Działa   0          30s   172.17.0.2   mink8s   <none>           <none>
$ curl -s 172.17.0.2 | head -4
<!DOCTYPE html>
<html>
<head>
<title>Witamy w nginx!</title>

Aby upewnić się, że sieć między podami działa poprawnie, możemy uruchomić curl z innego poda:

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

Dosyć ciekawie jest grzebać w tym środowisku i sprawdzić, co działa, a co nie. Odkryłem, że ConfigMap i Secret działają zgodnie z oczekiwaniami, a Service i Deployment nie.

Sukces!

Ten post staje się długi, więc zamierzam ogłosić zwycięstwo i powiedzieć, że to wykonalna konfiguracja, którą można nazwać „Kubernetes”. Podsumowując: cztery pliki binarne, pięć opcji wiersza poleceń i „zaledwie” 45 wierszy YAML (nie tak wiele według standardów Kubernetes) i działa nam sporo rzeczy:

  • Pody są zarządzane za pomocą standardowego API Kubernetes (z kilkoma hackami)
  • Można załadować publiczne obrazy kontenerów i nimi zarządzać
  • Pody pozostają aktywne i są automatycznie restartowane
  • Sieć między podami w ramach jednego węzła działa całkiem dobrze
  • ConfigMap, Secret i najprostsze montowanie magazynów działają jak należy

Ale większość z tego, co czyni Kubernetes naprawdę użytecznym, nadal brakuje, na przykład:

  • Planista podów
  • Uwierzytelnianie / autoryzacja
  • Wiele węzłów
  • Sieć usług
  • Klastra wewnętrzny DNS
  • Kontrolery dla kont usług, wdrożeń, integracji z dostawcami chmurowymi oraz większość innych „fajerwerków”, które przynosi Kubernetes

Co więc faktycznie otrzymaliśmy? API Kubernetes, które działa samo w sobie, jest tak naprawdę tylko platformą do automatyzacji kontenerów. Nie robi zbyt wiele — to zadanie dla różnych kontrolerów i operatorów, którzy wykorzystują API — ale zapewnia spójną środowisko do automatyzacji.

Dowiedz się więcej o kursie na darmowym webinarze.

Czytaj więcej:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster