Kubernetes od zera w domu: k3s, Helm, Argo CD i Sealed Secrets

Kubernetes był moją największą luką, więc zamiast czytać o nim, postawiłem go na domowym serwerze i uruchomiłem tam kopie prawdziwych usług. Po kilku dniach cały klaster dało się odtworzyć z jednego repozytorium, razem z hasłami.

· 5 min czytania

Spisane w listopadzie 2026 na podstawie notatek z tamtego czasu.

Linuksa administruję od lat, ale Kubernetes długo był dla mnie czymś, o czym się czyta, a nie czymś, co się utrzymuje. W lipcu 2026 postanowiłem to zmienić. Zasada była prosta: żadnych przykładów z tutoriala, które po godzinie się kasuje. Każdy krok miał zostawić po sobie działającą usługę i commit w repozytorium.

Dlaczego k3s, a nie od razu „prawdziwy” Kubernetes

Na start dostałem jedną maszynę wirtualną z 4 GiB RAM-u na domowym serwerze. Na tym nie postawi się wygodnie pełnego klastra z osobnym etcd i kilkoma węzłami, a k3s mieści się w jednym procesie i od razu przynosi to, czego klaster potrzebuje do życia: sieć podów, Traefika jako ingress, provisioner wolumenów na lokalnym dysku i prosty load balancer. Chciałem uczyć się obiektów Kubernetesa, a nie walczyć pierwszego dnia z CNI, więc ten kompromis był dla mnie oczywisty.

Kolejność nauki była taka: Pod, Deployment, Service i Ingress na prostych aplikacjach, potem wolumeny i sekrety na BookStacku z MariaDB, potem własny chart Helm, a na końcu GitOps. Każdy etap budował na poprzednim. Ten sam BookStack, który na początku miał ręcznie zaaplikowane manifesty, kilka dni później był już zarządzany przez Argo CD, bez przestoju: Argo przejęło istniejące obiekty, zamiast stawiać je od nowa.

Helm: najpierw własny chart, potem cudze

Własny chart do speedtestu napisałem po to, żeby zrozumieć, co właściwie robi helm install. Okazuje się, że niewiele magii: szablony Go, plik values.yaml i wersjonowany zestaw manifestów. Ta wiedza zwróciła się od razu przy kube-prometheus-stack, gdzie values mają setki linii i bez zrozumienia, co z czego się renderuje, łatwo zgubić się w nadpisaniach.

Przy tym samym charcie trafiłem na pierwszy prawdziwy problem. CRD-y Prometheusa są tak duże, że nie mieszczą się w adnotacji, którą zwykłe kubectl apply zapisuje po stronie klienta. Rozwiązaniem jest Server-Side Apply, w Argo CD włączane jedną opcją synchronizacji.

GitOps: repozytorium jest źródłem prawdy

Argo CD czyta repozytorium z mojej własnej Gitei, z tokenem tylko do odczytu. Struktura jest wzorcem „app-of-apps”: jedna aplikacja root patrzy na katalog apps/, a każdy plik w tym katalogu to kolejna aplikacja. Dodanie usługi to jeden nowy plik i jeden katalog z manifestami. Po kilku dniach Argo CD zarządzało nawet samym sobą.

# apps/root.yaml – jedna aplikacja, która pilnuje wszystkich pozostałych
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://git.example.com/me/homelab-k3s.git
    targetRevision: main
    path: apps
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd

Świadomie nie włączyłem automatycznej synchronizacji. Push do repozytorium pokazuje się w Argo jako „OutOfSync” i czeka, aż go zastosuję. Na klastrze do nauki czasem coś zmieniam ręcznie, żeby zobaczyć, co się stanie, i nie chciałem, żeby self-heal cofał moje eksperymenty, zanim zdążę je obejrzeć. Przy okazji ręczna synchronizacja wymusza czytanie diffu przed każdym wdrożeniem, a to nawyk, który zostawiłem sobie do dziś, także na produkcji.

Sealed Secrets, czyli hasła w gicie bez wstydu

Został jeden problem: skoro klaster ma się odtwarzać z repozytorium, to hasło do bazy też musi tam być. Wybrałem Sealed Secrets, bo jest natywne dla Kubernetesa i Argo CD nie potrzebuje żadnej wtyczki. Sekret szyfruje się kluczem publicznym kontrolera i do repozytorium trafia tylko szyfrogram. Odszyfrować go może wyłącznie kontroler w klastrze.

W domyślnym trybie szyfrogram jest związany z nazwą i namespace’em sekretu. To ma konsekwencje: to samo hasło użyte w dwóch namespace’ach wymaga dwóch osobno zapieczętowanych plików. Na początku to irytuje, potem doceniasz, że nikt nie skopiuje zaszyfrowanego sekretu do innej aplikacji i nie odczyta go tam.

Nie uwierzyłem w to na słowo, tylko zrobiłem test ogniowy:

Hasła leżą w pliku poza repozytorium i od razu idą do kubeseal
$ kubectl create secret generic bookstack-env -n bookstack --from-env-file=/run/user/$UID/bookstack.env --dry-run=client -o yaml | kubeseal --format yaml > bookstack/bookstack-sealedsecret.yaml
$ git add bookstack/bookstack-sealedsecret.yaml && git commit -m "bookstack: secrets as SealedSecrets"
# test ogniowy: kasuję odszyfrowany Secret i patrzę, czy kontroler go odtworzy
$ kubectl -n bookstack delete secret bookstack-env
$ kubectl -n bookstack get secret bookstack-env

Kontroler odtworzył Secret z zapieczętowanej wersji. Dopiero wtedy uznałem, że to rozwiązanie zostaje.

Czego się nauczyłem

  • Klucz kontrolera Sealed Secrets to najważniejszy sekret w całym klastrze. Bez niego wszystkie zaszyfrowane pliki w repozytorium są bezużyteczne. Kopię trzymam w menedżerze haseł. Dwa tygodnie później przebudowałem ten klaster od zera na innym systemie i właśnie dzięki temu wszystkie sekrety odszyfrowały się bez ponownego pieczętowania: klucz wgrywa się, zanim wystartuje kontroler.
  • Uczyć się na prawdziwych usługach. Tutorial kończy się, gdy działa „hello world”. Prawdziwa aplikacja ma bazę danych, wolumen, hasło i ludzi, którzy jej używają, a dopiero to uczy, co znaczy wdrożenie bez przestoju.
  • Ręczna synchronizacja to nie lenistwo. Na początek wolę widzieć każdą różnicę między gitem a klastrem i zatwierdzać ją sam, niż odkrywać po fakcie, co automat zrobił w moim imieniu.

Wszystkie wpisy