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.
RB5009 + CRS310 · API RouterOS · mostek z filtrowaniem VLAN · firewall · WireGuard
Rozdzielone świadomie — inny zasięg awarii, inny cykl życia.
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
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ą
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.
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ł.
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.
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.
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.
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.
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.
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.
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
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.
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.
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ę.