Czym jest Kubernetes i jak używać go w aplikacjach Java

sages.pl 3 lat temu
Z artykułu dowiecie się więcej o Kubernetes (K8s). w uproszczeniu jest platforma, na której są instalowane aplikacje biznesowe i operacyjne. Dowiecie się dlaczego, według autora, K8s jest odpowiednim rozwiązaniem do pełnienia funkcji jeżeli nie platformy, to przynajmniej jej solidnych podwalin.
W dalszej części artykułu wyjaśniamy, dlaczego instalując K8s, warto postawić na Infrastructure as Code, na przykładzie skryptu w Terraform „stawiający” K8s na Digital Ocean.

Ponadto opisane zostało przygotowanie aplikacji napisanej w języku Java i wykorzystującej biblioteki ze stajni Spring Framework do zainstalowania jej na K8s. Dowiecie się też więcej na temat sposobów „konteneryzacji” aplikacji Java. Pokazane w artykule przykłady zawierają najlepsze praktyki konfiguracji niektórych artefaktów K8s.
W ostatnim rozdziale wymienione zostały aplikacje, które ułatwiają codzienną pracę z Java, Java@K8s i K8s.

# Dlaczego Kubernetes?

Cytując [kubernetes.io/pl](https://kubernetes.io/pl/): “Kubernetes, znany też jako K8s, to otwarte oprogramowanie, służące do automatyzacji procesów uruchamiania, skalowania i zarządzania aplikacjami w kontenerach”. To nie jedyny produkt z podobnym opisem, bo innym przykładem może być OpenShift albo Docker Swarm i jeżeli trafiłeś tutaj, bo musisz wybrać któryś produkt z tej rodziny, to za Kubernetesem przemawiają następujące argumenty:
* Bazuje na systemie Borg, który z sukcesem, od wielu lat dźwiga serwisy Google.
* Ma duży i dynamicznie rozwijający się ekosystem.
* Szeroko dostępne są serwisy, wsparcie i dodatkowe narzędzia.
* System sprawdzony na wielu produkcjach.
* Jest częścią CNCF. Ma bardzo bogatą dokumentację, choćby pewna jej część napisana jest w języku polskim.

**Wymagania:**
Żeby w pełni korzystać z możlwości K8s konieczna jest aplikacja w kontenerze. Twórcy umożliwili pracę tylko z kontenerami. Czy w ten sposób wskazują sposób, w jaki w tej chwili powinny być robione aplikacje? Być może właśnie tak jest.
Poznajcie najważniejsze zalety kontenerów:
- Efektywne wykorzystanie zasobów maszyny.
- Kontener buduje się gwałtownie i łatwiej niż VM.
- Spójność aplikacji na różnych środowiskach: upraszczając - aplikacja działa w ten sam sposób niezależnie czy jest uruchamiana podczas developmentu na laptopie, czy zainstalowana w chmurze.
- Przewidywalna wydajność, poprzez dedykowanie zasobów.
- Aplikacja może być uruchomiona na różnych systemach operacyjnych i platformach chmurowych: Ubuntu, Container Optimized System, Windows, dowolnego dostawcy usług chmurowych itd.

**Co zapewnia Kubernetes:**

* Jasno opisane API + szeroka adaptacja produktu = duży wybór aplikacji, rozwiązujących problemy operacyjne oraz biznesowe.
* Samo zainstalowanie aplikacji na produkci nie jest wystarczające. Trzeba dbać o nią – na przykład uruchomiać ponownie, jeżeli z jakiegoś powodu przestała działać lub przenieść na inną maszynę, kiedy obecna “niedomaga”.
* Balansowanie ruchu. Uruchomiając kilka instancji aplikacji, Kubernetes może bilansować ruch między nimi.
* Umożliwia pracę z różnymi systemami składowania danych w ten sam sposób.
* Instalacja aplikacji, jej zmiana i wycofywanie jest automatyczne. Używając składni, Kubernetesowi opisuje się stan oczekiwany, a on zajmuje się jego realizacją.
* Zarządzanie dostępnymi zasobami. Po dostarczeniu klastra maszyn Kubernetesowi, zleca się uruchamianie aplikacji, z kolei Kubernetes zajmuję się wyszukiwaniem odpowiedniej maszyny, aby jak najefektywniej wykorzystać zasoby.
* Monitoruje aplikacje i automatycznie je restartuje albo wymienia na nowe, jeżeli przestają działać. Co więcej, nie kieruje ruchu do aplikacji jeżeli nie są one gotowe.
* Udostępnia narzędzia do zarządzania informacjami poufnymi i konfiguracją.

# Pojęcie “Infrastructure as Code”

Pojęcie **Infrastructure as Code (IaC)** nie jest nowe, ale dopiero od niedawna stało się standardem w tworzeniu infrastruktury i jej komponentów. Poprzez infrastrukturę rozumiemy np. utworzenie sieci i podsieci u dostawcy chmury publicznej lub „postawienia” klastra Kubernetes na wirtualkach.

IaC nakazuje tworzyć infrastrukturę wyłącznie jako kod, w narzędziach dedykowanych. Narzędzia cechują się tym, iż kod można uruchamiać bezpiecznie wiele razy, nie narażając się na nadpisanie albo usunięcie tego co już działa. Systemy takie powinny też umożliwiać postawienie infrastruktury od nowa.

Rozwiązaniem nierzetelnym i niebezpiecznym, chociaż najszybszym, jest zainstalowanie komponentów i ich skonfigurowanie poprzez kliknięcie albo uruchomienie instalatorów manualnie. Nie zawsze notujemy, co dzieje się podczas takie go uruchamiania. Dlatego też nie jest to dobre rozwiązanie. Dlaczego jeszcze? Poniżej powody:
* Jest to nieefektywne czasowo, a co za tym idzie również finansowo.
* Jest to niebezpieczne, bo może się nam wydawać, iż notatki są kompletne. Jednak z uwagi na to, iż nigdy nie były powtórzone od początku, to jakiegoś kroku może brakować. W sytuacji kiedy np. trzeba coś zainstalować i jeszcze skonfigurować, to nie używając dedykowanego narzędzia, praktycznie niemożliwym staje się rzetelne udokumentowanie kroków.

Używanie dedykowanych systemów umożliwia wykorzystanie aplikacji i technik, które powstały wokół IaC. Na przykład technika **Shift Left** zakłada, iż najtańszym (nie tylko pod względem finansowym) miejscem na wyłapanie błędów i nieprawidłowości w produkcie technicznym jest faza budowy. Stąd też kładzie się duży nacisk na testowanie i analizowanie produktu jak najbliżej fazy budowania. Przykładami są:
* Pisanie testów jednostkowych i uniemożliwianie zmiany kodu, póki testy nie pokrywają np. 80% całego kodu.
* Użycie analizatora wykorzystywanych bibliotek w celu wykrycia tych „z dziurami” bezpieczeństwa.
* Używanie **Snyk**, **checkov**, czyli narzędzi do analizy kodu **Terraform** (i nie tylko) i wskazywanie miejsc niebezpiecznych albo niezgodnych z tak zwanymi „best practices”. jeżeli stawiając infrastrukturę instalowany jest system operacyjny z dziurą bezpieczeństwa, albo utworzymy publicznie dostępny zasób S3, to te systemy informują o takiej sytuacji.

# Kubernetes a Java

Dostosowanie aplikacji, w tym aplikacji Java, do zainstalowania na produkcji jest często kontekstowe, zależne od tego, w jakiej firmie i z kim pracujesz. Nie mniej można wyróżnić praktyki, które są uznane przez większość programistów.

## Konfiguracja jest niezależnym artefaktem

Niezależnie od tego, jak jest zbudowana aplikacja – *jar*, *war*, *Docker image*, konfiguracja w większości przypadków nie powinna być częścią wspomnianego artefaktu. Aplikacja powinna jej szukać na systemie plików albo np. odpytywać serwis konfiguracyjny. jeżeli jest częścią „pakietu", wówczas zmiana konfiguracji będzie wyzwalała proces budowania aplikacji. To oznacza, że:
* W przypadku developmentu niepotrzebnie wydłuża proces „develop – release – test”.
* W dojrzałych projektach Continuous Integration pipeline jest rozbudowany i wieloetapowy, co znacznie wydłuża proces.
* Spowalnia „hot fixing” i „troubleshooting”. W przeciwnym przypadku można byłoby zmienić konfigurację bezpośrednio na środowisku i uruchomiać ponownie aplikację (bez jej przebudowy), która zaczyta nowy config.

Poniżej przykładowa konfiguracja aplikacji Java w Spring Framework - /etc/app/application.yaml:
```
app:
welcome-message: Welcome!
management:
server.port: 8090
```

Natomiast to jest konfiguracja biblioteki logującej - /etc/app/logback.xml
```
<?xml version="1.0" encoding="UTF-8"?>
```

Uruchamiając tę aplikację, trzeba wskazać miejsca, gdzie te dwa pliki konfiguracyjne się znajdują.
Można to zrobić poprzez argumenty JVM:

```
Java … -Dspring.config.location=file:/etc/app/ -Dlogback.configurationFile=/etc/app/logback.xml -Dlogback.statusListenerClass=ch.qos.logback.core.status.OnConsoleStatusListener
```

Co w przypadku instalacji aplikacji na Kubernetes można zrobić następującym sposobem:
1. Tworzony jest ConfigMap z treścią z application.yaml, który później zostanie zamontowany pod odpowiednią ścieżkę w kontenerze:
```
kind: ConfigMapapiVersion: v1metadata: name: app-configdata: application.yaml: |- app:
welcome-message: Welcome!
management:
server.port: 8090
```

2. Tworzony jest kolejny ConfigMap z treścią z logback.xml, który również będzie zamontowany w kontenerze:
```
kind: ConfigMapapiVersion: v1metadata: name: app-config-logbackdata: logback.xml: |- <?xml version="1.0" encoding="UTF-8"?>
```

3. Instalując aplikację na Kubernetes, montowane są poniższe pliki konfiguracyjne:

```
...
spec:
...
template:
...
spec:
...
containers:
- name: k8s-java-hello-world
livenessProbe:
httpGet:
path: /health
port: 8090
...
volumeMounts:
- mountPath: /etc/app/application.yaml
name: application-configuration
subPath: application.yaml
readOnly: true
- mountPath: /etc/app/logback.xml
name: application-configuration-logback
subPath: logback.xml
readOnly: true
...
env:
- name: "JAVA_TOOL_OPTIONS"
value: |
-Dspring.config.location=file:/etc/app/
...
volumes:
- configMap:
name: app-config
name: application-configuration
- configMap:
name: app-config-logback
name: application-configuration-logback
```

## Udostępnij status aplikacji

Aby Kubernetes mógł wykonać jedną ze swoich odpowiedzialności, należy:
* Uruchomić ponownie aplikację jeżeli ta nie działa prawidłowo (na przykład Spring Context nie wstał).
* Nie kierować ruchu do aplikacji, jeżeli ta jest niegotowa (na przykład Spring Context jeszcze nie wstał albo nie udaje się połączyć z Kafka).

Jedną z możliwości jest dodanie zależności Spring Actuator:
```

org.springframework.boot
spring-boot-starter-actuator
${spring-boot.version}


```

Aktywacja:

```
kind: ConfigMap
...
data:
application.yaml: |-
app:
welcome-message: Welcome!
management:
server.port: 8090
health.binders.enabled: true
endpoints:
web:
base-path: /
exposure.include: [health]
enabled-by-default: false
endpoint:
health.enabled: true
```

Wskazanie ścieżki do zasobu Health Kubernetes:
```
kind: Deployment
...
spec:
...
template:
...
spec:
...
containers:
- name: k8s-java-hello-world
...
livenessProbe:
httpGet:
path: /health
port: 8090
```

## Czas by aplikacja opowiedziała o sobie
Bardzo ważna jest wiedza o funkcjonowaniu aplikacji – musimy wiedzieć, czy aplikacja „chodzi”, czy odnotowany jest ruch, jak długo wykonuje zadania biznesowe (np. ile trwa odpowiedź na zapytanie HTTP), ile zadań kończy się błędami, jakie jest zużycie procesora i pamięci, jakie są pauzy GC itd. Dopiero mając tę wiedzę można wnioskować o poprawny UX.

Wyżej użyta biblioteka Spring Actuator instrumentuje wiele popularnych framework’ów, takich jak Spring MVC, RestTemplate, Kafka Client itd. Co oznacza, iż nie robiąc praktycznie nic, aplikacja informuje np. o liczbie request’ów HTTP, ich statusie wykonania jak długo trwały. W przypadku Kafka takie informacje jak ilość wysłanych bajtów, LAG per topic itd.

```

org.springframework.boot
spring-boot-starter-web
${spring-boot.version}

```

Musimy jeszcze zdecydować jakim narzędziem te metryki będą zbierane. Spring Actuator pod spodem używa Micrometer a jego implementację wspiera format rozumiany przez Prometheus. Aplikacje składowe Kubernetes generują metryki właśnie w tym standardzie. Wskazujemy ten standard poprzez dodanie zależności:

```

io.micrometer
micrometer-registry-prometheus
${micrometer-registry-prometheus.version}

```

Następnie aktywujemy endpoint:

```
kind: ConfigMap
...
data:
application.yaml: |-
app:
welcome-message: Welcome!
management:
server.port: 8090
health.binders.enabled: true
endpoints:
web:
base-path: /
path-mapping.prometheus: metrics
exposure.include: [prometheus, health]
enabled-by-default: false
endpoint:
prometheus.enabled: true
health.enabled: true
server:
tomcat:
mbeanregistry:
enabled: true
```

Po uruchomieniu aplikacji na lokalnym PC wynik powyższych kroków można zaobserwować poprzez odpytanie endpoint’a:

```
➜ mvn clean spring-boot:run

➜ curl localhost:8090/health
{"status":"UP"}%

➜ curl localhost:8090/metrics -s | grep http
tomcat_global_received_bytes_total{name="http-nio-8080",} 0.0
tomcat_connections_current_connections{name="http-nio-8080",} 1.0
tomcat_global_request_max_seconds{name="http-nio-8080",} 0.0
tomcat_connections_keepalive_current_connections{name="http-nio-8080",} 0.0
tomcat_connections_config_max_connections{name="http-nio-8080",} 8192.0
tomcat_threads_busy_threads{name="http-nio-8080",} 0.0
tomcat_global_request_seconds_count{name="http-nio-8080",} 0.0
tomcat_global_request_seconds_sum{name="http-nio-8080",} 0.0
```

Kolejnym krokiem jest zainstalowanie Prometheus i Grafana i obserwacja:

![k8s-blog.webp](/uploads/k8s_blog_d69ef57bc0.webp)


## Uruchamianie serwisów sieciowych na różnych portach

Obsługa zapytań “biznesowych” jest innym zagadnieniem niż metryki. Dlatego najlepiej, żeby były dostępne na różnych portach. Ułatwi to pracę np. DevOps’om, którzy mogą chcieć wyłączyć autentykację jeżeli ruch kierowany jest na port metryk lub zastosować inne reguły bezpieczeństwa.

Ułatwi to też konfigurację Istio (problem kubelet odpytujący /health vs mTLS):
```
data:
application.yaml: |-
management:
# Different port for metrics and healthchecks solve problem Istio vs Kubelet
server.port: 8090
```

## Stosowanie się do zaleceń - czy to się opłaca?

Kubernetes zaleca użycie ogólnych labelek, które tworzą jego [zasoby](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/). Biorąc pod uwagę, jak wiele aplikacji, tworzonych wokół kontraktu i zaleceń, powstaje dla Kubernetes. Takim przykładem jest Kiali, która potrafi zobrazować nasz system, jeżeli trzymamy się zaleceń Kubernetes odnośnie labelek:

```
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-java-hello-world
labels:
# K8s standard label - https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
# Tools around assume such naming convention and building product around it. Ex Istio and Kiali
app.kubernetes.io/name: k8s-java-hello-world
# K8s standard label - https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
# Tools around assume such naming convention and building product around it. Ex Istio and Kiali
app.kubernetes.io/version: 1.0.0
# K8s standard label - https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
# Tools around assume such naming convention and building product around it. Ex Istio and Kiali
app.kubernetes.io/part-of: k8s-java-hello-world
```

![k8s-blog-2.webp](/uploads/k8s_blog_2_a79df71c4c.webp)

## Reguła uprawnień na miarę zadań
Ta reguła dotyczy każdego środowiska – uprawnienia powinny być zminimalizowane do tych niezbędnych, a nie root/admin od początku. W przypadku Kubernetes należy ustawić securityContext w specyfikacji, aby kontener nie był uruchamiany spod root:

```
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
```

Również należy zadbać, aby sam serwis w Kubernetes używał innego konta niż default. Umożliwi to adekwatną konfigurację np. IaM u dostawcy chmury, Istio itd.

```
apiVersion: v1
kind: ServiceAccount
metadata:
name: k8s-java-hello-world
```

## Aktualizacja bibliotek

Zazwyczaj aplikacja używa biblioteki Spring MVC itd. Właściciele bibliotek od czasu do czasu naprawiają znalezione w nich błędy albo problemy wydajnościowe. Teoretycznie nowsza wersja, to lepszy produkt.

Żeby trzymać rękę na pulsie, wystarczy aktywować plugin w maven albo gradle.

```

org.codehaus.mojo
versions-maven-plugin
${versions-maven-plugin.version}


verify


display-dependency-updates
display-property-updates
display-plugin-updates
dependency-updates-report
plugin-updates-report
property-updates-report


```

## Weryfikacja bibliotek pod kątem bezpieczeństwa

OWASP to uznana organizacja zajmująca się bezpieczeństwem w IT – publikuje artykuły na temat znanych dziur w bezpieczeństwie i podpowiada jak im zaradzić. Co więcej, utworzyła i udostępniła wtyczkę, która weryfikuje zależności użyte w naszej aplikacji i bada czy przypadkiem nie znajdują się w bazie bibliotek ([National Vulnerability Database](https://nvd.nist.gov/vuln/search)), w których wykryto dziurę.

```

org.owasp
dependency-check-maven
${dependency-check-maven.version}


8


check


```

## Tworzenie obrazu aplikacji Java w kontenerze

Początkowo jedynym sposobem tworzenia obrazów aplikacji w kontenerze było utworzenie i wypełnienie pliku Dockerfile i uruchomienie aplikacji Docker, która tworzy obraz zgodnie z poleceniami zawartymi w Dockerfile. Stworzenie „dobrego” obrazu wymaga wiedzy z paru technologii:
* Docker - np. pojęcie warstw obrazu
* Security – jakiego systemu operacyjnego użyć, jakie aplikacje włączyć/wyłączyć i jak je skonfigurować.

W międzyczasie powstały aplikacje, które agregują w sobie tę wiedzę i odpowiednio tworzą obrazy:

1. JIB od Google’a. Narzędzie jest dostępne jako plugin do Maven i Gradle, wykorzystywane jest do skonteneryzowania aplikacji wewnątrz Google. Nie wymaga instalacji Docker. Jedno z ważniejszych cech obrazów tworzonych przez JIB jest „distroless” obraz bazowy. JIB trzyma się zasady, iż kontener na produkcji powinien mieć (chociaż lepiej napisać czego nie powinien mieć) odinstalowane/wyłączone aplikacje, które nie kładą nacisku wartość biznesową albo nie są wymagane do funkcjonowania systemu operacyjnego. W ten sposób w tak utworzonym obrazie nie znajdzie się bash ani sh.

```

pl.project13.maven
git-commit-id-plugin
${git-commit-id-plugin.version}


get-the-git-infos


revision


validate


${project.basedir}/.git


com.google.cloud.tools
jib-maven-plugin
${jib-maven-plugin.version}


${container.registry.url}/${container.name}:${git.commit.id.abbrev}


8080


USE_CURRENT_TIMESTAMP


-server


```

2. Cloud Native Buildpacks (CNB). Specyfikacja i jej implementacja utworzona na potrzeby platformy Heroku w 2011. Od tego czasu zyskała adaptację Cloud Foundry, Google App Engine, Gitlab, Knative, Deis, Dokku i Drie. CNB implementuje najlepsze praktyki przy tworzeniu OCI obrazów. Narzędzie dostępne „out of the box” w wielu CI/CD systemach jako krok do dodania do „pipeline”. Należy pobrać aplikację pack i użyć jej np. w poniższy sposób:

```
export GIT_SHA="eb7c60b"

pack build --path . --builder paketobuildpacks/builder:base registry.digitalocean.com/learning/k8s-java-hello-world:${GIT_SHA} --publish

base: Pulling from paketobuildpacks/builder
===> ANALYZING
Previous image with name "registry.digitalocean.com/learning/k8s-java-hello-world:eb7c60b" not found
===> RESTORING
===> BUILDING

*** Images (sha256:dc7bbcc2a5fd3d20d0e506126f7181f57c358e5c9b5842544e6fc1302b934afe):
registry.digitalocean.com/learning/k8s-java-hello-world:eb7c60b
Adding cache layer 'paketo-buildpacks/bellsoft-liberica:jdk'
Adding cache layer 'paketo-buildpacks/maven:application'
Adding cache layer 'paketo-buildpacks/maven:cache'
Adding cache layer 'paketo-buildpacks/maven:maven'
Successfully built image registry.digitalocean.com/learning/k8s-java-hello-world:eb7c60b
```

## Poznaj więcej możliwości Kubernetesa

Jeżeli poszukujesz kompleksowego szkolenia z kontenerów, sprawdź nasze propozycje:
* [Kubernetes w praktyce](https://www.sages.pl/szkolenia/kubernetes-w-praktyce)
* [Docker w praktyce](https://www.sages.pl/szkolenia/docker-w-praktyce)
* [Istio w praktyce](https://www.sages.pl/szkolenia/istio-w-praktyce)

Więcej szkoleń z tej kategorii znajdziesz na stronie: [https://www.sages.pl/szkolenia/kategoria/chmura](https://www.sages.pl/szkolenia/kategoria/chmura)
Idź do oryginalnego materiału