Sprawa Ingressu, którego nie było

Krótka historia detektywistyczna ze świata GitOps i kontrolerów ingress.

┌──────────────────────────────────────────────────────────┐
│                   TABLICA ŚLEDCZA                        │
├──────────────────────────────────────────────────────────┤
│                                                          │
│ ZAGADKA: własny ingress ArgoCD nie ma ADRESU             │
│                                                          │
│ PODEJRZANY #1: certyfikat self-signed bez IP w SAN       │
│ PODEJRZANY #2: trwająca migracja kontrolera ingress      │
│ KLUCZOWY TROP: „zniknięty” obiekt IngressClass           │
│                                                          │
└──────────────────────────────────────────────────────────┘

Zgłoszenie

Firma utrzymuje małą flotę zdalnych urządzeń brzegowych, z których każde to samodzielny mały klaster Kubernetesa zarządzany w całości przez GitOps. Po świeżej reinstalacji jednego z nich zespół platformowy zauważył coś dziwnego: własny interfejs webowy ArgoCD, normalnie dostępny przez ingress, po prostu nie miał adresu. Nie wolny. Nie z oczywistym błędem. Po prostu... nie było go.

Żeby było trudniej, Projekt był w trakcie migracji — wymiany jednego kontrolera ingress na inny (Traefik w miejsce kontrolera opartego na nginx) — więc od początku dwóch wiarygodnych podejrzanych było ze sobą splątanych: zepsuta migracja albo coś, co naruszyła sama reinstalacja.

W tym jednym objawie okazały się ukrywać dwie osobne zagadki. To historia obu.

Rozdział 1 — Certyfikat bez adresu

Pierwszy przystanek: sprawdzić, jak faktycznie skonfigurowano ingress ArgoCD. Wyglądał standardowo — TLS kończony na serwerze samego ArgoCD, kontroler ingress rozmawia z nim po HTTPS.

Tyle że logi mówiły co innego:

x509: certificate is valid for argocd-server-<pod-id>, not <service-cluster-ip>

argocd-server w ArgoCD dostarcza certyfikat self-signed wystawiony na nazwę hosta własnego poda — w ogóle bez adresów IP w Subject Alternative Names. Kontroler ingress oczywiście łączył się z nim przez ClusterIP jego Service. Adres IP próbujący spełnić certyfikat, który twierdzi, że jest ważny tylko dla nazwy hosta. Klasyczny błąd handshake TLS, tylko przebrany tak, że chwilę zajęło jego rozpoznanie.

Poprawka okazała się już opisana w dokumentacji upstream, tylko nie zastosowana tutaj: nie każ ingressowi weryfikować certyfikatu backendu, który nigdy nie miał być tak weryfikowany. Uruchom argocd-server w trybie insecure i kończ TLS na ingressie:

configs:
  params:
    server.insecure: true

...a potem skieruj ingress na port HTTP serwisu ArgoCD, a nie na port HTTPS.

Zwrot akcji: zastosowanie połowy tej poprawki — samo kazanie ingressowi, żeby przestał weryfikować certyfikat backendu — nie zadziałało w pełni. Zaraz potem pojawił się inny błąd: zwykłe 502, bez żadnej skargi na TLS. Okazało się, że nowy kontroler ingress decyduje, czy mówić do backendu po HTTP czy HTTPS, na podstawie numeru portu, na który wskazuje obiekt Ingress — port 443 wymusza HTTPS niezależnie od jakiejkolwiek adnotacji mówiącej inaczej. Adnotacja żądająca backendu po zwykłym HTTP była ustawiona poprawnie. Ingress nadal wskazywał port 443. Adnotacja była po prostu po cichu ignorowana, bo port decydował pierwszy.

Musiały się zgadzać dwa ustawienia, nie jedno:

   adnotacja mówi:    backend-protocol: HTTP
   Ingress wskazuje:  port 443          <-- to wygrywa, zawsze
   -----------------------------------------------------------
   faktycznie:        HTTPS do backendu, adnotacja zignorowana

Naprawione przez skierowanie Ingressu na port zmapowany na zwykłe HTTP. Błąd certyfikatu zniknął, 502 zniknęło.

Rozdział 2 — IngressClass, który zniknął

Sprawa pozornie zamknięta — tyle że kilka dni później, w trakcie tej samej migracji kontrolera, drugi, niezwiązany ingress zaczął się źle zachowywać. I tym razem żaden komunikat błędu nie wskazywał niczego przydatnego.

Plan migracji był w zasadzie prosty: postaw nowy kontroler, potwierdź, że działa, potem usuń zarządzaną przez GitOps aplikację starego. Ten ostatni krok wszystko wykoleił.

        ┌──────────────────────────────────────┐
        │ 1. Migracja kontrolerów ingress:     │
        │ usunięcie zarządzanej przez GitOps   │
        │ aplikacji starego kontrolera         │
        └──────────────────────────────────────┘
                          │
                          ▼
        ┌──────────────────────────────────────┐
        │ 2. Automatyczny prune + self-heal    │
        │ usuwa każdy zasób tej aplikacji      │
        └──────────────────────────────────────┘
                          │
                          ▼
        ┌──────────────────────────────────────┐
        │ 3. ...w tym współdzielony            │
        │ obiekt IngressClass                  │
        └──────────────────────────────────────┘
                          │
                          ▼
        ┌───────────────────────────────────────┐
        │ 4. Tryb zgodności nowego kontrolera   │
        │ POTRZEBUJE tego IngressClass          │
        │ — ale sam go nigdy nie tworzy         │
        └───────────────────────────────────────┘
                          │
                          ▼
        ┌──────────────────────────────────────┐
        │ 5. Niezwiązany Ingress po cichu się  │
        │ psuje (nic nie wskazuje przyczyny)   │
        └──────────────────────────────────────┘

Automatyczny prune z self-healingiem w GitOps nie usuwa tylko oczywistych, widocznych zasobów aplikacji — usuwa wszystko, co ta aplikacja posiadała, łącznie z obiektami o zasięgu klastra, od których istnienia po cichu zależą inne, niezwiązane rzeczy. W tym przypadku: obiekt IngressClass, utworzony jako efekt uboczny instalacji starego kontrolera, który tryb zgodności z nginx w nowym kontrolerze potrzebował jako już istniejący — bo ten tryb zbudowano jako zamiennik wpinany w miejsce starego, a nie świeżą instalację, a takie zamienniki nie tworzą klasy, tylko zakładają, że ona jest.

Usunięcie starego kontrolera usunęło współdzielony obiekt, którego na papierze nie „posiadał” żaden z kontrolerów, a nowy kontroler nie miał pojęcia, że powinien go odtworzyć. Awaria wyszła na zupełnie niezwiązanym ingressie (znowu na tym od ArgoCD — mały klaster, wszyscy są sąsiadami), a nic w żadnym logu nie łączyło kropek z migracją kontrolera, która na pozór poszła dobrze.

Możliwe poprawki, od najtańszej w przygotowaniu:

  1. Odtworzyć współdzielony obiekt jako osobny, samodzielny manifest, nienależący do aplikacji żadnego z kontrolerów — żeby usunięcie lub reinstalacja któregokolwiek z nich nigdy więcej go nie dotknęły.
  2. Albo, jeśli chart nowego kontrolera to wspiera, pozwolić temu chartowi posiadać i odtwarzać obiekt w ramach własnego cyklu życia — wiąże go to z kontrolerem, który aktualnie go potrzebuje. Tutaj użyto tej opcji.

Rozdział 3 — Jeszcze jeden luźny wątek

Przy domykaniu migracji wyszło jeszcze jedno małe, podstępne ustawienie: pozostały, zahardkodowany „adres statusu” z konfiguracji innego środowiska, po cichu nadpisujący automatycznie wykryty adres kontrolera na każdym zarządzanym przez niego ingressie. Istniały dwa niezależne ustawienia o tym samym celu — jedno zahardkodowane, jedno automatyczne — i zahardkodowane zawsze wygrywało, kiedy były oba, bez ostrzeżenia, że automatyczne jest ignorowane. Każdy status Ingressu w klastrze po cichu raportował zły adres, dopóki tej pozostałości nie usunięto.

Drobiazg, ale z tych, które same potrafią zabrać całe kolejne popołudnie.

Podsumowanie — Wskazówki dla kolegów śledczych

Jeśli uruchamiasz ArgoCD (albo cokolwiek innego) za ingressem, zwłaszcza w trakcie migracji między kontrolerami ingress, kilka wniosków:

  1. Nie weryfikuj certyfikatu, który nigdy nie miał być tak weryfikowany. Certyfikat self-signed backendu bez IP w SAN i Service, z którym łączysz się przez ClusterIP, to niedopasowanie z założenia, a nie błąd do obejścia — kończ TLS na ingressie zamiast walczyć z certyfikatem backendu.
  2. Jedna adnotacja to nie cała historia. Kontrolery ingress często decydują o protokole backendu na podstawie więcej niż jednego sygnału (tutaj numeru portu i adnotacji). Jeśli ustawienie zdaje się nie działać, sprawdź, co jeszcze może decydować o tym samym z wyższym priorytetem.
  3. Automatyczny prune usuwa wszystko, co aplikacja posiadała — nie tylko to, co zamierzałeś usunąć. Zanim usuniesz aplikację zarządzaną przez GitOps, zapytaj, jakie zasoby o zasięgu klastra utworzyła jako efekt uboczny i czy coś innego po cichu nie zależy od ich istnienia.
  4. Tryb zgodności czy „drop-in” jest węższy, niż wygląda. „Zamiennik wpinany w miejsce starego” zwykle znaczy „zakłada, że obiekty pomocnicze starej instalacji nadal są”, a nie „odtworzy je za ciebie”.
  5. Kiedy dwa ustawienia mogą osiągnąć ten sam efekt, jedno z nich po cichu wygrywa. Szukaj starych, zahardkodowanych nadpisań, zanim uwierzysz, że „właściwe” automatyczne ustawienie faktycznie działa.