Krzysztof Kaletka

Administrator Linux / Infrastructure Engineer

Utrzymuję dwie warstwy domowej infrastruktury jako kod: sieć na MikroTiku, która przenosi ruch, i skonteneryzowany stos usług, który na niej stoi. Obie odtwarzają się od pustego urządzenia. Większość tego, co niżej, to rzeczy, które się zepsuły — siedem awarii, sposób diagnozy każdej z nich i decyzja, którą każda wymusiła.

System

Obie połowy to osobne repozytoria i jest to decyzja, nie przypadek. Zmiana w firewallu potrafi odciąć od sieci cały dom; podbicie wersji obrazu kontenera nie. Inny zasięg awarii, inny sposób testowania, inne tempo wydań — więc nie dzielą repozytorium ani jednego przebiegu.

Warstwa sieciowa

ansible-network-template

Dwa urządzenia MikroTik konfigurowane w całości przez API RouterOS — bez SSH do konfiguracji. Uplink PPPoE na tagowanym VLAN-ie, mostek z filtrowaniem VLAN rozdzielony na main i servers, lustrzana tablica VLAN na switchu, DHCP i DNS dla obu segmentów, serwer WireGuard dla zdalnego dostępu oraz firewall, który przechwytuje nieszyfrowany DNS, blokuje obejście przez DoT/DoH i pilnuje granicy między segmentami.

6 ról · 10 ADR-ów · próba na jednorazowej maszynie RouterOS CHR przed sprzętem

Warstwa usług

ansible-homelab-template

Osiem ról Ansible wdrażających skonteneryzowany stos na maszynie AlmaLinux 9: resolwer DNS z blokowaniem reklam, reverse proxy Caddy budowane przez xcaddy pod certyfikaty wildcard przez ACME DNS-01, menedżer haseł, dashboard usług, stos Prometheus i Grafana z dashboardami provisionowanymi jako kod, ta strona oraz Cloudflare Tunnel wystawiający domenę bez ani jednego otwartego portu przychodzącego.

8 ról · 10 ADR-ów · próba na maszynie Vagrant przed produkcją

Siedem rzeczy, które się zepsuły

Każda z nich została zdiagnozowana od objawu, nie wyszukana w internecie. Tam, gdzie awaria zmieniła sposób działania kodu, obok znajduje się zapis decyzji.

  1. Warstwa sieciowa

    Czysty test, który niczego nie dowiódł

    W momencie włączenia vlan-filtering na RB5009 wszystkie porty dostępowe zachowały link i straciły całą łączność warstwy trzeciej. Mostek z filtrowaniem VLAN traktuje sam siebie jako port — port CPU — a adres bramy siedzi bezpośrednio na mostku, nietagowany. Wpisanie mostka jako tagged dla tego VLAN-u mówi mu, żeby oczekiwał ramek 802.1Q, które nigdy nie przychodzą. Reguła, którą z tego wyciągnąłem: przynależność mostka wynika z tego, skąd osiągany jest adres danego VLAN-u, a nie z założenia, że CPU potrzebuje każdego VLAN-u.

    Warta zapamiętania jest przyczyna, dla której próba tego nie złapała. Ścieżka zarządzania maszyny testowej leży poza mostkiem celowo — to właśnie czyni zawieszoną maszynę odzyskiwalną. Uczynienie testu zdolnym do wykrycia tego błędu kosztowałoby próbę jej siatkę bezpieczeństwa. Kontrola przeniosła się więc na przegląd tablicy na sprzęcie względem trzech jawnych przypadków, zamiast udawania, że zielony przebieg coś znaczył.

  2. Warstwa sieciowa

    Zmiana trasy stworzyła drugą trasę

    Zmiana bramy domyślnej switcha dodała trasę zamiast zmodyfikować istniejącą, zostawiając dwie. RouterOS wystawia ip route i ip firewall mangle bez klucza głównego, więc moduł nie ma po czym dopasować istniejącego wpisu. Oczywiste rozwiązanie — pozwolić Ansible przejąć całą ścieżkę i kasować wszystko, czego nie ma na liście — usunęłoby wpisy, do których rola nie ma prawa. Zamiast tego każdy zarządzany wpis nosi komentarz ansible:<cel>, a parowany task sprzątający usuwa nieaktualny dopiero po tym, jak zastępczy jest już na miejscu.

  3. Warstwa sieciowa

    Switch nieosiągalny przez własny trunk

    CRS310 nie dawał się osiągnąć z routera przy pierwszym przebiegu konfiguracji. VLAN 1 jest tagowany na trunku między urządzeniami, a switch w ustawieniach fabrycznych mówi wyłącznie ramkami nietagowanymi — więc link był podniesiony, a urządzenie milczało. Trzeba go zbootstrapować kablem prosto do laptopa i dopiero potem wstawić na miejsce. Zapisane w dokumentacji bootstrapu, a nie w pamięci, bo świeże wdrożenie natrafi na to ponownie.

  4. Warstwa usług

    Metryki, które kłamały z pełnym przekonaniem

    Panel sieciowy w Grafanie pokazywał wiarygodne liczby, które nie zgadzały się z rzeczywistością. node_exporter uruchomiony bez network_mode: host widzi wyłącznie interfejsy własnego namespace'u kontenera, a zamontowanie /proc hosta tego nie naprawia — /proc/net jest per-namespace, nie per-mount. Zdiagnozowane przez porównanie eksportowanych metryk z zawartością /proc/net/dev bezpośrednio na hoście. Zamiast zostawić panel, który wyglądał poprawnie i taki nie był, panel został usunięty; metryki CPU, pamięci i dysku są dokładne i zostały.

  5. Warstwa usług

    Dwa sposoby, na jakie kontener czyta przeszłość

    Przebudowa obrazu pod niezmienionym tagiem zostawia działający kontener wskazujący na sumę kontrolną, która już nie istnieje, a Compose przy najbliższym odświeżeniu wywala się błędem No such image. Osobno: pojedynczy plik zamontowany do kontenera wiąże się z jednym inode'em w chwili startu — a edytory zapisujące przez utworzenie zamiennika, w tym nano i sed, tworzą nowy inode, więc kontener dalej czyta osierocony oryginał, mimo że na dysku widać już nową treść.

    Oba objawiają się jak problemy z konfiguracją i żaden nim nie jest. Oba wymagają odtworzenia kontenera, nie restartu — różnica niewidoczna, dopóki nie kosztuje godziny.

  6. Warstwa usług

    Kontener monitorujący, który nic nie monitorował

    cAdvisor startował czysto, raportował, że jest zdrowy, i nie widział ani jednego kontenera. Wersje poniżej v0.54.0 nie rozumieją, jak containerd-snapshotter przechowuje warstwy kontenerów. Aktualizacja miała własną pułapkę: rejestr obrazu przeniósł się z gcr.io na ghcr.io przy v0.53.0, więc podbicie samego tagu bez zmiany rejestru kończy się błędem pobierania. Wersja i rejestr są dziś przypięte razem, z komentarzem w roli tłumaczącym, dlaczego żadnego nie wolno zmieniać osobno.

  7. Ta strona

    Rekord DNS utworzony o jeden krok za wcześnie

    Przegląd tej strony ujawnił, że www.kaletkadev.com nie miało rekordu DNS w ogóle. Utworzyłem go, zanim tunel dostał pasującą regułę ingress — w złej kolejności. Negatywne cache'owanie ukrywało błąd przez trzydzieści minut, po czym nazwa zaczęła się rozwiązywać i zwracać 404, aż do wdrożenia konfiguracji. Samodzielnie stworzone okno awarii, niewielkie i całkowicie do uniknięcia: najpierw backend przyjmuje ruch, potem kierujesz na niego ruch.

Decyzje i to, ile każda kosztowała

Dwadzieścia zapisów decyzji w obu repozytoriach. Każdy nazywa opcję wybraną, opcje odrzucone i konsekwencje — łącznie z tymi niewygodnymi. Sześć ciekawszych poniżej.

Wybrane zapisy decyzji architektonicznych wraz z ich kosztami
Decyzja Co kosztowała
Warstwa sieciowa we własnym repozytorium Sieć Dwa repozytoria do utrzymania w spójności i wspólne fakty o tym samym domu żyjące w dwóch miejscach.
API RouterOS zamiast network_cli Sieć Dodatkowa zależność Pythona na kontrolerze i minimalna wersja kolekcji, której pakiety dystrybucyjne nie spełniają.
VLAN serwerowy, default-deny tylko w jedną stronę Sieć Asymetria z założenia: segment serwerowy sięga do głównego swobodnie, bo rejestrator wideo sam wybiera połączenia do kamer. Nazwane, nie ukryte.
Skrypt powłoki na maszynę testową zamiast narzędzia IaC Sieć Mniej eleganckie niż zadeklarowanie maszyny. Narzędzie było na tyle niestabilne, że elegancja kosztowałaby więcej, niż dawała.
Cloudflare Tunnel zamiast przekierowania portów Usługi Dostępność zależy teraz od infrastruktury zewnętrznej firmy, a cały ruch jest dla niej widoczny w tranzycie. Przyjęte, by nie wystawiać magazynu haseł do publicznego internetu.
Nagłówki bezpieczeństwa zawężone do jednego handlera Usługi Restrykcyjna polityka nałożona na całą domenę rozwaliłaby Grafanę i menedżer haseł. Węższy zakres oznacza, że każda nowa usługa publiczna wymaga własnej decyzji.

Jak powstaje ta praca

Bramki przed produkcją

Zmiana nie jest skończona, kiedy działa. Trzeci przebieg Ansible z rzędu musi zwrócić changed=0 — inaczej rola nie jest idempotentna i zmiana nie wchodzi. Każdy pull request uruchamia ansible-lint, yamllint i sprawdzenie składni playbooka. Role przechodzą próbę na jednorazowej maszynie wirtualnej — RouterOS CHR dla sieci, Vagrant dla usług — zanim cokolwiek dotknie sprzętu.

Praca na routerze bez konsoli

Żadne z urządzeń MikroTik nie ma konsoli szeregowej, więc zła zmiana w firewallu lub VLAN-ach oznacza reset do ustawień fabrycznych i przekładanie kabli. Każdy taki przebieg chronią trzy warstwy: zaplanowany rollback uzbrojony przed zmianą i rozbrajany dopiero po potwierdzeniu łączności, otwarta obok sesja RouterOS Safe Mode oraz port zarządzania świadomie trzymany poza mostkiem, dopóki nowa ścieżka nie jest sprawdzona. Dwa lockouty jednego dnia to sygnał, żeby skończyć na dziś, a nie przeć dalej.

Sekrety

Zmienne szyfrowane Vaultem, szablony .example jako jedyna commitowana konfiguracja i 0600 na każdym generowanym pliku zawierającym poświadczenie. Ta ostatnia zasada istnieje, bo raz plik powstał z prawami do odczytu dla wszystkich — dziś jest wymuszona i zapisana w pliku kontekstowym repozytorium, żeby wymuszona została.

Gdzie jest miejsce dla AI, a gdzie go nie ma

Oba repozytoria noszą plik kontekstowy CLAUDE.md, który ogranicza pracę wspomaganą AI: nigdy nie commituj realnych sekretów, nigdy nie osłabiaj reguły lintera, żeby hook przeszedł, respektuj kolejność ról, od której zależy wdrożenie, i pamiętaj o pułapkach, w które ten kod już raz wpadł. Wygenerowane zmiany przechodzą to samo review, to samo CI i tę samą maszynę testową co wszystko inne. Ciekawe są tu bariery, nie generowanie.

Węzeł GPU, który wyłączyłem

Zbudowałem bezgłowy węzeł wokół RTX 5070 podłączonego przez OCuLink i uruchamiałem na nim lokalne modele językowe, żeby sprawdzić, co ten sprzęt faktycznie potrafi. Jest odstawiony. Węzeł stał bezczynnie niemal cały czas, a bezczynny sprzęt nie uzasadnia ani poboru prądu, ani powierzchni utrzymaniowej, którą dokłada. Wolałbym być oceniany po tym, że wiem, kiedy coś wyłączyć.

Ta strona jest częścią systemu

To, co czytasz, jest serwowane przez opisaną wyżej instancję Caddy, z tej samej maszyny AlmaLinux, przez ten sam tunel — bez ani jednego otwartego portu przychodzącego. To osobne publiczne repozytorium, wdrażane tym samym przebiegiem playbooka co wszystko inne.

Jest też traktowana jak produkcja. Review od seniora ujawniło brakujący rekord www i brak nagłówków bezpieczeństwa; oba naprawione, oba w Git, oba z zapisem decyzji tłumaczącym kompromisy — dlaczego HSTS nie trafia na listę preload przeglądarek i dlaczego polityka treści obejmuje tę stronę, a nie całą domenę.

Kontakt

Szukam roli w Linuksie, infrastrukturze lub platformie. Dostępny od zaraz, pełny etat, Opole, Wrocław lub zdalnie.