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ć:

Ilustracja

[ź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 poleceniakubectl -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"

Zobacz kod na GitHubie i zapisz się na bezpłatny newsletter!

.

Leave a comment

Your email address will not be published.


*