Praktyczne wprowadzenie do Kubernetesa z użyciem spring boota i dockera
Praktyczne wprowadzenie do Kubernetesa z użyciem spring boota i dockera
Wprowadzenie:
Kubernetes w skrócie jako k8s to platforma na zasadach Open Source do zarządzania kontenerami. Kubernetes grupuje kontenery, które są częścią aplikacji w logicznie grupy tzw. pody, ułatwiając ich odnajdywanie i zarządzanie nimi. Jego pierwotna wersja została stworzona w 2014 roku przez Google a obecnie projekt rozwijany jest przez Cloud Native Computing Foundation. Kubernetes daje nam możliwość szybkiego wprowadzania zmian w infrastrukturze bez konieczności zatrzymywania działających na produkcji aplikacji!
Zanim jednak przejdziemy do praktycznej aplikacji, poniżej kilka podstawowych i niezbędnych pojęć:
– Cluster – zbiór paru lub więcej node’s (węzłów) – komputerów czy maszyn wirtualnych/fizycznych na których działa agent k8s. Jeden z tych węzłów nazywany jest Master Node.
– Node (węzeł) – element w sieci k8s który może być zarówno wirtualną czy też fizyczną maszyną na której będą instalowane pody. Na węźle zainstalowany jest Docker oraz narzędzia Kubernetesa.
– Pod jest obiektem abstrakcyjnym Kubernetesa, który reprezentuje grupę jednego bądź wielu kontenerów. Pod jest uruchamiany na węźle. Pod ma jeden adres IP i zakres portów, który jest współdzielony przez kontenery w nim się znajdujące.
– Service – abstrakcyjny obiekt grupujący wiele identycznych podów (identyczne pody to pody uruchamiane z tego samego obrazu i z tą samą konfiguracją startową – realizowane jest to za pomocą tzw. kontrolerów) w logiczną całość. Do grupowania wszystkich obiektów Kubernetesa wykorzystywany jest mechanizm etykiet. Przykładowo, aby połączyć grupę podów w serwis, musimy nadać im etykietę, a następnie w konfiguracji serwisu użyć ją jako kryterium grupowania. Serwis posiada swój własny adres IP oraz nazwę DNS, a Kubernetes zapewnia mechanizmy równoważenia obciążenia.
– Kubectl – główne narzędzie tekstowe do zarządzania klastrem Kubernetesa.
– MicroK8s – projekt firmy Canonical przygotowany z myślą o wykorzystaniu w środowiskach deweloperskich. To znacznie lżejszy odpowiednik pełnowymiarowego Kubernetesa, zaprojektowany z myślą o obsłudze klastrów z jedną instancją. W przypadku dużych i wielonodowych środowisk Kubernetes jest często wdrażany jako gotowa usługa, taka jak np. Google Kubernetes Engine.
– ClusterIP – udostępnia usługę wewnętrznie – i tylko klaster może się z nią skontaktować.
– NodePort – wystawia serwis na tym samym porcie na każdym z wybranych węzłów klastra przy pomocy NAT (z ang. Network Address Translator). W ten sposób serwis jest dostępny z zewnątrz klastra poprzez:
<NodeIP>:<NodePort>
Graficznie Kubernetesa można opisać:
[źródło] https://www.zyxist.com/blog/kubernetes-i-prywatny-rejestr-dockera
Przykładowa aplikacja – Spring Boot & Docker & Kubernetes:
Aplikacja składa się z dwóch projektów – pierwszy wystawia Rest API – przykładowe informacje o pracownikach pobierane z ogólnodostępnego w internecie źródła, drugi natomiast projekt z tego API korzysta.
Projekt EmployeeApi:
plik pom.xml:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
Klasa startowa – EmployeeApi – wystawiająca Rest API – przykładowe informacje o pracownikach:
@SpringBootApplication @RestController public class EmployeeApi { final static String dumnyRestApi = "http://dummy.restapiexample.com/api/v1/employees"; private static RestTemplate request = new RestTemplate(); public static void main(String[] args) { SpringApplication.run(EmployeeApi.class, args); } @GetMapping("/getAllEmployees") public static String requestCodeData(){ String result = request.getForObject(dumnyRestApi, String.class); return (result); } }
Przykładowy wynik:
{ "status":"success", "data": [ { "id":"1", "employee_name": "Tiger Nixon", "employee_salary":"320800", "employee_age":"61", "profile_image":"" } ... ] }
API dostępne jest pod adresem (port zdefiniowany jest w pliku konfiguracyjnym application.properties):
http://localhost:8081/getAllEmployees
W głównym katalogu projektu zamieszczamy plik Dockerfile – potrzebny do utworzenia obrazu:
FROM openjdk:8u212-jdk-slim LABEL maintainer="kontakt@javaleader.pl" VOLUME /tmp EXPOSE 8081 ARG JAR_FILE=target/EmployeeApi-0.0.1-SNAPSHOT.jar ADD ${JAR_FILE} EmployeeApi.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/EmployeeApi.jar"]
Projekt EmployeeClient:
plik pom.xml:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot</artifactId> <version>2.1.4.RELEASE</version> </dependency> <dependency> <groupId>com.vaadin.external.google</groupId> <artifactId>android-json</artifactId> <version>0.0.20131108.vaadin1</version> </dependency> </dependencies>
Klasa startowa – EmployeeClient – konsumuje wystawione API:
@SpringBootApplication @RestController public class EmployeeClient { public static final String serverUrl = "http://localhost:8081/getAllEmployees"; public static void main(String[] args) { SpringApplication.run(EmployeeClient.class, args); } public static String requestProcessedData(String url){ RestTemplate request = new RestTemplate(); String result = request.getForObject(url, String.class); return (result); } @GetMapping("/employee/{id}") public static String getEmployeeById1(@PathVariable("id") int id){ System.out.println(serverUrl); String findEmployee = null; try { String response = requestProcessedData(serverUrl); JSONObject jsonObject = new JSONObject(response); String clearData = jsonObject.getString("data"); JSONArray all_employess = new JSONArray(clearData); for (int i = 0; i < all_employess.length(); i++) { JSONObject emp = new JSONObject(all_employess.getString(i)); int id_employee = Integer.parseInt(emp.getString("id")); if(id_employee == id) { findEmployee = emp.toString(); } } } catch (Exception e) { System.out.println("[ERROR] : [CUSTOM_LOG] : " + e); } if(findEmployee == null){ findEmployee = "No Match Found"; } return findEmployee; } }
Pobieranie danych z przykładowego API testujemy pod adresem:
localhost:9090/employee/2
w wyniku powinniśmy uzyskać informację o pracowniku o id = 2:
{ "profile_image":"", "employee_name":"Garrett Winters", "employee_salary":"170750", "id":"2", "employee_age":"63" }
W głównym katalogu projektu zamieszczamy plik Dockerfile – potrzebny do utworzenia obrazu:
FROM openjdk:8u212-jdk-slim LABEL maintainer="kontakt@javaleader.pl" VOLUME /tmp EXPOSE 9090 ARG JAR_FILE=target/EmployeeClient-0.0.1-SNAPSHOT.jar ADD ${JAR_FILE} EmployeeClient.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/EmployeeClient.jar"]
Wdrożenie z użyciem Kubernetesa:
Z racji tego, że będziemy używać serwera nazw to zanim przejdziesz dalej koniecznym jest zmiana adresu zadeklarowanego w zmiennej (w projekcie EmployeeClient):
public static final String serverUrl = "http://localhost:8081/getAllEmployees";
na:
public static final String serverUrl = "http://emp-api.default.svc.cluster.local:8081/getAllEmployees";
Przede wszystkim wymagana jest instalacja Dockera (o ile nie jest oczywiście już zainstalowany):
sudo apt-get install docker
lub:
apt install docker.io
weryfikacja wersji:
docker --version
tworzymy obrazy obydwu projektów z użyciem ich plików Dockerfile a następnie zamieszczamy je na platformie Docker Hub:
docker build -t employee-api .
oraz:
docker build -t employee-client .
weryfikujemy poleceniem:
docker images
tworzymy teraz tagi do utworzonych obrazów:
docker tag 7078fa386a58 mwarycha/kubernetes:emp-api-tag docker tag 7078fa386a58 mwarycha/kubernetes:emp-client-tag
logujemy się do Docker Huba:
docker login --username=login
i wysyłamy utworzone obrazy dockera do repozytorium Docker Hub:
docker push mwarycha/kubernetes:emp-api-tag docker push mwarycha/kubernetes:emp-client-tag
wynik:
obrazy obydwu projektów zostały prawidłowo zamieszczone na platformie Docker Hub!
Instalujemy teraz Kubernetesa – (dystrybucja linuxa 16.04 LTS):
sudo snap install microk8s --classic
wynik:
microk8s v1.17.2 from Canonical✓ installed
weryfikacja:
snap info microk8s
dla uproszczenia dodajemy alias na polecenie microk8s.kubectl:
sudo snap alias microk8s.kubectl kubectl
wynik:
Added: - microk8s.kubectl as kubectl
wgrywamy projekt EmployeeApi:
kubectl run emp-api --image mwarycha/kubernetes:emp-api-tag --port 8081 --labels="app=emp-api,tier=backend"
weryfkujemy utworzonego poda i deployment:
kubectl get pods
wynik:
NAME READY STATUS RESTARTS AGE emp-api-6c48b58548-zz4nd 1/1 Running 0 2m22s
kubectl get deployment
wynik:
NAME READY UP-TO-DATE AVAILABLE AGE emp-api 1/1 1 1 2m50s
tworzymy serwis jako ClusterIP:
kubectl expose deployment emp-api --type=ClusterIP
wgrywamy projekt EmployeeClient:
kubectl run emp-client --image mwarycha/kubernetes:emp-client-tag --port 9090 --labels="app=emp-client,tier=backend"
tworzymy serwis jako NodePort:
kubectl expose deployment emp-client --type=NodePort
po wejsciu na adres:
http://ip:31961/employee/2
gdzie 31961 to port przydzielony przez kubernetesa:
kubectl describe service emp-client
wynik:
Name: emp-client Namespace: default Labels: app=emp-client tier=backend Annotations: <none> Selector: app=emp-client,tier=backend Type: NodePort IP: 10.152.183.155 Port: <unset> 9090/TCP TargetPort: 9090/TCP NodePort: <unset> 31961/TCP Endpoints: 10.1.21.4:9090 Session Affinity: None External Traffic Policy: Cluster Events: <none>
otrzymamy wynik:
No Match Found
co oznacza, że dane o pracownikach nie są poprawnie pobierane.
instalujemy DNS i dashboard!
sudo microk8s.enable dns dashboard
następnie wykonujemy restart serwera:
sudo reboot
następnie weryfikujemy ustawienia dns:
kubectl create -f https://k8s.io/examples/admin/dns/busybox.yaml
kubectl get pods busybox
kubectl exec -ti busybox -- nslookup kubernetes.default
wynik:
Server: 10.152.183.10 Address 1: 10.152.183.10 kube-dns.kube-system.svc.cluster.local Name: kubernetes.default Address 1: 10.152.183.1 kubernetes.default.svc.cluster.local
sprawdzamy dns – emp-api.default.svc.cluster.local:
kubectl exec -ti busybox -- nslookup emp-api.default.svc.cluster.local
wynik:
Server: 10.152.183.10 Address 1: 10.152.183.10 kube-dns.kube-system.svc.cluster.local Name: emp-api.default.svc.cluster.local Address 1: 10.152.183.179 emp-api.default.svc.cluster.local
przechodzimy ponownie na adres:
http://ip:31090/employee/2
otrzymujemy prawidłowy wynik!
{"profile_image":"","employee_name":"Tiger Nixon","employee_salary":"320800","id":"1","employee_age":"61"}
Przydatne komendy:
wyświetlenie usług:
kubectl get services
usunięcie konkretnj usługi:
kubectl delete svc emp-client
wyświetlenie logów dla danego poda:
kubectl logs emp-client-767fdd6c55-rtrs5
gdzie emp-client-767fdd6c55-rtrs5 to identyfikator konkretnego poda.
TroubleShooting:
Może zdarzyć się, że po wykonaniu komendy:
kubectl run emp-client --image mwarycha/kubernetes:emp-client-tag --port 9090 --labels="app=emp-client,tier=backend"
nie zostanie utworzona jednostka – „unit deployment„ – czyli po wykonaniu polecenia:
kubectl get deployments
wyszkoczy komunikat, że nic nie znaleziono! W takiej sytuacji zamiast polecenia „kubectl -run..” należy wykonać poniżej zamieszczone polecenia:
emp-api:
kubectl create deployment emp-api --image=mwarycha/kubernetes:emp-api-tag kubectl expose deployment emp-api --type=ClusterIP --port 8081 --labels="app=emp-api,tier=backend"
emp-client:
kubectl create deployment emp-client --image=mwarycha/kubernetes:emp-client-tag kubectl expose deployment emp-client --type=NodePort --port 9090 --labels="app=emp-client,tier=backend"
Leave a comment