W jaki sposób można w środowisku skonteneryzowanym utrzymywać dane i stan ?
Jak wiadomo każdy kontener ma swój system plików dostępny tylko dla niego. Ten system jest ulotny (efemeryczny). Oznacza to, ze wszelki dane jakie zapiszemy w kontenerze podczas jego pracy po ponownym uruchomienia będą utracone.Jeżeli mamy do czynienia z aplikacja, która działa samodzielnie i nie musi zapisywać swojego stanu nie mamy problemu, ale co zrobić w sytuacji, gdy aplikacja składa się z wielu kontenerów i musi wymieniać swój stan i utrzymywać go między restartami?
O tym jest dzisiejsza opowieść:
Wolumeny danych
Na początku pracy z Kubernetes pewną trudność sprawiała mi budowa manifestów, które wykorzystują wolumeny. Nie uczmy się na pamięć, wykorzystajmy zarówno system pomocy jak i wbudowane narzędzia, niekoniecznie zgodnie z intencją ich twórców. Jak można sprawnie budować takie manifesty?
Na początku zacznijmy od prostego ćwiczenia
Czym różnią się dwa tryby kubectl –dry-run ? (client, server)
W przypadku opcji client polecenie kubectl wyświetli tylko obiekt ( dla opcji -o yaml) , jaki zostanie wysłany do klastra, a w przypadku opcji server obiekt zostanie wysłany na klaster, zostaną dopisane różne domyślne wartości, ale bez końcowego zapisu obiektu.
Przykładowe polecenie generujące manifest obiektu pod.
kubectl run test --image=nginx.alpine -o yaml --dry-run=client
Zwrotny manifest wygląda w taki sposób
apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: test name: test spec: containers: - image: nginx.alpine name: test resources: {} dnsPolicy: ClusterFirst restartPolicy: Always status: {}
W wersji “serwerowej” wygląda to nieco inaczej.
kubectl run test --image=nginx:alpine -o yaml --dry-run=server
Manifest zawiera w sobie o wiele więcej danych. Zwróćmy uwagę na jeden aspekt jakim jest podłączenie za pomocą obiektu secret tokena pochodzącego od konta serwisowego (serviceaccount) o nazwie default. Każda przestrzeń nazw ma utworzoną parę obiektów konto serwisowe (serviceaccount) o nazwie default i skojarzony z tym kontem token zawarty w obiekcie secret default-token-HASH.
apiVersion: v1 kind: Pod metadata: creationTimestamp: "2021-06-17T20:03:21Z" labels: run: test # managedfields ommited (...) name: test namespace: default selfLink: /api/v1/namespaces/default/pods/test uid: 2049eb41-0974-4017-b5b6-1eddc29fbd9a spec: containers: - image: nginx:alpine imagePullPolicy: IfNotPresent name: test resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: default-token-hzgn6 readOnly: true dnsPolicy: ClusterFirst enableServiceLinks: true preemptionPolicy: PreemptLowerPriority priority: 0 restartPolicy: Always schedulerName: default-scheduler securityContext: {} serviceAccount: default serviceAccountName: default terminationGracePeriodSeconds: 30 tolerations: - effect: NoExecute key: node.kubernetes.io/not-ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io/unreachable operator: Exists tolerationSeconds: 300 volumes: - name: default-token-hzgn6 secret: defaultMode: 420 secretName: default-token-hzgn6 status: phase: Pending qosClass: BestEffort
Zobaczmy jak to wygląda w naszym przypadku. Wylistujmy wszystkie obiekty kont serwisowych i obiekty secret w przestrzeni nazw default
kubectl get sa,secret -n default
NAME SECRETS AGE serviceaccount/default 1 8m17s NAME TYPE DATA AGE secret/default-token-hzgn6 kubernetes.io/service-account-token 3 8m17s
kubectl get sa default -o yaml -n default
apiVersion: v1 kind: ServiceAccount metadata: creationTimestamp: "2021-06-17T19:57:18Z" name: default namespace: default resourceVersion: "380" selfLink: /api/v1/namespaces/default/serviceaccounts/default uid: 9e41fc13-3d63-4b10-ac6c-f1cd32d2a643 secrets: - name: default-token-hzgn6
Nazwa obiektu secret dla konta serwisowego jest zawarta w mapie secrets.
Wracając do tematu podłączania do kontenera wolumenu, to warto zauważyć, że lista volumenów jest na poziomie spec. całego obiektu pod, natomiast podłączenie tego wolumenu jest już na poziomie kontenera w obiekcie pod, czyli spec.containers[*].
Przypominam, iż pod jest obiektem grupującym wiele kontenerów i te kontenery umieszczone są w tym samej linuksowej przestrzeni nazw, komunikują się przez localhost i mogą dzielić ten sam wolumen. Wolumeny mają w ramach obiektu pod unikalne nazwy.
Poniżej umieściłem część manifestu na poziomie listy wolumenów
spec: # (...) volumes: - name: default-token-hzgn6 secret: defaultMode: 420 secretName: default-token-hzgn6
Mamy tu do czynienia z jednym elementem listy na poziomie pod.spec.volumes, gdzie unikalną wartością jest name. Dodatkowo widzimy informację, o tym, że źródłem wolumenu jest obiekt secret. Lista takich źródeł jest dosyć długa i zależy od dostawcy usługi magazynowej (storage).
Patrząc na część manifestu na poziomie miejsca podłączenia (volumeMounts)
spec: containers: # (...) volumeMounts: - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: default-token-hzgn6 readOnly: true
W tym przypadku na poziomie pod.spec.containers[*].volumeMounts widzimy tę samą wartość name co wcześniej i mamy dodany punkt podłączenia (mountPath) w kontenerze. W naszym przypadku jest to katalog wewnątrz kontenera o ścieżce /var/run/secrets/kubernetes.io/serviceaccount z ustawionym dodatkowo znacznikiem trybu tylko do odczytu (readOnly: true).
Część manifestu obrazującego oba poziomy dotyczące wolumenów.
apiVersion: v1 kind: Pod metadata: labels: run: pod # managedfields ommited (...) name: pod spec: containers: # (...) volumeMounts: - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: default-token-hzgn6 readOnly: true # (...) volumes: - name: default-token-hzgn6 secret: defaultMode: 420 secretName: default-token-hzgn6
Jak wygląda nasz obiekt secret default-token-hzgn6 ?
kubectl get secret default-token-hzgn6
NAME TYPE DATA AGE default-token-hzgn6 kubernetes.io/service-account-token 3 12m
Jak widać obiekt secret zawiera trzy pary klucz:wartość
kubectl get secret default-token-hzgn6 -o yaml
Obrobiony manifest wygląda tak:
apiVersion: v1 data: ca.crt: REDACTED namespace: ZGVmYXVsdA== token: REDACTED annotations: kubernetes.io/service-account.name: default kubernetes.io/service-account.uid: 9e41fc13-3d63-4b10-ac6c-f1cd32d2a643 creationTimestamp: "2021-06-17T19:57:18Z" # managedFields ommitted manager: kube-controller-manager operation: Update time: "2021-06-17T19:57:18Z" name: default-token-hzgn6 namespace: default resourceVersion: "379" selfLink: /api/v1/namespaces/default/secrets/default-token-hzgn6 uid: 8ed2d192-b058-4147-b811-6e43a7bc2824 type: kubernetes.io/service-account-token
Dane , które można przekazać do kontenera są na poziomie data.
data: ca.crt: REDACTED namespace: ZGVmYXVsdA== token: REDACTED
Wdróżmy nasz obiekt na klaster
kubectl run test --image=nginx:alpine
pod/test created
Spróbujmy wejść do środka kontenera w obiekcie o nazwie test. Wykorzystamy do tego polecenie kubectl exec
kubectl exec test -it -- sh
To co widzimy w środku naszego kontenera
# zmieniamy katalog / cd /var/run/secrets/kubernetes.io # listujemy pliki ktore w nim są /run/secrets/kubernetes.io # ls -la total 8 drwxr-xr-x 3 root root 4096 Jun 17 20:13 . drwxr-xr-x 3 root root 4096 Jun 17 20:13 .. drwxrwxrwt 3 root root 140 Jun 17 20:12 serviceaccount # wyswietlamy zawartosc podkatalogu serviceaccount /run/secrets/kubernetes.io # ls -la serviceaccount/ total 4 drwxrwxrwt 3 root root 140 Jun 17 20:12 . drwxr-xr-x 3 root root 4096 Jun 17 20:13 .. drwxr-xr-x 2 root root 100 Jun 17 20:12 ..2021_06_17_20_12_55.229534384 lrwxrwxrwx 1 root root 31 Jun 17 20:12 ..data -> ..2021_06_17_20_12_55.229534384 lrwxrwxrwx 1 root root 13 Jun 17 20:12 ca.crt -> ..data/ca.crt lrwxrwxrwx 1 root root 16 Jun 17 20:12 namespace -> ..data/namespace lrwxrwxrwx 1 root root 12 Jun 17 20:12 token -> ..data/token # wyswietlamy zawartość pliku namespace /run/secrets/kubernetes.io # cat serviceaccount/namespace default
Mamy plik manifestu w obiekcie typu secret zawierający trzy pary klucz:wartość. W prosty sposób możemy podłączyć te dane jako trzy osobne pliki. Takie pliki traktowane są przez system plikowy kontenera w trybie do odczytu. Co więcej zmiana obiektu secret powoduje automatyczną zmianę podłączonego pliku (dla danej pary klucz: wartość) bez restartu obiektu pod.
Uważna osoba zada na pewno pytanie, po co montujemy niejako w tle takie rzeczy. Otóż obiekt pod uruchomiony na klastrze Kubernetes ma dostęp do Kubernetes API. Uprawnienia z jakimi ma do tego API dostęp wynikają z uprawnień jakie ma konto serwisowe (w naszym przypadku o nazwie default) w zadanej przestrzeni nazw. Nie jest to zagadnienie zakresu egzaminacyjnego, ale warto wiedzieć, że jeśli nie potrzebujemy dostępu do API Kubernetes, to możemy zablokować podłączenie danych związanych z obiektem secret stowarzyszonym z danym kontem serwisowym (serviceaccount). Warto dokładniej poczytać o RBAC.
https://kubernetes.io/docs/reference/access-authn-authz/rbac/
Mamy do dyspozycji dwie metody, które zablokują dostęp do API Kubernetes:
Można wyłączyć dla danego konta możliwość korzystania z API Kubernetes dodając do manifestu automountServiceAccountToken: false
apiVersion: v1 kind: ServiceAccount metadata: name: no-token-please namespace: default automountServiceAccountToken: false
Można też w definicji np. obiektu typu pod przy definicji konta serwisowego poprosić o niepodłączanie do API dodając automountServiceAccountToken: false
apiVersion: v1 kind: Pod metadata: name: my-pod spec: serviceAccountName: yes-token-please automountServiceAccountToken: false
Zniecierpliwiona osoba, zacznie się pytać, po co tyle opowieści o korzystaniu z API i podłączaniu plików do kontenera. Niniejszy wpis nie jest częścią jakiegoś kursu Kubernetes, których jest już dużo i w których można znaleźć więcej informacji. Jest to próba pomocy dla osób, które przygotowują się do certyfikacji CKA, ale jednocześnie nie chciałbym, aby był to jedynie spis gotowych rozwiązań, które co prawda pozwolą wyrobić sobie manualną sprawność, ale niewiele więcej.
W jaki sposób Kubernetes jest w stanie zapewnić nam przechowanie danych pomimo restartu kontenera ?
Dotarliśmy do początku naszej opowieści. Przypomnę obrazek z jednej z poprzednich części
Tym razem zajmiemy się częścią prawą (zieloną) zarówno dla obiektu configmap jak i dla obiektu secret, z tym, że dla obiektu secret zamiast mapy configMap będzie mapa secret.
Wolumeny ulotne
EmptyDir
Wolumen typu emptyDir jest tworzony w momencie, gdy na danym węźle pojawia się obiekt pod po raz pierwszy. Wolumen ten istnieje tak długo jak obiekt pod na danym węźle. Zgodnie z nazwą początkowy stan jest pusty. Wszystkie kontenery w danym obiekcie pod mogą czytać i pisać do tego samego wolumenu. Kiedy pod jest zrestartowany lub usunięty dane w tego typu wolumenie są tracone.
Jakie jest główne zastosowanie?
- cache
- długotrwałe obliczenia, które muszą być wykonane w pamięci
- dane tymczasowe na przykład podczas sortowania
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- image: my-app-image
name: my-app
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
HostPath
Wolumen typu hostPath pozwala na podłączenie pliku lub katalogu znajdującego się na węźle, na którym został uruchomiony pod. Można ustalić, czy plik lub katalog musi wcześniej istnieć, czy też ma być utworzony podczas startu obiektu pod.
type: Directory
oznacza, że katalog o tej nazwie musi istnieć na węźle. Należy go utworzyć wcześniej,
type: DirectoryOrCreate
oznacza, że katalog o tej nazwie zostanie utworzony jeśli nie istnieje na węźle,
type: File
oznacza, że plik o tej nazwie musi istnieć na węźle. Należy go utworzyć wcześniej,
type: FileOrCreate
oznacza, że plik o tej nazwie zostanie utworzony jeśli nie istnieje na węźle,
Jakie jest główne zastosowanie?
Uruchamianie kontenerów, które wymagają dostępu do wewnętrznych mechanizmów Docker. Można wykorzystać ścieżkę hostPath /var/lib/docker
Uruchamianie wewnątrz kontenera cAdvisor Można wykorzystać ścieżkę hostPath /sys
Wady tego typu rozwiązania:
- Obiekty pod utworzone z tego samego szablonu mogą się różnie zachowywać na różnych węzłach w zależności od zawartości scieżki hostPath
- Pliki i katalogi tworzone z hostPath na poziomie węzła są zapisywalne tylko z uprawnieniami użytkownika root. Czyli albo musimy uruchamiać takie obiekty z uprawnieniami root, albo modyfikować uprawnienia do plików/katalogów na poziomie hosta. Oba przypadki prowadzą do problemów z bezpieczeństwem i powinny być stosowane w bardzo uzasadnionych przypadkach.
- Jeżeli obiekt pod jest częścią obiektu StatefulSet nie może korzystać z hostPath.
Przykładowy manifest wykorzystujący hostPath
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- image: my-app-image
name: my-app
volumeMounts:
- mountPath: /test-pd
name: test-volume
volumes:
- name: test-volume
hostPath:
path: /data #directory on host
type: Directory #optional
Wolumeny trwałe
Najpopularniejsze typy wolumenów, z którymi można się spotkać
- configMap: Możliwość podłączenia plików zawierających dane niewrażliwe na podstawie zawartości obiektu typu configMap
- secret: Możliwość podłączenia plików zawierających dane wrażliwe na podstawie zawartości obiektu typu secret
- persistentVolumeClaim: Możliwość podłączenia obiektu typu persistentVolume do obiektu typu pod za pomocą obiektu persistentVolumeClaim
Umówmy ten ostatni typu wolumenu.
Persistent Volumes
Obiekty PersistentVolume (PV) mogą powstawać statycznie na podstawie manifestów wdrażanym na klaster przez jego administratora lub dynamicznie na podstawie wskazania obiektu StorageClass. Obiekty StorageClass (SC) zawiera predefiniowaną konfigurację dostarczyciela (provider) i parametry, na podstawie których ma powstać obiekt PersistentVolume. Obiekty te maja zasięg w całym klastrze i nie są umieszczane w przestrzeni nazw.
Obiekt PersistemVolumeClaim (PVC) jest żądaniem podłączenia dysku przez uzytkownika, który prosi o obiekt typu PersistentVolume na podstawie wielkości, trybu dostępu do danych, itp. Jeżeli znajdzie się wolny wolumen PV to zostanie on dowiązany (bound) do obiektu PVC. Obiekt PV nie jest bezpośrednio powiązany z obiektem typu pod, ale wykorzystuje jako pośrednika obiekty typu PVC. Obiekty PVC w przeciwieństwie do VP są umieszczane w przestrzeni nazw.
PersistentVolume może mieć następujące tryby dostępu do danych (access mode) :
- ReadWriteOnce – wolumen może być podłączony w trybie zapisu (read-write) tylko przez jeden obiekt
- ReadOnlyMany – wolumen może być podłączony w trybie odczytu (read-only) przez wiele obiektów
- ReadWriteMany – wolumen może być podłączony w trybie zapisu (read-write) przez wiele obiektów
Tryby te są podczas korzystania z kubectl skracane do
- RWO – ReadWriteOnce
- ROX – ReadOnlyMany
- RWX – ReadWriteMany
Ćwiczenia
Zadanie pierwsze
Utwórz obiekt typu pod o nazwie secret-test zawierający obraz nginx:latest pracujący na porcie 80 .
W kontenerze należy podłączyć obiekt secret o nazwie test-secret w katalogu /etc/secret-volume. Nazwa wolumenu to secret-volume
Umieść go w przestrzeni nazw delta. Jeżeli przestrzeń nazw nie istnieje należy ją utworzyć.
Manifest tworzący obiekt secret wygląda tak:
apiVersion: v1 kind: Secret metadata: name: test-secret namespace: delta data: username: bXktYXBw password: Mzk1MjgkdmRnN0pi
Obiekt można wdrożyć na klaster m.in w ten sposób:
cat <<EOF | kubectl apply -f - apiVersion: v1 kind: Secret metadata: name: test-secret namespace: delta data: username: bXktYXBw password: Mzk1MjgkdmRnN0pi EOF
To od czego powinniśmy zacząć to przygotowanie manifestu obiektu pod. Robiliśmy to już wiele razy
kubectl run secret-test --image=nginx:latest --port=80 --dry-run=client -o yaml > 01.pod.secret-test.yaml
W pliku dodajemy brakującą przestrzeń nazwa delta i usuwamy zbędne dane. Manifest powinien wyglądać tak:
apiVersion: v1 kind: Pod metadata: labels: run: secret-test name: secret-test namespace: delta spec: containers: - image: nginx:latest name: secret-test ports: - containerPort: 80 dnsPolicy: ClusterFirst restartPolicy: Always
Teraz zabieramy się za brakujący wolumen
Dodajemy go w dwóch miejscach:
- na poziomie specyfikacji całego obiektu pod
apiVersion: v1 kind: Pod metadata: name: secret-test namespace: delta spec: # (...) volumes: - name: secret-volume secret: secretName: test-secre
Tutaj zawsze będziemy mieli do czynienia z listą wewnątrz volumes.
- name (nazwa jest unikalna w obrębie obiektu pod)
- typ wolumenu (w naszym przypadku jest to secret), a potem w zależności od typu wolumenu wewnętrzne mapy. W naszym przypadku jest to secretName wskazujący na nazwę obiektu secret.
- na poziomie specyfikacji kontenera w obiekcie pod
apiVersion: v1 kind: Pod metadata: name: secret-test namespace: delta spec: containers: - name: test-container # (...) volumeMounts: - name: secret-volume mountPath: /etc/secret-volume
Tutaj zawsze będziemy mieć do czynienia z listą wewnątrz volumeMounts:
- name (nazwa jest unikalna w obrębie obiektu pod), który musi być wyszczegolniony na poziomie spec.volumes. Wiązane wolumenu z punktem podłączenia jest na podstawie nazwy tego wolumenu.
- mountPath ( wskazuje miejsce, w którym zostanie podłączony nasz wokumen w kontenerze)
Po uwzględnieniu obu części nasz manifest powinien wyglądać tak:
apiVersion: v1 kind: Pod metadata: name: secret-test namespace: delta spec: containers: - name: test-container image: nginx:latest ports: - containerPort: 80 volumeMounts: - name: secret-volume mountPath: /etc/secret-volume volumes: - name: secret-volume secret: secretName: test-secret
Wdrażamy obiekt na klaster
kubectl apply -f 01.pod.secret-test.yaml
pod/secret-test created
Zadanie drugie
Utwórz obiekt typu pod o nazwie configmap-test zawierający obraz nginx:latest pracujący na porcie 80 .
W kontenerze należy podłączyć obiekt configmap o nazwie test-configmap w katalogu /etc/configmap. Nazwa wolumenu to configmap-volume
Umieść go w przestrzeni nazw delta. Jeżeli przestrzeń nazw nie istnieje należy ją utworzyć.
Manifest tworzący obiekt configmap wygląda tak:
apiVersion: v1 kind: ConfigMap metadata: name: test-configmap namespace: delta data: SPECIAL_LEVEL: very SPECIAL_TYPE: charm
Tak jak poprzednio obiekt można wdrożyć na klaster za pomocą kubectl apply -f –
cat <<EOF | kubectl apply -f - apiVersion: v1 kind: ConfigMap metadata: name: test-configmap namespace: delta data: SPECIAL_LEVEL: very SPECIAL_TYPE: charm EOF
Standardowo zaczynamy od wygenerowania bazowego obiektu pod
kubectl run configmap-test --image=nginx:latest --port=80 --dry-run=client -o yaml > 02.pod.configmap-test.yaml
Po podaniu części związanych z wolumenem pochodzącym z obiektu configmap manifest powinien wyglądać tak:
apiVersion: v1 kind: Pod metadata: labels: run: configmap-test name: configmap-test namespace: delta spec: containers: - image: nginx:latest name: configmap-test ports: - containerPort: 80 volumeMounts: - name: configmap-volume mountPath: /etc/configmap volumes: - name: configmap-volume configMap: name: test-configmap
Wdrażamy obiekt na klaster
kubectl apply -f 02.pod.configmap-test.yaml
pod/configmap-test created
Zobaczmy co możemy zobaczyć w środku kontenera za pomocą polecenia kubectl exec
kubectl exec configmap-test -n delta -it -- bash
root@configmap-test:/# ls -la /etc/configmap/ total 12 drwxrwxrwx 3 root root 4096 Jun 20 11:59 . drwxr-xr-x 1 root root 4096 Jun 20 11:59 .. drwxr-xr-x 2 root root 4096 Jun 20 11:59 ..2021_06_20_11_59_16.816891944 lrwxrwxrwx 1 root root 31 Jun 20 11:59 ..data -> ..2021_06_20_11_59_16.816891944 lrwxrwxrwx 1 root root 20 Jun 20 11:59 SPECIAL_LEVEL -> ..data/SPECIAL_LEVEL lrwxrwxrwx 1 root root 19 Jun 20 11:59 SPECIAL_TYPE -> ..data/SPECIAL_TYPE root@configmap-test:/# cat /etc/configmap/SPECIAL_LEVEL very root@configmap-test:/# cat /etc/configmap/SPECIAL_TYPE charm
Po wykonaniu polecenia ls /etc/configmap/
Otrzymamy wówczas listę plików
SPECIAL_LEVEL SPECIAL_TYPE
Jak widać są to nazwy kluczy z obiektu configmap o nazwie test-configmap. A zawartością każdego z tych plików jest wartość związana z danym kluczem.
Uważna osoba zauważy pewną niekonsekwencję nazewnictwa pól.
W jednym przypadku użyliśmy name (dla obiektu configMap) a w drugim secretName (dla obiektu secret).
W razie wątpliwości można skorzystać z wbudowanej pomocy
kubectl explain pod.spec.volumes.secret --recursive
KIND: Pod VERSION: v1 RESOURCE: secret <Object> DESCRIPTION: Secret represents a secret that should populate this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret Adapts a Secret into a volume. The contents of the target Secret's Data field will be presented in a volume as files using the keys in the Data field as the file names. Secret volumes support ownership management and SELinux relabeling. FIELDS: defaultMode <integer> items <[]Object> key <string> mode <integer> path <string> optional <boolean> secretName <string>
kubectl explain pod.spec.volumes.configMap --recursive
KIND: Pod VERSION: v1 RESOURCE: configMap <Object> DESCRIPTION: ConfigMap represents a configMap that should populate this volume Adapts a ConfigMap into a volume. The contents of the target ConfigMap's Data field will be presented in a volume as files using the keys in the Data field as the file names, unless the items element is populated with specific mappings of keys to paths. ConfigMap volumes support ownership management and SELinux relabeling. FIELDS: defaultMode <integer> items <[]Object> key <string> mode <integer> path <string> name <string> optional <boolean>
Nie uczymy się na pamięć. Wbudowana w narzędzie kubectl pomoc może nam znacznie ułatwić pracę.
Zadanie trzecie
Utwórz obiekt typu pod o nazwie configmap-test-key zawierający obraz nginx:latest pracujący na porcie 80 .
W kontenerze należy podłączyć obiekt configmap o nazwie test-configmap w katalogu /etc/configmap, z tego obiektu należy wykorzystać jedynie klucz SPECIAL_LEVEL, który powinien być umiezczony w Nazwa wolumenu to configmap-volume-key
Umieść go w przestrzeni nazw delta. Jeżeli przestrzeń nazw nie istnieje należy ją utworzyć.
Zadanie wygląda na bardzo podobne do poprzedniego. Zacznijmy od skopiowania manifestu
cp 02.pod.configmap-test.yaml 03.pod.configmap-test-key.yaml
Podczas edycji pliku należy zmienić nazwę obiektu pod, nazwę wskazanego obiektu configmap.
apiVersion: v1 kind: Pod metadata: labels: run: configmap-test-key name: configmap-test-key namespace: delta spec: containers: - name: configmap-test-key image: nginx:latest volumeMounts: - name: configmap-volume-key mountPath: /etc/configmap volumes: - name: configmap-volume-key configMap: name: test-configmap items: - key: SPECIAL_LEVEL path: keys
Podczas edycji pliku należy zmienić nazwę obiektu pod, nazwę wskazanego obiektu configmap.
spec: # (...) volumes: - name: configmap-volume-key configMap: name: test-configmap items: - key: SPECIAL_LEVEL path: keys
Ponieważ nie chcemy korzystać z całości danych zawartych w obiekcie configmap w naszej definicji wolumenu pojawiły się dodatkowe elementy listy. Dane wartości klucza o nazwie SPECIAL_KEY zostaną umieszczone w pliku o nazwie keys, który zostanie umieszczony w katalogu mountPath: /etc/configmap.
https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/
Wdrażamy obiekt na klaster
kubectl apply -f 01.pod.secret-test.yaml
pod/secret-test created
Zobaczmy co możemy zobaczyć weanątrz kontenera:
kubectl exec configmap-test-key -n delta -it -- sh
W środku kontenera proponuję wykonać polecenie cat /etc/configmap/keys
ls -la /etc/configmap/ total 12 drwxrwxrwx 3 root root 4096 Jun 20 11:53 . drwxr-xr-x 1 root root 4096 Jun 20 11:53 .. drwxr-xr-x 2 root root 4096 Jun 20 11:53 ..2021_06_20_11_53_34.366829492 lrwxrwxrwx 1 root root 31 Jun 20 11:53 ..data -> ..2021_06_20_11_53_34.366829492 lrwxrwxrwx 1 root root 11 Jun 20 11:53 keys -> ..data/keys root@configmap-test-key:/# cat /etc/configmap/keys very
otrzymamy wówczas zawartość tego pliku:
very
Jak widać, w środku wskazanej do podmontowania ścieżki pojawił się plik o nazwie keys zawierający wartość klucza SPECIAL_KEY.
Następne zadanie jest na rozgrzewkę.
Zadanie czwarte
Utwórz obiekt typu pod o nazwie pod-webapp zawierający obraz nginx:latest pracujący na porcie 80 . Umieść go w przestrzeni nazw delta. Jeżeli przestrzeń nazw nie istnieje należy ją utworzyć.
kubectl run webapp --image=nginx --port=80 --dry-run=client -o yaml > 04.pod.pod-webapp.yaml
Masz manifest powinien wyglądać mniej wiecej tak. Pamiętajmy o dodawaniu przestrzeni nazw, w tym przypadku delta.
apiVersion: v1 kind: Pod metadata: labels: run: pod-webapp name: pod-webapp namespace: gamma spec: containers: - image: nginx name: webapp ports: - containerPort: 80 dnsPolicy: ClusterFirst restartPolicy: Always
Wdrażamy nasz obiekt na klaster
kubectl apply -f 04.pod.pod-webapp.yaml
Zadanie piąte
Utwórz obiekt typu pod o nazwie pod-webapp-volume zawierający obraz nginx:latest pracujący na porcie 80 . Umieść go w przestrzeni nazw delta.
Do kontenera należy podłączyć wolumen o nazwie nginx-volume typu emptyDir, który będzie podłączony w kontenerze nginx pod ścieżką /opt/data/.
Jeżeli przestrzeń nazw nie istnieje należy ją utworzyć.
Wszystkie kluczowe parametry są poniżej
Najłatwiej będzie wykorzystać istniejący juz plik manifestu z poprzedniego zadania
cp 04.pod.pod-webapp.yaml 05.pod.pod-webapp-volume.yaml
Nasz manifest powinien wyglądać tak:
apiVersion: v1 kind: Pod metadata: labels: run: pod-webapp-volume name: pod-webapp-volume namespace: delta spec: containers: - image: nginx:latest name: webapp-volume ports: - containerPort: 80 resources: {} volumeMounts: - name: nginx-volume mountPath: /opt/data dnsPolicy: ClusterFirst restartPolicy: Always volumes: - name: nginx-volume emptyDir: {}
spec: # (...) volumes: - name: nginx-volume emptyDir: {}
Czym to się właściwie różni od wolumenów z poprzednich zadań ?
Zawsze będziemy mieli do czynienia z listą wewnątrz volumes.
- name (nazwa jest unikalna w obrębie obiektu pod)
- typ wolumenu (w naszym przypadku jest to emptyDir), a potem w zależności od typu wolumenu wewnętrzne mapy. W naszym przypadku jest to wartość pusta, czyli {}
Wdrażamy nasz obiekt na klaster
kubectl apply -f 05.pod.pod-webapp-volume.yaml
Zadanie szóste
Utwórz obiekt typu pod o nazwie pod-webapp-volume-host zawierający obraz nginx:latest pracujący na porcie 80 . Umieść go w przestrzeni nazw delta.
Do kontenera należy podłączyć wolumen o nazwie webapp-host typu hostPath wskazujący na ścieżkę/var/log/nginx/, który będzie podłączony w kontenerze nginx pod ścieżką /var/log/nginx/ Jeżeli przestrzeń nazw nie istnieje należy ją utworzyć.
Wszystkie kluczowe parametry są poniżej
Po raz kolejny wykorzystamy istniejący juz plik manifestu z poprzedniego zadania
cp 05.pod.pod-webapp-volume.yaml 06.pod.pod-webapp-volume-host.yaml
Zmieniamy nazwę obiektu pod i nazwę wolumenu (w dwóch miejscach)
Masz plik manifestu powinien wyglądać tak:
apiVersion: v1 kind: Pod metadata: labels: run: pod-webapp-volume-host name: pod-webapp-volume-host namespace: delta spec: containers: - image: nginx:latest name: webapp-volume-host ports: - containerPort: 80 resources: {} volumeMounts: - name: webapp-host mountPath: /var/log/nginx/ dnsPolicy: ClusterFirst restartPolicy: Always volumes: - name: webapp-host hostPath: path: /var/log/nginx/
Sprawdzmy czym się to różni w porównaniu z manifestem poprzedniego zadania (pod z volumenem typu emptyDir).
Masz plik manifestu powinien wyglądać tak:
spec: # (...) volumes: - name: webapp-host hostPath: path: /var/log/nginx/
Zawsze będziemy mieli do czynienia z listą wewnątrz volumes.
- name (nazwa jest unikalna w obrębie obiektu pod)
- typ wolumenu (w naszym przypadku jest to hostPath), a potem w zależności od typu wolumenu wewnętrzne mapy. W naszym przypadku jest to mapa path: /var/log/nginx/
Wdrażamy nasz obiekt na klaster
kubectl apply -f 06.pod.pod-webapp-volume-host.yaml
Zadanie siódme
Utwórz obiekt typu persistentVolume o nazwie pv-data, który potrafi przechować 50Mi danych w trybie ReadWriteMany. Volumem ma korzystać z hostPath /var/log/data
Wszystkie kluczowe parametry są poniżej
Tu po raz pierwszy nie możemy zastosować generatora w kubectl. Mamy dwie możliwości .
a) Skorzystać z systemu pomocy kubectl explain pv
b) Sięgnąć do przykładowych manifestów z dokumentacji.
Jak wygląda informacja zwrotna dla polecenia
kubectl explain persistentvolume.spec --recursive
KIND: PersistentVolume VERSION: v1 RESOURCE: spec <Object> DESCRIPTION: Spec defines a specification of a persistent volume owned by the cluster. Provisioned by an administrator. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistent-volumes PersistentVolumeSpec is the specification of a persistent volume. FIELDS: accessModes <[]string> awsElasticBlockStore <Object> fsType <string> partition <integer> readOnly <boolean> volumeID <string> azureDisk <Object> cachingMode <string> diskName <string> diskURI <string> fsType <string> kind <string> readOnly <boolean> azureFile <Object> readOnly <boolean> secretName <string> secretNamespace <string> shareName <string> capacity <map[string]string> cephfs <Object> monitors <[]string> path <string> readOnly <boolean> secretFile <string> secretRef <Object> name <string> namespace <string> user <string> cinder <Object> fsType <string> readOnly <boolean> secretRef <Object> name <string> namespace <string> volumeID <string> claimRef <Object> apiVersion <string> fieldPath <string> kind <string> name <string> namespace <string> resourceVersion <string> uid <string> csi <Object> controllerExpandSecretRef <Object> name <string> namespace <string> controllerPublishSecretRef <Object> name <string> namespace <string> driver <string> fsType <string> nodePublishSecretRef <Object> name <string> namespace <string> nodeStageSecretRef <Object> name <string> namespace <string> readOnly <boolean> volumeAttributes <map[string]string> volumeHandle <string> fc <Object> fsType <string> lun <integer> readOnly <boolean> targetWWNs <[]string> wwids <[]string> flexVolume <Object> driver <string> fsType <string> options <map[string]string> readOnly <boolean> secretRef <Object> name <string> namespace <string> flocker <Object> datasetName <string> datasetUUID <string> gcePersistentDisk <Object> fsType <string> partition <integer> pdName <string> readOnly <boolean> glusterfs <Object> endpoints <string> endpointsNamespace <string> path <string> readOnly <boolean> hostPath <Object> path <string> type <string> iscsi <Object> chapAuthDiscovery <boolean> chapAuthSession <boolean> fsType <string> initiatorName <string> iqn <string> iscsiInterface <string> lun <integer> portals <[]string> readOnly <boolean> secretRef <Object> name <string> namespace <string> targetPortal <string> local <Object> fsType <string> path <string> mountOptions <[]string> nfs <Object> path <string> readOnly <boolean> server <string> nodeAffinity <Object> required <Object> nodeSelectorTerms <[]Object> matchExpressions <[]Object> key <string> operator <string> values <[]string> matchFields <[]Object> key <string> operator <string> values <[]string> persistentVolumeReclaimPolicy <string> photonPersistentDisk <Object> fsType <string> pdID <string> portworxVolume <Object> fsType <string> readOnly <boolean> volumeID <string> quobyte <Object> group <string> readOnly <boolean> registry <string> tenant <string> user <string> volume <string> rbd <Object> fsType <string> image <string> keyring <string> monitors <[]string> pool <string> readOnly <boolean> secretRef <Object> name <string> namespace <string> user <string> scaleIO <Object> fsType <string> gateway <string> protectionDomain <string> readOnly <boolean> secretRef <Object> name <string> namespace <string> sslEnabled <boolean> storageMode <string> storagePool <string> system <string> volumeName <string> storageClassName <string> storageos <Object> fsType <string> readOnly <boolean> secretRef <Object> apiVersion <string> fieldPath <string> kind <string> name <string> namespace <string> resourceVersion <string> uid <string> volumeName <string> volumeNamespace <string> volumeMode <string> vsphereVolume <Object> fsType <string> storagePolicyID <string> storagePolicyName <string> volumePath <string>
Na początek trochę za dużo informacji, ale wiemy już, że manifest obiektu tego typu powinien zaczynać się tak
apiVersion: v1 kind: PersistentVolume metadata: name: pv-data spec:
Dodatkowo dla typu hostPath mamy wewnątrz spec.
hostPath <Object> path <string> type <string>
W tym przypadku szybciej będzie skorzystanie z dokumentacji persistent-volumes na stronie produktu, gdzie już pierwszy manifest od góry jest przydatny
apiVersion: v1 kind: PersistentVolume metadata: name: pv0003 spec: capacity: storage: 5Gi volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Recycle storageClassName: slow mountOptions: - hard - nfsvers=4.1 nfs: path: /tmp server: 172.17.0.2
Po usunięciu zbędnych informacji nasz plik zgodnie z wymaganiami zadania powninien wyglądać tak:
apiVersion: v1 kind: PersistentVolume metadata: name: pv-data spec: capacity: storage: 50Mi accessModes: - ReadWriteMany hostPath: path: /var/log/data
kubectl apply -f 07.pv.pv-data.yaml
persistentvolume/pv-data created
Zadanie ósme
Utwórz obiekt typu persistentVolumeClaim o nazwie pvc-log, który ma potrafić przechować 30Mi danych w trybie ReadWriteOnce.
Umieść go w przestrzeni nazw delta. Jeżeli przestrzeń nazw nie istnieje należy ją utworzyć.
Wszystkie kluczowe parametry są poniżej
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: myclaim spec: accessModes: - ReadWriteOnce volumeMode: Filesystem resources: requests: storage: 8Gi storageClassName: slow selector: matchLabels: release: "stable" matchExpressions: - {key: environment, operator: In, values: [dev]}
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-log namespace: delta spec: accessModes: - ReadWriteOnce resources: requests: storage: 30Mi
Wdrażamy nasz obiekt na klaster
kubectl apply -f 08.pvc.pvc-log.yaml
persistentvolumeclaim/pvc-log created
W tym momencie warto obejrzeć jak wyglądają nasze obiekty PV oraz PVC.
kubectl get pv,pvc -n delta
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE persistentvolume/pv-data 50Mi RWX Retain Available 2m13s NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE persistentvolumeclaim/pvc-log Pending 42s
Jak widać mamy obiekt PV o nazwie PV-data, który ma ustawiony status na Available i obiekt typu PVC o nazwie pvc-log, który ma ustawiony status Pending. Jak widać nie doszło do powiązania tych dwóch obiektów ? Dlatego, że oba z nich nie mają wspólnej wartościa dla trybu dostępu (accessModes).
Spróbujmy to naprawić w następnym zadaniu.
Zadanie dziewiąte
Utwórz obiekt typu persistentVolumeClaim o nazwie pvc-log-fix, który ma potrafić przechować 30Mi danych w trybie ReadWriteMany.
Umieść go w przestrzeni nazw delta. Jeżeli przestrzeń nazw nie istnieje należy ją utworzyć.
Wszystkie kluczowe parametry są poniżej
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc-log-fix namespace: delta spec: accessModes: - ReadWriteMany resources: requests: storage: 30Mi
Wdrażamy nasz obiekt na klaster
kubectl apply -f 09.pvc.pvc-log-fix.yaml
persistentvolumeclaim/pvc-log-fix created
Warto obejrzeć jak wyglądają nasze obiekty PV oraz PVC.
kubectl get pv,pvc -n delta
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE persistentvolume/pv-data 50Mi RWX Retain Bound delta/pvc-log-fix 8m47s NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE persistentvolumeclaim/pvc-log Pending 7m16s persistentvolumeclaim/pvc-log-fix Bound pv-data 50Mi RWX 70s
Jak widać mamy teraz obiekt PV o nazwie pv-data , który ma ustawiony status na Bound ze wskazaniem na PVC o nazwie delta/pvc-log-fix i i dwa obiekty typu PVC o nazwach pvc-log i pvc-log-fix , z których pierwszy ma ustawiony status Pending a drugi status Bound ze wskazaniem na obiekt PV o nazwie pv-data.
Poprawa trybu dostępu na RWX (czyli ReadWriteMany) pomogła w powiązaniu obiektów PV i PVC.
Zadanie dziesiąte
Utwórz obiekt typu pod o nazwie pod-webapp-volume-pvc zawierający obraz nginx:latest pracujący na porcie 80 . Umieść go w przestrzeni nazw delta.
Do kontenera należy podłączyć wolumen o nazwie pvc-log-fix typu PersistentVolumeClaim , który będzie podłączony w kontenerze nginx pod ścieżką /var/log/nginx/ Jeżeli przestrzeń nazw nie istnieje należy ją utworzyć.
Wszystkie kluczowe parametry są poniżej
Name: pod-webapp-volume-pvc
cp 06.pod-webapp-volume-host 10.pod.pod-webapp-volume-pvc.yaml
spec: volumes: - name: webapp-host hostPath: path: /var/log/nginx/
spec: # (...) volumes: - name: pvc-log persistentVolumeClaim: claimName: pvc-log-fix
kubectl explain pod.spec.volumes.persistentVolumeClaim --recursive
KIND: Pod VERSION: v1 RESOURCE: persistentVolumeClaim <Object> DESCRIPTION: PersistentVolumeClaimVolumeSource represents a reference to a PersistentVolumeClaim in the same namespace. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims PersistentVolumeClaimVolumeSource references the user's PVC in the same namespace. This volume finds the bound PV and mounts that volume for the pod. A PersistentVolumeClaimVolumeSource is, essentially, a wrapper around another type of volume that is owned by someone else (the system). FIELDS: claimName <string> readOnly <boolean>
apiVersion: v1 kind: Pod metadata: labels: run: pod-webapp-volume-pvc name: pod-webapp-volume-pvc namespace: delta spec: containers: - image: nginx:latest name: webapp-volume-pvc ports: - containerPort: 80 resources: {} volumeMounts: - name: pvc-log mountPath: /var/log/nginx/ dnsPolicy: ClusterFirst restartPolicy: Always volumes: - name: pvc-log persistentVolumeClaim: claimName: pvc-log-fix
Wdrażamy obiekt na klaster
kubectl apply -f 10.pod.pod-webapp-volume-pvc.yaml
pod/pod-webapp-volume-pvc created
Sprawdźmy czy nasz wolumen został podłączony do obiektu pod. Najłatwiej jest to zrobić za pomocą kubectl describe pod
kubectl describe pod pod-webapp-volume-pvc -n delta
Volumes: pvc-log: Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace) ClaimName: pvc-log-fix ReadOnly: false default-token-g5fdr: Type: Secret (a volume populated by a Secret) SecretName: default-token-g5fdr Optional: false QoS Class: BestEffort Node-Selectors: <none> Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s node.kubernetes.io/unreachable:NoExecute for 300s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 9s default-scheduler Successfully assigned delta/pod-webapp-volume-pvc to node01 Normal Pulling 9s kubelet, node01 Pulling image "nginx:latest" Normal Pulled 8s kubelet, node01 Successfully pulled image "nginx:latest" in 746.348637ms Normal Created 8s kubelet, node01 Created container webapp-volume-pvc Normal Started 8s kubelet, node01 Started container webapp-volume-pvc
Jak wydać oprócz zadeklarowanego w manifescie wolumenu pojawił się ten związany z obiektem secret umożliwiający dostęp do API Kubernetes, o czy wspominałem na samym początku tego wpisu.
Nie zostało opisanych jeszcze wiele aspektów pracy z wolumenami danych, nic nie wspomniałem o reclaimPolicy i nie ma żadnych przykładów dotyczących storageClass, temat jest obszerny i nie chciałem dodatkowo rozszerzać liczby minut do przeczytania i przećwiczenia. Mam nadzieję, że chętni sięgną do dokumentacji.
Na tym kończymy dzisiejszą audycję, do usłyszenia wkrótce.
Poprzednie części
Certified Kubernetes Administrator (CKA) krok po kroku – część 1
Certified Kubernetes Administrator (CKA) krok po kroku – część 2
Certified Kubernetes Administrator (CKA) krok po kroku – część 3
Certified Kubernetes Administrator (CKA) krok po kroku – część 4
Następne części:
TODO
Literatura:
https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/
https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands
https://kubernetes.io/docs/concepts/configuration/secret/
https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistent-volumes
https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/
Dodaj komentarz