Minimalny opłacalny Kubernetes

Tłumaczenie artykułu zostało przygotowane w przeddzień rozpoczęcia kursu „Praktyki i narzędzia DevOps”.

Minimalny opłacalny Kubernetes

Jeśli to czytasz, prawdopodobnie słyszałeś coś o Kubernetesie (a jeśli nie, jak tu trafiłeś?). Ale czym właściwie jest Kubernetes? Ten „Orkiestracja kontenerów przemysłowych”? Lub „Natywny dla chmury system operacyjny”? Co to w ogóle znaczy?

Szczerze mówiąc, nie jestem pewien na 100%. Myślę jednak, że ciekawie jest zajrzeć do wnętrza i zobaczyć, co naprawdę dzieje się w Kubernetesie pod wieloma warstwami abstrakcji. A więc dla zabawy przyjrzyjmy się, jak faktycznie wygląda minimalny „klaster Kubernetes”. (Będzie to znacznie łatwiejsze niż Kubernetes na własnej skórze.)

Zakładam, że masz podstawową wiedzę na temat Kubernetesa, Linuksa i kontenerów. Wszystko, o czym tutaj mówimy, służy wyłącznie celom badawczym/naukowym, nie wprowadzaj niczego do produkcji!

Przegląd

Kubernetes zawiera wiele komponentów. Według Wikipedia, architektura wygląda następująco:

Minimalny opłacalny Kubernetes

Pokazano tutaj co najmniej osiem komponentów, ale większość z nich zignorujemy. Chcę stwierdzić, że minimum, które można rozsądnie nazwać Kubernetesem, składa się z trzech głównych elementów:

  • kubelet
  • kube-apiserver (który zależy od etcd - jego bazy danych)
  • środowisko wykonawcze kontenera (w tym przypadku Docker)

Zobaczmy, co dokumentacja mówi o każdym z nich (Rosyjski., Angielski.). Najpierw kubelet:

Agent działający w każdym węźle klastra. Zapewnia, że ​​kontenery działają w zasobniku.

Brzmi dość prosto. Co powiesz na środowiska wykonawcze kontenerów (czas wykonania kontenera)?

Środowisko wykonawcze kontenera to program przeznaczony do uruchamiania kontenerów.

Bardzo informujące. Ale jeśli znasz Dockera, powinieneś mieć ogólne pojęcie o tym, co robi. (Szczegóły podziału odpowiedzialności pomiędzy środowiskiem wykonawczym kontenera a kubeletem są w rzeczywistości dość subtelne i nie będę się nimi tutaj zajmować.)

И Serwer API?

Serwer API to komponent panelu sterowania Kubernetes, który udostępnia interfejs API Kubernetes. Serwer API jest stroną klienta panelu sterowania Kubernetes

Każdy, kto kiedykolwiek robił cokolwiek z Kubernetesem, musiał wchodzić w interakcję z API bezpośrednio lub poprzez kubectl. To jest serce tego, co czyni Kubernetes Kubernetes — mózg, który zamienia 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.

Warunki wstępne

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

Nudna instalacja

Musimy zainstalować Dockera na maszynie, z której będziemy korzystać. (Nie będę wdawał się w szczegóły działania Dockera i kontenerów; jeśli jesteś zainteresowany, jest to możliwe wspaniałe artykuły). Po prostu zainstalujmy to za pomocą apt:

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

Następnie musimy zdobyć pliki binarne Kubernetes. Tak naprawdę do pierwszego uruchomienia naszego „klastra” potrzebujemy jedynie kubelet, ponieważ do uruchomienia innych komponentów serwera możemy użyć kubelet. Do interakcji z naszym klastrem po jego uruchomieniu będziemy również używać 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 uciekniemy kubelet?

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

kubelet musi działać jako root. Całkiem logiczne, ponieważ musi zarządzać całym węzłem. Spójrzmy na jego parametry:

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

Wow, tak wiele 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 opisującego statyczne pody. Pliki zaczynające się od kropek są ignorowane. (WYCOFANO: tę opcję należy ustawić w pliku konfiguracyjnym przekazywanym do Kubelet za pomocą opcji --config. Aby uzyskać więcej informacji, zobacz kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file .)

Ta opcja pozwala nam uruchomić statyczne kapsuły — pody, które nie są zarządzane poprzez Kubernetes API. Pody statyczne są rzadko używane, ale są bardzo wygodne do szybkiego podnoszenia klastra, a właśnie tego potrzebujemy. Zignorujemy to ważne ostrzeżenie (ponownie: nie uruchamiaj tego w środowisku produkcyjnym!) i zobaczmy, czy uda nam się uruchomić pod.

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

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

Następnie w innym terminalu/oknie tmux/cokolwiek utworzymy 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 zaczyna pisać ostrzeżenia i wygląda na to, że nic się nie dzieje. Ale to nieprawda! Spójrzmy na Dockera:

$ 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 Przeczytałem manifest poda i wydałem Dockerowi polecenie uruchomienia kilku kontenerów zgodnie z naszymi specyfikacjami. (Jeśli zastanawiasz się nad kontenerem „pauzy”, jest to hack Kubernetesa - patrz tego bloga.) Kubelet uruchomi nasz kontener busybox za pomocą określonego polecenia i uruchomi je ponownie na czas nieokreślony, dopóki statyczny pod nie zostanie usunięty.

Pogratuluj sobie. Właśnie wymyśliliśmy jeden z najbardziej zagmatwanych sposobów wysyłania tekstu do terminala!

Uruchom itp

Naszym ostatecznym celem jest uruchomienie interfejsu API Kubernetes, ale aby to zrobić, musimy najpierw uruchomić itd. Uruchommy 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 Kubernetesem, te pliki YAML powinny być Ci znane. Warto tutaj zwrócić uwagę tylko na dwie kwestie:

Zamontowaliśmy folder hosta /var/lib/etcd w zasobniku, aby dane etcd zostały zachowane po ponownym uruchomieniu (jeśli nie zostanie to zrobione, stan klastra zostanie usunięty przy każdym ponownym uruchomieniu zasobnika, co nie będzie dobre nawet w przypadku minimalnej instalacji Kubernetes).

Zainstalowaliśmy hostNetwork: true. To ustawienie, co nie jest zaskakujące, konfiguruje etcd tak, aby korzystał z sieci hosta zamiast z sieci wewnętrznej modułu (ułatwi to serwerowi API znalezienie klastra etcd).

Proste sprawdzenie pokazuje, że etcd rzeczywiście działa na localhost i zapisuje dane na dysku:

$ 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

Uruchomienie serwera API

Uruchamianie serwera Kubernetes API jest jeszcze łatwiejsze. Jedynym parametrem, który należy przekazać, jest --etcd-servers, robi to, czego oczekujesz:

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 zostanie uruchomiony. Sprawdzanie z curl pokazuje, że Kubernetes API nasłuchuje na porcie 8080 z całkowicie otwartym dostępem - nie jest wymagane żadne uwierzytelnianie!

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

(Ponownie nie uruchamiaj tego w środowisku produkcyjnym! Byłem trochę zaskoczony, że ustawienie domyślne jest tak niepewne. Ale domyślam się, że ma to na celu ułatwienie programowania 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

Ale jeśli kopiesz trochę głębiej, wydaje się, że coś idzie nie tak:

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

Statyczne kapsuły, które stworzyliśmy, zniknęły! Tak naprawdę nasz węzeł kubelet w ogóle nie został odkryty:

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

O co chodzi? Jeśli pamiętasz kilka akapitów temu, uruchomiliśmy kubelet z niezwykle prostym zestawem parametrów wiersza poleceń, więc kubelet nie wie, jak skontaktować się z serwerem API i powiadomić go o swoim stanie. Po przestudiowaniu dokumentacji znajdujemy odpowiednią flagę:

--kubeconfig string

Ścieżka do pliku kubeconfig, który określa sposób łączenia się z serwerem API. Dostępność --kubeconfig włącza tryb serwera API, nie --kubeconfig włącza tryb offline.

Przez cały ten czas nieświadomie uruchamialiśmy kubelet w „trybie offline”. (Gdybyśmy byli pedantyczni, moglibyśmy pomyśleć o samodzielnym kubelecie jako o „minimalnie wykonalnym Kubernetesie”, ale byłoby to bardzo nudne). Aby „prawdziwa” konfiguracja działała, musimy przekazać plik kubeconfig do kubelet, aby wiedział, jak rozmawiać z serwerem API. Na szczęście jest to dość proste (ponieważ nie mamy żadnych problemów z uwierzytelnianiem 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 niezbędnymi parametrami:

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

(Nawiasem mówiąc, jeśli spróbujesz uzyskać dostęp do API poprzez curl, gdy kubelet nie jest uruchomiony, przekonasz się, że nadal działa! Kubelet nie jest „rodzicem” swoich podów jak Docker, bardziej przypomina „kontrolę” demon.” Kontenery zarządzane przez kubelet będą nadal działać, dopóki kubelet ich nie zatrzyma.)

W ciągu kilku minut kubectl powinien pokazać nam strąki i węzły zgodnie z oczekiwaniami:

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

Tym razem naprawdę sobie pogratulujmy (wiem, że już sobie pogratulowałem) - mamy minimalny „klaster” Kubernetesa działający z w pełni funkcjonalnym API!

Startujemy pod

Zobaczmy teraz, do czego zdolne jest API. Zacznijmy od kapsuły Nginx:

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

Tutaj pojawia się 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 żałośnie niekompletne jest nasze środowisko Kubernetes – nie mamy kont dla usług. Spróbujmy ponownie, ręcznie tworząc konto usługi 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 gdy konto usługi utworzyliśmy ręcznie, token uwierzytelniający nie jest generowany. Kontynuując eksperymenty z naszym minimalistycznym „klasterem”, odkryjemy, że będzie brakować większości przydatnych rzeczy, które zwykle dzieją się automatycznie. Serwer Kubernetes API jest dość minimalistyczny, a większość ciężkich prac i automatycznej konfiguracji odbywa się na różnych kontrolerach i zadaniach w tle, które jeszcze nie działają.

Możemy obejść ten problem, ustawiając opcję automountServiceAccountToken dla konta usługi (ponieważ i tak nie będziemy musieli z niego korzystać):

$ 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 pojawił się kapsuła! Ale tak naprawdę to się nie uruchomi, bo nie mamy planista (harmonogram) to kolejny ważny komponent Kubernetes. Ponownie widzimy, że Kubernetes API jest zaskakująco „głupi” — gdy tworzysz Pod w API, rejestruje go, ale nie próbuje dowiedzieć się, na którym węźle go uruchomić.

Tak naprawdę do uruchomienia poda nie jest potrzebny program planujący. 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ępować mink8s do nazwy węzła.) Po usunięciu i zastosowaniu widzimy, że nginx został uruchomiony i nasłuchuje wewnętrznego adresu IP:

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

Aby mieć pewność, że sieć pomiędzy podami działa poprawnie, możemy uruchomić curl z innego poda:

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

To całkiem interesujące zagłębić się w to środowisko i zobaczyć, co działa, a co nie. Odkryłem, że ConfigMap i Secret działają zgodnie z oczekiwaniami, ale usługi i wdrażanie nie.

Powodzenie!

Ten post robi się długi, więc ogłoszę zwycięstwo i powiem, że jest to wykonalna konfiguracja, którą można nazwać „Kubernetes”. Podsumowując: cztery pliki binarne, pięć parametrów wiersza poleceń i „tylko” 45 linii YAML (nie tyle według standardów Kubernetes) i mamy sporo rzeczy, które działają:

  • Podami zarządza się za pomocą zwykłego API Kubernetes (z kilkoma hackami)
  • Możesz przesyłać publiczne obrazy kontenerów i zarządzać nimi
  • Pody pozostają aktywne i automatycznie uruchamiają się ponownie
  • Sieć między kapsułami w tym samym węźle działa całkiem dobrze
  • ConfigMap, Secret i prosty montaż pamięci działają zgodnie z oczekiwaniami

Jednak nadal brakuje wielu elementów, które czynią Kubernetes naprawdę użytecznym, takich jak:

  • Harmonogram podów
  • Uwierzytelnianie/autoryzacja
  • Wiele węzłów
  • Sieć usług
  • Klastrowany wewnętrzny DNS
  • Kontrolery do kont usług, wdrożeń, integracji z dostawcami usług chmurowych i większości innych gadżetów, które przynosi Kubernetes

Co więc właściwie dostaliśmy? Działający samodzielnie interfejs API Kubernetes jest tak naprawdę tylko platformą dla automatyzacja kontenerów. Nie robi to wiele – jest to zadanie dla różnych kontrolerów i operatorów korzystających z API – ale zapewnia spójne środowisko dla automatyzacji.

Więcej o kursie dowiesz się z bezpłatnego webinaru.

Czytaj więcej:

Źródło: www.habr.com

Dodaj komentarz