Dane oryginału

Original edition copyright © 2018 by Manning Publications Co. All rights reserved. Title of English-language original: Kubernetes in Action by Marko Lukša, ISBN 978-1-61729-372-6. Polish-language edition © 2021 by Polish Scientific Publishers PWN Wydawnictwo Naukowe PWN Spółka Akcyjna. All Rights reserved.

Przekład Marek Włodarz na zlecenie WITKOM Witold Sikorski

Projekt okładki INT-MEDIA Dawid Mazur na podstawie oryginału

Wydawca Wioleta Szczygielska-Dybciak

Redaktor prowadzący Jolanta Kowalczuk

Redaktor Anna Marecka

Redaktor techniczny Maria Czekaj

Koordynator produkcji Anna Bączkowska

Skład wersji elektronicznej na zlecenie Wydawnictwo Naukowe PWN
Michał Latusek

Konsultanci merytoryczni

Aleksander Mamla, Paweł Hajduk

Zastrzeżonych nazw firm i produktów użyto w książce wyłącznie w celu identyfikacji.

Książka, którą nabyłeś, jest dziełem twórcy i wydawcy. Prosimy, abyś przestrzegał praw, jakie im przysługują. Jej zawartość możesz udostępnić nieodpłatnie osobom bliskim lub osobiście znanym. Ale nie publikuj jej w internecie. Jeśli cytujesz jej fragmenty, nie zmieniaj ich treści i koniecznie zaznacz, czyje to dzieło. A kopiując jej część, rób to jedynie na użytek osobisty.

Szanujmy cudzą własność i prawo

Więcej na www.legalnakultura.pl

Polska Izba Książki

Copyright © for the Polish edition by Wydawnictwo Naukowe PWN SA

Warszawa 2021

ISBN 978-83-01-21739-6

eBook został przygotowany na podstawie wydania papierowego z 2021r. (Wydanie I)

Warszawa 2021

Wydawnictwo Naukowe PWN SA

02-460 Warszawa, ul. Gottlieba Daimlera 2

tel. 22 69 54 321, faks 22 69 54 288

infolinia 801 33 33 88

e-mail: pwn@pwn.com.pl, reklama@pwn.pl
www.pwn.pl

Patroni medialni:

Bezpłatny fragment

przedmowa

podziękowania

o książce

o autorze

o ilustracji na okładce

Część I. Przegląd

1. Wprowadzenie do Kubernetes

1.1 Zapotrzebowanie na system taki jak Kubernetes

1.1.1 Przechodzenie od aplikacji monolitycznych do mikroserwisów

1.1.2 Zapewnianie spójnego środowiska dla aplikacji

1.1.3 Przechodzenie do ciągłego dostarczania: DevOps oraz NoOps

1.2 Przedstawiamy technologie kontenerowe

1.2.1 Czym są kontenery

1.2.2 Platforma kontenerów Docker

1.3 Wprowadzenie do Kubernetes

1.3.1 Pochodzenie

1.3.2 Ogólny obraz Kubernetes

1.3.3 Architektura klastra Kubernetes

1.3.4 Uruchamianie aplikacji w Kubernetes

1.3.5 Korzyści z używania Kubernetes

1.4 Podsumowanie

2. Pierwsze kroki z Dockerem i Kubernetes

2.1 Tworzenie, uruchamianie i udostępnianie obrazu kontenera

2.1.1 Instalowanie Dockera i uruchamianie powitalnego kontenera

2.1.2 Tworzenie trywialnej aplikacji Node.js

2.1.3 Tworzenie Dockerfile dla obrazu

2.1.4 Budowanie obrazu kontenera

2.1.5 Uruchamianie obrazu kontenera

2.1.6 Badanie wnętrza uruchomionego kontenera

2.1.7 Zatrzymywanie i usuwanie kontenera

2.1.8 Wypychanie obrazu do rejestru obrazów

2.2 Konfigurowanie klastra Kubernetes

2.2.1 Uruchamianie lokalnego jednowęzłowego klastra Kubernetes przy użyciu Minikube

2.2.2 Korzystanie z hostowanego klastra Kubernetes przy użyciu Google Kubernetes Engine

2.2.3 Konfigurowanie aliasu i dopełniania poleceń dla kubectl

2.3 Uruchamianie naszej pierwszej aplikacji w Kubernetes

2.3.1 Wdrażanie naszej aplikacji Node.js

2.3.2 Uzyskiwanie dostępu do aplikacji

2.3.3 Logiczne części naszego systemu

2.3.4 Skalowanie aplikacji w poziomie

2.3.5 Badanie, na których węzłach działa aplikacja

2.3.6 Przedstawiamy tablicę kontrolną Kubernetes

2.4 Podsumowanie

Część II. Podstawowe koncepcje

3. Pody: uruchamianie kontenerów w Kubernetes

3.1 Przedstawiamy pody

3.1.1 Dlaczego potrzebujemy podów

3.1.2 Istota podów

3.1.3 Właściwe porządkowanie kontenerów między pody

3.2 Tworzenie podów na podstawie deskryptorów YAML lub JSON

3.2.1 Badanie deskryptora YAML istniejącego podu

3.2.2 Tworzenie prostego deskryptora YAML dla podu

3.2.3 Wykorzystanie kubectl create do utworzenia podu

3.2.4 Przeglądanie dzienników aplikacji

3.2.5 Wysyłanie żądań do podu

3.3 Porządkowanie podów przy użyciu etykiet

3.3.1 Przedstawiamy etykiety

3.3.2 Specyfikowanie etykiet przy tworzeniu podu

3.3.3 Modyfikowanie etykiet istniejących podów

3.4 Wyliczanie podzbiorów podów przy użyciu selektorów etykiet

3.4.1 Wyliczanie podów przy użyciu selektora etykiet

3.4.2 Stosowanie wielu warunków w selektorze etykiet

3.5 Używanie etykiet i selektorów do ograniczania rozmieszczania podów

3.5.1 Używanie etykiet do kategoryzowania węzłów roboczych

3.5.2 Przypisywanie podów do określonych węzłów

3.5.3 Umieszczanie podu na jednym, określonym węźle

3.6 Opisywanie podów

3.6.1 Wyszukiwanie adnotacji obiektów

3.6.2 Dodawanie i modyfikowanie adnotacji

3.7 Wykorzystanie przestrzeni nazw do grupowania zasobów

3.7.1 Dlaczego przestrzenie nazw są potrzebne

3.7.2 Odkrywanie innych przestrzeni nazw i ich podów

3.7.3 Tworzenie przestrzeni nazw

3.7.4 Zarządzanie obiektami w innych przestrzeniach nazw

3.7.5 Izolacja zapewniana przez przestrzenie nazw

3.8 Zatrzymywanie i usuwanie podów

3.8.1 Usuwanie podu według nazwy

3.8.2 Usuwanie podów przy użyciu selektorów etykiet

3.8.3 Usuwanie podów przez usunięcie całej przestrzeni nazw

3.8.4 Usuwanie wszystkich podów w przestrzeni nazw, ale z zachowaniem tej przestrzeni nazw

3.8.5 Usuwanie (niemal) wszystkich zasobów w przestrzeni nazw

3.9 Podsumowanie

4. Replikacja i inne kontrolery: wdrażanie zarządzanych podów

4.1 Utrzymywanie dobrej kondycji podów

4.1.1 Wprowadzenie do sond żywotności

4.1.2 Tworzenie sondy żywotności opartej na HTTP

4.1.3 Obserwowanie sondy żywotności w działaniu

4.1.4 Konfigurowanie dodatkowych właściwości sondy żywotności

4.1.5 Tworzenie skutecznych sond żywotności

4.2 Przedstawiamy kontrolery replikacji

4.2.1 Działanie kontrolera replikacji

4.2.2 Tworzenie kontrolera replikacji

4.2.3 Obserwowanie kontrolera replikacji w działaniu

4.2.4 Przenoszenie podów do zakresu i poza zakres kontrolera replikacji

4.2.5 Zmienianie szablonu podu

4.2.6 Skalowanie podów w poziomie

4.2.7 Usuwanie kontrolera replikacji

4.3 Używanie obiektów ReplicaSet zamiast ReplicationController

4.3.1 Porównanie ReplicaSet z ReplicationController

4.3.2 Definiowanie ReplicaSet

4.3.3 Tworzenie i badanie obiektu ReplicaSet

4.3.4 Korzystanie z bardziej ekspresyjnych selektorów etykiet w ReplicaSet

4.3.5 Podsumowanie ReplicaSet

4.4 Uruchamianie dokładnie jednego podu w każdym węźle przy użyciu DaemonSet

4.4.1 Wykorzystanie DaemonSet do uruchomienia podu w każdym węźle

4.4.2 Używanie DaemonSet do uruchamiania podów tylko na wybranych węzłach

4.5 Uruchamianie podów, które wykonują pojedyncze, kompletne zadanie

4.5.1 Przedstawiamy zasób Job

4.5.2 Definiowanie zasobu Job

4.5.3 Obserwowanie uruchamiania podu przez obiekt Job

4.5.4 Uruchamianie wielu wystąpień podu w zasobie Job

4.5.5 Ograniczanie czasu, w którym pod obiektu Job musi ukończyć pracę

4.6 Planowanie okresowego wykonywania zadania lub jeden raz w przyszłości

4.6.1 Tworzenie obiektu CronJob

4.6.2 Jak uruchamiane są zaplanowane zadania

4.7 Podsumowanie

5. Usługi: umożliwianie odkrywania i komunikowania się klientów z podami

5.1 Wprowadzenie do usług

5.1.1 Tworzenie usług

5.1.2 Odkrywanie usług

5.2 Łączenie się z usługami działającymi poza klastrem

5.2.1 Wprowadzenie do punktów końcowych usług

5.2.2 Ręczne konfigurowanie punktów końcowych usługi

5.2.3 Tworzenie aliasu dla zewnętrznej usługi

5.3 Eksponowanie usług klientom zewnętrznym

5.3.1 Korzystanie z usługi typu NodePort

5.3.2 Eksponowanie usługi przez zewnętrzny moduł równoważenia obciążeń

5.3.3 Osobliwości połączeń zewnętrznych

5.4 Zewnętrzne eksponowanie usług przy użyciu zasobu Ingress

5.4.1 Tworzenie zasobu Ingress

5.4.2 Uzyskiwanie dostępu do usługi za pośrednictwem Ingress

5.4.3 Eksponowanie wielu usług przez ten sam obiekt Ingress

5.4.4 Konfigurowanie obsługi ruchu TLS przez Ingress

5.5 Sygnalizowanie, kiedy pod jest gotowy do akceptowania połączeń

5.5.1 Przedstawiamy sondy gotowości

5.5.2 Dodawanie sondy gotowości do podu

5.5.3 Co powinny robić rzeczywiste sondy gotowości

5.6 Używanie usługi headless w celu odkrywania indywidualnych podów

5.6.1 Tworzenie usługi headless

5.6.2 Odkrywanie podów przy użyciu DNS

5.6.3 Odkrywanie wszystkich podów – łącznie z tymi, które nie są gotowe

5.7 Rozwiązywanie problemów z usługami

5.8 Podsumowanie

6. Woluminy: dołączanie do kontenerów dyskowej pamięci masowej

6.1 Przedstawiamy woluminy

6.1.1 Objaśnianie woluminów na przykładzie

6.1.2 Przedstawiamy dostępne typy woluminów

6.2 Używanie woluminów do współużytkowania danych między kontenerami

6.2.1 Korzystanie z woluminu emptyDir

6.2.2 Wykorzystanie repozytorium Git jako punktu wyjścia dla naszego woluminu

6.3 Uzyskiwanie dostępu do plików w systemie plików węzła roboczego

6.3.1 Przedstawiamy wolumin hostPath

6.3.2 Badanie podów systemowych używających woluminów hostPath

6.4 Korzystanie z trwałej pamięci masowej

6.4.1 Korzystanie z GCE Persistent Disk w woluminie podu

6.4.2 Korzystanie z innych typów woluminów z leżącą w tle trwałą pamięcią masową

6.5 Oddzielanie podów od leżącej w tle technologii pamięci masowej

6.5.1 Wprowadzenie do obiektów PersistentVolume i PersistentVolumeClaim

6.5.2 Tworzenie zasobu PersistentVolume

6.5.3 Żądanie PersistentVolume przez utworzenie PersistentVolumeClaim

6.5.4 Używanie obiektu PersistentVolumeClaim w podzie

6.5.5 Zalety korzystania z obiektów PersistentVolume i żądań

6.5.6 Recykling obiektów PersistentVolume

6.6 Dynamiczne wyposażanie obiektów PersistentVolume

6.6.1 Definiowanie dostępnych typów pamięci masowej przez zasoby StorageClass

6.6.2 Żądanie klasy magazynowej w PersistentVolumeClaim

6.6.3 Dynamiczne wyposażanie bez specyfikowania klasy magazynowej

6.7 Podsumowanie

7. Obiekty ConfigMap oraz Secret: konfigurowanie aplikacji

7.1 Konfigurowanie skontenerowanych aplikacji

7.2 Przekazywanie do kontenerów argumentów wiersza polecenia

7.2.1 Definiowanie polecenia i argumentów w Dockerze

7.2.2 Nadpisywanie polecenia i argumentów w Kubernetes

7.3 Ustawianie zmiennych środowiskowych dla kontenera

7.3.1 Specyfikowanie zmiennych środowiskowych w definicji kontenera

7.3.2 Odwoływanie się do innych zmiennych środowiskowych w wartości zmiennej

7.3.3 Wady kodowania zmiennych środowiskowych na sztywno

7.4 Oddzielanie konfiguracji przy użyciu ConfigMap

7.4.1 Przedstawiamy obiekty ConfigMap

7.4.2 Tworzenie ConfigMap

7.4.3 Przekazywanie wpisu ConfigMap do kontenera jako zmiennej środowiskowej

7.4.4 Przekazywanie naraz wszystkich wpisów w obiekcie ConfigMap jako zmiennych środowiskowych

7.4.5 Przekazywanie wpisu ConfigMap jako argumentu wiersza polecenia

7.4.6 Używanie woluminu configMap do eksponowania wpisów ConfigMap jako plików

7.4.7 Aktualizowanie konfiguracji aplikacji bez konieczności jej restartowania

7.5 Wykorzystanie obiektów Secret do przekazywania poufnych danych do kontenerów

7.5.1 Przedstawiamy obiekty Secret

7.5.2 Przedstawiamy sekret domyślnego tokenu

7.5.3 Tworzenie sekretu

7.5.4 Porównanie obiektów ConfigMap i Secret

7.5.5 Korzystanie z sekretu w podzie

7.5.6 Jak obrazy pobierają sekrety

7.6 Podsumowanie

8. Dostęp z aplikacji do metadanych podu i innych zasobów

8.1 Przekazywanie metadanych za pośrednictwem Downward API

8.1.1 Dostępne metadane

8.1.2 Eksponowanie metadanych za pośrednictwem zmiennych środowiskowych

8.1.3 Przekazywanie metadanych przez pliki w woluminie downwardAPI

8.2 Komunikowanie się z serwerem API Kubernetes

8.2.1 Poznawanie API typu REST w Kubernetes

8.2.2 Komunikowanie się z serwerem API z wnętrza podu

8.2.3 Upraszczanie komunikacji z serwerem API za pomocą kontenerów ambasadorów

8.2.4 Wykorzystanie bibliotek klienckich w celu komunikacji z serwerem API

8.3 Podsumowanie

9. Obiekty Deployment: deklaratywne aktualizowanie aplikacji

9.1 Aktualizowanie aplikacji działających w podach

9.1.1 Usuwanie starych podów i zastępowanie ich nowymi

9.1.2 Wystartowanie nowych podów, a następnie usuwanie starych

9.2 Wykonywanie automatycznej aktualizacji kroczącej przy użyciu kontrolera replikacji

9.2.1 Uruchamianie wstępnej wersji aplikacji

9.2.2 Wykonywanie aktualizacji kroczącej przy użyciu kubectl

9.2.3 Dlaczego kubectl rolling-update jest już przestarzałe

9.3 Korzystanie z obiektów Deployment w celu deklaratywnego aktualizowania aplikacji

9.3.1 Tworzenie zasobu Deployment

9.3.2 Aktualizowanie zasobu Deployment

9.3.3 Wycofywanie wdrożenia

9.3.4 Kontrolowanie tempa aktualizacji

9.3.5 Wstrzymywanie procesu aktualizacji

9.3.6 Blokowanie rozpowszechniania wadliwych wersji

9.4 Podsumowanie

10. Zasoby StatefulSet: wdrażanie replikowanych aplikacji stanowych

10.1 Replikowanie podów stanowych

10.1.1 Uruchamianie wielu replik, każdej z oddzielną pamięcią masową

10.1.2 Zapewnianie stabilnej tożsamości każdego podu

10.2 Istota obiektów StatefulSet

10.2.1 Porównanie obiektów StatefulSet i ReplicaSet

10.2.2 Zapewnianie stabilnej tożsamości sieciowej

10.2.3 Zapewnianie stabilnej dedykowanej pamięci masowej dla każdego wystąpienia z pamięcią stanu

10.2.4 Gwarancje StatefulSet

10.3 Korzystanie ze StatefulSet

10.3.1 Tworzenie aplikacji i obrazu kontenera

10.3.2 Wdrażanie aplikacji przy użyciu StatefulSet

10.3.3 Testowanie naszych podów

10.4 Odkrywanie partnerów w StatefulSet

10.4.1 Implementowanie odkrywania partnerów przy użyciu DNS

10.4.2 Aktualizowanie StatefulSet

10.4.3 Testowanie klastrowanego magazynu danych

10.5 Jak zasoby StatefulSet radzą sobie z awariami węzłów

10.5.1 Symulowanie odłączenia węzła od sieci

10.5.2 Ręczne usuwanie podu

10.6 Podsumowanie

Część III. Poza podstawami

11. Poznawanie wewnętrznych mechanizmów Kubernetes

11.1 Poznajemy architekturę

11.1.1 Rozproszona natura komponentów Kubernetes

11.1.2 Jak Kubernetes używa etcd

11.1.3 Co robi serwer API

11.1.4 Jak serwer API powiadamia klientów o zmianach zasobów

11.1.5 Poznajemy Scheduler

11.1.6 Przedstawiamy kontrolery działające w komponencie Controller Manager

11.1.7 Co robi Kubelet

11.1.8 Rola komponentu Kubernetes Service Proxy

11.1.9 Przedstawiamy dodatki Kubernetes

11.1.10 Zbieranie wszystkiego razem

11.2 Jak współpracują kontrolery

11.2.1 Jakie komponenty są zaangażowane

11.2.2 Łańcuch zdarzeń

11.2.3 Obserwowanie zdarzeń klastra

11.3 Czym jest uruchomiony pod

11.4 Łączność sieciowa wewnątrz podu

11.4.1 Jaka musi być ta sieć

11.4.2 Zagłębianie się w działanie sieci

11.4.3 Przedstawiamy Container Network Interface

11.5 Jak są implementowane usługi

11.5.1 Przedstawiamy kube-proxy

11.5.2 Jak proces kube-proxy używa iptables

11.6 Uruchamianie klastrów wysokiej dostępności

11.6.1 Zapewnianie wysokiej dostępności naszych aplikacji

11.6.2 Zapewnianie wysokiej dostępności komponentów warstwy sterowania Kubernetes

11.7 Podsumowanie

12. Zabezpieczanie serwera API Kubernetes

12.1 Istota uwierzytelniania

12.1.1 Użytkownicy i grupy

12.1.2 Przedstawiamy zasoby ServiceAccount

12.1.3 Tworzenie zasobów ServiceAccount

12.1.4 Przypisywanie konta usługowego do podu

12.2 Zabezpieczanie klastra przy użyciu kontroli dostępu opartej na rolach

12.2.1 Przedstawiamy wtyczkę autoryzującą RBAC

12.2.2 Przedstawiamy zasoby RBAC

12.2.3 Używanie zasobów Role i RoleBinding

12.2.4 Używanie ról klastra i wiązań ról klastra

12.2.5 Domyślne role klastra i wiązania ról klastra

12.2.6 Rozważne przyznawanie uprawnień autoryzacyjnych

12.3 Podsumowanie

13. Zabezpieczanie węzłów i sieci klastra

13.1 Używanie w podzie przestrzeni nazw hostującego węzła

13.1.1 Używanie w podzie przestrzeni nazw sieciowych węzła

13.1.2 Dowiązywanie do portu hosta bez używania przestrzeni nazw sieciowych hosta

13.1.3 Używanie przestrzeni nazw PID i IPC węzła

13.2 Konfigurowanie kontekstu zabezpieczeń kontenera

13.2.1 Uruchamianie kontenera jako określony użytkownik

13.2.2 Powstrzymywanie kontenera przed działaniem jako root

13.2.3 Uruchamianie podów w trybie uprzywilejowanym

13.2.4 Dołączanie do kontenera indywidualnych możliwości jądra

13.2.5 Odrzucanie możliwości z kontenera

13.2.6 Powstrzymywanie procesów przed zapisywaniem w systemie plików kontenera

13.2.7 Współużytkowanie woluminów, gdy kontenery działają jako różni użytkownicy

13.3 Ograniczanie stosowania w podach funkcji związanych z zabezpieczeniami

13.3.1 Przedstawiamy zasób PodSecurityPolicy

13.3.2 Zasady runAsUser, fsGroup oraz supplementalGroups

13.3.3 Konfigurowanie dozwolonych, domyślnych i zabronionych możliwości

13.3.4 Ograniczanie typów woluminów używanych przez pody

13.3.5 Przypisywanie różnych zasobów PodSecurityPolicy do różnych użytkowników i grup

13.4 Izolowanie sieci podów

13.4.1 Włączanie izolacji sieciowej w przestrzeni nazw

13.4.2 Zezwalanie tylko niektórym podom z przestrzeni nazw na łączenie się z podem serwera

13.4.3 Izolacja sieci między przestrzeniami nazw Kubernetes

13.4.4 Izolowanie sieci przy użyciu notacji CIDR

13.4.5 Ograniczanie ruchu wychodzącego dla zbioru podów

13.5 Podsumowanie

14. Zarządzanie zasobami obliczeniowymi podów

14.1 Żądanie zasobów dla kontenerów podu

14.1.1 Tworzenie podów z żądaniami zasobów

14.1.2 Jak żądania zasobów wpływają na rozmieszczanie podów

14.1.3 Jak żądania procesora wpływają na współdzielenie czasu procesora

14.1.4 Definiowanie i żądanie niestandardowych zasobów

14.2 Ograniczanie zasobów dostępnych dla kontenera

14.2.1 Ustawienie sztywnego limitu ilości zasobów, których może używać kontener

14.2.2 Przekraczanie limitów

14.2.3 Jak aplikacje w kontenerach postrzegają limity

14.3 Klasy QoS dla podów

14.3.1 Definiowanie klasy QoS dla podu

14.3.2 Który proces zostanie zabity, gdy brakuje pamięci

14.4 Ustawianie domyślnych żądań i limitów dla podów w przestrzeni nazw

14.4.1 Przedstawiamy zasób LimitRange

14.4.2 Tworzenie obiektu LimitRange

14.4.3 Wymuszanie limitów

14.4.4 Stosowanie domyślnych żądań i limitów zasobów

14.5 Ograniczanie łącznych zasobów dostępnych w przestrzeni nazw

14.5.1 Przedstawiamy obiekt ResourceQuota

14.5.2 Specyfikowanie przydziałów trwałej pamięci masowej

14.5.3 Ograniczenie liczby dozwolonych obiektów

14.5.4 Specyfikowanie przydziałów dla określonych stanów podów i/lub klas QoS

14.6 Monitorowanie użycia zasobów przez pody

14.6.1 Gromadzenie i odczytywanie rzeczywistego użycia zasobów

14.6.2 Zapisywanie i analizowanie historycznych statystyk zużycia zasobów

14.7 Podsumowanie

15. Automatyczne skalowanie podów i węzłów klastra

15.1 Automatyczne skalowanie podów w poziomie

15.1.1 Proces skalowania automatycznego

15.1.2 Skalowanie oparte na użyciu procesora

15.1.3 Skalowanie oparte na użyciu pamięci

15.1.4 Skalowanie oparte na innych i niestandardowych miarach

15.1.5 Ustalanie, jakie miary są odpowiednie dla automatycznego skalowania

15.1.6 Skalowanie w dół do zera replik

15.2 Automatyczne skalowanie podów w pionie

15.2.1 Automatyczne konfigurowanie żądań zasobów

15.2.2 Modyfikowanie żądań zasobów w czasie działania podu

15.3 Horyzontalne skalowanie węzłów klastra

15.3.1 Przedstawiamy Cluster Autoscaler

15.3.2 Włączanie funkcjonalności Cluster Autoscaler

15.3.3 Ograniczanie przerw w działaniu usługi w trakcie skalowania klastra w dół

15.4 Podsumowanie

16. Zaawansowane rozmieszczanie

16.1 Używanie skaz i tolerancji w celu odpychania podów od pewnych węzłów

16.1.1 Skazy i tolerancje

16.1.2 Dodawanie niestandardowych skaz do węzła

16.1.3 Dodawanie tolerancji do podów

16.1.4 Do czego można używać skaz i tolerancji

16.2 Używanie koligacji węzła w celu przyciągania podów do określonych węzłów

16.2.1 Specyfikowanie sztywnych reguł koligacji węzła

16.2.2 Priorytetyzowanie węzłów podczas rozmieszczania podów

16.3 Kolokacja podów przy użyciu koligacji i anty-koligacji

16.3.1 Używanie koligacji między podami w celu wdrażania podów w tym samym węźle

16.3.2 Wdrażanie podów w tej samej szafie, strefie dostępności lub regionie geograficznym

16.3.3 Formułowanie preferencji koligacji zamiast sztywnych wymagań

16.3.4 Rozmieszczanie podów z dala od siebie nawzajem przy użyciu anty-koligacji

16.4 Podsumowanie

17. Zalecenia dla tworzenia aplikacji

17.1 Zbieranie wszystkiego razem

17.2 Cykl życia podu

17.2.1 Aplikacje muszą się spodziewać, że zostaną zabite i relokowane

17.2.2 Ponowne rozmieszczanie martwych lub częściowo martwych podów

17.2.3 Uruchamianie podów w określonej kolejności

17.2.4 Dodawanie hooków cyklu życia

17.2.5 Wyłączanie

17.3 Zapewnianie, że wszystkie żądania klientów są właściwie obsłużone

17.3.1 Unikanie zrywania połączeń klienckich podczas uruchamiania podu

17.3.2 Unikanie zrywania połączeń klienckich w trakcie wyłączania

17.4 Tworzenie aplikacji łatwych do zarządzania w Kubernetes

17.4.1 Tworzenie zarządzalnych obrazów kontenerów

17.4.2 Właściwe oznakowanie obrazów i rozważne używanie imagePullPolicy

17.4.3 Używanie etykiet wielowymiarowych zamiast jednowymiarowych

17.4.4 Opisywanie zasobów za pomocą adnotacji

17.4.5 Udostępnianie informacji o przyczynach zakończenia procesu

17.4.6 Obsługa dzienników aplikacji

17.5 Najlepsze praktyki programowania i testowania

17.5.1 Uruchamianie aplikacji poza Kubernetes w trakcie programowania

17.5.2 Korzystanie z Minikube w wytwarzaniu

17.5.3 Wersjonowanie i automatyczne wdrażanie manifestów zasobów

17.5.4 Ksonnet jako alternatywa dla pisania manifestów YAML/JSON

17.5.5 Stosowanie ciągłej integracji i ciągłego dostarczania (Continuous Integration i Continuous Delivery – CI/CD)

17.6 Podsumowanie

18. Rozszerzanie Kubernetes

18.1 Definiowanie niestandardowych obiektów API

18.1.1 Przedstawiamy CustomResourceDefinition

18.1.2 Automatyzowanie niestandardowych zasobów przy użyciu niestandardowych kontrolerów

18.1.3 Walidacja niestandardowych obiektów

18.1.4 Udostępnianie niestandardowego serwera API dla niestandardowych obiektów

18.2 Rozszerzanie Kubernetes przez Service Catalog

18.2.1 Przedstawiamy Service Catalog

18.2.2 Przedstawiamy serwer API Service Catalog oraz Controller Manager

18.2.3 Brokery usług i OpenServiceBroker API

18.2.4 Przygotowywanie i używanie usługi

18.2.5 Odwiązanie i wyrejestrowanie

18.2.6 Co przynosi Service Catalog

18.3 Platformy zbudowane na bazie Kubernetes

18.3.1 Platforma kontenerów Red Hat OpenShift

18.3.2 Deis Workflow oraz Helm

18.4 Podsumowanie

dodatek A. Używanie kubectl z wieloma klastrami

A.1 Przełączanie między Minikube a Google Kubernetes Engine

A.2 Używanie kubectl wobec wielu klastrów lub przestrzeni nazw

A.2.1 Konfigurowanie lokalizacji pliku kubeconfig

A.2.2 Zawartości pliku kubeconfig

A.2.3 Listowanie, dodawanie i modyfikowanie wpisów pliku kubeconfig

A.2.4 Używanie kubectl wobec różnych klastrów, użytkowników i kontekstów

A.2.5 Przełączanie się między kontekstami

A.2.6 Listowanie kontekstów i klastrów

A.2.7 Usuwanie kontekstów i klastrów

dodatek B. Tworzenie wielowęzłowego klastra przy użyciu kubeadm

B.1 Konfigurowanie systemu operacyjnego i wymaganych pakietów

B.1.1 Tworzenie maszyny wirtualnej

B.1.2 Konfigurowanie karty sieciowej maszyny wirtualnej

B.1.3 Instalowanie systemu operacyjnego

B.1.4 Instalowanie Dockera i Kubernetes

B.1.5 Klonowanie maszyny wirtualnej

B.2 Konfigurowanie węzła master przy użyciu kubeadm

B.2.1 Jak kubeadm uruchamia komponenty

B.3 Konfigurowanie węzłów roboczych przy użyciu kubeadm

B.3.1 Konfigurowanie sieci kontenerów

B.4 Korzystanie z klastra z maszyny lokalnej

dodatek C. Korzystanie z innych środowisk wykonawczych kontenerów

C.1 Używanie innych środowisk kontenerów za pośrednictwem CRI

C.1.1 Środowisko wykonawcze kontenerów CRI-O

C.1.2 Uruchamianie aplikacji w maszynach wirtualnych zamiast w kontenerach

dodatek D. Cluster Federation

D.1 Przedstawiamy Kubernetes Cluster Federation

D.2 Architektura

D.3 Sfederowane obiekty API

D.3.1 Sfederowane wersje zasobów Kubernetes

D.3.2 Co robią sfederowane zasoby

Przypisy

przedmowa

Pod koniec roku 2014, po kilku latach pracy w Red Hat, zostałem przydzielony do nowo utworzonego zespołu o nazwie Cloud Enablement. Naszym zadaniem było wprowadzenie całego szeregu produktów oprogramowania pośredniczącego naszej firmy do platformy kontenerów OpenShift, którą właśnie opracowywaliśmy na bazie Kubernetes. W tym czasie Kubernetes był w wieku niemowlęcym – nie wydano jeszcze nawet wersji 1.0.

Nasz zespół musiał szybko poznać tajniki Kubernetes, aby nadać właściwy kierunek naszemu oprogramowaniu i wykorzystać wszystko, co Kubernetes miał do zaoferowania. Gdy napotykaliśmy jakiś problem, trudno było nam stwierdzić, czy to my coś zrobiliśmy źle, czy po prostu trafiliśmy na jeden z wczesnych błędów Kubernetes.

Zarówno Kubernetes, jak i moja wiedza na ten temat pokonały od tego czasu długą drogę. Gdy zaczynałem używać Kubernetes, większość ludzi w ogóle jeszcze o nim nie słyszała. Obecnie niemal każdy inżynier oprogramowania wie, co to jest, i system ten stał się jednym z najszybciej rozwijających się i najszerzej stosowanych sposobów uruchamiania aplikacji, zarówno w chmurze, jak i we własnych centrach danych.

W pierwszym miesiącu zajmowania się Kubernetes napisałem dwuczęściowy post na blogu na temat uruchamiania klastra serwera aplikacji JBoss WildFly w OpenShift/Kubernetes. W tym czasie nie mogłem sobie wyobrazić, że ten prosty post ostatecznie sprawi, że redaktorzy z wydawnictwa Manning zwrócą się do mnie z pytaniem, czy nie zechciałbym napisać książki o Kubernetes. Naturalnie nie mogłem odrzucić takiej oferty, choć byłem pewien, że kontaktowali się również z innymi i ostatecznie mogli wybrać kogoś innego.

A jednak dotarliśmy tutaj. Po ponad półtora roku badań i pisania książka jest gotowa. To była niezwykła podróż. Pisanie o technologii jest zdecydowanie najlepszym sposobem, aby poznać ją znacznie dokładniej, niż moglibyśmy ją opanować po prostu jako użytkownik. W miarę pisania rosła moja wiedza o Kubernetes, ale i sam system ewoluował, zatem musiałem ciągle wracać do wcześniejszych rozdziałów, które już napisałem, i uzupełniać informacje. Jestem perfekcjonistą, zatem nigdy nie będę naprawdę usatysfakcjonowany książką, ale jestem szczęśliwy, słysząc, że wielu Czytelników wersji wstępnej udostępnionej w ramach Manning Early Access Program (MEAP) uznało ją za świetny przewodnik po Kubernetes.

Moim celem jest zapewnienie zrozumienia samej technologii i pokazanie, jak używać dostępnych narzędzi, aby skutecznie i wydajnie tworzyć oraz wdrażać aplikacje w klastrach Kubernetes. W tej książce nie położyłem większego nacisku na to, jak faktycznie zbudować i utrzymywać prawdziwie wysoko dostępny klaster Kubernetes, ale ostatnia część powinna dać Czytelnikom solidną wiedzę, z czego taki klaster się składa, i pomóc w łatwym przyswojeniu dodatkowych źródeł poświęconych tej tematyce.

Mam nadzieję, że lektura sprawi wam przyjemność i pokaże, jak wydobyć jak najwięcej z tak wspaniałego systemu, jakim jest Kubernetes.

podziękowania

Zanim zacząłem pisać tę książkę, nie miałem pojęcia, jak wielu ludzi będzie zaangażowanych w przekształcenie surowego manuskryptu w opublikowane dzieło. Oznacza to, że jest mnóstwo osób, którym należą się podziękowania.

Przede wszystkim dziękuję Erin Twohey za nakłonienie mnie do napisania tej książki oraz Michaelowi Stephensowi z wydawnictwa Manning, który zawsze był w pełni przekonany, że jestem w stanie ją napisać. Jego słowa zachęty zmotywowały mnie od samego początku i podtrzymywały tę motywację przez ostatnie półtora roku.

Chciałbym też podziękować mojemu pierwszemu redaktorowi, Andrew Warrenowi, który pomógł doprowadzić do końca pierwszy rozdział, oraz Elesha Hyde, która przejęła pałeczkę od Andrew i pracowała ze mną aż do ostatniego rozdziału. Dziękuję za to, że mnie znosiłaś, mimo że nie jestem łatwą osobą, jako że mam skłonność do regularnego znikania z pola widzenia.

Podziękować chciałbym również Jeanne Boyarsky, która była pierwszym recenzentem, sczytującym i komentującym moje rozdziały w trakcie ich pisania. To Jeanne i Elesha głównie sprawiły, że książka jest tak dobra, jak – mam nadzieję – jest. Bez ich komentarzy nigdy nie uzyskałaby tak dobrych opinii od zewnętrznych recenzentów i Czytelników.

Chcę też podziękować korektorowi technicznemu, Antonio Magnaghi, i oczywiście wszystkim zewnętrznym recenzentom, w kolejności alfabetycznej byli to: Al Krinker, Alessandro Campeis, Alexander Myltsev, Csaba Sari, David DiMaria, Elias Rangel, Erisk Zelenka, Fabrizio Cucci, Jared Duncan, Keith Donaldson, Michael Bright, Paolo Antinori, Peter Perlepes i Tiklu Ganguly. Ich pozytywne komentarze podtrzymywały mnie w pracy w tych chwilach, gdy zdawało mi się, że moja pisanina jest okropna i zupełnie bezużyteczna. Jednocześnie ich konstruktywny krytycyzm pomógł ulepszyć te części, które w innym przypadku wrzuciłbym razem bez dostatecznej staranności. Dziękuję za wskazywanie trudnych do zrozumienia fragmentów i sugerowanie sposobów ulepszenia książki. Dziękuję też za zadawanie właściwych pytań, które pozwoliły mi zrozumieć, że byłem w błędzie odnośnie do dwóch czy trzech kwestii we wstępnych wersjach manuskryptu.

Pragnę również podziękować Czytelnikom, którzy kupili wczesną wersję książki przez program MEAP i zgłaszali swoje komentarze na forum online lub zwracali się do mnie bezpośrednio. Do osób tych należą zwłaszcza: Vimal Kansal, Paolo Patierno i Roland Huß, którzy zwrócili uwagę na całkiem sporo niespójności i innych pomyłek. Chciałbym też podziękować wszystkim pracownikom Manning, którzy uczestniczyli w doprowadzeniu do wydania tej książki. Zanim zakończę, chcę też podziękować mojemu koledze i przyjacielowi szkolnemu, Aleš Justinowi, który wprowadził mnie do Red Hat, oraz moim wspaniałym kolegom z zespołu Cloud Enablement. Gdybym nie znalazł się w Red Hat i w tym zespole, nie mógłbym być tym, który napisał tę książkę.

Na koniec chcę podziękować mojej żonie i synowi, którzy wykazali aż nazbyt dużo zrozumienia i wsparcia w ostatnich 18 miesiącach, gdy tkwiłem zamknięty w swoim biurze, zamiast spędzać czas z nimi.

Dziękuję wam wszystkim!

o książce

Kubernetes w akcji ma na celu sprawić, by Czytelnik stał się wydajnym użytkownikiem Kubernetes. Przedstawia niemal wszystkie koncepcje, które trzeba poznać, aby skutecznie tworzyć i uruchamiać aplikacje w środowisku Kubernetes.

Przed zanurzeniem się w tematykę Kubernetes książka przedstawia ogólny przegląd technologii kontenerowych, takich jak Docker, włącznie z tym, jak budować kontenery, tak by nawet ci Czytelnicy, którzy wcześniej nigdy nie używali tych rozwiązań, mogli od razu przystąpić do pracy. Następnie powoli prowadzi Czytelnika przez niemal wszystko, co trzeba wiedzieć o Kubernetes – od podstawowych pojęć po kwestie kryjące się pod powierzchnią.

KTO POWINIEN PRZECZYTAĆ TĘ KSIĄŻKĘ

Książka skupia się głównie na twórcach aplikacji, ale udostępnia też przegląd zarządzania aplikacjami z perspektywy operacyjnej. Jest przeznaczona dla każdego zainteresowanego uruchamianiem i zarządzaniem aplikacjami skonteneryzowanymi na więcej niż tylko pojedynczym serwerze.

Zarówno początkujący, jak i zaawansowani inżynierowie oprogramowania, którzy chcieliby się nauczyć czegoś o technologiach kontenerów i orkiestrowaniu wielu powiązanych kontenerów w dużej skali, mogą tu zdobyć wiedzę konieczną do tworzenia, konteneryzowania i uruchamiania swoich aplikacji w środowisku Kubernetes.

Nie jest wymagana wcześniejsza znajomość ani technologii kontenerów, ani Kubernetes. Tematyka jest przedstawiana z narastającą szczegółowością i nie używa żadnego kodu źródłowego, który byłby zbyt trudny do zrozumienia dla programistów niebędących ekspertami.

Niemniej jednak Czytelnicy powinni dysponować przynajmniej podstawową wiedzą na temat programowania, sieci komputerowych i wykonywania podstawowych poleceń w systemie Linux oraz znać dobrze znane protokoły, takie jak HTTP.

ORGANIZACJA KSIĄŻKI: MAPA DROGOWA

Książka składa się z trzech części obejmujących 18 rozdziałów.

Część 1 przedstawia krótkie wprowadzenie do Dockera i Kubernetes, pokazuje, jak skonfigurować klaster Kubernetes i jak uruchomić w nim prostą aplikację. Zawiera dwa rozdziały:

■ Rozdział 1 wyjaśnia, czym jest Kubernetes, skąd się wziął i jak pomaga rozwiązywać dzisiejsze problemy zarządzania aplikacjami w wielkiej skali.

■ Rozdział 2 to praktyczny przewodnik po budowaniu obrazu kontenera i uruchamianiu go w klastrze Kubernetes. Wyjaśnia też, jak uruchomić lokalny jednowęzłowy klaster Kubernetes oraz prawdziwy, wielowęzłowy klaster w chmurze.

Część 2 przedstawia kluczowe koncepcje, które trzeba opanować, aby uruchamiać aplikacje w Kubernetes. Zawiera następujące rozdziały:

■ Rozdział 3 wprowadza podstawowy blok konstrukcyjny Kubernetes – pod – i wyjaśnia, jak porządkować pody i inne obiekty Kubernetes za pomocą etykiet.

■ Rozdział 4 pokazuje, jak Kubernetes utrzymuje dobrą kondycję aplikacji przez automatyczne restartowanie kontenerów. Pokazuje też, jak poprawnie uruchamiać zarządzane pody, skalować je w poziomie, zapewnić im odporność na awarie węzłów klastra i uruchamiać w określonej chwili w przyszłości albo okresowo.

■ Rozdział 5 pokazuje, jak pody mogą eksponować usługę, którą udostępniają klientom działającym zarówno wewnątrz, jak i na zewnątrz klastra. Pokazuje też, jak pody działające w klastrze mogą odkrywać usługi i uzyskiwać do nich dostęp, niezależnie od tego, czy są one zlokalizowane wewnątrz klastra, czy poza nim.

■ Rozdział 6 przedstawia współużytkowanie plików przez wiele kontenerów działających w tym samym podzie i jak można zarządzać trwałą pamięcią masową oraz zapewnić jej dostępność dla podów.

■ Rozdział 7 pokazuje, jak przekazywać do aplikacji działających wewnątrz podów dane konfiguracyjne i informacje poufne, takie jak poświadczenia.

■ Rozdział 8 opisuje, jak aplikacje mogą uzyskiwać informacje o środowisku Kubernetes, w którym działają, i jak mogą komunikować się z Kubernetes w celu zmiany stanu klastra.

■ Rozdział 9 przedstawia koncepcję zasobu Deployment oraz wyjaśnia właściwy sposób uruchamiania i aktualizowania aplikacji w środowisku Kubernetes.

■ Rozdział 10 wprowadza dedykowaną metodę uruchamiania aplikacji stanowych, które zazwyczaj wymagają stabilnej tożsamości i pamięci stanu.

W części 3 zanurzymy się głębiej w wewnętrzne mechanizmy klastra Kubernetes, przedstawimy kilka dodatkowych koncepcji i podsumujemy wszystko, czego nauczyliśmy się w dwóch pierwszych częściach, ale z szerszej perspektywy. Oto ostatnia grupa rozdziałów:

■ Rozdział 11 schodzi poniżej powierzchni Kubernetes i przedstawia wszystkie komponenty budujące klaster oraz to, co robi każdy z nich. Wyjaśnia też, jak pody komunikują się ze sobą przez sieć i jak usługi realizują równoważenie obciążeń między wieloma podami.

■ Rozdział 12 wyjaśnia, jak zabezpieczyć serwer API Kubernetes, a w konsekwencji cały klaster, przy użyciu uwierzytelnienia i autoryzacji.

■ Rozdział 13 pokazuje, jak pody mogą uzyskiwać dostęp do zasobów węzła i jak administrator klastra może im to uniemożliwić.

■ W rozdziale 14 zagłębimy się w ograniczanie zasobów obliczeniowych, jakiego może zużyć każda aplikacja przez skonfigurowanie jej gwarancji jakości usługi (Quality of Service), a także w monitorowanie zużycia zasobów przez indywidualne aplikacje. Pokazuje też, jak powstrzymać użytkowników przed konsumowaniem zbyt wielu zasobów.

■ Rozdział 15 zawiera omówienie konfigurowania Kubernetes w celu automatycznego skalowania liczby działających replik aplikacji, a także powiększania rozmiarów klastra, gdy bieżąca liczba węzłów nie może zaakceptować żadnych dodatkowych aplikacji.

■ Rozdział 16 pokazuje, jak zagwarantować, że pody będą rozmieszczane tylko na wybranych węzłach lub jak powstrzymać ich umieszczanie na innych. Pokazuje też, jak zagwarantować, że pewne pody będą rozmieszczane razem albo jak sprawić, aby tak się nie stało.

■ Rozdział 17 przedstawia najlepsze praktyki tworzenia aplikacji, aby stały się dobrymi obywatelami klastra. Zawiera również kilka wskazówek, jak skonfigurować swoje przepływy pracy programowania i testowania, aby zredukować opory podczas wytwarzania oprogramowania.

■ Rozdział 18 pokazuje sposoby rozszerzania Kubernetes przy użyciu własnych, niestandardowych obiektów, a także jak zrobili to inni, tworząc platformy aplikacji klasy przedsiębiorstwa.

W miarę postępów lektury Czytelnik pozna nie tylko indywidualne bloki konstrukcyjne Kubernetes, ale będzie również poszerzał swoją wiedzę na temat posługiwania się narzędziem wiersza polecenia kubectl.

KOD ZAWARTY W KSIĄŻCE

Choć ta książka nie zawiera zbyt dużo rzeczywistego kodu źródłowego, znalazło się w niej wiele manifestów zasobów Kubernetes w formacie YAML oraz poleceń powłoki wraz z ich wyjściem. Wszystkie one są sformatowane przy użyciu czcionki o stałej szerokości, jak w tym miejscu w celu odróżnienia od zwykłego tekstu.

Polecenia powłoki są głównie przedstawiane z wytłuszczeniem, aby jednoznacznie oddzielić je od danych wyjściowych, ale niekiedy dla wyróżnienia wytłuszczone są tylko najważniejsze części poleceń lub ich wyjścia. W większości przypadków wyjście poleceń zostało przeformatowane, aby mieściło się na ograniczonej powierzchni dostępnej w książce. Dodatkowo ze względu na fakt, że narzędzie CLI Kubernetes kubectl stale ewoluuje, nowsze wersje mogą zwracać więcej informacji, niż pokazano w książce. Trzeba to mieć na uwadze i nie zdziwić się, jeśli uzyskiwane wyniki nie będą dokładnie pasować.

Listingi niekiedy zawierają znacznik kontynuacji wiersza () pokazujący, że dany wiersz tekstu został zawinięty do kolejnej linii. Zawierają też odnośniki wyróżniające i objaśniające najważniejsze części.

W akapitach treściowych niektóre bardzo typowe elementy, takie jak Pod, ReplicationController, ReplicaSet, DaemonSet i tak dalej są złożone zwykłą czcionką, aby uniknąć rozpanoszenia się czcionki dla kodu i poprawić czytelność. W niektórych miejscach słowo „Pod” pisane jest wielką literą, gdy odnosimy się do zasobu Pod, natomiast małą literą, gdy odwołujemy się do rzeczywistej grupy uruchomionych kontenerów.

Wszystkie przykłady zamieszczone w książce zostały przetestowane w Kubernetes w wersji 1.8, działającym w Google Kubernetes Engine oraz w lokalnym klastrze uruchamianym przy użyciu Minikube. Pełny kod źródłowy i manifesty YAML można znaleźć w repozytorium https://github.com/luksa/kubernetesin-action albo pobrać z witryny wydawcy pod adresem www.manning.com/books/kubernetes-in-action.

FORUM KSIĄŻKI

Zakup książki Kubernetes w akcji zapewnia darmowy dostęp do prywatnego forum WWW[1] utrzymywanego przez Manning Publications, w którym można zamieszczać komentarze na temat książki, zadawać pytania techniczne i uzyskiwać pomoc od autora oraz od innych użytkowników. W celu uzyskania dostępu do forum należy przejść do strony https://forums.manning.com/forums/kubernetes-in-action. Więcej informacji na temat różnych forów firmy Manning i obowiązujących reguł można znaleźć pod adresem https://forums.manning.com/forums/about.

Zobowiązanie firmy Manning względem Czytelników to zapewnienie miejsca, w którym może się toczyć istotny dialog między indywidualnymi Czytelnikami i między Czytelnikami a autorem. Nie gwarantuje żadnego określonego poziomu udziału ze strony autora, którego wkład w forum pozostaje ochotniczy (i nieopłacony). Sugerujemy, aby zadawać autorowi prawdziwie trudne pytania, by przyciągnąć jego uwagę! Forum oraz archiwa wcześniejszych dyskusji będą dostępne w witrynie wydawcy, jak długo książka będzie obecna na rynku.

INNE ŹRÓDŁA ONLINE

Czytelnik może znaleźć szeroki wybór dodatkowych źródeł na temat Kubernetes w następujących lokalizacjach:

■ Witryna Kubernetes: https://kubernetes.io

■ Blog Kubernetes, na którym regularnie publikowane są interesujące informacje (http://blog.kubernetes.io)

■ Kanał Slack społeczności Kubernetes: http://slack.k8s.io

■ Kanały Kubernetes oraz Cloud Native Computing Foundation na YouTube:
https://www.youtube.com/channel/UCZ2bu0qutTOM0tHYa_jkIwg
https://www.youtube.com/channel/UCvqbFHwN-nwalWPjPUKpvTA

Aby uzyskać pogłębioną wiedzę o indywidualnych zagadnieniach lub nawet wnieść wkład w Kubernetes, można również sprawdzić jedną z Kubernetes Special Interest Groups (SIGs), dostępnych pod adresem https://github.com/kubernetes/community.

Na koniec, jako że Kubernetes jest oprogramowaniem open source, bogactwo informacji dostępne jest w samym kodzie źródłowym Kubernetes. Można go znaleźć w repozytorium https://github.com/kubernetes/kubernetes i innych powiązanych repozytoriach.

o autorze

Marko Lukša jest inżynierem oprogramowania z ponad dwudziestoletnim doświadczeniem przy tworzeniu wszystkiego, od prostych aplikacji WWW po pełne systemy ERP, platformy i oprogramowanie pośredniczące. Pierwsze kroki w programowaniu poczynił już w roku 1985, w wieku sześciu lat, na używanym komputerze ZX Spectrum, który kupił mu ojciec. W szkole podstawowej został zwycięzcą krajowych zawodów w programowaniu Logo i uczestniczył w letnich obozach kodowania, gdzie nauczył się programowania w Pascalu. Od tego czasu tworzył oprogramowanie w szerokiej gamie języków programowania.

W szkole średniej zaczął tworzyć dynamiczne witryny WWW, gdy sieć WWW była jeszcze względnie młoda. Następnie przeszedł do tworzenia oprogramowania dla branży opieki zdrowotnej i telekomunikacji w lokalnej firmy, podczas gdy studiował informatykę na Uniwersytecie Ljubljana w Słowenii. Ostatecznie trafił do pracy w Red Hat, początkowo tworząc implementację open source Google App Engine API, wykorzystującą opracowane przez Red Hat oprogramowanie pośredniczące JBoss. Pracował także lub wniósł wkład w takie projekty, jak CDI/Weld, Infinispan/JBoss DataGrid i inne.

Począwszy od końca roku 2014, jest członkiem zespołu Cloud Enablement w firmie Red Hat, gdzie do jego obowiązków należy śledzenie najnowszych osiągnięć w Kubernetes i powiązanych technologiach oraz zagwarantowanie, że firmowe oprogramowanie pośredniczące w najlepszy sposób wykorzystuje funkcjonalności Kubernetes i OpenShift.

o ilustracji na okładce

Obraz na okładce Kubernetes w akcji to „Członek Dywanu”, tureckiej rady stanu czy też rządu. Ilustracja pochodzi ze zbioru strojów imperium otomańskiego, opublikowanego 1 stycznia 1802 roku przez Williama Millera z Old Bond Street w Londynie. Zbiór ten utracił stronę tytułową i nie byliśmy w stanie prześledzić jego dziejów do tej pory. Spis treści książki identyfikuje ilustracje po angielsku oraz francusku i każda opatrzona jest nazwiskami dwóch artystów, którzy nad nią pracowali. Obydwaj byliby niewątpliwie zaskoczeni, gdyby dowiedzieli się, że ich dzieło zdobi frontową okładkę książki o programowaniu komputerów… 200 lat później.

Zbiór ten został zakupiony przez redaktora wydawnictwa Manning na pchlim targu w „Garage” na West 26th Street na Manhattanie. Sprzedawcą był Amerykanin mieszkający w Ankarze w Turcji, a do transakcji doszło w chwili, gdy właśnie pakował swoje stoisko. Redaktor nie miał przy sobie dostatecznej ilości gotówki na ten zakup, a propozycja użycia karty kredytowej lub czeku została uprzejmie odrzucona. Jako że sprzedawca odlatywał tego wieczora z powrotem do Ankary, sytuacja wydawała się beznadziejna. Rozwiązanie? Jak się okazało, nie było potrzebne nic więcej, jak staromodna ustna umowa przypieczętowana uściskiem dłoni. Sprzedawca zaproponował, aby przelać mu pieniądze, i redaktor odszedł z informacją o banku na świstku papieru oraz zbiorem obrazów pod pachą. Nie trzeba dodawać, że pieniądze przesłał następnego dnia i nadal jesteśmy wdzięczni i pod wrażeniem zaufania, jakie ten nieznany człowiek okazał jednemu z nas. Przypomina to coś, co mogło wydarzyć się dawno temu.

Wydawnictwo Manning celebruje innowacyjność, inicjatywę i – tak – frajdę związaną z branżą komputerową, używając okładek opartych na bogatej różnorodności regionalnych strojów sprzed dwóch stuleci, przywróconych do życia dzięki ilustracjom z tej kolekcji.

Część I

Przegląd

1

Wprowadzenie do Kubernetes

W tym rozdziale:

■ Wyjaśnienie, jak w ostatnich latach zmieniło się tworzenie i wdrażanie oprogramowania

■ Izolowanie aplikacji i redukowanie różnic środowiskowych przy użyciu kontenerów

■ Wyjaśnienie, jak w Kubernetes są wykorzystywane kontenery i Docker

■ Ułatwianie pracy programistów i administratorów systemów dzięki Kubernetes

Lata temu większość aplikacji oprogramowania stanowiły wielkie monolity działające albo jako pojedynczy proces, albo jako niewielka liczba procesów rozproszonych między garstkę serwerów. Takie starsze systemy nadal są szeroko rozpowszechnione. Mają one powolne cykle wydawnicze i są rzadko aktualizowane. Pod koniec każdego cyklu wydań programiści pakują cały system i przekazują go zespołowi operacyjnemu, który go następnie wdraża i monitoruje. W razie awarii sprzętowej zespół operacyjny ręcznie wykonuje migrację systemu do pozostałych sprawnych serwerów.

Obecnie te wielkie, monolityczne aplikacje powoli są rozbijane na mniejsze, niezależnie uruchamiane komponenty zwane mikroserwisami. Jako że mikroserwisy są oddzielone od siebie nawzajem, mogą być niezależnie opracowywane, wdrażane, aktualizowane i skalowane. Pozwala to wymieniać komponenty szybko i tak często, jak to konieczne, aby dotrzymać kroku błyskawicznie zmieniającym się wymaganiom biznesowym.

Jednak wraz z rosnącą liczbą wdrażanych komponentów oraz coraz większymi rozmiarami samych centrów danych coraz trudniejsze staje się konfigurowanie, zarządzanie i zapewnianie gładkiego działania całego systemu. Znacznie trudniejsze jest ustalenie, gdzie umieścić każdy z tych komponentów, aby uzyskać wysokie wykorzystanie zasobów, a tym samym ograniczyć koszty sprzętu. Ręczne wykonywanie tej pracy to trudne zadanie. Potrzebujemy automatyzacji zapewniającej automatyczne rozmieszczanie tych komponentów na naszych serwerach, automatyczną konfigurację, nadzór i obsługę awarii. I tu właśnie na scenę wkracza Kubernetes.

Kubernetes pozwala programistom wdrażać swoje aplikacje samodzielnie i tak często, jak zechcą, bez potrzeby żadnej asysty ze strony zespołu operacyjnego (operatorów). Jednak Kubernetes zapewnia korzyści nie tylko programistom. Pomaga też zespołowi operacyjnemu przez automatyczne monitorowanie i ponowne rozmieszczanie tych aplikacji w razie awarii sprzętu. Uwaga administratorów systemów przesuwa się z nadzorowania indywidualnych aplikacji na nadzorowanie i zarządzanie Kubernetes i resztą infrastruktury, podczas gdy Kubernetes sam zajmuje się aplikacjami.

UWAGA Kubernetes to greckie słowo (κυβερνήτης) oznaczające pilota lub sternika (osobę przy kole sterowym statku). Ludzie wymawiają słowo Kubernetes na kilka różnych sposobów. Wielu wymawia je jako Ku-ber-ne-tes, podczas gdy inni powiedzą coś w rodzaju Ku-ber-netis. Jednak bez względu na to, której wersji użyjesz, ludzie zrozumieją, co masz na myśli.

Kubernetes zapewnia abstrakcję infrastruktury sprzętowej i eksponuje całe centrum danych jako pojedynczy, ogromny zasób obliczeniowy. Pozwala wdrażać i uruchamiać komponenty programowe bez potrzeby znajomości rzeczywistych serwerów działających w tle. Gdy przez Kubernetes wdrażamy aplikację złożoną z wielu komponentów, wybiera on serwer dla każdego komponentu, instaluje go i umożliwia łatwe odszukanie i komunikowanie się ze wszystkimi pozostałymi komponentami aplikacji.

Sprawia to, że Kubernetes jest wspaniałym rozwiązaniem dla większości serwerowni w siedzibie, ale zaczyna świecić prawdziwym blaskiem, gdy zostanie użyty w największych centrach danych, takich jak te zbudowane i utrzymywane przez dostawców chmurowych. Kubernetes pozwala tym firmom oferować programistom prostą platformę dla wdrażania i uruchamiania dowolnego typu aplikacji, jednocześnie nie wymagając, aby ich administratorzy musieli wiedzieć cokolwiek o dziesiątkach tysięcy aplikacji uruchamianych na ich sprzęcie.

W miarę jak coraz więcej wielkich firm przyjmuje model Kubernetes jako najlepszy sposób uruchamiania aplikacji, staje się on standardową metodą uruchamiania aplikacji rozproszonych, zarówno w chmurze, jak i w lokalnej infrastrukturze w siedzibie.

1.1 Zapotrzebowanie na system taki jak Kubernetes

Zanim zaczniemy szczegółowo poznawać Kubernetes, przyjrzyjmy się krótko zmianom, które w ostatnich latach nastąpiły w wytwarzaniu i wdrażaniu aplikacji. Zmiany te są zarówno konsekwencją podziału wielkich monolitycznych aplikacji na mniejsze mikroserwisy, jak i zmian w infrastrukturach, na których działają te aplikacje. Zrozumienie tych zmian pomoże lepiej dostrzec korzyści z wykorzystania Kubernetes oraz technologii kontenerowych, takich jak Docker.

1.1.1 Przechodzenie od aplikacji monolitycznych do mikroserwisów

Monolityczne aplikacje składają się z komponentów, które są ściśle powiązane ze sobą i muszą być tworzone, wdrażane i zarządzane jako pojedyncza jednostka, gdyż wszystkie one działają jako pojedynczy proces systemu operacyjnego. Zmiany w jednej części aplikacji wymagają ponownego wdrożenia całej aplikacji. Z czasem brak ustalonych granic między częściami powoduje wzrost złożoności, a w konsekwencji degradację jakości całego systemu z powodu nieograniczonego wzrostu wewnętrznych zależności między tymi częściami.

Działanie monolitycznej aplikacji zazwyczaj wymaga niewielkiej liczby potężnych serwerów, które zdołają zapewnić dostateczne zasoby dla jej uruchomienia. Aby poradzić sobie z rosnącymi obciążeniami systemu, musimy albo skalować serwery pionowo, dodając więcej procesorów, pamięci i innych komponentów serwerowych, albo skalować poziomo cały system, konfigurując dodatkowe serwery i uruchamiając wiele kopii (replik) aplikacji (skalowanie w poziomie). Podczas gdy skalowanie w pionie zazwyczaj nie wymaga żadnych zmian w aplikacji, dość szybko staje się bardzo kosztowne i w praktyce zawsze ma górną granicę. Jednocześnie skalowanie w poziomie jest względnie tanie z punktu widzenia sprzętowego, ale może wymagać dużych zmian w kodzie aplikacji i nie zawsze jest możliwe – skalowanie w poziomie pewnych części aplikacji może być skrajnie trudne lub niemal niemożliwe (np. relacyjnych baz danych). Jeśli dowolna część monolitycznej aplikacji nie jest skalowalna, cała aplikacja staje się nieskalowalna, jeżeli jakoś nie podzielimy tego monolitu.

ROZDZIELANIE APLIKACJI NA MIKROSERWISY

Te i inne problemy skłoniły nas do rozdzielania złożonych, monolitycznych aplikacji na mniejsze, niezależnie wdrażane komponenty, nazywane mikroserwisami. Każdy mikroserwis działa jako niezależny proces (patrz rysunek 1.1) i komunikuje się z innymi mikroserwisami przez proste, dobrze zdefiniowane interfejsy (API).

Rysunek 1.1 Komponenty wewnątrz aplikacji monolitycznej kontra samodzielne mikroserwisy

Mikroserwisy komunikują się przez protokoły synchroniczne, takie jak HTTP, przez które zazwyczaj eksponują API typu RESTful (REpresentational State Transfer), albo przez protokoły asynchroniczne, takie jak AMQP (Advanced Message Queueing Protocol). Protokoły te są proste, dobrze rozumiane przez większość programistów i nie są przypisane do żadnego konkretnego języka programowania. Każdy mikroserwis może być napisana w języku najwłaściwszym dla implementacji tej specyficznej usługi.

Jako że każdy mikroserwis jest samodzielnym procesem z dość statycznym zewnętrznym API, możliwe jest oddzielne tworzenie i wdrażanie każdego mikroserwisu. Zmiana jednej z nich nie wymaga zmian ani ponownego wdrażania żadnej innej usługi, jeśli API nie ulegnie zmianie albo zmieni się tylko w sposób zapewniający wsteczną kompatybilność.

SKALOWANIE MIKROSERWISÓW

Skalowanie mikroserwisów, w przeciwieństwie do systemów monolitycznych, w których konieczne jest skalowanie systemu jako całości, jest wykonywane dla każdej usługi oddzielnie, co oznacza, że możemy skalować tylko te usługi, które wymagają więcej zasobów, pozostawiając inne w oryginalnej skali. Przykład pokazuje rysunek 1.2. Pewne komponenty są replikowane i działają jako wiele procesów wdrożonych na różnych serwerach, podczas gdy inne działają jako pojedynczy proces. Gdy aplikacja monolityczna nie może być skalowana wszerz, gdyż jedna z jej części jest nieskalowalna, rozdzielenie jej na mikroserwisy pozwala skalować poziomo te części, które na to pozwalają, a te części, gdzie jest to niewykonalne, skalować w pionie zamiast w poziomie.

Rysunek 1.2 Każdy mikroserwis może być indywidualnie skalowany

WDRAŻANIE MIKROSERWISÓW

Jak zwykle mikroserwisy mają też swoje wady. Jeśli nasz system zawiera tylko niewielką liczbę komponentów do wdrożenia, zarządzanie tymi komponentami jest łatwe. Trywialne jest decydowanie, gdzie wdrożyć każdy komponent, gdyż nie ma zbyt wielkiego wyboru. Gdy liczba tych komponentów wzrasta, decyzje związane z wdrożeniem robią się coraz trudniejsze, gdyż nie tylko wzrasta liczba kombinacji wdrożeniowych, ale rośnie też liczba wzajemnych zależności między komponentami i to o jeszcze większy czynnik.

Mikroserwisy wykonują swoją pracę łącznie, jako zespół, zatem muszą się wzajemnie odnajdywać i ze sobą komunikować. Przy ich wdrażaniu ktoś lub coś musi je wszystkie właściwie skonfigurować, aby umożliwić ich wspólną pracę jako pojedynczego systemu. Przy rosnącej liczbie mikroserwisów praca ta staje się żmudna i podatna na błędy, szczególnie gdy uwzględnimy działania, które zespół operatorów/administratorów systemu musi wykonać, gdy serwer ulegnie awarii.

Mikroserwisy wprowadzają też inne problemy, takie jak utrudnienie debugowania i śledzenia wywołań w trakcie wykonywania, gdyż obejmują wiele procesów i maszyn. Na szczęście problemy te można obecnie rozwiązywać przez rozproszone systemy śledzenia, takie jak Zipkin.

ROZBIEŻNOŚCI W WYMAGANIACH ŚRODOWISKOWYCH

Jak już wspomnieliśmy, komponenty w architekturze mikroserwisów nie tylko są niezależnie wdrażane, ale również w taki sposób wytwarzane. Ze względu na ich niezależność i fakt, że typowe jest stosowanie oddzielnych zespołów programujących każdy komponent, nic nie powstrzymuje każdego z tych zespołów przed użyciem różnych bibliotek i zastępowania ich, ilekroć pojawi się potrzeba. Nieuniknione są rozbieżności w zależnościach między komponentami aplikacji, takimi jak pokazane na rysunku 1.3, gdzie aplikacje wymagają różnych wersji tych samych bibliotek.

Rysunek 1.3 Wiele aplikacji uruchomionych na tym samym hoście może mieć sprzeczne wymagania

Wdrażanie dynamicznie połączonych aplikacji, które wymagają różnych wersji wspólnych bibliotek lub wymagają innych cech środowiskowych, szybko staje się koszmarem dla zespołu operatorów, który je wdraża i zarządza nimi na serwerach docelowych. Im większa jest liczb komponentów, które musimy wdrożyć na tym samym komputerze, tym trudniejsze będzie zarządzanie wszystkimi ich zależnościami, aby spełnić wszystkie wymagania.

1.1.2 Zapewnianie spójnego środowiska dla aplikacji

Niezależnie od tego, jak wiele indywidualnych komponentów będziemy tworzyć i wdrażać, jednym z największych problemów, z którymi zawsze muszą sobie radzić programiści i zespoły operatorów, są różnice między środowiskami, w których działają aplikacje. Mamy tu nie tylko wielką różnicę między środowiskiem programistycznym a docelowym, ale różnice występują też między poszczególnymi maszynami produkcyjnymi. Drugi nieunikniony problem polega na tym, że środowisko pojedynczej maszyny produkcyjnej zmienia się z upływem czasu.

Różnice te rozciągają się od sprzętu, przez system operacyjny, aż po biblioteki dostępne na każdej maszynie. Środowiska produkcyjne zarządzane są przez zespół operacyjny, podczas gdy programiści zwykle sami zajmują się swoimi komputerami. Różnica leży w tym, jak dużo te dwie grupy ludzi wiedzą o administrowaniu systemami i ta zdolność rozumienia prowadzi do dużych różnic między tymi kategoriami systemów, nie wspominając o tym, że administratorzy systemu znacznie większą uwagę zwracają na zapewnienie aktualności systemu i instalowanie najświeższych poprawek zabezpieczeń, podczas gdy wielu programistów nie uważa tego za aż tak istotne.

Ponadto systemy produkcyjne mogą uruchamiać aplikacje pochodzące od wielu programistów lub zespołów programistycznych, co niekoniecznie musi być prawdą w przypadku komputerów programistów. System produkcyjny musi zapewnić właściwe środowisko dla wszystkich utrzymywanych na nim aplikacji, nawet jeśli wymagają one różnych, być może sprzecznych ze sobą wersji bibliotek.

Dążąc do zredukowania liczby problemów, które ujawniają się dopiero w środowisku produkcyjnym, byłoby idealnie, gdyby aplikacje mogły działać w dokładnie takim samym środowisku podczas ich tworzenia, jak w środowisku produkcyjnym, czyli miały dokładnie ten sam system operacyjny, biblioteki, konfigurację systemu, środowisko sieciowe i wszystko inne. Wolelibyśmy także, aby to środowisko nie zmieniało się zbytnio z upływem czasu, jeśli w ogóle. Dodatkowo, jeśli to możliwe, chcemy mieć możliwość dodawania aplikacji do tego samego serwera bez wpływania na dowolną z aplikacji istniejących już na tym serwerze.

1.1.3 Przechodzenie do ciągłego dostarczania: DevOps oraz NoOps

W ciągu ostatnich kilku lat można było zauważyć zmiany w całym procesie wytwarzania aplikacji i tym, jak zajmujemy się aplikacjami w środowisku produkcyjnym. W przeszłości zadaniem zespołu programistycznego było utworzenie aplikacji, po czym przekazanie go zespołowi operacyjnemu, który ją następnie wdrażał, dbał o nią i zapewniał jej działanie. Obecnie jednak organizacje rozumieją, że lepiej będzie, gdy ten sam zespół, który tworzy aplikację, bierze również udział w jej wdrażaniu i utrzymywaniu przez cały jej czas życia. Oznacza to, że zespoły deweloperskie, QA i operacji muszą obecnie współpracować w całym procesie.

Praktyka ta nosi nazwę DevOps.

KORZYŚCI

Zapewnienie większego zaangażowania programistów w działanie aplikacji w środowisku produkcyjnym prowadzi do uzyskania przez nich lepszego zrozumienia zarówno potrzeb użytkowników, jak i zagadnień i problemów, przed którymi stoi zespół operatorów podczas utrzymywania aplikacji. Twórcy aplikacji są teraz znacznie bardziej skłonni do wcześniejszego dostarczania jej użytkownikom, a następnie wykorzystania informacji zwrotnych przy dalszym rozwijaniu aplikacji.

Aby móc częściej publikować nowe wersje aplikacji, konieczne jest uporządkowanie procesu wytwarzania. W wariancie idealnym będziemy chcieli, aby programiści samodzielnie wdrażali aplikację, bez konieczności czekania na ludzi z zespołu operacyjnego. Jednak wdrożenie aplikacji często wymaga znajomości leżącej u podstaw infrastruktury oraz organizacji sprzętu w centrum danych. Programiści nie zawsze znają te szczegóły, a w większości przypadków wręcz nie chcą nic o tym wiedzieć.

POZWALANIE PROGRAMISTOM I ADMINISTRATOROM NA ROBIENIE TEGO, CO ROBIĄ NAJLEPIEJ

Mimo że programiści i administratorzy systemu pracują, dążąc do osiągnięcia tego samego celu, czyli sprawnego działania aplikacji jako usługi dla klientów, mają odmienne cele indywidualne i czynniki motywujące. Programiści lubią tworzyć nowe funkcje i poprawiać wrażenia użytkowników. Zazwyczaj nie będą chcieli być tymi, którzy się upewniają, że leżący u podstaw system operacyjny jest aktualizowany i ma zainstalowane wszystkie poprawki zabezpieczeń oraz inne podobne elementy. Wolą to pozostawić administratorom systemu.

Zespół operacyjny jest odpowiedzialny za wdrożenia produkcyjne oraz infrastrukturę sprzętową, na której one działają. Zajmują się bezpieczeństwem systemu, jego wykorzystaniem i innymi aspektami, które nie są priorytetowe dla programistów. Operatorzy nie chcą musieć sobie radzić z niejawnymi wzajemnymi zależnościami między wszystkimi komponentami aplikacji i nie chcą rozważać, jak zmiany, czy to systemu operacyjnego, czy też infrastruktury, mogą wpływać na działanie aplikacji jako całości – ale muszą to robić.

W idealnym wariancie chcielibyśmy, aby programiści samodzielnie wdrażali aplikacje bez konieczności znajomości infrastruktury sprzętowej i bez negocjacji z zespołem operacyjnym. Określa się to terminem NoOps. Oczywiście nadal potrzebujemy kogoś, kto będzie zajmował się infrastrukturą sprzętową, ale najlepiej bez konieczności radzenia sobie ze szczególnymi cechami każdej działającej na niej aplikacji.

Jak zobaczymy, Kubernetes pozwala nam osiągnąć wszystkie te cele. Dzięki abstrakcji rzeczywistego sprzętu i prezentowaniu go jako pojedynczej platformy dla wdrażania i uruchamiania aplikacji pozwala programistom konfigurować i wdrażać ich aplikacje bez żadnej pomocy ze strony administratorów systemów, a także pozwala administratorom skupić się na zapewnieniu sprawnego działania leżącej w tle infrastruktury, bez konieczności żadnej wiedzy o faktycznie działających w niej aplikacjach.

1.2 Przedstawiamy technologie kontenerowe

W podrozdziale 1.1 przedstawiłem niepełną listę problemów, przed którymi stoją współcześni programiści i zespoły operacyjne. Choć istnieje wiele sposobów radzenia sobie z tymi kwestiami, w tej książce skupimy się na tym, jak są one rozwiązywane przy użyciu Kubernetes.

Kubernetes używa technologii kontenerów systemu Linux w celu zapewnienia izolacji działających aplikacji, więc zanim zagłębimy się w samą technologię Kubernetes, trzeba poznać podstawy kontenerów, aby zrozumieć, co Kubernetes wykonuje samodzielnie, a co przekazuje technologiom kontenerowym, takim jak Docker.

1.2.1 Czym są kontenery

W punkcie 1.1.1 widzieliśmy, że różne komponenty oprogramowania działające na tej samej maszynie mogą wymagać różnych, potencjalnie sprzecznych ze sobą wersji wymaganych bibliotek lub w ogólności mogą mieć odmienne wymagania środowiskowe.

Gdy aplikacja składa się z tylko niewielkiej liczby dużych komponentów, w pełni akceptowalne jest przypisanie dedykowanej maszyny wirtualnej (Virtual Machine – VM) każdemu komponentowi i odizolowanie ich środowisk, udostępniając każdemu z nich jego własny system operacyjny. Jednak gdy te komponenty robią się coraz mniejsze, a ich liczba zaczyna rosnąć, nie możemy każdemu przydzielić jego własnej maszyny wirtualnej, jeśli nie chcemy marnować zasobów i utrzymać niskie koszty sprzętu. Jednak nie chodzi tylko o marnowanie zasobów sprzętowych. Ponieważ każda maszyna wirtualna zazwyczaj wymaga indywidualnej konfiguracji i zarządzania, zwiększanie liczby maszyn wirtualnych prowadzi również do marnowania zasobów ludzkich, gdyż znacząco zwiększa obciążenie pracą administratorów systemu.

IZOLOWANIE KOMPONENTÓW PRZY UŻYCIU TECHNOLOGII KONTENEROWYCH SYSTEMU LINUX

Zamiast używania maszyn wirtualnych w celu izolowania środowisk dla każdego mikroserwisu (lub w ogólności procesów oprogramowania) programiści zwracają się do technologii kontenerowych systemu Linux. Pozwalają one na uruchamianie wielu usług na tym samym hoście, jednocześnie nie tylko udostępniając każdej z nich inne środowisko, ale również izolując je od siebie nawzajem, analogicznie do maszyn wirtualnych, ale ze znacznie mniejszym narzutem.

Proces działający w kontenerze działa w macierzystym systemie operacyjnym, podobnie jak wszystkie inne procesy (w odróżnieniu od maszyn wirtualnych, gdzie procesy działają w oddzielnych systemach operacyjnych). Jednak proces w kontenerze nadal jest odizolowany od innych procesów. Z punktu widzenia procesu wygląda to tak, jakby był jedynym procesem działającym na tej maszynie, w swoim własnym systemie operacyjnym.

PORÓWNANIE MASZYN WIRTUALNYCH I KONTENERÓW

W porównaniu z maszynami wirtualnymi kontenery są znacznie lżejsze, co pozwala na uruchomienie większej liczby komponentów oprogramowania na tym samym sprzęcie, głównie dlatego, że każda maszyna wirtualna musi uruchamiać swój własny zestaw procesów systemowych, co wymaga dodatkowych zasobów obliczeniowych oprócz tych, które zużywa własny proces komponentu. Jednocześnie kontener nie jest niczym więcej niż pojedynczym, izolowanym procesem działającym w macierzystym systemie operacyjnym, zużywającym tylko te zasoby, które konsumuje aplikacja i bez narzutu jakichkolwiek dodatkowych procesów.

Ze względu na dodatkowe obciążenie w maszynach wirtualnych często jesteśmy zmuszeni grupować wiele aplikacji w każdej maszynie wirtualnej, gdyż nie mamy dość zasobów, aby dedykować całą maszynę wirtualną każdej aplikacji. Przy używaniu kontenerów możemy (i powinniśmy) mieć po jednym kontenerze na każdą aplikację, co pokazuje rysunek 1.4. Efekt jest taki, że możemy zmieścić znacznie więcej aplikacji na tej samej maszynie fizycznej.

Rysunek 1.4 Korzystanie z maszyn wirtualnych do izolowania grup aplikacji kontra izolowanie indywidualnych aplikacji przy użyciu kontenerów

Gdy w macierzystym systemie działają trzy maszyny wirtualne, mamy do czynienia z trzema zupełnie odrębnymi systemami operacyjnymi działającymi na tym samym sprzęcie fizycznym i go współużytkującymi. Poniżej tych maszyn wirtualnych znajduje się system operacyjny hosta i hiperwizor, który rozdziela zasoby fizycznego sprzętu na mniejsze zestawy zasobów wirtualnych, używane przez systemy operacyjne wewnątrz każdej maszyny wirtualnej. Aplikacje działające w tych maszynach wirtualnych wykonują wywołania systemowe do jądra systemu operacyjnego gościa w maszynie wirtualnej, jądro to wykonuje zaś instrukcje x86 na fizycznym procesorze hosta za pośrednictwem hiperwizora.

UWAGA Istnieją dwa typy hiperwizorów. Hiperwizory typu 1 nie używają systemu operacyjnego hosta, podczas gdy te typu 2 to robią.

Jednocześnie kontenery wykonują wywołania systemowe wobec tego samego jądra działającego w systemie operacyjnym hosta. To pojedyncze jądro jest jedynym, które wykonuje instrukcje x86 w procesorze hosta. Procesor nie potrzebuje żadnego rodzaju wirtualizacji, jaką wykorzystuje w przypadku maszyn wirtualnych (rysunek 1.5).

Rysunek 1.5 Różnica między tym, jak maszyny wirtualne używają procesora, a tym, jak jest on używany w kontenerach

Główną zaletą maszyn wirtualnych jest zapewniana przez nie pełna izolacja, gdyż w każdej maszynie wirtualnej działa jej własne jądro Linux, podczas gdy wszystkie kontenery wywołują to samo jądro, co oczywiście stwarza ryzyko zabezpieczeń. Jeśli mamy ograniczoną ilość zasobów sprzętowych, maszyny wirtualne mogą być opcją tylko wtedy, gdy mamy niewielką liczbę procesów, które chcemy izolować. Dla uruchamiania większej liczby izolowanych procesów na tej samej maszynie kontenery są znacznie lepszym wyborem ze względu na mniejszy narzut. Przypomnijmy, że w każdej maszynie wirtualnej działa jej własny zestaw usług systemowych, a w kontenerach nie, gdyż wszystkie one działają w tym samym systemie operacyjnym. Oznacza to również, że aby uruchomić kontener, nie potrzeba żadnego rozruchu jak w przypadku maszyny wirtualnej. Proces działający w kontenerze uruchamia się natychmiast.

MECHANIZMY UMOŻLIWIAJĄCE IZOLACJĘ KONTENERÓW

W tym momencie Czytelnik zapewne zastanawia się, jak kontenery mogą izolować procesy, jeśli działają one w tym samym systemie operacyjnym. Dwa mechanizmy sprawiają, że jest to możliwe. Pierwszy z nich, przestrzenie nazw (Linux Namespaces), gwarantują, że każdy proces ma swój osobisty widok systemu (pliki, procesy, interfejsy sieciowe, nazwę hosta itd.). Drugim są grupy kontroli (Linux Control Groups – cgroups) ograniczające ilość zasobów, które może zużyć dany proces (procesor, pamięć, pasmo sieciowe itd.).

IZOLOWANIE PROCESÓW PRZY UŻYCIU PRZESTRZENI NAZW LINUKSA

Domyślnie każda instalacja Linuksa zawiera początkowo tylko jedną przestrzeń nazw. Wszystkie zasoby systemu, takie jak systemy plików, identyfikatory procesów, identyfikatory użytkowników, interfejsy sieciowe i inne, należą do pojedynczej przestrzeni nazw. Możemy jednak tworzyć dodatkowe przestrzenie nazw i porządkować w nich zasoby. Przy tworzeniu procesu można uruchomić go w jednej z tych przestrzeni nazw. Proces będzie widział tylko te zasoby, które znajdują się w tej samej przestrzeni nazw. W istocie jednak istnieje kilka rodzajów przestrzeni nazw, zatem proces nie należy do jednej przestrzeni nazw, ale do jednej z każdego rodzaju.

Istnieją następujące rodzaje przestrzeni nazw:

■ zamontowanych systemów plików (Mount – mnt),

■ identyfikatorów procesów (Process ID – PID),

■ zasobów sieciowych (Network – net),

■ komunikacji międzyprocesowej (Inter-process communication – IPC),

■ stacji roboczej i domeny (UTS),

■ identyfikatorów użytkowników (User ID – user).

Każdy rodzaj przestrzeni nazw służy do izolowania określonej grupy zasobów. Na przykład przestrzeń nazw UTS określa, jaka jest nazwa hosta i domeny, którą widzi proces działający wewnątrz tej przestrzeni nazw. Przydzielając dwie różne przestrzenie nazw UTS parze procesów, można sprawić, że każdy z nich będzie postrzegać inną nazwę lokalnego hosta. Innymi słowy, z punktu widzenia tych procesów będzie to wyglądało tak, jakby działały one na dwóch różnych maszynach (przynajmniej jeśli chodzi o nazwę hosta).

Analogicznie, to, do której przestrzeni nazw zasobów sieciowych należy proces, określa, jakie interfejsy sieciowe będzie widzieć aplikacja działająca w tym procesie. Każdy interfejs sieciowy należy do dokładnie jednej przestrzeni nazw, ale może być przenoszony z jednej przestrzeni nazw do innej. Każdy kontener używa swojej własnej przestrzeni nazw zasobów sieciowych, a tym samym każdy widzi swój własny zbiór interfejsów sieciowych.

Powinno to zapewnić podstawowe wyobrażenie tego, jak przestrzenie nazw są wykorzystywane do izolowania od siebie aplikacji działających w kontenerach.

OGRANICZANIE ZASOBÓW DOSTĘPNYCH DLA PROCESU

Druga połowa izolacji kontenerów zajmuje się ograniczaniem zasobów systemowych, które może zużyć kontener. Jest to osiągane przez użycie cgroups, funkcji jądra Linuksa, która ogranicza wykorzystanie zasobów przez proces (albo grupę procesów). Proces nie może użyć większej niż skonfigurowana ilości czasu procesora, pamięci, pasma sieciowego i tak dalej. W ten sposób procesy nie mogą zawłaszczać zasobów zarezerwowanych dla innych procesów, co jest podobne do sytuacji, gdy każdy proces działa na odrębnej maszynie.

1.2.2 Platforma kontenerów Docker

Choć technologie kontenerowe obecne są już od dłuższego czasu, stały się szerzej znane wraz z pojawieniem się platformy kontenerowej Docker. Platforma Docker była pierwszym systemem kontenerów, który sprawił, że kontenery stały się łatwo przenośne między różnymi maszynami. Uprościła proces pakowania razem nie tylko aplikacji, ale również jej wszystkich bibliotek i innych elementów wymaganych, a nawet całego systemu plików systemu operacyjnego, w prosty, przenośny pakiet, który może zostać użyty w celu wdrożenia aplikacji na dowolnej innej maszynie, na której działa Docker.

Gdy uruchamiamy aplikację spakowaną przy użyciu Dockera, widzi ona dokładnie tę zawartość systemu plików, którą do niej dołączyliśmy. Widzi te same pliki bez względu na to, czy jest uruchomiona na komputerze programisty, czy na maszynie produkcyjnej, nawet jeśli serwer produkcyjny używa zupełnie innej wersji systemu operacyjnego Linux. Aplikacja nie zobaczy niczego z samego serwera, na którym jest uruchomiona, zatem nie jest istotne, jeśli serwer ma zupełnie inny zestaw zainstalowanych bibliotek w porównaniu do maszyny programisty.

Jeśli na przykład spakujemy naszą aplikację z plikami całego systemu operacyjnego Red Hat Enterprise Linux (RHEL), aplikacja będzie przekonana, że działa wewnątrz RHEL, zarówno wtedy, gdy uruchomimy ją na swoim komputerze z systemem Fedora, jak i wtedy, gdy zostanie uruchomiona na serwerze z Debianem lub z jakąś inną dystrybucją Linuksa. Jedynie jądro może być inne.

Jest to analogiczne do tworzenia obrazu maszyny wirtualnej przez zainstalowanie systemu operacyjnego w maszynie wirtualnej, zainstalowanie w nim aplikacji, a następnie dystrybucji całego obrazu maszyny wirtualnej i jego uruchamiania. Docker zapewnia ten sam efekt, ale zamiast wykorzystywania maszyn wirtualnych dla osiągnięcia izolacji aplikacji, używa wspomnianych w poprzednim punkcie technologii kontenerów Linux w celu zapewnienia (niemal) tego samego poziomu izolacji co w przypadku maszyn wirtualnych. Zamiast użyć wielkich, monolitycznych obrazów maszyn wirtualnych, wykorzystuje obrazy kontenerów, które zazwyczaj są mniejsze.

Wielka różnica między obrazami kontenerów Dockera a obrazami maszyny wirtualnej polega na tym, że obrazy kontenerów są złożone z warstw, które mogą być współdzielone i współużytkowane przez wiele obrazów. Oznacza to, że tylko określone warstwy obrazu muszą zostać pobrane, jeśli inne warstwy zostały już wcześniej pobrane przy uruchamianiu innego obrazu kontenera, który również zawiera te same warstwy.

KONCEPCJE DOCKERA

Docker jest platformą pakowania, dystrybuowania i uruchamiania aplikacji. Jak wspomnieliśmy już, pozwala spakować aplikację wraz z jej całym środowiskiem. Może to być kilka bibliotek wymaganych przez aplikację, ale nawet wszystkie pliki, które są zwykle dostępne w systemie plików zainstalowanego systemu operacyjnego. Docker sprawia, że możliwe jest przesłanie takiego pakietu do centralnego repozytorium, z którego może on zostać następnie przekazany do dowolnego komputera z uruchomionym Dockerem i tam wykonany (w większości przypadków, choć nie zawsze, jak wyjaśnimy za chwilę).

Na ten scenariusz składają się trzy główne koncepcje Dockera:

■ Obrazy – Obraz kontenera Dockera jest czymś, do czego pakujemy naszą aplikację i jej środowisko. Zawiera system plików, który będzie dostępny dla aplikacji, oraz inne metadane, takie jak ścieżka do pliku wykonywalnego, który ma zostać wywołany przy uruchamianiu obrazu.

■ Rejestry – Rejestr Dockera to repozytorium przechowujące obrazy Dockera i umożliwiające łatwe udostępnianie tych obrazów różnym ludziom i komputerom. Po zbudowaniu obrazu możemy albo uruchomić go na tym komputerze, na którym go zbudowaliśmy, albo go wypchnąć (push), czyli załadować do rejestru, a następnie ściągnąć (pull), czyli pobrać na inny komputer i tam uruchomić. Pewne rejestry są publiczne i pozwalają każdemu na ściąganie z nich obrazów, podczas gdy inne są prywatne, dostępne tylko dla określonych osób lub maszyn.

■ Kontenery – Kontener Dockera to zwykły kontener Linux, utworzony z obrazu kontenera Dockera. Uruchomiony kontener to proces działający na komputerze z uruchomionym Dockerem, ale całkowicie odizolowany zarówno od samego hosta, jak i od wszystkich innych uruchomionych w nim procesów. Proces ma też ograniczone zasoby, co oznacza, że może uzyskiwać dostęp i zużyć tylko taką ilość zasobów (procesora, pamięci RAM itd.), która została mu przydzielona.

BUDOWANIE, DYSTRYBUOWANIE I URUCHAMIANIE OBRAZU DOCKERA

Rysunek 1.6 pokazuje wszystkie trzy koncepcje i ich wzajemne powiązania. Najpierw programista buduje obraz i wypycha go do rejestru. Obraz jest wówczas osiągalny dla wszystkich, którzy mają dostęp do rejestru. Mogą oni następnie ściągnąć obraz na dowolną inną maszynę z działającym Dockerem i uruchomić obraz. Docker tworzy izolowany kontener na podstawie obrazu i uruchamia binarny plik wykonywalny wyspecyfikowany jako część tego obrazu.

Rysunek 1.6 Obrazy, rejestry i kontenery Dockera

PORÓWNYWANIE MASZYN WIRTUALNYCH I KONTENERÓW DOCKERA

Wyjaśniłem już, że linuksowe kontenery są zasadniczo podobne do maszyn wirtualnych, ale znacznie lżejsze. Przyjrzyjmy się teraz, jak wygląda porównanie kontenerów Dockera z maszynami wirtualnymi (jak obrazy Dockera mają się do obrazów maszyny wirtualnej). Rysunek 1.7 ponownie pokazuje tych samych sześć aplikacji uruchomionych w maszynach wirtualnych i jako kontenery Dockera.

Rysunek 1.7 Uruchamianie sześciu aplikacji w trzech maszynach wirtualnych a uruchamianie ich w kontenerach Dockera

Możemy zauważyć, że aplikacje A i B mają dostęp do tych samych plików binarnych i bibliotek zarówno wtedy, gdy są uruchomione w maszynie wirtualnej, jak i wtedy, gdy działają jako dwa oddzielne kontenery. W przypadku maszyny wirtualnej jest to oczywiste, gdyż obie aplikacje widzą swój własny system plików (należący do maszyny wirtualnej). Powiedzieliśmy jednak, że każdy kontener ma swój własny, izolowany system plików. Jak obie aplikacje A i B mogą dzielić te same pliki?

Istota warstw obrazów

Wspomniałem wcześniej, że obrazy Dockera składają się z warstw. Różne obrazy mogą zawierać dokładnie te same warstwy, gdyż każdy obraz Dockera jest budowany na podstawie innego obrazu i dwa różne obrazy mogą używać jako podstawy tego samego obrazu macierzystego. Przyśpiesza to dystrybucję obrazów w sieci, gdyż warstwy już przesłane jako część pierwszego obrazu nie muszą być przesyłane ponownie podczas transferu drugiego obrazu.

Jednak warstwy nie tylko poprawiają wydajność dystrybucji. Pomagają również zmniejszyć objętość pamięciową obrazów. Każda warstwa jest przechowywana tylko raz. Dwa kontenery utworzone z dwóch obrazów opartych na tych samych warstwach bazowych mogą zatem odczytywać te same pliki, ale jeśli jeden z nich dokona zapisu do tych plików, drugi nie zobaczy tych zmian. Tak więc nawet jeśli współużytkują pliki, nadal są izolowane od siebie nawzajem. Mechanizm ten działa, gdyż warstwy obrazu kontenera są tylko do odczytu. Przy uruchamianiu kontenera na szczycie warstw obrazu tworzona jest nowa, zapisywalna warstwa. Gdy proces w kontenerze wykonuje zapis do pliku zlokalizowanego w jednej z głębszych warstw, w najwyższej warstwie tworzona jest kopia całego pliku i operacja zapisu jest wykonywana na tej kopii.

OGRANICZENIA PRZENOŚNOŚCI OBRAZÓW KONTENERÓW

W teorii obraz kontenera może zostać uruchomiony na dowolnym komputerze z systemem Linux i uruchomionym Dockerem, istnieje jednak mała pułapka związana z tym, że wszystkie uruchomione kontenery używają jądra Linuksa należącego do hosta. Jeśli skonteneryzowana aplikacja wymaga konkretnej wersji jądra, może nie działać na każdej maszynie. Jeśli komputer używa innej wersji jądra Linuksa albo nie ma dostępnych tych samych modułów jądra, aplikacja nie będzie mogła na nim działać.

Wprawdzie kontenery są znacznie lżejsze w porównaniu do maszyn wirtualnych, narzucają określone ograniczenia na uruchamiane w nich aplikacje. Maszyny wirtualne nie mają takich ograniczeń, gdyż każda maszyna wirtualna ma swoje własne jądro.

Nie dotyczy to tylko jądra. Powinno być jasne, że kontenerowa aplikacja zbudowana dla określonej architektury sprzętowej może działać tylko na innych maszynach o tej samej architekturze. Nie możemy skonteneryzować aplikacji zbudowanej dla architektury x86 i oczekiwać, że będzie działać na komputerze opartym na ARM, gdyż również działa w nim Docker. Do tego będziemy potrzebować maszyny wirtualnej.

1.3 Wprowadzenie do Kubernetes

Pokazaliśmy już, że gdy rośnie liczba możliwych do wdrożenia komponentów aplikacji naszego systemu, coraz trudniejsze staje się zarządzanie nimi wszystkimi. Google było zapewne pierwszą firmą, która zrozumiała, że potrzebuje znacznie lepszego sposobu wdrażania i zarządzania swoimi komponentami oprogramowania i ich infrastrukturą na globalną skalę. Na świecie jest tylko kilka firm, które uruchamiają setki tysięcy serwerów i muszą sobie radzić z zarządzaniem wdrożeń na tak ogromną skalę. Zmusiło to ich do wypracowania rozwiązań sprawiających, że tworzenie i wdrażanie tysięcy komponentów oprogramowania stanie się możliwe do zarządzania i opłacalne.

1.3.1 Pochodzenie

W ciągu lat Google opracowała wewnętrzny system o nazwie Borg (a później nowy system nazywany Omega), który pomagał zarówno twórcom aplikacji, jak i administratorom systemu zarządzać tymi tysiącami aplikacji i usług. Oprócz uproszczenia programowania i zarządzania, pomagał również osiągnąć znacznie wyższe wykorzystanie infrastruktury, co jest ważne, gdy organizacja jest tak wielka. Gdy wykorzystujemy setki tysięcy maszyn, nawet nieznaczne usprawnienia w wykorzystaniu oznaczają oszczędności liczone w milionach dolarów, zatem bodźce dla opracowania takiego systemu są oczywiste.

Systemy Borg i Omega były utrzymywane w tajemnicy przez całą dekadę, ale w 2014 roku Google przedstawiło Kubernetes, system open-source oparty na doświadczeniach, które firma zdobyła dzięki Borg, Omedze i innym wewnętrznym systemom.

1.3.2 Ogólny obraz Kubernetes

Kubernetes jest systemem oprogramowania, który pozwala na łatwe wdrażanie i zarządzanie działającymi w nim skonteneryzowanymi aplikacjami. Opiera się na funkcjonalnościach kontenerów Linux w celu uruchamiania heterogenicznych aplikacji bez konieczności znajomości żadnych wewnętrznych szczegółów tych aplikacji i bez potrzeby ręcznego instalowania tych aplikacji na każdym hoście. Ponieważ aplikacje te działają w kontenerach, nie wpływają na inne aplikacje uruchomione na tym samym serwerze, co jest krytyczne, gdy uruchamiamy aplikacje dla zupełnie różnych organizacji na tym samym sprzęcie. Ma to ogromne znaczenie dla dostawców chmurowych, gdyż dążą oni do najlepszego możliwego wykorzystania swojego sprzętu przy konieczności utrzymania pełnej izolacji hostowanych aplikacji.

Kubernetes pozwala uruchamiać aplikacje na tysiącach komputerowych węzłów, tak jakby te wszystkie węzły stanowiły pojedynczy, gigantyczny komputer. Zapewnia abstrakcję całej leżącej w tle infrastruktury, a dzięki temu upraszcza programowanie, wdrażanie i zarządzanie, zarówno dla zespołów programistycznych, jak i operacyjnych.

Wdrażanie aplikacji za pośrednictwem Kubernetes zawsze odbywa się tak samo, bez względu na to, czy klaster zawiera tylko kilka węzłów, czy ich tysiące. Rozmiar klastra nie robi żadnej różnicy. Dodatkowe węzły klastra oznaczają po prostu dodatkową ilość zasobów dostępnych dla wdrażanych aplikacji.

KLUCZOWE ZADANIA WYKONYWANE PRZEZ KUBERNETES

Rysunek 1.8 przedstawia najprostszy możliwy obraz systemu Kubernetes. System składa się z węzła master (głównego) i dowolnej liczby węzłów roboczych (worker). Gdy programista wysyła listę aplikacji do węzła master, Kubernetes rozpowszechnia je w klastrze węzłów roboczych. To, na którym węźle wyląduje dany komponent, nie ma (i nie powinno mieć) znaczenia – ani dla programisty, ani administratora systemu.

Rysunek 1.8 Kubernetes eksponuje całe centrum danych jako pojedynczą platformę wdrożenia

Programista może określić, że pewne aplikacje muszą działać razem, a Kubernetes rozmieści je na tym samym węźle roboczym. Inne zostaną rozproszone po całym klastrze, ale będą mogły porozumiewać się ze sobą w taki sam sposób, niezależnie od tego, gdzie zostały wdrożone.

POMAGANIE PROGRAMISTOM W SKUPIENIU SIĘ NA KLUCZOWYCH FUNKCJACH APLIKACJI

O Kubernetes można myśleć jako o systemie operacyjnym dla klastra. Uwalnia programistów aplikacji od konieczności implementowania w ich aplikacjach określonych usług dotyczących infrastruktury; zamiast tego mogą oni polegać na dostarczeniu tych usług przez Kubernetes. Obejmuje to takie zagadnienia, jak odkrywanie usług, skalowanie, równoważenie obciążeń, samonaprawę, a nawet wybór lidera. Twórcy aplikacji mogą się zatem skoncentrować na implementowaniu rzeczywistej funkcjonalności aplikacji i nie tracą czasu na ustalanie, jak zintegrować je z infrastrukturą.

POMAGANIE ZESPOŁOM OPERACYJNYM W OSIĄGNIĘCIU LEPSZEGO WYKORZYSTANIA ZASOBÓW

Kubernetes będzie uruchamiać nasze skonteneryzowane aplikacje gdzieś w klastrze, udostępniać komponentom informacje potrzebne do odszukiwania się nawzajem i zapewni działanie ich wszystkich. Jako że aplikacja nie dba o to, na którym węźle jest uruchomiona, Kubernetes może wykonać jej relokację w dowolnej chwili i przez mieszanie i dopasowywanie aplikacji osiągnąć znacznie lepsze wykorzystanie zasobów, niż byłoby możliwe przy ręcznym rozmieszczaniu.

1.3.3 Architektura klastra Kubernetes

Widzieliśmy już ogólny obraz architektury Kubernetes. Przyjrzyjmy się teraz bliżej temu, z czego składa się klaster Kubernetes. Na poziomie sprzętu klaster Kubernetes zbudowany jest z wielu węzłów, które można podzielić na dwa typy:

■ Master (węzeł główny), który utrzymuje warstwę sterowania Kubernetes (Kubernetes Control Plane), kontrolującą cały system Kubernetes i nim zarządzającą.

■ Węzły robocze (worker nodes), na których działają rzeczywiste, wdrożone aplikacje.

Rysunek 1.9 pokazuje komponenty działające w tych dwóch zbiorach węzłów. Omówimy je w kolejnych podpunktach.

Rysunek 1.9 Komponenty tworzące klaster Kubernetes

WARSTWA STEROWANIA

Warstwa sterowania kontroluje klaster i zapewnia jego funkcjonowanie. Składa się z wielu komponentów, które mogą działać na pojedynczym węźle głównym albo zostać rozproszone między wiele węzłów i zreplikowane w celu zapewnienia wysokiej dostępności. Te komponenty to:

■ Serwer API Kubernetes, który zapewnia komunikację z operatorem (czyli nami) i innymi komponentami warstwy sterowania.

■ Scheduler (dyspozytor), który rozmieszcza nasze aplikacje (przydziela węzeł roboczy każdemu możliwemu do wdrożenia komponentowi aplikacji).

■ Controller Manager (menedżer sterowania), który wykonuje funkcje poziomu klastra, takie jak replikowanie komponentów, śledzenie węzłów roboczych, obsługa awarii węzłów i tak dalej.

■ etcd, niezawodny rozproszony magazyn danych, który trwale przechowuje konfigurację klastra.

Komponenty warstwy sterowania przechowują i kontrolują stan klastra, ale nie uruchamiają naszych aplikacji. To zadanie jest realizowane przez węzły (robocze).

WĘZŁY

Węzły robocze to komputery, na których działają nasze skonteneryzowane aplikacje. Zadanie uruchamiania, monitorowania i udostępniania usług tym aplikacjom jest realizowane przez następujące komponenty:

■ Docker, rkt albo inne środowisko wykonawcze kontenerów, w którym działają nasze kontenery.

■ Kubelet, który komunikuje się z serwerem API i zarządza kontenerami w swoim węźle.

■ Usługa proxy Kubernetes (kube-proxy), która równoważy obciążenie ruchem sieciowym między komponentami aplikacji.

Wszystkie te komponenty omówimy szczegółowo w rozdziale 11. Nie jestem zwolennikiem wyjaśniania, jak coś działa, bez wcześniejszego wyjaśnienia, co to robi, i pokazania, jak należy tego używać. Przypomina to naukę jazdy samochodem. Nie chcemy wiedzieć, co znajduje się pod maską. Najpierw chcemy się nauczyć, jak dojechać z punktu A do punktu B. Dopiero gdy nauczymy się, jak to zrobić, możemy zainteresować się tym, jak samochód to umożliwia. Ostatecznie poznanie tego, co kryje się pod maską, może pewnego dnia pomóc w ponownym uruchomieniu samochodu, gdy się popsuje i znajdziemy się bezradni na poboczu drogi.

1.3.4 Uruchamianie aplikacji w Kubernetes

Aby uruchomić aplikację w Kubernetes, musimy najpierw zapakować ją w jeden lub więcej obrazów kontenerów, wypchnąć te obrazy do rejestru, po czym wysłać opis naszej aplikacji do serwera API Kubernetes.

Opis ten zawiera takie informacje, jak obraz lub obrazy kontenerów zawierające komponenty naszej aplikacji, wzajemne powiązania między tymi komponentami oraz to, które muszą być kolokowane (umieszczone wspólnie na tym samym węźle), a które nie muszą. Dla każdego komponentu możemy również wyspecyfikować liczbę kopii (replik), które chcemy uruchomić. Dodatkowo opis zawiera informacje, które z tych komponentów udostępniają usługi dla wewnętrznych lub zewnętrznych klientów, a więc powinny być eksponowane przez pojedynczy adres IP i wykrywalne przez inne komponenty.

JAK OPIS POWODUJE URUCHOMIENIE KONTENERA

Gdy serwer API przetwarza opis aplikacji, Scheduler rozmieszcza wskazane grupy kontenerów między dostępne węzły robocze, opierając się zasobach obliczeniowych wymaganych przez każdą grupę oraz na dostępnych (nieprzydzielonych) w tym momencie zasobach każdego węzła. Kubelet na tych węzłach nakazuje następnie środowisku wykonawczemu kontenerów (np. Dockerowi) ściągnięcie wymaganych obrazów kontenerów i uruchomienie tych kontenerów.

Przyjrzyjmy się rysunkowi 1.10, aby uzyskać lepsze pojęcie o tym, jak aplikacje są wdrażane w Kubernetes. Deskryptor aplikacji wylicza cztery kontenery zgrupowane w trzy zbiory (zbiory te noszą nazwę podów; czym są, wyjaśnimy w rozdziale 3). Pierwsze dwa pody zawierają po jednym kontenerze, ostatni zaś zawiera dwa. Oznacza to, że oba kontenery muszą być kolokowane i nie powinny być izolowane od siebie nawzajem. Obok każdego podu widzimy liczbę reprezentującą ilość replik każdego podu, które muszą działać równolegle. Po wysłaniu deskryptora do Kubernetes rozplanuje on wskazaną liczbę replik każdego podu między dostępne węzły robocze. Kubelety w tych węzłach nakażą następnie Dockerowi ściągnąć obrazy kontenerów z rejestru obrazów i uruchomić kontenery.

Rysunek 1.10 Podstawowy przegląd architektury Kubernetes i uruchomionej w nim aplikacji

UTRZYMYWANIE DZIAŁANIA KONTENERÓW

Gdy aplikacja już działa, Kubernetes stale zapewnia, że wdrożony stan aplikacji zawsze pasuje do dostarczonego opisu. Jeśli na przykład określimy, że zawsze potrzebujemy działających pięciu wystąpień serwera WWW, Kubernetes zawsze będzie utrzymywać dokładnie pięć uruchomionych wystąpień. Jeśli któreś z tych wystąpień przestanie działać poprawnie, jak w przypadku awarii procesu lub gdy przestanie odpowiadać, Kubernetes wykona jej automatyczny restart.

Analogicznie, jeśli cały węzeł roboczy padnie lub stanie się niedostępny, Kubernetes wybierze nowe węzły dla wszystkich kontenerów, które działały na tym węźle, i uruchomi je na nowo wybranych węzłach.

SKALOWANIE LICZBY KOPII

Gdy aplikacja już działa, możemy zdecydować, że chcemy zwiększyć lub zmniejszyć liczbę kopii, a Kubernetes odpowiednio wystartuje dodatkowe lub zatrzyma nadmiarowe. Możemy nawet pozostawić Kubernetes zadanie wyboru optymalnej liczby kopii. Będzie wówczas automatycznie dopasowywać liczbę kopii na podstawie miar czasu rzeczywistego, takich jak obciążenie procesora, zużycie pamięci, zapytania na sekundę lub dowolnej innej miary eksponowanej przez naszą aplikację.

OSIĄGANIE RUCHOMEGO CELU

Powiedzieliśmy, że Kubernetes może potrzebować przenosić nasze kontenery wewnątrz klastra. Może to nastąpić, gdy węzeł, na którym działały, uległ awarii albo dlatego że zostały one usunięte z węzła w celu przygotowania miejsca na inne kontenery. Jeśli kontener udostępnia usługę zewnętrznym klientom lub innym kontenerom działającym w klastrze, jak mogą one poprawnie korzystać z tego kontenera, jeśli jest stale przenoszony wewnątrz klastra? I jak klienci mogą łączyć się z kontenerami udostępniającymi usługi, gdy kontenery te są zreplikowane i rozproszone po całym klastrze?

Aby umożliwić klientom łatwe odszukanie kontenerów udostępniających określoną usługę, możemy poinformować Kubernetes, które kontenery udostępniają tę samą usługę, a Kubernetes będzie eksponować je wszystkie pod jednym, statycznym adresem IP i udostępni ten adres wszystkim aplikacjom działającym w klastrze. Jest to realizowane przy użyciu zmiennych środowiskowych, ale klienci mogą również szukać adresu IP usługi przy użyciu starego, dobrego DNS. Komponent kube-proxy zapewni, że połączenia do usługi będą rozdzielane między wszystkie kontenery udostępniające tę usługę. Adres IP usługi pozostaje niezmienny, zatem klienci zawsze mogą połączyć się z jej kontenerami, nawet jeśli zostaną one przeniesione wewnątrz klastra.

1.3.5 Korzyści z używania Kubernetes

Jeśli wdrożymy Kubernetes na wszystkich naszych serwerach, zespół operacyjny nie będzie musiał więcej zajmować się wdrażaniem naszych aplikacji. Jako że skonteneryzowane aplikacje już zawierają wszystko, czego potrzebują do działania, administratorzy systemu nie muszą instalować czegokolwiek, aby wdrożyć i uruchomić aplikację. Na każdym węźle, na którym wdrożony jest Kubernetes, może on uruchamiać aplikacje natychmiast bez żadnej pomocy ze strony administratorów.

UPRASZCZANIE WDRAŻANIA APLIKACJI

Ponieważ Kubernetes eksponuje wszystkie węzły robocze jako pojedynczą platformę wdrożeniową, programiści aplikacji mogą zacząć samodzielnie wdrażać aplikacje i nie muszą nic wiedzieć o serwerach, które tworzą klaster.

Zasadniczo wszystkie węzły stanowią teraz pojedynczy zbiór zasobów obliczeniowych, czekających na ich skonsumowanie przez aplikacje. Programista zazwyczaj nie zajmuje się tym, na jakiego typu serwerze działa aplikacja, o ile tylko ten serwer może zapewnić aplikacji potrzebne zasoby systemowe.

Istnieją szczególne przypadki, w których programista jest zainteresowany rodzajem sprzętu, na którym aplikacja powinna działać. Jeśli węzły są heterogeniczne, możemy znaleźć się w sytuacji, w których będziemy chcieli, aby pewne aplikacje działały na węzłach o określonych możliwościach, a pozostałe na innych węzłach. Dla przykładu jedna z naszych aplikacji może wymagać uruchomienia na komputerze wyposażonym w dyski SSD, a nie HDD, podczas gdy inne będą świetnie działać na HDD. W takich przypadkach będziemy oczywiście chcieli zagwarantować, że ta wybrana aplikacja zawsze zostanie przydzielona do węzła wyposażonego w SSD.

Bez korzystania z Kubernetes administrator systemu wybrałby jeden konkretny węzeł, który zawiera SSD, aby wdrożyć na nim aplikację. Jednak przy używaniu Kubernetes, zamiast wybierania określonego węzła, na którym aplikacja ma działać, właściwe będzie nakazanie Kubernetes, aby wybierał tylko spośród węzłów wyposażonymi w SSD. Jak to zrobić, pokażemy w rozdziale 3.

UZYSKIWANIE LEPSZEGO WYKORZYSTANIA SPRZĘTU

Przez skonfigurowanie Kubernetes na naszych serwerach i wykorzystanie go do uruchamiania aplikacji zamiast ich ręcznego uruchamiania, możemy odłączyć aplikację od infrastruktury. Gdy nakazujemy Kubernetes uruchomienie aplikacji, pozostawiamy mu wybór najbardziej odpowiedniego węzła na podstawie opisu wymagań zasobów aplikacji i zasobów dostępnych w każdym węźle.

Dzięki używaniu kontenerów i nieprzywiązywaniu aplikacji do konkretnego węzła w klastrze pozwalamy na swobodne przenoszenie aplikacji po całym klastrze w dowolnym momencie, tak że komponenty różnych aplikacji działających w klastrze mogą być mieszane i dopasowywane, aby uzyskać ścisłe upakowanie na węzłach klastra. Gwarantuje to, że zasoby sprzętowe węzła będą wykorzystane tak dobrze, jak to możliwe.

Zdolność do przenoszenia aplikacji wewnątrz klastra w dowolnym momencie pozwala Kubernetes na znacznie lepsze wykorzystanie infrastruktury, niż możliwe jest to do osiągnięcia ręcznie. Ludzie nie radzą sobie najlepiej z wyszukiwaniem optymalnych kombinacji, szczególnie wtedy, gdy liczba wszystkich możliwych opcji jest bardzo wielka, jak w przypadku gdy mamy wiele komponentów aplikacji i wiele węzłów serwerów, na których można je rozmieszczać. Komputery zdecydowanie wykonują tę pracę znacznie lepiej i szybciej niż ludzie.

SPRAWDZANIE KONDYCJI I SAMOUZDRAWIANIE

Dysponowanie systemem, w którym możliwe jest przenoszenie aplikacji wewnątrz klastra w dowolnym czasie, jest cenne również w razie awarii serwera. W miarę jak rośnie rozmiar klastra, musimy jeszcze częściej sobie radzić z awariami komputerów.

Kubernetes monitoruje komponenty aplikacji oraz węzły, na których są uruchomione, i automatycznie przekierowuje je do innych węzłów w razie awarii węzła. Uwalnia to zespół operacyjny od konieczności ręcznego migrowania komponentów aplikacji i pozwala mu natychmiast skupić się na naprawie węzła i przywróceniu go do puli dostępnych zasobów, zamiast zajmować się relokowaniem aplikacji.

Jeśli nasza infrastruktura ma dostatecznie nadmiarowe zasoby, aby pozwolić na normalne działanie systemu nawet przy popsutym węźle, zespół operacyjny nie musi nawet natychmiast reagować na awarię, na przykład o trzeciej nad ranem. Mogą spać spokojnie i zająć się zepsutym węzłem w normalnych godzinach pracy.

AUTOMATYCZNE SKALOWANIE

Wykorzystanie Kubernetes do zarządzania wdrożonymi aplikacjami oznacza też, że zespół operacyjny nie musi stale monitorować obciążenia indywidualnych aplikacji, aby reagować na nagłe wzrosty obciążenia. Jak wspomniano wcześniej, można nakazać Kubernetes monitorowanie zasobów używanych przez każdą aplikację i dostosowywać liczbę uruchomionych wystąpień każdej aplikacji.

Jeśli Kubernetes działa w infrastrukturze chmurowej, gdzie dołączenie dodatkowych węzłów jest tak proste, jak zażądanie ich przez API dostawcy chmurowego, Kubernetes może nawet automatycznie przeskalować cały klaster w górę lub w dół, opierając się na wymaganiach wdrożonych aplikacji.

UPRASZCZANIE WYTWARZANIA APLIKACJI

Cechy opisane w poprzednim punkcie są korzystne głównie dla zespołu operacyjnego. Ale co z programistami? Czy Kubernetes daje im cokolwiek? Zdecydowanie tak.

Jeśli zastanowimy się nad tym, że aplikacje działają w tym samym środowisku zarówno podczas wytwarzania, jak i w produkcji, ma to ogromny wpływ na to, kiedy będą wykrywane błędy. Zgadzamy się wszyscy, że im wcześniej wykryjemy błąd, tym łatwiej będzie go naprawić i będzie to wymagało mniej pracy. To programiści wykonują te naprawy, zatem oznacza to mniej pracy dla nich.

Następnie można zauważyć, że programiści nie muszą implementować funkcji, które zazwyczaj potrzebowali realizować. Obejmuje to wykrywanie usług i/lub partnerów w aplikacji klastrowanej. Kubernetes robi to zamiast aplikacji. Zazwyczaj aplikacja musi tylko wyszukać określone zmienne środowiskowe lub wykonać zapytanie DNS. Jeśli to nie wystarczy, aplikacja może bezpośrednio odpytywać serwer API Kubernetes, aby uzyskać te i/lub inne informacje. Odpytywanie serwera API Kubernetes w taki sposób może nawet oszczędzić programistom implementacji złożonych mechanizmów, takich jak wybór lidera.

Jako ostatni przykład tego, co Kubernetes wnosi do gry, możemy również rozważyć wzrost poczucia pewności siebie programistów, gdy ma zostać wdrożona nowa wersja aplikacji. Kubernetes może automatycznie wykryć, że nowa wersja jest wadliwa i natychmiast zatrzymać jej rozpowszechnianie. Ten wzrost poczucia pewności zazwyczaj przyśpiesza ciągłe dostarczanie aplikacji, co jest korzystne dla całej organizacji.

1.4 Podsumowanie

W tym wprowadzającym rozdziale pokazaliśmy, jak aplikacje zmieniały się w ciągu ostatnich lat i dlaczego dziś trudniejsze jest ich wdrażanie i zarządzanie nimi. Przedstawiliśmy koncepcję Kubernetes i pokazaliśmy, jak ta technologia w powiązaniu z Dockerem i innymi platformami kontenerowymi pomaga wdrażać i utrzymywać aplikacje i infrastrukturę, w której działają. Pokazaliśmy, że:

■ Monolityczne aplikacje są łatwiejsze we wdrażaniu, ale trudniejsze w utrzymaniu przez dłuższy czas, a ich skalowanie niekiedy jest niemożliwe.

■ Architektury aplikacji oparte na mikroserwisach ułatwiają tworzenie każdego komponentu, ale trudniejsze jest ich wdrażanie i konfigurowanie, aby działały jako pojedynczy system.

■ Kontenery Linux zapewniają większość tych samych korzyści co maszyny wirtualne, ale są znacznie lżejsze i pozwalają na lepsze wykorzystanie sprzętu.

■ Docker ulepsza istniejące technologie kontenerów Linux, pozwalając na łatwiejsze i szybsze wyposażanie skonteneryzowanych aplikacji wraz z ich środowiskiem systemu operacyjnego.

■ Kubernetes eksponuje uruchamianym aplikacjom całe centrum danych jako pojedynczy zasób obliczeniowy.

■ Programiści mogą wdrażać aplikacje za pośrednictwem Kubernetes bez pomocy administratorów systemów.

■ Administratorzy mogą spać lepiej, gdyż Kubernetes automatycznie radzi sobie z awariami węzłów.

W kolejnym rozdziale popracujemy ciężej, budując aplikację i uruchamiając ją w Dockerze, a następnie w Kubernetes.

2

Pierwsze kroki z Dockerem i Kubernetes

W tym rozdziale:

■ Tworzenie, uruchamianie i udostępnianie obrazu kontenera przy użyciu Dockera

■ Uruchamianie lokalnego, jednowęzłowego klastra Kubernetes

■ Konfigurowanie klastra Kubernetes w Google

■ Kubernetes Engine

■ Konfigurowanie i używanie klienta wiersza poleceń kubectl

■ Wdrażanie aplikacji w Kubernetes i skalowanie jej w poziomie

Zanim zaczniemy szczegółowo poznawać koncepcje Kubernetes, przyjrzyjmy się, jak utworzyć prostą aplikację, opakować ją w obraz kontenera i uruchomić w zarządzanym klastrze Kubernetes (w Google Kubernetes Engine) albo w lokalnym jednowęzłowym klastrze. Powinno to dać Czytelnikowi nieco lepszy obraz całego systemu Kubernetes i ułatwi lekturę kilku następnych rozdziałów, w których zajmiemy się podstawowymi blokami konstrukcyjnymi i koncepcjami używanymi w Kubernetes.

2.1 Tworzenie, uruchamianie i udostępnianie obrazu kontenera

Jak wiemy z poprzedniego rozdziału, uruchamianie aplikacji w Kubernetes wymaga ich zapakowania w obrazy kontenerów. Pokażemy teraz podstawowe wprowadzenie do korzystania z Dockera na wypadek, gdyby Czytelnik jeszcze go nie używał. W kolejnych kilku podrozdziałach wykonamy następujące działania:

1 Zainstalujemy Dockera i uruchomimy swój pierwszy powitalny kontener „Hello world”.

2 Utworzymy trywialną aplikację Node.js, którą później wdrożymy w Kubernetes.

3 Opakujemy aplikację w obraz kontenera, tak by mogła zostać uruchomiona jako izolowany kontener.

4 Uruchomimy kontener oparty na tym obrazie.

5 Wypchniemy obraz do Docker Hub, tak by każdy mógł go uruchomić gdziekolwiek.

2.1.1 Instalowanie Dockera i uruchamianie powitalnego kontenera

Najpierw musimy zainstalować Dockera w swoim komputerze systemu Linux. Jeśli ktoś nie używa Linuksa, może uruchomić linuksową maszynę wirtualną (VM) i uruchomić Dockera wewnątrz tej maszyny wirtualnej. Jeśli ktoś używa Maca lub Windowsów i zainstaluje Dockera zgodnie z instrukcją, Docker skonfiguruje potrzebną maszynę wirtualną i uruchomi demona Docker wewnątrz tej maszyny wirtualnej. W macierzystym systemie operacyjnym dostępny będzie wykonywalny klient Dockera, który może komunikować się z demonem wewnątrz maszyny wirtualnej.

W celu zainstalowania Dockera należy postępować zgodnie z instrukcjami dla konkretnego systemu operacyjnego, dostępnymi pod adresem http://docs.docker.com/engine/install/. Po ukończeniu instalacji będzie można użyć wykonywalnego programu klienckiego w celu uruchamiania różnych poleceń Dockera. Możemy na przykład próbować pobrać i uruchomić istniejący obraz z Docker Hub, publicznego rejestru Dockera, który zawiera gotowe do użycia obrazy kontenerów dla wielu dobrze znanych pakietów oprogramowania. Jednym z nich jest obraz busybox, którego użyjemy do wykonania prostego polecenia echo „Hello world”.

URUCHAMIANIE POWITALNEGO KONTENERA

Dla osób, które nie znają narzędzia busybox – jest to pojedynczy plik wykonywalny, który łączy w sobie wiele standardowych uniksowych narzędzi wiersza poleceń, takich jak echo, ls, gzip i tak dalej. Zamiast obrazu busybox moglibyśmy użyć również innych w pełni wyposażonych kontenerów systemu operacyjnego, takich jak Fedora, Ubuntu lub inne podobne, jeśli tylko będą one zawierać plik wykonywalny echo.

Jak uruchomić obraz busybox? Nie trzeba pobierać ani instalować czegokolwiek. Wystarczy użyć polecenia docker run i wskazać, jaki obraz ma zostać pobrany i uruchomiony oraz (opcjonalnie), jakie polecenie ma być wykonane, co widać w poniższym listingu.

Listing 2.1 Uruchamianie powitalnego kontenera przy użyciu Dockera

$ docker run busybox echo „Hello world"

Unable to find image ‘busybox:latest' locally

latest: Pulling from docker.io/busybox

9a163e0b8d13: Pull complete

fef924a0204a: Pull complete

Digest: sha256:97473e34e311e6c1b3f61f2a721d038d1e5eef17d98d1353a513007cf46ca6bd

Status: Downloaded newer image for docker.io/busybox:latest

Hello world

Nie robi to może szczególnego wrażenia, ale jeśli uwzględnimy, że cała „aplikacja” została pobrana i uruchomiona jednym poleceniem, bez konieczności jej instalowania ani czegokolwiek innego, zgodzimy się, że jest to zadziwiające. W tym przypadku aplikacja była pojedynczym plikiem wykonywalnym (busybox), ale mogłaby równie dobrze być niebywale złożoną aplikacją z mnóstwem zależności. Cały proces konfigurowania i uruchamiania aplikacji byłby dokładnie taki sam. Równie ważny jest fakt, że aplikacja została wykonana wewnątrz kontenera, w całkowitej izolacji od wszystkich innych procesów działających na naszej maszynie.

CO SIĘ DZIEJE W TLE

Rysunek 2.1 pokazuje, co dokładnie się wydarzyło po wykonaniu polecenia docker run. Najpierw Docker sprawdził, czy obraz busybox:latest jest obecny już na lokalnym komputerze. Nie był, zatem Docker ściągnął go z rejestru Docker Hub, dostępnego pod adresem https://www.docker.com/. Po pobraniu obrazu Docker utworzył kontener z tego obrazu i wykonał w nim polecenie. Polecenie echo wypisało tekst na STDOUT, po czym proces się zakończył, a kontener został zatrzymany.

Rysunek 2.1 Uruchamianie echo „Hello world” w kontenerze opartym na obrazie kontenera busybox

URUCHAMIANIE INNYCH OBRAZÓW

Uruchamianie innych istniejących obrazów kontenerów odbywa się zasadniczo tak samo jak w przypadku obrazu busybox. W istocie jest to często jeszcze prostsze, gdyż zazwyczaj nie potrzebujemy specyfikować polecenia do wykonania, jak w tym przykładzie (echo "Hello world"). Polecenie, które powinno zostać wykonane, zazwyczaj jest zaszyte w samym obrazie, ale można to zastąpić w razie potrzeby. Po wyszukaniu lub przejrzeniu obrazów publicznie dostępnych w witrynie http://hub.docker.com lub innym publicznym rejestrze możemy nakazać Dockerowi uruchomić obraz jak poniżej:

$ docker run <image>

WERSJONOWANIE OBRAZÓW KONTENERÓW

Wszystkie pakiety oprogramowania są aktualizowane, zatem zazwyczaj istnieje więcej niż jedna wersja pakietu. Docker wspiera posługiwanie się wieloma wersjami lub wariantami tego samego obrazu pod tą samą nazwą. Każdy wariant musi mieć unikatowy znacznik (tag). Przy odwoływaniu się do obrazu bez jawnego wyspecyfikowania znacznika Docker przyjmie, że odwołujemy się do tak zwanego znacznika latest (najświeższy). Aby uruchomić inną wersję obrazu, możemy wyspecyfikować znacznik wraz z nazwą obrazu, jak poniżej:

$ docker run <image>:<tag>

2.1.2 Tworzenie trywialnej aplikacji Node.js

Teraz, gdy mamy już działającą konfigurację Dockera, utworzymy aplikację. Zbudujemy trywialną aplikację sieciową Node.js i spakujemy ją w obraz kontenera. Aplikacja będzie przyjmować żądania HTTP i odpowiadać nazwą hosta maszyny, na której jest uruchomiona. W ten sposób będziemy mogli zobaczyć, że aplikacja działająca w kontenerze widzi swoją własną nazwę hosta, a nie tę komputera macierzystego, mimo że działa w nim jak każdy inny proces. Przyda się to później, gdy wdrożymy aplikację w Kubernetes i przeskalujemy ją wszerz (przeskalujemy w poziomie, czyli uruchomimy wiele wystąpień aplikacji). Będziemy mogli zobaczyć, że nasze żądania HTTP docierają do różnych wystąpień aplikacji.

Nasza aplikacja będzie składać się z pojedynczego pliku o nazwie app.js, którego zawartość pokazana jest w poniższym listingu.

Listing 2.2 Prosta aplikacja Node.js: app.js

const http = require('http');

const os = require('os');

console.log("Kubia server starting...");

var handler = function(request, response) {

console.log("Received request from " + request.connection.remoteAddress);

response.writeHead(200);

response.end("You've hit " + os.hostname() + "\n");

};

var www = http.createServer(handler);

www.listen(8080);

Powinno być jasne, co ten kod robi. Uruchamia on serwer HTTP na porcie 8080. Serwer odpowiada na każde żądanie kodem statusu HTTP 200 OK oraz tekstem "You've hit <hostname>". Procedura obsługi żądania wypisuje także do wyjścia standardowego adres IP klienta, czego będziemy później potrzebować.

UWAGA Zwrócona nazwa hosta jest rzeczywistą nazwą hosta serwera, a nie nazwą, którą klient wysyła w nagłówku Host żądania HTTP.

Możemy następnie pobrać i zainstalować Node.js, aby bezpośrednio przetestować naszą aplikację, ale nie jest to konieczne, gdyż użyjemy Dockera do spakowania aplikacji w obraz kontenera i umożliwimy jego uruchomienie w dowolnym miejscu, bez konieczności pobierania ani instalowania czegokolwiek (z wyjątkiem Dockera, który musi być zainstalowany na komputerze, na którym chcemy uruchomić obraz).

2.1.3 Tworzenie Dockerfile dla obrazu

Aby spakować aplikację w obraz, musimy najpierw utworzyć plik o nazwie Dockerfile, zawierający listę instrukcji, które Docker będzie wykonywać podczas budowania obrazu. Dockerfile musi znajdować się w tym samym katalogu co plik app.js i powinien zawierać polecenia pokazane w poniższym listingu.

Listing 2.3 Plik Dockerfile dla budowania obrazu kontenera dla naszej aplikacji

FROM node:7

ADD app.js /app.js

ENTRYPOINT ["node", "app.js"]

Wiersz FROM definiuje obraz kontenera, którego użyjemy jako punktu wyjścia (obrazu, na którym zbudujemy nasz obraz). W tym przypadku używamy obrazu kontenera node ze znacznikiem 7. W drugim wierszu dodajemy plik app.js z katalogu lokalnego do głównego katalogu w obrazie, pod tą samą nazwą (app.js). Na koniec w trzecim wierszu definiujemy, jakie polecenie powinno zostać wykonane, gdy ktoś uruchomi obraz. W tym przypadku polecenie to node app.js.

Wybieranie obrazu bazowego

Ktoś mógłby się zastanawiać, dlaczego wybraliśmy ten konkretny obraz jako punkt wyjścia. Ponieważ nasza aplikacja jest aplikacją Node.js, potrzebujemy, aby nasz obraz zawierał binarny plik wykonywalny node, aby móc ją uruchomić. Moglibyśmy użyć dowolnego obrazu zawierającego ten plik binarny, a nawet moglibyśmy użyć bazowego obrazu dystrybucji Linuksa, takiego jak fedora lub ubuntu, i zainstalować Node.js w tym kontenerze podczas budowania obrazu. Jednak ponieważ obraz node został sporządzony specjalnie w celu uruchamiania aplikacji Node.js i zawiera wszystko, czego potrzebujemy do uruchomienia aplikacji, użyliśmy jej jako obrazu bazowego.

2.1.4 Budowanie obrazu kontenera

Teraz, gdy mamy już pliki Dockerfile i app.js, mamy wszystko, czego potrzebujemy do zbudowania obrazu. Aby to zrobić, należy wykonać poniższe polecenie Dockera:

$ docker build -t kubia .

Rysunek 2.2 pokazuje, co się dzieje w trakcie procesu budowania. Nakazaliśmy Dockerowi zbudować obraz o nazwie kubia, oparty na zawartości bieżącego katalogu (zwróćmy uwagę na kropkę na końcu polecenia build). Docker wyszuka w tym katalogu plik Dockerfile i zbuduje obraz na podstawie instrukcji zawartych w tym pliku.

Rysunek 2.2 Budowanie nowego obrazu kontenera na podstawie Dockerfile

JAK OBRAZ JEST BUDOWANY

Proces budowania nie jest wykonywany przez klienta Dockera. Zamiast tego zawartość całego katalogu jest ładowana do demona Docker i tam budowany jest obraz. Klient i demon nie muszą nawet działać na tej samej maszynie. Jeśli używamy Dockera w systemie operacyjnym innym niż Linux, klient działa w systemie operacyjnym hosta, ale demon jest uruchomiony wewnątrz maszyny wirtualnej. Ponieważ wszystkie pliki z katalogu budowania są ładowane do demona, jeśli katalog ten zawiera wiele dużych plików, a demon nie jest uruchomiony lokalnie, ładowanie to może potrwać dłużej.

WSKAZÓWKA Nie należy dołączać żadnych niepotrzebnych plików do katalogu budowania, gdyż spowolni to proces budowania – szczególnie w przypadku, gdy demon Dockera jest zlokalizowany na komputerze zdalnym.

W trakcie procesu budowania Docker najpierw ściąga obraz bazowy (node:7) z publicznego repozytorium obrazów (Docker Hub), chyba że obraz został już wcześniej ściągnięty i jest przechowywany na komputerze lokalnym.

WARSTWY OBRAZÓW

Obraz nie jest pojedynczym, wielkim plikiem binarnym, ale składa się z wielu warstw, co już mogliśmy zauważyć przy uruchamianiu przykładu busybox (było tam kilka wierszy Pull complete – po jednym dla każdej warstwy). Różne obrazy mogą współużytkować wiele warstw, co znacznie zwiększa wydajność przechowywania i transferowania obrazów. Jeśli na przykład utworzymy wiele obrazów opartych na tym samym obrazie bazowym (takim jak node:7 w naszym przykładzie), wszystkie warstwy tworzące obraz bazowy będą przechowywane tylko raz. Również podczas ściągania obrazu Docker będzie pobierać indywidualnie każdą warstwę. Wiele warstw może być już przechowywanych na naszym komputerze, zatem Docker będzie pobierać tylko te, których jeszcze nie ma.

Można sądzić, że każdy plik Dockerfile tworzy tylko jedną nową warstwę, jednak tak nie jest. Przy budowaniu obrazu tworzona jest nowa warstwa dla każdego indywidualnego polecenia w Dockerfile. W trakcie budowania obrazu, po ściągnięciu wszystkich warstw obrazu bazowego Docker utworzy nową warstwę nad nimi i doda do niej plik app.js. Następnie utworzy jeszcze jedną warstwę, która będzie specyfikować polecenie do wykonania przy uruchamianiu obrazu. Ta ostatnia warstwa zostanie oznakowana jako kubia:latest. Widać to na rysunku 2.3, który pokazuje również, jak inny obraz o nazwie other:latest będzie używać tych samych warstw obrazu Node.js, z których korzysta nasz obraz.

Rysunek 2.3 Obrazy kontenerów składają się z warstw, które mogą być współużytkowane przez różne obrazy

Po ukończeniu procesu budowania mamy nowy, lokalnie przechowywany obraz. Możemy go zobaczyć, nakazując Dockerowi wylistowanie wszystkich lokalnie przechowywanych obrazów, co pokazuje poniższy listing.

Listing 2.4 Wyliczanie obrazów przechowywanych lokalnie

$ docker images

REPOSITORY   TAG      IMAGE ID           CREATED             VIRTUAL SIZE

kubia        latest   d30ecc7419e7       1 minute ago        637.1 MB ...

PORÓWNYWANIE BUDOWANIA OBRAZÓW PRZY UŻYCIU DOCKERFILE Z RĘCZNYM TWORZENIEM

Pliki Dockerfile to typowy sposób budowania obrazów kontenerów przy użyciu Dockera, ale można również zbudować obraz ręcznie, uruchamiając kontener z istniejącego obrazu, wykonując polecenia w tym kontenerze, zamykając kontener i zatwierdzając jego stan finalny jako nowy obraz. Dokładnie to następuje, gdy budujemy obraz przy użyciu Dockerfile, ale jest wykonywane automatycznie i jest powtarzalne, co pozwala na dokonanie zmian w Dockerfile i odbudowanie obrazu w dowolnej chwili bez konieczności ponownego ręcznego wpisywania wszystkich poleceń.

2.1.5 Uruchamianie obrazu kontenera

Możemy teraz uruchomić nasz obraz poniższym poleceniem:

$ docker run --name kubia-container -p 8080:8080 -d kubia

Nakazuje ono Dockerowi uruchomienie nowego kontenera o nazwie kubia-container na podstawie obrazu kubia. Kontener ten zostanie odłączony (detached) od konsoli (flaga -d), co oznacza, że będzie działać w tle. Port 8080 lokalnego komputera zostanie zamapowany na port 8080 wewnątrz kontenera (opcja -p 8080:8080), zatem będziemy mogli uzyskać dostęp do aplikacji przez http://localhost:8080.

Jeśli demon Docker nie działa na komputerze lokalnym (jeśli używamy komputera Mac lub Windows, demon uruchomiony jest wewnątrz maszyny wirtualnej), trzeba użyć nazwy hosta lub adresu IP maszyny wirtualnej z działającym demonem zamiast localhost. Można ją ustalić przy użyciu zmiennej środowiskowej DOCKER_HOST.

UZYSKIWANIE DOSTĘPU DO NASZEJ APLIKACJI

Spróbujmy teraz odwołać się do aplikacji pod adresem http://localhost:8080.(trzeba pamiętać, aby zastąpić localhost nazwą hosta lub adresem IP hosta Docker, jeśli to konieczne):

$ curl localhost:8080

You've hit 44d76963e8e1

Taka jest odpowiedź naszej aplikacji. Nasza malutka aplikacja działa teraz wewnątrz kontenera, izolowana od wszystkiego innego. Jak widzimy, zwraca ona 44d76963e8e1 jako nazwę hosta, a nie rzeczywistą nazwę macierzystej maszyny. Ta szesnastkowa liczba jest identyfikatorem kontenera Docker.

WYLICZANIE WSZYSTKICH DZIAŁAJĄCYCH KONTENERÓW

Wyliczmy teraz wszystkie działające kontenery w poniższym listingu, aby móc zbadać tę listę (przeedytowałem wyjście, aby było bardziej czytelne – należy sobie wyobrazić, że dwa ostatnie wiersze są kontynuacją dwóch pierwszych).

Listing 2.5 Wyliczanie działających kontenerów

$ docker ps

CONTAINER ID  IMAGE         COMMAND               CREATED        ...

44d76963e8e1  kubia:latest  "/bin/sh -c 'node ap  6 minutes ago  ...

...  STATUS              PORTS                    NAMES

...  Up 6 minutes        0.0.0.0:8080->8080/tcp   kubia-container

Uruchomiony jest pojedynczy kontener. Dla każdego kontenera Docker wypisuje jego ID oraz nazwę, obraz użyty do uruchomienia oraz polecenie wykonywane wewnątrz kontenera.

UZYSKIWANIE DODATKOWYCH INFORMACJI O KONTENERZE

Polecenie docker ps pokazuje tylko najbardziej podstawowe informacje o kontenerach. Aby zobaczyć informacje dodatkowe, możemy użyć docker inspect:

$ docker inspect kubia-container

Docker wypisze długi plik JSON zawierający niskopoziomowe informacje o kontenerze.

2.1.6 Badanie wnętrza uruchomionego kontenera

Co jednak, jeśli chcielibyśmy zobaczyć, jak wygląda środowisko wewnątrz kontenera? Jako że wiele procesów może działać wewnątrz tego samego kontenera, możemy zawsze uruchomić dodatkowy proces, aby zobaczyć, co się dzieje wewnątrz. Można nawet uruchomić powłokę, jeśli binarny plik wykonywalny powłoki jest dostępny w obrazie.

URUCHAMIANIE POWŁOKI WEWNĄTRZ ISTNIEJĄCEGO KONTENERA

Obraz Node.js, na którym oparliśmy nasz obraz, zawiera powłokę bash, zatem możemy uruchomić powłokę wewnątrz kontenera jak poniżej:

$ docker exec -it kubia-container bash

Spowoduje to uruchomienie bash wewnątrz istniejącego kontenera kubia-container. Proces bash będzie używać tych samych przestrzeni nazw Linuksa co główny proces kontenera. Umożliwia to zbadanie kontenera od środka i zobaczenie, jak Node.js i nasza aplikacja postrzega system, gdy działa wewnątrz kontenera. Opcja -it jest skrótem dwóch opcji:

■ -i, który zapewnia, że STDIN jest otwarte. Będziemy go potrzebować, aby wpisywać polecenia do powłoki.

■ -t, który alokuje pseudo terminal (TTY).

Potrzebujemy obydwu, jeśli chcemy użyć powłoki zgodnie z naszymi przyzwyczajeniami. (Jeśli pominiemy pierwszą opcję, nie będziemy mogli wpisać żadnych poleceń, a jeśli pominiemy drugą, nie pojawi się znak zachęty i niektóre polecenia zgłoszą brak ustawionej zmiennej TERM).

BADANIE KONTENERA OD ŚRODKA

Przyjrzyjmy się teraz, jak użyć powłoki w poniższym listingu, aby zobaczyć procesy uruchomione w kontenerze.

Listing 2.6 Wyliczanie procesów z wnętrza kontenera

root@44d76963e8e1:/# ps aux

USER  PID %CPU %MEM    VSZ   RSS TTY STAT START TIME COMMAND

root    1  0.0  0.1 676380 16504 ?   Sl   12:31 0:00 node app.js

root   10  0.0  0.0  20216  1924 ?   Ss   12:31 0:00 bash

root   19  0.0  0.0  17492  1136 ?   R+   12:38 0:00 ps aux

Widzimy tu tylko trzy procesy. Nie widzimy żadnych innych procesów należących do macierzystego systemu operacyjnego.

PROCESY W KONTENERZE DZIAŁAJĄ W MACIERZYSTYM SYSTEMIE OPERACYJNYM

Jeśli teraz otworzymy inny terminal i wyliczymy procesy w samym macierzystym systemie operacyjnym, wśród innych procesów hosta ujrzymy również procesy działające w kontenerze, co pokazuje listing 2.7.

UWAGA Jeśli ktoś używa Maca lub Windows, musi zalogować się do maszyny wirtualnej, w której działa demon Docker, aby zobaczyć te procesy.

Przypisy

[1] Forum WWW w języku angielskim [przyp. red.].