Kubernetes dla początkujących: pierwsze wdrożenie aplikacji i podstawy skalowania

0
247
3.7/5 - (7 votes)

Nawigacja:

Krótka scenka: pierwsze zderzenie z Kubernetesem

Projekt działa od miesięcy w Docker Compose, kilka kontenerów, prosty plik docker-compose.yml. Szef mówi: „Klient chce wysoką dostępność, monitoring, autoskalowanie – wrzućmy to na Kubernetesa”. Otwierasz pierwszego YAML-a z Deploymentem i Ingressem, widzisz kilkadziesiąt linii niewiadomych – i pojawia się pytanie: od czego w ogóle zacząć.

W takim momencie łatwo uznać, że Kubernetes to czarna magia zarezerwowana dla „prawdziwych” DevOpsów. W praktyce to zestaw powtarzalnych klocków, które można zrozumieć krok po kroku. Klucz tkwi w tym, żeby najpierw ogarnąć jeden działający klaster i jedną prostą aplikację, a dopiero potem sięgać po autoskalowanie, ingressy i resztę fajerwerków.

Cel jest dość prosty: przejść z etapu „umiem zbudować obraz Dockera” do poziomu „potrafię wdrożyć tę aplikację na Kubernetesie, wiem jak ją skalować i jak diagnozować problemy”. Reszta to już tylko powtarzanie sprawdzonych wzorców.

Co to właściwie jest Kubernetes i kiedy ma sens

Orkiestracja kontenerów w praktyce

Kubernetes to platforma do orkiestracji kontenerów. Można o nim myśleć jak o automacie, który wykonuje za admina trzy kluczowe zadania:

  • uruchom kontenery zgodnie z deklaracją w YAML-u,
  • utrzymaj zadaną liczbę replik (jeśli jedna replika padnie – podnieś kolejną),
  • udostępnij aplikację w przewidywalny sposób (Service, Ingress, load balancing).

Ty deklarujesz w pliku Deploymentu: „chcę trzy repliki aplikacji w kontenerze z takim obrazem, z taką ilością pamięci, odsłuchujące port 8080”. Kubernetes sam dobiera, na których węzłach je uruchomi, jak zarestartuje kontener, jeśli ten się wysypie, i jak równoważyć ruch między replikami. To jest kluczowa różnica względem gołego Dockera – nie musisz ręcznie odpalać kontenerów na każdym serwerze.

Składniki klastra bez przeładowania teorią

Klaster Kubernetes składa się z dwóch grup elementów: control plane (często mówimy „master”) i worker nodes (węzły robocze). W praktyce:

  • Control plane – zestaw komponentów, które podejmują decyzje: scheduler, API server, etcd (przechowuje stan), controller manager. To mózg klastra. Zwykle nie dotykasz go ręcznie – rozmawiasz z nim przez kube-API, używając kubectl.
  • Worker node – zwykła maszyna (VM lub fizyczna), na której faktycznie działają pody z kontenerami. Na każdym węźle pracują:
    • kubelet – agent, który pilnuje, by na danym węźle działało to, co zadeklarowane w klastrze,
    • kube-proxy – odpowiada za sieć i przekierowanie ruchu do odpowiednich podów,
    • silnik kontenerowy (np. containerd, czasem Docker).

Jako osoba wdrażająca aplikację najczęściej pracujesz z trzema rzeczami: kubectl, plikami YAML i rejestrem obrazów (Docker Hub, GitHub Container Registry, ECR, GCR). Resztę zarządzają dostawcy chmury lub administratorzy.

Kiedy Kubernetes jest dobrym wyborem, a kiedy to przesada

Kubernetes błyszczy w scenariuszach, gdzie:

  • masz wiele usług (mikrousługi, kilka aplikacji współdzielących infrastrukturę),
  • wymagasz wysokiej dostępności – kilka węzłów, automatyczny restart i przenoszenie podów,
  • masz zróżnicowane środowiska: dev, test, stage, prod i chcesz, żeby były spójne,
  • chcesz skalować poziomo na podstawie CPU, pamięci lub metryk aplikacyjnych.

Z drugiej strony, Kubernetes bywa „armatą na muchę”, gdy:

  • masz jedną prostą aplikację hostowaną na małym VPS,
  • nie potrzebujesz skalowania, a restart kontenera możesz ogarnąć systemd,
  • budżet i czas na utrzymanie klastra są znikome.

Docker Compose vs Kubernetes – kiedy co wybrać

Wiele projektów zaczyna od Docker Compose. To wygodne: jeden plik, jedna komenda, lokalne środowisko działa. Pojawia się jednak ściana, gdy:

  • chcesz uruchomić to samo na kilku serwerach,
  • musisz mieć roll-outy, roll-backi i kontrolę wersji konfiguracji,
  • chcesz płynnie dodawać kolejne repliki w reakcji na ruch.
CechaDocker ComposeKubernetes
Skala projektuMałe, proste aplikacjeŚrednie i duże systemy, mikrousługi
ŚrodowiskaGłównie lokalne devDev, test, stage, prod
SkalowanieRęczne, ograniczoneAutomatyczne, HPA, wiele węzłów
Wysoka dostępnośćBrak natywnego wsparciaRepliki, rozłożenie na węzły
ZłożonośćNiskaŚrednia / wysoka

Jeżeli aplikacja ma szansę urosnąć – nowe usługi, więcej użytkowników, nowe środowiska – sensownie jest poznać podstawy Kubernetes wcześniej. Unikniesz gwałtownej migracji „na wczoraj”, gdy zacznie się sypać obecna infrastruktura.

Wniosek z perspektywy praktyka

Kubernetes nie jest celem samym w sobie. To narzędzie, które opłaca się poznać, gdy potrzebujesz powtarzalności, skalowania i odporności na awarie. Jeśli twój projekt rośnie lub dopiero go planujesz, znajomość podstawowych obiektów i mechanizmów oszczędzi sporo zaskoczeń na późniejszych etapach.

Przygotowanie środowiska: jak postawić klaster do nauki

Wybór narzędzia: minikube, kind, k3d

Do nauki lokalnej nie potrzebujesz od razu klastra w chmurze. Wystarczy narzędzie, które na twoim laptopie postawi mały, jedno- lub kilku-węzłowy klaster. Najpopularniejsze opcje:

  • minikube – klasyk. Tworzy pojedynczy węzeł (VM lub kontener) z działającym clusterem. Ma sporo dodatków (addons) i dobrze odzwierciedla zachowanie „prawdziwego” klastra.
  • kind (Kubernetes in Docker) – klaster budowany z kontenerów Dockera. Szybki start, świetny do CI, bardzo lekki, ale nieco więcej ręcznej konfiguracji przy sieci.
  • k3d (K3s in Docker) – oparty na lekkiej dystrybucji K3s, także działa w oparciu o kontenery. Lżejszy niż klasyczny Kubernetes, dobra opcja na słabsze laptopy.

Dla początkujących najwygodniejszy jest minikube. Ma dużo dokumentacji, a większość tutoriali zakłada jego użycie.

Wymagania sprzętowe i typowe problemy przy starcie

Aby komfortowo uruchomić minikube, przydaje się:

  • co najmniej 4 GB RAM dostępne dla klastra (8 GB RAM w całym laptopie to rozsądne minimum),
  • kilka wolnych CPU (2 vCPU to sensowna baza),
  • działająca wirtualizacja (VT-x/AMD-V) w BIOS/UEFI, jeśli używasz drivera VM,
  • zainstalowany Docker lub inny driver (VirtualBox, Hyper-V) – w zależności od systemu.

Najczęstsze problemy przy starcie lokalnego klastra:

  • wyłączona wirtualizacja w BIOS/UEFI,
  • porty zajęte przez inne usługi (np. 8443, 10250),
  • antywirus lub firewall blokujący połączenia do lokalnej VM,
  • zbyt mało RAM – minikube startuje, ale pody wiecznie wiszą w stanie Pending.

Jeśli masz Docker Desktop, najwygodniej jest użyć sterownika docker, bo unikasz VM i problemów z wirtualizacją.

Instalacja minikube krok po kroku

Zakładając, że Docker już działa, możesz postawić minikube bardzo szybko:

curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube

(na macOS lub Windows użyjesz odpowiedniego instalatora z dokumentacji minikube).

Start klastra z użyciem Dockera:

minikube start --driver=docker

Jeśli wszystko pójdzie dobrze, zobaczysz informację o uruchomionym klastrze i skonfigurowanym kubectl. Podstawowa komenda weryfikująca:

kubectl get nodes

Oczekiwany wynik: jeden node w stanie Ready. To znak, że można przejść do pierwszego wdrożenia aplikacji.

Kontekst kubeconfig i praca z wieloma klastrami

Plik ~/.kube/config przechowuje dane o klastrach, użytkownikach i kontekstach. Kontekst mówi kubectl, do którego klastra ma się łączyć domyślnie. Gdy do minikube dojdzie np. klaster w chmurze, łatwo się pogubić, dlatego warto znać podstawowe komendy:

  • kubectl config get-contexts – lista wszystkich dostępnych kontekstów,
  • kubectl config current-context – jaki kontekst jest obecnie ustawiony,
  • kubectl config use-context minikube – przełączenie się na kontekst minikube.

Prosta zasada bezpieczeństwa: przed każdym zastosowaniem kubectl apply lub kubectl delete rzuć okiem na wynik kubectl config current-context. Jedna pomyłka może sprawić, że zamiast usunąć zasoby w minikube, skasujesz coś w produkcji.

Stabilne środowisko przed YAML-ami

Dopiero gdy masz pewność, że klaster działa, kubectl łączy się bez błędów, a context jest ustawiony na minikube, ma sens przechodzenie do pisania plików YAML. Wiele frustracji z Kubernetesem wynika z prób debugowania aplikacji przy niedziałającym lub niestabilnym klastrze. Najpierw solidny fundament, potem reszta.

Najważniejsze obiekty Kubernetes w praktyce, bez przesytu teorii

Pod, ReplicaSet, Deployment – trio do codziennej pracy

Podstawowa jednostka, którą uruchamia Kubernetes, to Pod. Pod to zazwyczaj jeden kontener (czasem kilka blisko ze sobą powiązanych). W praktyce jednak rzadko tworzysz pody bezpośrednio. Zamiast tego korzystasz z wyższego poziomu abstrakcji.

  • Pod – pojedyncza instancja aplikacji (lub kilku kontenerów). Jeśli padnie, sam go nie wstanie – potrzebny jest mechanizm wyżej.
  • ReplicaSet – obiekt pilnujący, aby liczba podów konkretnego typu się zgadzała (np. 3 repliki). Gdy jeden padnie, tworzy nowy.
  • Deployment – obiekt, z którym pracujesz na co dzień. Deklarujesz w nim szablon poda, liczbę replik i sposób aktualizacji (rollout). Deployment sam tworzy ReplicaSety.

Typowy workflow: edytujesz YAML Deploymentu, następnie stosujesz go komendą kubectl apply. Deployment aktualizuje ReplicaSet, a ten odpowiednio skaluje pody. Przy aktualizacji obrazu Deployment stworzy nowego ReplicaSeta, powoli przełączy ruch i usunie stary, jeśli wszystko poszło dobrze.

Service: stały adres dla zmiennych podów

Pod to twór ulotny: może zostać przeniesiony na inny węzeł, zrestartowany, dostać nowy IP. Dlatego komunikowanie się z podami bezpośrednio jest kiepskim pomysłem. Tu wchodzi Service – stabilny punkt kontaktu dla aplikacji.

Najczęstsze typy Service:

  • ClusterIP – domyślny. Wystawia usługę pod stałym adresem IP dostępnym tylko wewnątrz klastra. Używany do komunikacji między usługami.
  • NodePort – wystawia usługę na każdym węźle na jednym porcie (zwykle wysokim). Przydatny lokalnie lub w prostych scenariuszach.
  • LoadBalancer – deleguje stworzenie zewnętrznego load balancera (np. w chmurze). Standardowy sposób wystawiania usług na świat w środowiskach produkcyjnych.

Service używa selektorów (np. app: my-app), aby znaleźć pody, do których ma kierować ruch. Zachowuje się jak wbudowany, prosty load balancer – równoważy zapytania między replikami.

ConfigMap i Secret – konfiguracja zamiast przepisywania obrazu

Wyobraź sobie, że masz ten sam obraz aplikacji webowej, ale trzy środowiska: dev, test i prod. Za każdym razem, gdy zmienia się adres bazy albo endpoint zewnętrznego API, ktoś proponuje „przebudujmy obraz z innym plikiem konfiguracyjnym”. Trzy iteracje później nikt nie pamięta, który tag obrazu jest do czego.

W Kubernetes konfiguracja powinna żyć obok obrazu, a nie w nim. Do tego służą dwa podstawowe obiekty:

  • ConfigMap – jawna konfiguracja typu „nie wstydzę się tego”: URL-e, flagi featurowe, nazwy kolejek,
  • Secret – dane poufne: hasła, tokeny, klucze API, certyfikaty.

Najprostszy przykład ConfigMap jako zbiór zmiennych środowiskowych:

apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  APP_ENV: "dev"
  APP_LOG_LEVEL: "debug"
  APP_API_URL: "https://api-dev.example.com"

Secret wygląda podobnie, ale przechowuje dane w formacie base64 (to nie jest pełne szyfrowanie, raczej utrudnienie czytania gołym okiem):

apiVersion: v1
kind: Secret
metadata:
  name: myapp-secret
type: Opaque
data:
  DB_USER: bXl1c2Vy
  DB_PASSWORD: c2VjcmV0cHdk

Prawdziwe wartości kodujesz tak:

echo -n "myuser" | base64
echo -n "secretpwd" | base64

Podłączenie ConfigMap i Secret do poda następuje zwykle przez zmienne środowiskowe w specyfikacji kontenera:

env:
  - name: APP_ENV
    valueFrom:
      configMapKeyRef:
        name: myapp-config
        key: APP_ENV
  - name: DB_USER
    valueFrom:
      secretKeyRef:
        name: myapp-secret
        key: DB_USER

Dzięki temu możesz używać tego samego obrazu w wielu środowiskach – różni je tylko zestaw ConfigMap i Secret. Gdy nagle trzeba zmienić endpoint zewnętrznego API, poprawiasz ConfigMap, robisz rollout i problem znika bez ingerencji w pipeline budowania obrazów.

Namespace – porządek w jednym klastrze

W pewnym momencie do lokalnego klastra zaczyna zaglądać więcej osób. Jeden z programistów wrzuca swoje pody do domyślnego namespace default, ktoś inny kasuje „stare” serwisy, żeby posprzątać. Po chwili okazuje się, że usunął testowy system kolegi.

Namespace to logiczna „przegroda” w klastrze. Pozwala trzymać różne środowiska lub projekty w jednym Kubernetesie, ale nie mieszać ich zasobów:

  • dev, test, stage – oddzielne miejsca na różne fazy cyklu życia aplikacji,
  • team-a, team-b – podział między zespoły,
  • specjalne namespace jak kube-system czy monitoring dla narzędzi platformowych.

Utworzenie prostego namespace:

kubectl create namespace dev

Można też deklaratywnie, w YAML:

apiVersion: v1
kind: Namespace
metadata:
  name: dev

Każdy Deployment, Service, ConfigMap czy Secret trafia do konkretnego namespace (o ile nie podasz innego, domyślny to default). W kubectl określasz go na dwa sposoby:

  • globalnie ustawiając kontekst:
    kubectl config set-context --current --namespace=dev
    
  • ad hoc:
    kubectl get pods -n dev
    

Prosta zasada z praktyki: osobny namespace na każde środowisko aplikacji. Dzięki temu nie kasujesz cudzych zasobów „sprzątając po sobie”, a przy okazji możesz precyzyjniej ustawiać uprawnienia, limity i polityki sieciowe.

Czerwone i niebieskie kontenery transportowe na tle czystego nieba
Źródło: Pexels | Autor: Jan van der Wolf

Przygotowanie aplikacji do wdrożenia: obraz kontenera i konfiguracja

Minimalny Dockerfile pod Kubernetes

Ktoś przynosi działającą aplikację Node.js albo Pythona i pyta: „To kiedy możemy ją wrzucić na Kubernetesa?”. Pierwsze pytanie brzmi: czy masz sensowny obraz kontenera. „Działa u mnie w Dockerze” jeszcze nie znaczy, że będzie komfortowo działać w klastrze.

Przykładowy, prosty Dockerfile dla aplikacji Node.js:

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

ENV PORT=3000
EXPOSE 3000

CMD ["node", "server.js"]

Kilka zasad, które ułatwiają życie w Kubernetesie:

  • używaj oficjalnych, lekkich baz (np. alpine) zamiast ciężkich obrazów z pełnym systemem,
  • buduj obraz tak, aby konfiguracja przychodziła z zewnątrz (env, pliki z ConfigMap),
  • proces główny musi działać na pierwszym planie (PID 1) – bez demonizowania w tle,
  • obsłuż poprawnie sygnały zakończenia (SIGTERM), żeby kontener zdążył się zamknąć po łagodnym shutdownie.

Budowanie i publikowanie obrazu

Przy lokalnym klastrze minikube możesz użyć lokalnego Dockera albo zewnętrznego rejestru. Dla prostego scenariusza wystarczy standardowy build:

docker build -t myuser/myapp:1.0.0 .

Następnie wypchnięcie do rejestru (np. Docker Hub):

docker push myuser/myapp:1.0.0

W prawdziwym projekcie lepiej zintegrować to z CI, ale na start ręczny build i push w zupełności wystarczą. Ważne, aby klaster miał dostęp do tego rejestru – lokalnie zwykle nie ma z tym problemu, w bardziej zamkniętych środowiskach trzeba dodać dane logowania jako Secret typu docker-registry.

Parametryzacja przez zmienne środowiskowe

Zamiast trzymać w obrazie stałe adresy czy hasła, aplikacja powinna czytać konfigurację z env. W praktyce często potrzebujesz:

  • adresu bazy danych (host, port, nazwa bazy),
  • credów (user, password) – z Secret,
  • flagi mówiącej, w jakim środowisku działa (dev/stage/prod),
  • ustawień logowania (poziom, format).

Jeżeli dziś aplikacja czyta plik konfiguracyjny z dysku, warto dodać obsługę zmiennych środowiskowych przynajmniej dla kluczowych parametrów. Później możesz ten plik generować z ConfigMap lub Secret, ale pierwszym krokiem jest „nauczenie” aplikacji patrzenia w env.

Pierwszy Deployment krok po kroku: od YAML do działającego poda

Prosty Deployment dla aplikacji webowej

Załóżmy, że masz obraz myuser/myapp:1.0.0, który wystawia HTTP na porcie 3000. Czas zamienić to na Deployment, który uruchomi jedną lub kilka replik.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
  labels:
    app: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myuser/myapp:1.0.0
          ports:
            - containerPort: 3000
          env:
            - name: APP_ENV
              value: "dev"

Zapisz to jako deployment.yaml i zastosuj:

kubectl apply -f deployment.yaml

Status podów sprawdzisz tak:

kubectl get pods

Powinny pojawić się dwa pody z nazwami w stylu myapp-deployment-xxxxxx-yyyyy w stanie Running. Jeśli któryś utknął w CrashLoopBackOff, trzeba zajrzeć do logów.

Diagnozowanie problemów z pierwszym Deployementem

Gdy pody nie startują, zamiast zgadywać, można przejść prostą ścieżkę diagnostyczną:

  1. Podgląd stanu Deploymentu:
    kubectl describe deployment myapp-deployment
    
  2. Lista podów z etykietą:
    kubectl get pods -l app=myapp
    
  3. Podgląd szczegółowy pada, który się nie podnosi:
    kubectl describe pod <nazwa-poda>
    
  4. Logi z aplikacji:
    kubectl logs <nazwa-poda>
    

Najczęstsze przyczyny problemów na starcie:

  • zła nazwa/tak obrazu (błąd ściągania image),
  • aplikacja natychmiast się wyłącza przez błąd konfiguracyjny,
  • port w kontenerze inny niż ten, który deklarujesz w containerPort (nie blokuje startu, ale myli przy Service),
  • brak dostępu do zewnętrznego serwisu wymagany przy starcie (np. baza danych), a aplikacja nie ma retry.

Jeżeli obraz nie chce się ściągnąć (status ImagePullBackOff), można szybciej testować, budując obraz bezpośrednio w środowisku Dockera używanym przez minikube. Wtedy używa lokalnego cache i nie musi uderzać do zewnętrznego rejestru.

Skalowanie ręczne: zmiana liczby replik

Pierwsza zabawa ze skalowaniem sprowadza się do prostej zmiany liczby replik. Masz 2 pody – zobacz, co się stanie, gdy ustawisz 5:

kubectl scale deployment myapp-deployment --replicas=5

Po chwili:

kubectl get pods -l app=myapp

Powinno być pięć podów w stanie Running. Ten sam efekt osiągniesz edytując YAML i używając kubectl apply -f. W praktyce ręczne skalowanie przydaje się w sytuacjach awaryjnych (np. nagły ruch marketingowy), zanim wdrożysz automatyczne HPA.

Rolowanie nowej wersji obrazu

Zmiana wersji aplikacji polega zazwyczaj na podmianie taga obrazu. Możesz to zrobić edytując YAML i stosując go na nowo albo używając prostego polecenia:

kubectl set image deployment/myapp-deployment 
  myapp=myuser/myapp:1.1.0

Deployment zacznie rollout: uruchomi nowe pody z wersją 1.1.0 i stopniowo wyłączy stare, jeśli wszystko działa. Postęp obserwujesz tak:

kubectl rollout status deployment/myapp-deployment

Gdyby nowa wersja okazała się wadliwa, możesz szybko wrócić do poprzedniej konfiguracji:

kubectl rollout undo deployment/myapp-deployment

Ta możliwość cofnięcia zmian bez grzebania w Git często ratuje skórę, szczególnie na testowych środowiskach, gdzie eksperymentujesz z parametrami aplikacji.

Udostępnienie aplikacji: Service i pierwszy dostęp z zewnątrz

Service typu ClusterIP – komunikacja wewnętrzna

Masz działające pody, ale bez Service są „bezimienne” dla reszty klastra. Nawet jeśli IP poda da się podejrzeć, zmieni się przy restarcie. Pierwszy krok to nadanie aplikacji stałego adresu w klastrze.

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
    - name: http
      port: 80
      targetPort: 3000
  type: ClusterIP

W tym przykładzie:

  • selector wskazuje na pody z etykietą app: myapp,
  • port 80 to port Service – taki, którego będą używać inni klienci,
  • targetPort 3000 to port w kontenerze.

Zastosowanie manifestu:

kubectl apply -f service.yaml

Service dostanie własne IP z puli klastra oraz wpis DNS: w tym samym namespace aplikacje odwołają się do niego po nazwie myapp-service. To wystarczy, aby np. backend mógł zawołać frontend lub odwrotnie, bez martwienia się o zmieniające się IP poszczególnych podów.

Service typu NodePort – pierwszy kontakt z przeglądarki

Na lokalnym minikube najszybsza droga do sprawdzenia, czy aplikacja faktycznie działa „z zewnątrz”, to Service typu NodePort. Wystawia wybrany port każdego węzła na zewnątrz, przekierowując ruch do podów.

Zmodyfikowana definicja Service może wyglądać tak:

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
    - name: http
      port: 80
      targetPort: 3000