Ruch wychodzący w Kubernetes i proxy: env poda, sidecar, brama egress i rola NO_PROXY
Spis treści
- Wprowadzenie: dlaczego konfiguracja „jak na zwykłym serwerze” nie działa w klastrze
- Podstawy: jak fizycznie zbudowany jest ruch wychodzący poda
- Trzy poziomy, na których można postawić proxy: kontener, sidecar, brama egress
- Zmienne środowiskowe w manifeście: http_proxy, https_proxy, no_proxy i dlaczego bez poprawnego no_proxy psuje się klaster
- Czego nie podchwytują te zmienne: kubelet, obrazy z własną konfiguracją, sdk
- Sekrety: dane uwierzytelniające proxy przez secret, a nie w manifeście
- Podejście sidecar: kiedy uzasadnione i co daje poza proxy
- Brama egress klastra: jeden punkt wyjścia
- Diagnostyka w klastrze: jak sprawdzić wychodzący ip z poda i gdzie szukać awarii
- Typowe błędy
- Narzędzia i zasoby
- Kazusy i wyniki
- Lista kontrolna wdrożenia
- Faq
- Zakończenie
Wprowadzenie: dlaczego konfiguracja „jak na zwykłym serwerze” nie działa w klastrze
Na zwykłej maszynie wirtualnej konfiguracja proxy zajmuje pięć minut. Wpisujesz HTTP_PROXY w profilu, dodajesz kilka adresów do wyjątków, restartujesz usługę i idziesz na kawę. W Kubernetesie taki scenariusz kończy się inaczej: połowa usług nagle odpowiada 502, health-check zaczyna wywalać pody, a serwis płatności skarży się, że nie może dosięgnąć sąsiedniego poda. I przy tym zapytania zewnętrzne, dla których to wszystko robiliśmy, właściwie działają.
Dlaczego tak? Bo w klastrze w jednym procesie jednocześnie żyją dwa światy komunikacji sieciowej. Pierwszy — zewnętrzny internet: API partnerów, marketplace'y, źródła danych do scrapingu, publiczne usługi. Drugi — wewnętrzna sieć klastra: inne usługi przez ClusterIP, serwer API Kubernetes, DNS, bazy danych, kolejki, metryki. Na zwykłym serwerze drugi świat jest prawie pusty, więc globalna zmienna proxy niczego nie psuje. W klastrze drugi świat jest ogromny, a każde proxy, które dostanie żądanie do adresu typu payments.default.svc.cluster.local, po prostu nie będzie potrafiło go rozwiązać. Proxy stoi na zewnątrz i o waszym wewnętrznym DNS nic nie wie.
Dodajmy do tego specyfikę samego Kubernetesa. Zmienne środowiskowe poda nie wpływają na kubelet ani container runtime. Obrazy często przynoszą własną konfigurację, która ignoruje środowisko. SDK różnych języków różnie rozumieją, co to jest wyjątek od proxy. I wszystko to — na tle tego, że wychodzący IP poda w ogóle nie jest widoczny dla świata zewnętrznego: na zewnątrz wychodzi adres node'a lub bramy NAT chmury.
W tym przewodniku rozbierzemy ruch wychodzący Kubernetesa „po warstwach”: jak pakiet fizycznie opuszcza poda, gdzie dokładnie można wpiąć proxy, dlaczego NO_PROXY w klastrze zmienia się z formalności w krytyczny element architektury, jak bezpiecznie przekazać dane uwierzytelniające, kiedy sidecar jest uzasadniony, a kiedy zbędny, i jak debugować awarie bez zgadywania. Podstawowej składni zmiennych środowiskowych nie będziemy powtarzać — tu tylko specyfika klastra. Wszystkie fragmenty manifestów są działające, można je brać za podstawę.
Podstawy: jak fizycznie zbudowany jest ruch wychodzący poda
Zanim postawisz proxy, warto rozumieć, jaką drogą idzie pakiet bez niego. Inaczej diagnostyka zamienia się w zgadywanie.
Od kontenera do node'a
Każdy pod dostaje własny IP z pod CIDR, na przykład 10.244.3.17. Wewnątrz poda wszystkie kontenery dzielą jedną przestrzeń nazw sieci: jeden IP, jeden zestaw interfejsów, wspólny localhost. To kluczowy fakt dla podejścia sidecar, do którego wrócimy. Pakiet z kontenera wychodzi przez wirtualny interfejs poda na node'a, gdzie zajmuje się nim plugin CNI. Dalej dwa scenariusze.
- Adresat wewnątrz klastra. Jeśli aplikacja odwołuje się do ClusterIP usługi (na przykład 10.96.14.2), kube-proxy lub dataplane eBPF CNI podmienia adres docelowy na IP konkretnego poda-backendu. Ruch idzie na właściwy node przez overlay lub bezpośrednio po L3 i nigdy nie opuszcza klastra.
- Adresat na zewnątrz. Jeśli adres nie należy ani do pod CIDR, ani do service CIDR, pakiet wychodzi przez trasę domyślną node'a. Na wyjściu z node'a prawie zawsze działa SNAT: adres źródłowy 10.244.3.17 zostaje zastąpiony IP node'a. W chmurach na wierzchu tego może działać brama NAT, i wtedy świat zewnętrzny widzi już jej adres.
Stąd pierwszy praktyczny wniosek: zewnętrzna usługa nigdy nie widzi IP poda. Widzi IP node'a lub NAT chmury. Jeśli masz autoskalowanie node'ów, adresy wychodzące zmieniają się razem z nimi. Dlatego autoryzacja na proxy po adresie IP bez dedykowanego node'a egress w klastrze działa słabo — o tym niżej.
DNS wewnątrz klastra
Pody używają klastrowego DNS (zwykle CoreDNS). W pliku resolv.conf poda wpisane są domeny wyszukiwania: namespace.svc.cluster.local, svc.cluster.local, cluster.local. Dzięki temu nazwa payments rozwija się do payments.default.svc.cluster.local. Zewnętrzne proxy o takim schemacie nie ma pojęcia. Dostanie żądanie do hosta payments, spróbuje rozwiązać go przez publiczny DNS i zwróci błąd. To fundamentalna przyczyna, dla której NO_PROXY w klastrze jest ważniejsze niż na jakimkolwiek osobnym serwerze.
Co w ogóle znaczy „postawić proxy” w klastrze
Pod tym hasłem inżynierowie rozumieją trzy różne rzeczy, i mylenie ich jest niebezpieczne:
- Zmusić konkretną aplikację do wychodzenia na zewnątrz przez serwer proxy (na przykład przez dedykowane adresy Proxeon), nie ruszając pozostałych usług.
- Zapewnić stabilny i przewidywalny wychodzący IP dla całego klastra lub jego części — zadanie sieci korporacyjnej, compliance, audytu.
- Scentralizować zarządzanie ruchem zewnętrznym: logowanie, limity, jeden punkt wyjścia.
Pierwsze zadanie rozwiązuje się na poziomie poda. Drugie i trzecie — na poziomie klastra. Mieszanie poziomów rodzi większość problemów, o których będziemy mówić.
Trzy poziomy, na których można postawić proxy: kontener, sidecar, brama egress
Wyobraź sobie klaster jak biurowiec. Proxy można dać każdemu pracownikowi osobiście (kontener), posadzić asystenta przy każdym stanowisku pracy (sidecar) lub postawić jedną przepustkę na wyjściu z budynku (brama egress). Każdy schemat ma swoją cenę.
Poziom 1: sam kontener
Aplikacja dostaje adres proxy przez zmienne środowiskowe, plik konfiguracyjny lub flagi uruchomienia i sama kieruje ruch przez niego. To najprostszy i najczęstszy sposób.
Plusy:
- Zero dodatkowej infrastruktury: zmiany tylko w manifeście Deployment.
- Granularność: jednemu serwisowi proxy jest potrzebne, sąsiedniemu nie.
- Proxy „rozumie” protokół: HTTP proxy widzi host docelowy, może logować, stosować reguły.
Minusy:
- Działa tylko, jeśli kod lub biblioteka respektują ustawienia. Java, część klientów Node.js, niektóre implementacje gRPC wymagają osobnej konfiguracji.
- Pełna zależność od poprawnego NO_PROXY: jeden błąd — i komunikacja wewnątrz klastra zepsuta.
- Dane uwierzytelniające proxy trafiają do środowiska procesu i są dostępne dla każdego, kto może wykonać exec w kontenerze.
- Rotacja proxy lub zmiana adresu wymaga odtworzenia podów.
Poziom 2: kontener sidecar
Obok aplikacji w tym samym podzie uruchamiany jest drugi kontener — lokalne proxy przekazujące dalej. Aplikacja wysyła ruch na localhost:3128 (lub przechwytują go reguły iptables), a sidecar już decyduje, gdzie i jak wysłać dalej: przez które z upstream proxy, z jaką autoryzacją, z jakim timeoutem.
Plusy:
- Aplikacja nie wie o zewnętrznych danych uwierzytelniających: żyją one tylko w sidecarze.
- Jeden punkt logiki: rotacja, powtórzenia, wybór puli adresów, metryki — bez zmian w kodzie aplikacji.
- Tryb transparentny przez iptables działa nawet z bibliotekami ignorującymi HTTP_PROXY.
- Aktualizacja logiki proxy to aktualizacja jednego obrazu sidecara.
Minusy:
- Dodatkowy kontener w każdym podzie: pamięć, CPU, jeszcze jeden obiekt do monitorowania.
- Transparentne przechwytywanie wymaga NET_ADMIN w kontenerze init, co nie wszędzie jest dopuszczalne przez polityki bezpieczeństwa.
- Trudniejsze debugowanie: dwa miejsca, w których może się coś zepsuć.
Poziom 3: brama egress klastra
Wydzielony zestaw podów (lub node'ów) staje się jedynym punktem wyjścia. Pozostałe pody albo jawnie chodzą przez wewnętrzny Service bramy, albo są do tego zmuszane politykami sieciowymi i trasowaniem. Brama sama wychodzi na zewnątrz bezpośrednio lub przez zewnętrzne proxy.
Plusy:
- Jeden stabilny wychodzący IP (lub ich niewielki zestaw) dla całego klastra. Idealne do autoryzacji po IP po stronie dostawcy proxy i do białych list partnerów.
- Scentralizowane logi, limity, audyt.
- Aplikacje nie przechowują żadnych danych uwierzytelniających: widzą tylko wewnętrzny adres bramy.
- Rotacja adresów zewnętrznych odbywa się w jednym miejscu.
Minusy:
- Pojedynczy punkt awarii: bramę trzeba zrobić odporną na awarie (kilka replik, PodDisruptionBudget, anti-affinity).
- Dodatkowy hop sieciowy i potencjalne wąskie gardło przepustowości.
- Wymaga przemyślanych NetworkPolicy, inaczej zostaje „dziura” z bezpośrednim wyjściem obok bramy.
- Trudniejsza granularność: jeśli różne zespoły potrzebują różnych adresów zewnętrznych, trzeba postawić kilka bram.
Jak wybrać
Praktyczna zasada, którą stosujemy przy projektowaniu:
- Jedna-dwie aplikacje, kod pod kontrolą, standardowe klienty HTTP — poziom kontenera.
- Wiele serwisów z różnymi stackami, wymagania co do rotacji i powtórzeń, niechęć do rozdawania danych uwierzytelniających — sidecar.
- Wymagania compliance, autoryzacja po IP, audyt całego ruchu zewnętrznego, dziesiątki zespołów — brama egress, często w połączeniu z sidecarem dla drobnej logiki.
Zwróć uwagę: poziomy się nie wykluczają. Typowy dojrzały schemat — brama egress jako obowiązkowa granica plus zmienne środowiskowe lub sidecar tam, gdzie potrzebna specyfika.
Zmienne środowiskowe w manifeście: HTTP_PROXY, HTTPS_PROXY, NO_PROXY i dlaczego bez poprawnego NO_PROXY psuje się klaster
Składnię samych zmiennych znasz. Nas interesuje, co dzieje się z nimi właśnie w podzie i dlaczego standardowa wartość NO_PROXY „localhost,127.0.0.1” jest tu katastrofalnie mała.
Anatomia awarii
Rozbierzmy łańcuch. Serwis orders wywołuje payments pod adresem http://payments:8080/charge. W środowisku ustawiony jest HTTP_PROXY. Klient HTTP patrzy na host docelowy, nie znajduje go w NO_PROXY i zamiast połączenia TCP z payments otwiera połączenie z proxy i wysyła mu żądanie GET http://payments:8080/charge. Proxy próbuje rozwiązać nazwę payments przez swój resolver. Nazwy nie ma. Proxy zwraca 502 lub 503. Twój serwis dostaje błąd od proxy, ale w logach wygląda to jak błąd od payments. Zespół idzie naprawiać payments, który jest absolutnie zdrowy.
Jest i drugi, bardziej zdradliwy wariant. Jeśli serwis odwołuje się do ClusterIP bezpośrednio po adresie typu 10.96.14.2, proxy dostanie żądanie do prywatnego adresu. Niektóre proxy takie żądania odrzucają od razu, inne próbują połączyć się z 10.96.14.2 w swojej własnej sieci. W najgorszym wypadku jest tam jakiś inny host z takim adresem.
Trzeci wariant dotyczy samego Kubernetesa. Biblioteki klienckie, które komunikują się z serwerem API (operatory, kontrolery, aplikacje z leader election), używają adresu kubernetes.default.svc lub IP ze zmiennej KUBERNETES_SERVICE_HOST. Bez wyjątku też pójdą przez proxy i stracą łączność z control plane.
Co musi być w NO_PROXY dla poda
Minimalny zestaw, który polecamy jako punkt wyjścia. Podstaw swoje CIDR — można je poznać z konfiguracji klastra lub z adnotacji usług:
- localhost, 127.0.0.1, ::1 — adresy lokalne, w tym komunikacja z sidecarem.
- .svc, .svc.cluster.local, .cluster.local — wszystkie nazwy usług. Wiodąca kropka oznacza „dowolna subdomena”.
- kubernetes.default.svc, kubernetes.default.svc.cluster.local — jawnie, dla klientów API.
- Service CIDR, na przykład 10.96.0.0/12 — bezpośrednie odwołania do ClusterIP.
- Pod CIDR, na przykład 10.244.0.0/16 — headless service i odwołania pod-to-pod.
- Node CIDR — jeśli aplikacja chodzi na porty node'a (metryki, usługi hostNetwork).
- 169.254.169.254 — metadane chmury, jeśli są używane.
- Wewnętrzne domeny firmy, na przykład .corp.local, jeśli jest do nich bezpośrednia trasa z klastra.
- Adresy wewnętrznych baz i kolejek, jeśli są poza klastrem, ale w prywatnej sieci.
Pułapka, o której nie wszyscy wiedzą: CIDR nie jest obsługiwany przez wszystkich klientów
Tu zaczyna się prawdziwa specyfika. Format NO_PROXY nie jest standaryzowany. Każda biblioteka parsuje go po swojemu:
- Go (net/http) rozumie notację CIDR, sufiksy z wiodącą kropką i pojedyncze hosty. To najlepszy przypadek, a większość narzędzi Kubernetesa jest napisana w Go.
- curl obsługuje CIDR dopiero od wersji 7.86. W starych obrazach opartych na przestarzałych dystrybucjach wpis 10.96.0.0/12 będzie po prostu zignorowany.
- Python (urllib, requests) CIDR nie rozumie w ogóle. Porównanie idzie po sufiksie ciągu. Wpis 10.96.0.0/12 nie zadziała nigdy; przy tym wpis 10.96. zadziała jako dopasowanie podobne do prefiksu tylko dla hostów kończących się tym ciągiem, czyli też nie pomoże.
- Node.js wbudowany klient HTTP zmienne środowiskowe ignoruje całkowicie. Potrzebny ProxyAgent w undici lub zewnętrzny agent, i już on decyduje, jak traktować NO_PROXY.
- Java używa swojej zmiennej http.nonProxyHosts ze składnią wildcards typu *.svc.cluster.local i separatorem w postaci pionowej kreski.
Praktyczny wniosek: jeśli w klastrze jest zoo języków, jedyny niezawodny sposób wykluczenia ruchu wewnętrznego po CIDR — nie polegać na NO_PROXY klienta, a rozwiązać to na poziomie sidecara lub bramy, gdzie parser jest pod twoją kontrolą. Dla nazw DNS sufiksy z kropką działają prawie wszędzie, dlatego w aplikacjach warto chodzić do usług po nazwach, a nie po ClusterIP.
Wielkość liter zmiennych
Jeszcze jeden moment, który regularnie kosztuje godziny debugowania. curl czyta http_proxy tylko małymi literami: wielkie HTTP_PROXY jest przez niego ignorowane celowo, z powodu historycznej podatności z CGI. Przy tym HTTPS_PROXY i NO_PROXY curl przyjmuje w obu wariantach. Go, Python i .NET czytają oba warianty. Dlatego w manifeście zawsze ustawiamy obie formy — to tanie i usuwa całą klasę problemów.
Działający fragment: ConfigMap z NO_PROXY i Deployment
Przenieś NO_PROXY do ConfigMap na poziomie namespace'u. Wtedy zmieniasz listę w jednym miejscu, a nie w dwudziestu Deploymentach:
apiVersion: v1
kind: ConfigMap
metadata:
name: egress-proxy-common
namespace: crawlers
data:
NO_PROXY: "localhost,127.0.0.1,::1,.svc,.svc.cluster.local,.cluster.local,kubernetes.default.svc,10.96.0.0/12,10.244.0.0/16,192.168.10.0/24,169.254.169.254,.corp.local"
no_proxy: "localhost,127.0.0.1,::1,.svc,.svc.cluster.local,.cluster.local,kubernetes.default.svc,10.96.0.0/12,10.244.0.0/16,192.168.10.0/24,169.254.169.254,.corp.local"I Deployment, który go używa. Adres proxy tu na razie bez danych uwierzytelniających — jak je przechowywać, omówimy w osobnym rozdziale:
apiVersion: apps/v1
kind: Deployment
metadata:
name: price-collector
namespace: crawlers
spec:
replicas: 3
selector:
matchLabels:
app: price-collector
template:
metadata:
labels:
app: price-collector
spec:
containers:
- name: app
image: registry.internal/price-collector:2.7.0
envFrom:
- configMapRef:
name: egress-proxy-common
env:
- name: HTTP_PROXY
value: "http://proxy-host.example:8080"
- name: http_proxy
value: "http://proxy-host.example:8080"
- name: HTTPS_PROXY
value: "http://proxy-host.example:8080"
- name: https_proxy
value: "http://proxy-host.example:8080"
readinessProbe:
httpGet:
path: /healthz
port: 8080Zwróć uwagę na schemat w HTTPS_PROXY: najczęściej jest to http://, bo do samego proxy klient łączy się zwykłym TCP, a już wewnątrz ustanawia tunel CONNECT dla żądań HTTPS. Wartość https:// oznacza tu TLS do samego proxy, co obsługują daleko nie wszystkie klienty i nie wszystkie serwery proxy. Sprawdź w panelu Proxeon, jaki typ połączenia oferuje twój plan.
Sprawdź, że sonda nie chodzi przez proxy
Liveness i readiness sprawdza kubelet, a nie aplikacja, więc nie widzi zmiennych środowiskowych i proxy mu niestraszne. Ale jeśli twoja aplikacja wewnątrz handlera /healthz sama wywołuje zależności po adresach wewnętrznych, te wywołania wejdą pod HTTP_PROXY. Właśnie tak wygląda klasyczny objaw „po dodaniu proxy pody przestały przechodzić readiness”.
Czego NIE podchwytują te zmienne: kubelet, obrazy z własną konfiguracją, SDK
Zmienne środowiskowe poda żyją wewnątrz procesu aplikacji. Wszystko, co znajduje się poza nim lub celowo ignoruje środowisko, wymaga osobnej konfiguracji. Rozbierzemy trzy grupy.
Kubelet i container runtime
Najczęstsze pytanie przy przenoszeniu klastra za proxy: „dlaczego obrazy się nie ściągają, przecież wpisaliśmy HTTP_PROXY w Deployment”. Odpowiedź: obrazy ściąga nie pod, a container runtime (containerd lub CRI-O) na node'zie na polecenie kubeleta. To usługi systemowe uruchomione przez systemd, i czytają tylko swoje środowisko.
Konfigurację robi się plikiem drop-in dla unitu systemd na każdym node'zie. Dla containerd wygląda to tak (plik /etc/systemd/system/containerd.service.d/http-proxy.conf):
[Service]
Environment="HTTP_PROXY=http://proxy-host.example:8080"
Environment="HTTPS_PROXY=http://proxy-host.example:8080"
Environment="NO_PROXY=localhost,127.0.0.1,10.96.0.0/12,10.244.0.0/16,192.168.10.0/24,.svc,.cluster.local,registry.internal"Po tym — systemctl daemon-reload i systemctl restart containerd. W NO_PROXY koniecznie musi trafić wewnętrzny registry, inaczej runtime pójdzie po twoje własne obrazy przez zewnętrzne proxy. Dla kubeleta osobny drop-in potrzebny jest rzadziej — tylko jeśli sam odwołuje się do API chmury lub zewnętrznych webhooków.
W managed-klastrach dostępu do node'ów może nie być, i wtedy konfiguracja runtime robi się przez mechanizmy dostawcy chmury lub przez DaemonSet z uprzywilejowanym kontenerem, który poprawia konfigurację na hoście. Drugi wariant — kompromis, który warto uzgodnić z zespołem bezpieczeństwa.
Obrazy z własną konfiguracją
Wiele gotowych obrazów przynosi plik konfiguracyjny, który nadpisuje lub ignoruje środowisko:
- Aplikacje JVM. Java nie czyta HTTP_PROXY. Potrzebne są właściwości systemowe. Wygodnie przekazać je przez JAVA_TOOL_OPTIONS, którą JVM podchwytuje bez zmian w skryptach uruchomieniowych:
env:
- name: JAVA_TOOL_OPTIONS
value: "-Dhttp.proxyHost=proxy-host.example -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy-host.example -Dhttps.proxyPort=8080 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.svc|*.svc.cluster.local|*.cluster.local|10.*|192.168.*"- Obrazy z własnym configiem klienta HTTP (niektóre agenty monitoringu, konektory, narzędzia ETL) czytają proxy ze swojego YAML lub TOML. Środowisko widzą, ale nie używają. Czytaj dokumentację obrazu i szukaj kluczy typu proxy_url.
- Obrazy z twardo zadanym resolverem DNS wewnątrz aplikacji psują samą ideę NO_PROXY po nazwach: aplikacja rozwiązuje nazwę sama, dostaje IP i już IP porównuje z listą, gdzie masz tylko sufiksy domenowe.
- Skrypty entrypoint, które nadpisują zmienne. Zdarza się rzadziej, ale warto sprawdzić: kubectl exec pod -- cat /proc/1/environ | tr bajt zerowy na nową linię pokaże rzeczywiste środowisko procesu głównego.
Osobne SDK i protokoły
Lista jest tu długa, wymienimy najczęstsze:
- gRPC. Wiele implementacji obsługuje HTTP_PROXY tylko przez CONNECT i z ograniczeniami; część ignoruje NO_PROXY. Dla ruchu gRPC wewnątrz klastra jest to podwójnie niebezpieczne.
- Node.js. Wbudowany fetch i moduł http/https nie czytają środowiska. Potrzebny jawny agent. Biblioteka axios czyta HTTP_PROXY przez swoją zależność, a natywny fetch — nie. W jednej aplikacji dwa klienty mogą zachowywać się różnie.
- SDK chmurowe. Większość respektuje HTTP_PROXY, ale odwołanie do endpointu metadanych (169.254.169.254) musi być w wyjątkach, inaczej pobranie tymczasowych danych uwierzytelniających się zepsuje.
- WebSocket i raw TCP. Klienty baz danych, brokerów wiadomości, Redis działają po swoich protokołach na TCP i przez HTTP proxy nie przechodzą w ogóle. Jeśli taki ruch musi iść przez proxy, potrzebny tryb SOCKS lub transparentne przechwytywanie na poziomie sidecara.
- Własne klienty DNS w aplikacjach (resolve przez osobną bibliotekę) nie współpracują z proxy i chodzą bezpośrednio po UDP 53. To normalne, ale trzeba o tym pamiętać przy pisaniu NetworkPolicy.
Wniosek rozdziału: zanim wypuścisz proxy na serwis, zrób mapę jego połączeń wychodzących po protokołach i bibliotekach. Pół godziny na inwentaryzację oszczędza dzień debugowania.
Sekrety: dane uwierzytelniające proxy przez Secret, a nie w manifeście
Adres proxy z loginem i hasłem w treści Deployment to wyciek czekający na swoją godzinę. Manifesty żyją w gicie, trafiają do logów CI, wyświetlają się w kubectl get deploy -o yaml każdemu z prawami do czytania namespace'u. Zróbmy to poprawnie.
Wariant 1: pełny URL proxy w Secret
Najprostszy schemat: w Secret przechowywany jest gotowy ciąg z danymi uwierzytelniającymi, aplikacja dostaje go jako zmienną środowiskową. Sekret tworzymy poza gitem (przez zmienne CI, zewnętrzny menedżer sekretów lub ręcznie):
kubectl -n crawlers create secret generic egress-proxy-creds --from-literal=HTTP_PROXY="http://user123:s3cretPass@proxy-host.example:8080" --from-literal=HTTPS_PROXY="http://user123:s3cretPass@proxy-host.example:8080" --from-literal=http_proxy="http://user123:s3cretPass@proxy-host.example:8080" --from-literal=https_proxy="http://user123:s3cretPass@proxy-host.example:8080"W Deploymencie zamiast jawnych env — odniesienie do Secret i do ConfigMap z NO_PROXY:
containers:
- name: app
image: registry.internal/price-collector:2.7.0
envFrom:
- configMapRef:
name: egress-proxy-common
- secretRef:
name: egress-proxy-credsWażny szczegół: jeśli login lub hasło zawierają znaki specjalne (@, :, /, %), trzeba je zakodować URL. Hasło ze znakiem @ bez kodowania rozerwie parsowanie URL i klient spróbuje połączyć się nie z tym hostem. W panelu Proxeon przy generowaniu dostępów wygodnie od razu wybrać hasło bez znaków specjalnych lub skopiować gotowy URL.
Wariant 2: osobne pola i składanie URL w kontenerze
Kiedy login i hasło są przechowywane osobno (na przykład synchronizowane z zewnętrznego magazynu sekretów), URL składa się w entrypoincie lub w samej aplikacji:
env:
- name: PROXY_USER
valueFrom:
secretKeyRef:
name: egress-proxy-creds
key: username
- name: PROXY_PASS
valueFrom:
secretKeyRef:
name: egress-proxy-creds
key: password
- name: PROXY_HOST
value: "proxy-host.example:8080"Taki wariant jest wygodniejszy przy rotacji hasła: zmienia się jeden klucz Secret, a nie cztery gotowe ciągi.
Wariant 3: plik zamiast zmiennej
Zmienne środowiskowe procesu czytane są z /proc i często trafiają do crash-dumpów i raportów błędów. Jeśli polityka bezpieczeństwa jest surowa, montuj Secret jako plik z prawami 0400 i czytaj go w aplikacji lub w sidecarze:
volumes:
- name: proxy-creds
secret:
secretName: egress-proxy-creds
defaultMode: 0400
containers:
- name: app
volumeMounts:
- name: proxy-creds
mountPath: /var/run/secrets/proxy
readOnly: trueWariant 4: całkiem usunąć dane uwierzytelniające
Jeśli masz bramę egress ze stabilnym wychodzącym IP, autoryzację na proxy można przenieść z loginu i hasła na białą listę IP. Wtedy w klastrze w ogóle nie przechowuje się niczego tajnego: pody chodzą do wewnętrznego Service bramy, brama wychodzi z ustalonego adresu, dostawca przepuszcza ruch po adresie. Proxeon obsługuje oba tryby autoryzacji, i dla klastra z wydzielonym egress zwykle polecamy właśnie tryb IP jako bezpieczniejszy i prostszy w eksploatacji.
Higiena wokół Secret
- Włącz szyfrowanie Secret at rest w etcd — domyślnie jest wyłączone w większości samodzielnych instalacji.
- Ogranicz RBAC: prawo do czytania sekretów namespace'u powinny mieć tylko konta serwisowe, którym naprawdę jest potrzebne. Aplikacja, która dostaje sekret przez envFrom, nie powinna czytać obiektu Secret przez API.
- Nie loguj środowiska. Sprawdź, że framework nie wypisuje env przy starcie w trybie debug.
- Uzgodnij rotację. Zmiana hasła proxy powinna być rutynową operacją: zaktualizowaliśmy Secret, zrobiliśmy rollout restart, stare hasło wyłączyliśmy.
- Pamiętaj, że przy użyciu envFrom aktualizacja Secret nie zmienia środowiska już uruchomionych podów. Potrzebny rollout restart. Przy montowaniu jako plik kubelet zaktualizuje zawartość w minutę-dwie, ale aplikacja musi umieć przeładować plik.
Podejście sidecar: kiedy uzasadnione i co daje poza proxy
Sidecar w kontekście egress to lokalny kontener proxy w podzie, który przyjmuje ruch aplikacji i wysyła go dalej przez zewnętrzne proxy. Zamienia pytanie „czy biblioteka respektuje HTTP_PROXY” na pytanie „czy sidecar umie przechwycić ruch”, a na drugie pytanie odpowiedzieć jest łatwiej.
Kiedy sidecar jest uzasadniony
- Aplikacja jest napisana w stacku, w którym zarządzanie proxy jest niewygodne (JVM z dziesiątkami różnych klientów, Node.js z mieszanką fetch i axios, legacy w C++).
- Potrzebna rotacja adresów według harmonogramu lub po błędach bez zmian w kodzie: sidecar trzyma pulę kilku proxy Proxeon i przełącza się sam.
- Chcesz wynieść dane uwierzytelniające z aplikacji: sidecar jest jedynym, kto zna hasło, a aplikacja chodzi na localhost bez autoryzacji.
- Potrzebna jednolita polityka powtórzeń, timeoutów i limitów żądań do świata zewnętrznego dla serwisu.
- Wymagany ruch nie tylko HTTP: tryb SOCKS lub transparentne przechwytywanie TCP.
Kiedy sidecar jest zbędny
- Jeden serwis w Go lub Pythonie z jednym klientem HTTP. Zmiennych środowiskowych wystarczy.
- Surowe limity zasobów: tysiące drobnych podów, gdzie dodatkowe 30–50 MB na sidecar mnożą się w tony pamięci.
- Jest już brama egress z potrzebną logiką, i duplikowanie jej w każdym podzie nie ma sensu.
Natywne kontenery sidecar
Od wersji Kubernetes 1.29 pojawił się, a do 1.33 ustabilizował mechanizm natywnych sidecarów: kontener init z restartPolicy: Always. Taki kontener startuje przed głównym, żyje przez cały czas życia poda i poprawnie kończy się po głównym. To rozwiązuje starą bolączkę: wcześniej aplikacja mogła wystartować przed proxy i dostać connection refused przy pierwszych żądaniach, a Job z sidecarem nigdy się nie kończył, bo sidecar dalej pracował.
Działający fragment: pod z proxy sidecar
Tu sidecar to niewielkie proxy przekazujące (umowny obraz egress-sidecar), które nasłuchuje na 127.0.0.1:3128, czyta dane uwierzytelniające upstream z Secret i wie, które adresy przepuszczać bezpośrednio:
apiVersion: apps/v1
kind: Deployment
metadata:
name: feed-importer
namespace: integrations
spec:
replicas: 2
selector:
matchLabels:
app: feed-importer
template:
metadata:
labels:
app: feed-importer
spec:
initContainers:
- name: egress-sidecar
image: registry.internal/egress-sidecar:1.3.0
restartPolicy: Always
env:
- name: LISTEN_ADDR
value: "127.0.0.1:3128"
- name: DIRECT_CIDRS
value: "10.96.0.0/12,10.244.0.0/16,192.168.10.0/24"
- name: DIRECT_SUFFIXES
value: ".svc,.cluster.local,.corp.local"
envFrom:
- secretRef:
name: egress-upstream-creds
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
memory: 96Mi
readinessProbe:
tcpSocket:
port: 3128
containers:
- name: app
image: registry.internal/feed-importer:4.1.0
envFrom:
- configMapRef:
name: egress-proxy-common
env:
- name: HTTP_PROXY
value: "http://127.0.0.1:3128"
- name: HTTPS_PROXY
value: "http://127.0.0.1:3128"
- name: http_proxy
value: "http://127.0.0.1:3128"
- name: https_proxy
value: "http://127.0.0.1:3128"Zwróć uwagę: NO_PROXY dla aplikacji jest wciąż potrzebne. Sidecar umie przepuszczać ruch wewnętrzny bezpośrednio, ale po co gonić go przez dodatkowy hop na localhost? Poza tym parsowanie CIDR w sidecarze jest pod twoją kontrolą, a w aplikacji — nie. Podwójna ochrona jest tu na miejscu.
Tryb transparentny przez iptables
Jeśli aplikacja ignoruje środowisko całkowicie, dodaje się kontener init, który przekierowuje cały wychodzący ruch TCP poda, poza sieciami wewnętrznymi i ruchem samego sidecara, na port sidecara. Sidecar przy tym musi pracować w trybie transparentnego proxy i odtwarzać oryginalny adres docelowy. Kontener init potrzebuje capability NET_ADMIN, a sidecar musi pracować pod osobnym UID, żeby jego własny ruch się nie zapętlił:
initContainers:
- name: egress-redirect
image: registry.internal/egress-init:1.0.0
securityContext:
capabilities:
add: ["NET_ADMIN"]
command: ["sh", "-c"]
args:
- |
iptables -t nat -N EGRESS_REDIRECT
iptables -t nat -A EGRESS_REDIRECT -d 10.96.0.0/12 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -d 10.244.0.0/16 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -d 127.0.0.0/8 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -m owner --uid-owner 1337 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -p tcp -j REDIRECT --to-ports 15001
iptables -t nat -A OUTPUT -p tcp -j EGRESS_REDIRECTTu 1337 to UID, pod którym uruchomiony jest sidecar, a 15001 to jego port transparentny. To ta sama zasada, której service mesh używa do przechwytywania ruchu. Jeśli w klastrze stoi już mesh, prawdopodobnie jego możliwości egress wystarczą i osobny sidecar nie będzie potrzebny.
Co sidecar daje poza proxy
- Metryki ruchu wychodzącego w przekroju hosta docelowego, kodu odpowiedzi, czasu: aplikacja o nich nie wie, a sidecar widzi wszystko.
- Circuit breaker na zewnętrzne API: przy serii błędów sidecar przestaje dobijać partnera i zwraca szybką odmowę.
- Pula adresów i rotacja: lista proxy Proxeon w configu, przełączanie według strategii round-robin lub po błędzie 407/403.
- Lokalny cache DNS dla nazw zewnętrznych, zmniejszający obciążenie CoreDNS.
- Ograniczanie szybkości per-destination, żeby dotrzymać limitów partnerskich API i nie wpaść pod ich ograniczenia.
Brama egress klastra: jeden punkt wyjścia
Brama egress rozwiązuje zadanie innej skali: nie „jak temu serwisowi chodzić przez proxy”, a „przez jakie adresy klaster w ogóle wychodzi w świat”. Rozważmy praktyczną implementację bez przywiązania do konkretnego CNI.
Architektura
- Wydziela się grupę node'ów ze stałymi zewnętrznymi IP (w chmurze — z zarezerwowanym publicznym adresem lub za osobną bramą NAT). Node'y dostają etykietę node-role: egress oraz taint, żeby nie osiedlały się na nich postronne pody.
- Na tych node'ach rozwijany jest Deployment proxy przekazującego z kilkoma replikami i anti-affinity. Przed nim — Service typu ClusterIP.
- Pody aplikacyjne dostają HTTP_PROXY wskazujący na ten Service. Ich NO_PROXY zawiera sieci wewnętrzne.
- NetworkPolicy zabrania podom aplikacyjnym jakiegokolwiek egress poza granice klastra, poza DNS i ruchem do Service bramy.
- Brama sama wychodzi na zewnątrz albo bezpośrednio (wtedy jej IP jest wychodzącym adresem klastra), albo przez zewnętrzne proxy Proxeon z autoryzacją po IP bramy.
Działający fragment: Deployment bramy i NetworkPolicy
apiVersion: apps/v1
kind: Deployment
metadata:
name: egress-gateway
namespace: egress
spec:
replicas: 3
selector:
matchLabels:
app: egress-gateway
template:
metadata:
labels:
app: egress-gateway
spec:
nodeSelector:
node-role: egress
tolerations:
- key: dedicated
value: egress
effect: NoSchedule
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: egress-gateway
topologyKey: kubernetes.io/hostname
containers:
- name: proxy
image: registry.internal/egress-gateway:2.0.1
ports:
- containerPort: 3128
envFrom:
- secretRef:
name: egress-upstream-creds
---
apiVersion: v1
kind: Service
metadata:
name: egress-gateway
namespace: egress
spec:
selector:
app: egress-gateway
ports:
- port: 3128
targetPort: 3128Polityka dla namespace'u aplikacyjnego, która zamyka bezpośrednie wyjście do internetu:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-only-via-gateway
namespace: crawlers
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: egress
podSelector:
matchLabels:
app: egress-gateway
ports:
- protocol: TCP
port: 3128
- to:
- ipBlock:
cidr: 10.244.0.0/16
- ipBlock:
cidr: 10.96.0.0/12Taka polityka zmienia bramę z rekomendacji w obowiązek: pod z zapomnianym HTTP_PROXY nie wyjdzie na zewnątrz obok niej, a po prostu dostanie timeout. To wykrywa się na środowisku testowym w minutach, a nie w produkcji w tygodniach.
Odporność na awarie i pojemność
- Minimum trzy repliki na różnych node'ach i PodDisruptionBudget z minAvailable: 2.
- Monitoring połączeń: liczba otwartych, czas ustanowienia CONNECT, udział 5xx od upstream.
- Limit liczby jednoczesnych połączeń od jednego poda klienta, żeby jeden wadliwy serwis nie zapchał bramy.
- Uwzględnienie conntrack na node'ach bramy: przy dziesiątkach tysięcy połączeń na sekundę tabela się przepełnia, i dostaniesz zagadkowe straty pakietów. Zwiększaj nf_conntrack_max z wyprzedzeniem.
Diagnostyka w klastrze: jak sprawdzić wychodzący IP z poda i gdzie szukać awarii
Problemy z proxy w klastrze są zdradliwe tym, że objaw pojawia się daleko od przyczyny. Poniżej — metodyka, która pozwala w kilku krokach zlokalizować awarię.
Krok 1: upewnij się, że środowisko naprawdę się zastosowało
kubectl -n crawlers exec deploy/price-collector -- env | grep -i proxySzukaj obu form wielkości liter, kompletności NO_PROXY, braku zbędnych spacji. Jeśli aplikacja uruchamia się przez skrypt entrypoint, dodatkowo sprawdź środowisko procesu głównego przez /proc/1/environ — może różnić się od tego, co pokazuje exec uruchamiający nową powłokę.
Krok 2: sprawdź wychodzący IP
Rozwiń w klastrze lub na zewnętrznym hoście firmy prosty serwis echo, który zwraca adres klienta i nagłówki. Następnie z poda:
kubectl -n crawlers exec deploy/price-collector -- curl -s https://echo.yourcompany.example/ip
kubectl -n crawlers exec deploy/price-collector -- curl -s --noproxy '*' https://echo.yourcompany.example/ipPierwsze polecenie idzie według reguł środowiska, drugie wymusza ominięcie proxy. Jeśli adresy są takie same — proxy nie jest używane. Jeśli pierwsze zwraca IP dostawcy proxy, a drugie — IP node'a lub NAT chmury, schemat działa. To także jedyny niezawodny sposób, żeby upewnić się, że widzisz właśnie ten adres, który wydał panel Proxeon, a nie adres node'a.
Krok 3: popatrz na dialog z proxy oczami klienta
kubectl -n crawlers exec deploy/price-collector -- curl -sv https://api.partner.example/status 2>&1 | head -30W wyjściu szukaj linii z CONNECT i kodu odpowiedzi proxy na nią. Typowe sygnały:
- 407 Proxy Authentication Required — dane uwierzytelniające nie dotarły lub są błędne. Sprawdź kodowanie URL hasła i to, że proxy oczekuje właśnie autoryzacji Basic.
- 403 od proxy na CONNECT — docelowy host lub port jest zabroniony przez politykę proxy, albo twojego IP nie ma na białej liście przy autoryzacji po IP.
- 502/503 na CONNECT — proxy nie mogło dosięgnąć celu. Jeśli cel jest wewnętrzny — to diagnoza „dziura w NO_PROXY”.
- Connection refused do samego proxy — błędny adres, port lub NetworkPolicy nie przepuszcza ruchu do bramy.
- Timeout bez odpowiedzi — najczęściej polityka sieciowa lub brakująca trasa. Sprawdź, czy DNS w ogóle się rozwiązuje.
Krok 4: sprawdź wywołania wewnątrz klastra
kubectl -n crawlers exec deploy/price-collector -- curl -sv http://payments.default.svc.cluster.local:8080/healthz 2>&1 | grep -iE "connected|proxy|HTTP/"Jeśli w wyjściu jest odwołanie do proxy, a nie bezpośrednie połączenie z ClusterIP, znaczy NO_PROXY nie pokrył tego adresu. Powtórz to samo dla krótkiej nazwy payments i dla IP usługi — wszystkie trzy powinny iść bezpośrednio.
Krok 5: kontener ephemeral dla obrazów bez narzędzi
W minimalnych obrazach nie ma ani curl, ani nslookup. Użyj kubectl debug, który podłączy kontener diagnostyczny do przestrzeni nazw sieci poda:
kubectl -n crawlers debug -it pod/price-collector-7d9f4-x2k8q --image=registry.internal/netdebug:latest --target=app -- shW środku dostępne są te same IP, ten sam resolv.conf i ten sam sidecar na localhost. Tylko środowisko aplikacji tu nie jest dziedziczone: zmienne sprawdzaj przez /proc/PID/environ procesu głównego.
Krok 6: logi i zdarzenia
- Logi sidecara lub bramy: kody odpowiedzi upstream, czas CONNECT, do jakich hostów odwoływano się. Właśnie tu widać próbę wysłania wewnętrznego adresu na zewnątrz.
- Zdarzenia poda: kubectl describe pod pokaże ImagePullBackOff — oznakę, że container runtime za proxy nie jest skonfigurowany lub registry nie ma w jego NO_PROXY.
- Logi CoreDNS w trybie debug: skok NXDOMAIN po nazwach zewnętrznych mówi o tym, że aplikacja sama rozwiązuje zewnętrzne hosty, choć powinna oddawać je proxy.
- Metryki conntrack na node'ach egress: wzrost nf_conntrack_count do limitu — zwiastun strat.
Krok 7: rozdzielaj problem proxy i problem celu
Ten sam 403 może przyjść od proxy i od docelowej strony. Odróżnić pomaga nagłówek Via lub specyficzne nagłówki dostawcy w odpowiedzi, a także porównanie z żądaniem przez --noproxy. Jeśli przez proxy 403, a bezpośrednio 200 — pytanie do ustawień proxy lub do tego, że docelowy zasób ogranicza ruch z adresów tej puli. Jeśli 403 w obu przypadkach — problem w samym żądaniu.
Typowe błędy
Zbierzmy razem grabie, na które nadepuje się najczęściej. Wiele z nich już omówiliśmy, tu — zestawienie do szybkiego sprawdzenia.
- NO_PROXY skopiowany z serwera. Wartość localhost,127.0.0.1 w klastrze gwarantowanie psuje komunikację z usługami. Potrzebne .svc, .cluster.local, service i pod CIDR.
- Postawienie na CIDR w NO_PROXY u Pythona lub starego curl. Biblioteka po cichu ignoruje wpis, ruch do ClusterIP idzie przez proxy. Chodź po nazwach DNS i używaj sufiksów.
- Tylko wielkie litery zmiennych. curl nie czyta HTTP_PROXY wielkimi literami. Ustawiaj obie formy.
- Dane uwierzytelniające w manifeście. Hasło w gicie i w wyjściu kubectl get -o yaml. Tylko Secret, lepiej autoryzacja po IP przez bramę.
- Hasło ze znakami specjalnymi bez kodowania URL. Klient parsuje URL niepoprawnie, dostajesz connection refused do nieistniejącego hosta.
- Oczekiwanie, że kubelet i containerd podchwycą env poda. Obrazy się nie ściągają, wszyscy idą szukać problemu w Deploymencie, a potrzebny jest systemd drop-in na node'ach.
- Zapomniany registry w NO_PROXY runtime. Wewnętrzny registry staje się niedostępny, bo containerd poszedł po niego przez zewnętrzne proxy.
- Java z HTTP_PROXY. JVM nie czyta tych zmiennych. Potrzebne właściwości systemowe przez JAVA_TOOL_OPTIONS.
- Sidecar jako zwykły kontener, a nie natywny init z restartPolicy: Always. Aplikacja startuje przed proxy i pada na pierwszych żądaniach, Job nigdy się nie kończy.
- Brama egress bez NetworkPolicy. Brama jest, ale połowa podów chodzi obok niej, i dowiadujesz się o tym, gdy partner przysyła listę nieznanych IP.
- Zaktualizowano Secret i nie zrobiono rollout restart. Stare pody dalej pracują ze starym hasłem, które już wyłączyłeś w panelu.
- Ignorowanie metadanych chmury. 169.254.169.254 nie ma w NO_PROXY — SDK chmurowe nie może pobrać tymczasowych kluczy, i aplikacja pada na zagadkowych błędach autoryzacji.
- Przeciążenie jednego adresu proxy przez wszystkie repliki. Trzydzieści replik dobija jeden adres wyjściowy, partnerskie API odpowiada limitami. Rozkładaj obciążenie na pulę adresów przez sidecar lub bramę.
Narzędzia i zasoby
Zestaw, który warto mieć pod ręką przy pracy z ruchem wychodzącym w klastrze.
Wbudowane narzędzia Kubernetesa
- kubectl exec i kubectl debug — sprawdzanie środowiska i połączeń sieciowych wewnątrz poda, w tym kontenery ephemeral dla minimalnych obrazów.
- kubectl describe pod i zdarzenia — ImagePullBackOff, odrzucenia sond, błędy montowania sekretów.
- NetworkPolicy — wymuszanie wyjścia przez bramę i testowanie hipotez „kto chodzi obok”.
- Natywne kontenery sidecar (restartPolicy: Always w initContainers) — poprawny cykl życia pomocnika proxy.
Diagnostyka sieciowa
- Obraz diagnostyczny z curl, dig, tcpdump, ss, iptables — zbuduj swój na bazie minimalnej dystrybucji i trzymaj w wewnętrznym registry.
- Własny serwis echo zwracający IP klienta i nagłówki — najszczerszy sposób zobaczenia adresu wychodzącego.
- tcpdump na node'zie z filtrem po IP poda — gdy trzeba dokładnie zobaczyć, do jakiego adresu otwiera się połączenie.
- Logi CoreDNS w trybie debug — dla zrozumienia, kto i co rozwiązuje.
Zarządzanie sekretami i konfiguracją
- Zewnętrzny menedżer sekretów z synchronizacją do Secret klastra — żeby dane uwierzytelniające proxy nie żyły w gicie i rotowały się centralnie.
- Kustomize lub Helm ze wspólnym ConfigMap dla NO_PROXY — jedna prawda dla wszystkich Deploymentów namespace'u.
- Polityki dopuszczenia (admission controllers) sprawdzające obecność NO_PROXY w każdym podzie z HTTP_PROXY — automatyczna ochrona przed kopiuj-wklej „z serwera”.
Dostawca proxy
Po stronie proxy ważne są trzy rzeczy: możliwość autoryzacji po IP (dla schematu z bramą), stabilne adresy z jasnym okresem rezerwacji i API do zarządzania dostępami, żeby rotację haseł można było automatyzować z CI. W Proxeon te mechanizmy są w panelu i przez API, co pozwala wbudować dostawcę w proces GitOps zamiast ręcznego kopiowania ciągów połączeń.
Kazusy i wyniki
Kazus 1: platforma ETL i „zepsute” wewnętrzne serwisy
Zespół danych dodał HTTP_PROXY w dwudziestu Deploymentach, żeby kolektory katalogów marketplace'ów chodziły przez dedykowane adresy. NO_PROXY skopiowano z dokumentacji do serwera: localhost i 127.0.0.1. W ciągu godziny udział 5xx między wewnętrznymi serwisami wzrósł z 0,1% do 34%, sondy readiness trzech serwisów zaczęły flappować. Po dodaniu .svc, .cluster.local i obu CIDR do wspólnego ConfigMap i rollout restarcie błędy zniknęły w pięć minut. Efekt dodatkowy: przeniesienie NO_PROXY do ConfigMap skróciło czas następnej zmiany listy z pół dnia (edycja dwudziestu manifestów) do jednego commita.
Kazus 2: integracje JVM i sidecar
Integrator finansowy w Javie z siedmioma różnymi klientami HTTP w jednej aplikacji (część — przez biblioteki, które nie czytają właściwości systemowych proxy). Próba skonfigurowania wszystkiego przez JAVA_TOOL_OPTIONS pokryła pięć klientów z siedmiu. Rozwiązanie — natywny sidecar z transparentnym przechwytywaniem przez iptables. Wszystkie siedem klientów poszło przez proxy bez zmian w kodzie, dane uwierzytelniające zniknęły z aplikacji, a metryki ruchu wychodzącego pojawiły się po raz pierwszy w historii serwisu. Koszt: 40 MB pamięci na pod, co przy dwunastu replikach okazało się nieistotne.
Kazus 3: brama egress i autoryzacja po IP
Klaster z sześćdziesięciu node'ów z autoskalowaniem odwoływał się do kilku partnerskich API wymagających białej listy IP. Każde skalowanie node'ów zmieniało zestaw adresów wychodzących, partnerzy blokowali ruch. Rozstawiono bramę egress na trzech wydzielonych node'ach z zarezerwowanymi adresami, wprowadzono NetworkPolicy we wszystkich aplikacyjnych namespace'ach, autoryzację na proxy Proxeon przeniesiono na tryb IP. Wynik: liczba incydentów „partner znowu nas zablokował” spadła z trzech-czterech miesięcznie do zera w kwartale, a z klastra zniknęły wszystkie Sekrety z hasłami proxy. Efekt uboczny: scentralizowane logi bramy wykryły dwa serwisy, które chodziły do zewnętrznych API, o których nikt nie wiedział.
Kazus 4: containerd i zagadkowy ImagePullBackOff
Po przeniesieniu klastra do sieci z obowiązkowym proxy pody z wpisanym HTTP_PROXY działały, ale nowe node'y nie mogły ściągnąć żadnego obrazu. Przyczyna — brak drop-in dla containerd. Po dodaniu pliku z proxy i wewnętrznym registry w NO_PROXY problem rozwiązany. Lekcję ujęto jako sprawdzenie w skrypcie bootstrap node'ów: przed dołączeniem do klastra node sprawdza, że containerd potrafi ściągnąć testowy obraz z wewnętrznego i zewnętrznego registry.
Lista kontrolna wdrożenia
Przejdź przez listę przed wypuszczeniem proxy na serwis produkcyjny. Każdy punkt — to czyjś nocny incydent.
Przygotowanie
- Sporządzona mapa połączeń wychodzących serwisu: zewnętrzne hosty, wewnętrzne serwisy, bazy, kolejki, endpointy chmurowe, protokoły.
- Znane są service CIDR, pod CIDR, node CIDR klastra i wewnętrzne domeny firmy.
- Sprawdzone, jakich klientów HTTP używa aplikacja i jak każdy z nich traktuje NO_PROXY (CIDR, sufiksy, wielkość liter).
- Wybrany poziom: kontener, sidecar lub brama. Decyzja udokumentowana.
Konfiguracja
- NO_PROXY wyniesiony do ConfigMap namespace'u i zawiera: localhost, 127.0.0.1, .svc, .svc.cluster.local, .cluster.local, kubernetes.default.svc, service CIDR, pod CIDR, 169.254.169.254, wewnętrzne domeny.
- Zmienne ustawione w obu wariantach wielkości liter.
- Dane uwierzytelniające tylko w Secret; hasło zakodowane URL; RBAC na sekrety ograniczony.
- Dla JVM ustawione JAVA_TOOL_OPTIONS z nonProxyHosts.
- Jeśli sidecar — jest natywny (initContainers z restartPolicy: Always), z readinessProbe i limitami zasobów.
- Jeśli brama — minimum trzy repliki, anti-affinity, PDB, NetworkPolicy w aplikacyjnych namespace'ach.
- Container runtime na node'ach skonfigurowany osobno; wewnętrzny registry w jego NO_PROXY.
Sprawdzenie na środowisku testowym
- env wewnątrz poda pokazuje oczekiwane wartości.
- Żądanie do serwisu echo zwraca adres proxy; z --noproxy — adres node'a.
- Żądania do wewnętrznych serwisów po krótkiej nazwie, pełnej nazwie i ClusterIP idą bezpośrednio (widać w curl -v).
- Readiness i liveness przechodzą stabilnie przez co najmniej dziesięć minut.
- Klient API Kubernetesa (jeśli jest) widzi control plane.
- Logi sidecara lub bramy nie zawierają odwołań do wewnętrznych adresów.
Wdrożenie i po
- Etapowy rollout z maxUnavailable nie wyższym niż 25%; monitoring 5xx między serwisami i błędów 407/403 od proxy.
- Udokumentowana procedura rotacji hasła: aktualizacja Secret, rollout restart, wyłączenie starego dostępu.
- Ustawione alerty na wzrost udziału błędów od proxy i na zbliżanie się conntrack do limitu na node'ach egress.
- Po tygodniu — przegląd logów bramy: czy nie pojawili się nieoczekiwani zewnętrzni adresaci.
FAQ
Czy można ustawić HTTP_PROXY globalnie dla całego klastra, a nie w każdym Deploymencie?
Bezpośredniego mechanizmu nie ma: zmienne środowiskowe są częścią specyfikacji poda. Praktyczne warianty — wspólny ConfigMap z envFrom we wszystkich manifestach przez Helm lub Kustomize, albo mutating admission controller, który automatycznie dodaje zmienne do podów z określoną adnotacją. Drugi sposób jest mocniejszy, ale wymaga staranności: musi też dodawać NO_PROXY, inaczej automatyzacja będzie masowo psuć serwisy.
Dlaczego curl w podzie idzie przez proxy, a aplikacja w Pythonie — nie, choć środowisko jest jedno?
Najpewniej chodzi o wielkość liter lub parsowanie NO_PROXY. curl czyta http_proxy tylko małymi literami, Python — w obu wariantach. Z drugiej strony Python nie rozumie CIDR w NO_PROXY, a świeży curl rozumie. Sprawdź obie hipotezy: ustaw zmienne w obu wariantach wielkości liter i używaj sufiksów domenowych zamiast CIDR.
Czy NO_PROXY jest potrzebne, jeśli cały ruch wewnętrzny i tak idzie przez sidecar z regułami direct?
Technicznie można się bez niego obejść, ale polecamy zostawić. Po pierwsze, dodatkowy hop przez localhost dla tysięcy wewnętrznych żądań — to niezerowe opóźnienia i obciążenie sidecara. Po drugie, jeśli sidecar padnie lub się zrestartuje, aplikacja z poprawnym NO_PROXY dalej będzie komunikować się z wewnętrznymi serwisami, a bez niego — straci wszystko.
Jak poznać, że żądanie do partnera poszło właśnie z adresu proxy, a nie z adresu node'a?
Jedyny niezawodny sposób — kontrolne żądanie do serwisu echo, który zwraca IP klienta, z tego samego poda i z tym samym środowiskiem. Porównaj odpowiedź z adresem wyświetlanym w panelu Proxeon dla twojego dostępu i z odpowiedzią przy --noproxy. Logi samego partnera też pomogą, ale zwykle nie ma ich pod ręką.
Czy warto szyfrować połączenie do proxy (schemat https:// w HTTPS_PROXY)?
Dla ruchu HTTPS do docelowych stron zawartość jest już zaszyfrowana end-to-end wewnątrz tunelu CONNECT, proxy widzi tylko host docelowy. TLS do proxy chroni dane uwierzytelniające i nazwy hostów na odcinku od poda do proxy. Jeśli ten odcinek przebiega przez niezaufaną sieć, TLS do proxy jest uzasadniony, ale upewnij się, że i klient, i proxy go obsługują: część bibliotek ignoruje schemat https:// w HTTPS_PROXY. Wewnątrz klastra między podem a bramą egress zwykle wystarczą polityki sieciowe.
Co zrobić z ruchem baz danych i brokerów, który musi wychodzić poza granice klastra?
Przez HTTP proxy nie przejdzie — to nie HTTP. Warianty: tryb SOCKS5 proxy, jeśli dostawca i biblioteka kliencka go obsługują; transparentne przechwytywanie na poziomie sidecara z tunelowaniem TCP; albo brama egress, która działa na poziomie trasowania, a nie protokołu. Wybór zależy od tego, czy potrzebujesz właśnie konkretnego zewnętrznego IP, czy wystarczy scentralizowane wyjście.
Czy można używać tego samego proxy dla containerd i dla aplikacji?
Technicznie tak, ale zwykle to różne zadania. Runtime ściąga obrazy z publicznych registry, i potrzebne mu proxy korporacyjnego perymetru. Aplikacje chodzą do partnerskich API, i potrzebują dedykowanych adresów o określonych właściwościach. Mieszanie powoduje, że ściąganie obrazów zużywa limity transferu przeznaczone na zadania aplikacyjne. Rozdzielaj.
Jak bezpiecznie zmienić hasło proxy bez przestojów?
Załóż drugi dostęp u dostawcy, zaktualizuj Secret na nowe dane uwierzytelniające, wykonaj rollout restart z etapową aktualizacją replik, upewnij się po logach, że nie ma 407, i dopiero wtedy wyłącz stary dostęp. Jeśli używasz autoryzacji po IP przez bramę, rotacja haseł w ogóle znika z twojej listy zadań.
Jak ograniczyć, żeby proxy było używane tylko dla konkretnych zewnętrznych domen?
NO_PROXY działa odwrotnie — opisuje, dokąd proxy nie ma chodzić, i białej listy na jego podstawie nie da się zbudować. Taka logika realizowana jest w sidecarze lub bramie: reguły typu „domeny z listy A — przez pulę adresów Proxeon, wszystko inne — bezpośrednio lub zakaz”. Plus NetworkPolicy, żeby bezpośrednie wyjście do internetu fizycznie nie istniało.
Zakończenie
Ruch wychodzący w Kubernetesie różni się od serwerowego nie detalami, a samą strukturą: każdy pod ma dwa światy — wewnętrzny i zewnętrzny, i proxy powinno widzieć tylko drugi. Stąd wynika wszystko pozostałe. NO_PROXY zmienia się z formalności w element architektury, który musi opisywać całą wewnętrzną sieć klastra i uwzględniać, że różne biblioteki traktują go różnie. Zmienne środowiskowe działają tylko wewnątrz procesu — kubelet, container runtime i uparte SDK wymagają osobnej uwagi. Dane uwierzytelniające nie mogą trafiać do manifestów, a najlepiej — w ogóle zniknąć z klastra dzięki bramie z autoryzacją po IP.
Trzy poziomy wpinania proxy — kontener, sidecar, brama egress — to nie konkurenci, a narzędzia do różnych zadań. Poziom kontenera jest szybki i granularny. Sidecar zdejmuje zależność od zachowania bibliotek i dodaje metryki, rotację, powtórzenia. Brama daje klastrowi przewidywalną twarz w świecie zewnętrznym i czyni wyjście przez proxy nie rekomendacją, a regułą podpartą politykami sieciowymi.
Co dalej? Zacznij od inwentaryzacji: zbierz mapę połączeń wychodzących swoich serwisów i zobacz, ile z nich jest wewnętrznych. Następnie wynieś NO_PROXY do wspólnego ConfigMap i sprawdź go na środowisku testowym według metodyki z rozdziału diagnostyki. Jeśli w klastrze jest więcej niż jeden stack lub są wymagania co do stabilnych adresów — zaprojektuj bramę egress i podłącz do niej dedykowane adresy Proxeon z autoryzacją po IP. I trzymaj listę kontrolną wdrożenia pod ręką: zwraca się już przy pierwszym incydencie, którego nie było.