Ruch wychodzący w Kubernetes i proxy: sidecar, egress i NO_PROXY
Spis treści
- Podstawy: dlaczego w klastrze wszystko wygląda inaczej
- Głębokie zanurzenie: trzy poziomy, na których żyje proxy
- Zmienne środowiskowe w manifeście i rola no_proxy
- Czego nie podchwytują zmienne środowiskowe
- Sekrety: dane logowania proxy przez secret, a nie w manifeście
- Podejście sidecar: kiedy się opłaca i co daje
- Brama egress: stabilny adres wychodzący dla klastra
- Diagnostyka ruchu wychodzącego w klastrze
- Typowe błędy, których warto unikać
- Narzędzia i zasoby
- Case'y i rezultaty
- Faq: częste pytania inżynierów
- Zakończenie: składamy checklistę wdrożenia
Inżynier przyzwyczajony do zwykłych serwerów przychodzi do Kubernetes i robi to, co zawsze: eksportuje HTTP_PROXY do środowiska, restartuje proces i czeka, aż cały ruch wychodzący pójdzie przez proxy. Czasem to działa. Znacznie częściej psuje połowę klastra, a druga połowa dalej łączy się z internetem bezpośrednio, z pominięciem proxy. I wtedy zaczyna się najlepsza część: dlaczego jedne pody podchwyciły zmienne, a inne nie, dlaczego serwisy przestały się widzieć i dlaczego healthcheck nagle poszedł przez serwer proxy, którego nie ma na liście zaufanych.
Ten artykuł to szczegółowe omówienie tego, jak naprawdę działa ruch wychodzący w klastrze i gdzie poprawnie wpiąć proxy. Nie będziemy powtarzać podstawowej konfiguracji zmiennych środowiskowych: to i tak już wiesz. Rozmowa pójdzie o specyfice Kubernetes, gdzie znane podejścia dają nieoczekiwane efekty. Wszystkie przykłady oparte są na prawdziwych fragmentach manifestów, a w roli infrastruktury proxy występuje Proxeon (proxeon.net).
Podstawy: dlaczego w klastrze wszystko wygląda inaczej
Zacznijmy od fundamentu. Na klasycznym serwerze pojęcie ruchu wychodzącego jest trywialne: jest jeden interfejs sieciowy, jest tablica routingu, są systemowe zmienne środowiskowe i prawie wszystko, co uruchamiasz, dziedziczy je po powłoce nadrzędnej. Wyeksportowałeś zmienną w profilu - widzi ją cały proces użytkownika.
W Kubernetes tego jednego punktu nie ma. Jest pod - najmniejsza jednostka wdrożenia, w której żyje jeden lub kilka kontenerów. Pod ma własną przestrzeń nazw sieci, własny adres IP, własny zestaw zmiennych środowiskowych określony w manifeście. Powłoki, z której proces mógłby coś odziedziczyć, po prostu nie ma. Zmienna środowiskowa pojawia się w kontenerze tylko wtedy, gdy jawnie zadeklarujesz ją w specyfikacji poda lub w obrazie.
Czym jest ruch wychodzący poda
Kiedy kontener łączy się z zewnętrznym adresem, pakiet przechodzi długą drogę. Najpierw wychodzi z przestrzeni nazw sieci kontenera przez wirtualny interfejs. Potem trafia do stosu sieciowego węzła, gdzie zajmuje się nim plugin CNI - komponent odpowiedzialny za sieć klastra. Dalej do gry wchodzą reguły iptables lub eBPF, mechanizm SNAT (podmiana adresu źródłowego), po czym pakiet wychodzi przez interfejs sieciowy węzła do świata zewnętrznego.
Kluczowy moment: z punktu widzenia zewnętrznego serwisu żądanie przychodzi nie z adresu poda, ale z adresu węzła klastra. Pod jest ukryty za translacją adresów. To pierwsza rzecz, która zaskakuje nowicjuszy przy próbie skonfigurowania dostępu po białej liście IP: dodają do listy adres poda, a ruch przychodzi z zupełnie innego adresu.
Dwa rodzaje ruchu wychodzącego
Od samego początku warto rozdzielić dwa zasadniczo różne strumienie:
- Wschód-zachód - ruch między serwisami wewnątrz klastra. Jeden pod łączy się z drugim przez serwis, ClusterIP lub nazwę DNS typu my-service.namespace.svc.cluster.local.
- Północ-południe - ruch na zewnątrz, do zewnętrznych API, baz danych, serwisów partnerskich, magazynów obiektów.
Proxy jest prawie zawsze potrzebne tylko dla ruchu północ-południe. A najczęstszy i najbardziej bolesny błąd polega na tym, że nieprawidłowa konfiguracja proxy przypadkowo przechwytuje także ruch wschód-zachód, przerywając komunikację wewnętrzną. Właśnie dlatego lista wyjątków jest tu ważniejsza niż gdziekolwiek indziej. Ale o tym za chwilę.
Głębokie zanurzenie: trzy poziomy, na których żyje proxy
Istnieją dokładnie trzy poziomy architektoniczne, na których można wpiąć proxy w ścieżkę wychodzącą poda. Każdy rozwiązuje zadanie po swojemu, każdy ma swoją cenę. Przeanalizujmy wszystkie trzy uczciwie, z zaletami i wadami.
Poziom 1: sam kontener
To sytuacja, w której aplikacja wewnątrz kontenera sama wie o proxy. Czyta zmienne środowiskowe HTTP_PROXY i HTTPS_PROXY albo ma proxy we własnej konfiguracji i kieruje przez nie swoje żądania HTTP. Logika proxy jest wbudowana w bibliotekę kliencką samej aplikacji.
Zalety. Maksymalna prostota wdrożenia na start. Nie trzeba nic dodawać do klastra, żadnych dodatkowych komponentów. Kontrola na poziomie konkretnego poda: dokładnie wiesz, która aplikacja gdzie się łączy.
Wady. Każda aplikacja musi umieć czytać te zmienne, a to potrafi daleko nie każda. Konfiguracja rozsmarowuje się po dziesiątkach manifestów. Aktualizacja adresu proxy oznacza przejście przez wszystkie deploymenty. Łatwo zapomnieć o jednym serwisie i ten pójdzie bezpośrednio. Brak scentralizowanej polityki.
Poziom 2: kontener sidecar
Tutaj obok kontenera głównego w tym samym podzie uruchamiany jest drugi - sidecar. Przechwytuje ruch wychodzący i kieruje go przez proxy. Aplikacja może w ogóle nie wiedzieć o istnieniu proxy: wysyła żądania jak zwykle, a sidecar po cichu je proxuje. Tak działają service mesh pokroju Istio i Linkerd, i według tej samej zasady można postawić lekki lokalny agent proxy.
Zalety. Przezroczystość dla aplikacji. Jednolita polityka przez szablon poda. Możliwość dodania ponad proxowanie obserwowalności: metryki, tracing, ponowne próby, limity czasu. Sidecar jest odizolowany w podzie i dzieli z nim cykl życia.
Wady. Narzut: na każdy pod teraz dwa kontenery, a więc więcej pamięci i CPU. Utrudnione debugowanie - w łańcuchu pojawia się zbędne ogniwo. Kolejność uruchamiania kontenerów ma znaczenie: jeśli aplikacja startuje przed sidecarem, pierwsze żądania mogą się nie udać. W Kubernetes 1.28+ ten problem rozwiązują native sidecar containers jako kontenery init z polityką restartPolicy Always.
Poziom 3: brama egress klastra
To dedykowany węzeł lub pod, przez który wymuszony jest cały ruch wychodzący klastra. Pakiety są routowane na bramę egress środkami CNI, a ona już komunikuje się z zewnętrznym proxy lub sama występuje jako punkt wyjścia ze stabilnym adresem wychodzącym.
Zalety. Jeden punkt kontroli dla całego klastra. Stabilny, przewidywalny adres wychodzący, który wygodnie dodać do białych list zewnętrznych partnerów. Politykę zmieniasz w jednym miejscu. Aplikacje nic nie wiedzą.
Wady. Brama egress staje się punktem krytycznym: padła - stanęło całe północ-południe. Potrzebna redundancja i monitoring. Konfiguracja trudniejsza i wymaga wsparcia ze strony CNI. Granularność niższa: trudniej ustawić różną politykę dla różnych aplikacji bez dodatkowych reguł.
Jak wybrać poziom
Praktyczny punkt odniesienia z doświadczenia: mały projekt z paroma serwisami, które potrzebują zewnętrznego proxy - poziom kontenera. Średni klaster z dziesiątkami serwisów i wymogiem jednolitej polityki - sidecar. Duża infrastruktura, gdzie zewnętrzni partnerzy wymagają stałego adresu wychodzącego i audytu całego ruchu - brama egress. Często poziomy się łączy: brama egress dla podstawowej kontroli plus zmienne środowiskowe dla precyzyjnej konfiguracji pojedynczych podów przez Proxeon.
Zmienne środowiskowe w manifeście i rola NO_PROXY
Przejdźmy do najbardziej niedocenianego szczegółu. Zmienne HTTP_PROXY, HTTPS_PROXY i NO_PROXY w manifeście ustawia się przez blok env specyfikacji kontenera. Klasyczne nazwy warto duplikować małymi literami, bo część bibliotek czyta właśnie warianty pisane małą literą.
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker
spec:
replicas: 2
selector:
matchLabels:
app: worker
template:
metadata:
labels:
app: worker
spec:
containers:
- name: app
image: registry.example.com/worker:1.4.0
env:
- name: HTTP_PROXY
value: "http://gate.proxeon.net:8080"
- name: HTTPS_PROXY
value: "http://gate.proxeon.net:8080"
- name: NO_PROXY
value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.svc.cluster.local,.cluster.local,kubernetes.default"
- name: http_proxy
value: "http://gate.proxeon.net:8080"
- name: https_proxy
value: "http://gate.proxeon.net:8080"
- name: no_proxy
value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.svc.cluster.local,.cluster.local,kubernetes.default"Dlaczego bez poprawnego NO_PROXY klaster się rozpada
Oto sedno problemu. Gdy tylko zadeklarujesz HTTP_PROXY, klient HTTP aplikacji zaczyna kierować przez proxy absolutnie wszystkie żądania, w tym odwołania do sąsiednich serwisów wewnątrz klastra. A wewnętrzne serwisy są dostępne tylko w sieci klastra - zewnętrzny serwer proxy fizycznie do nich nie dotrze. Wynik: żądanie do orders.default.svc.cluster.local idzie na zewnętrzne proxy, ono próbuje rozwiązać tę nazwę, nie może, i zwraca błąd. Komunikacja wewnętrzna natychmiast się psuje.
NO_PROXY to lista wyjątków, adresów i domen, które należy omijać z pominięciem proxy i wysyłać bezpośrednio. Na zwykłym serwerze wpisuje się tam localhost i może parę wewnętrznych podsieci. W Kubernetes ta lista staje się krytycznie ważna, bo musi pokrywać całą wewnętrzną topologię klastra. Pominąłeś jeden sufiks - i część ruchu między serwisami pojechała przez zewnętrzne proxy.
Co bezwzględnie musi znaleźć się w NO_PROXY klastra
- localhost i 127.0.0.1 - odwołania wewnątrz samego poda.
- Zakres Pod CIDR - podsieć, z której przydzielane są adresy podom, na przykład 10.0.0.0/8 lub dokładniejszy zakres twojego klastra.
- Zakres Service CIDR - podsieć ClusterIP serwisów, często 10.96.0.0/12.
- Sufiksy DNS klastra - .svc, .svc.cluster.local, .cluster.local. Właśnie one pokrywają wszystkie wewnętrzne nazwy DNS serwisów.
- kubernetes.default - nazwa serwera API, z którym łączą się aplikacje i agenci.
- Metadane chmury - adres 169.254.169.254, jeśli jesteś w środowisku chmurowym, aby odwołania po metadane nie szły przez proxy.
Niuanse składni NO_PROXY
Tu kryje się cała warstwa nieoczywistości, na których potykają się nawet doświadczeni inżynierowie.
Po pierwsze, różne biblioteki różnie interpretują wpisy. Jedne uznają, że .cluster.local z wiodącą kropką oznacza sufiks i pasuje do wszystkich subdomen. Inne wymagają formatu cluster.local bez kropki. Praktyka: podawaj oba warianty, z kropką i bez, żeby pokryć maksimum klientów.
Po drugie, wsparcie notacji CIDR nie jest uniwersalne. Biblioteka Go rozumie 10.0.0.0/8, ale stare wersje niektórych klientów w innych językach - nie, im trzeba podać osobne adresy lub zakresy w innym formacie. Sprawdzaj zachowanie właśnie swojego stosu.
Po trzecie, porty. Jeśli wpis w NO_PROXY podano bez portu, zwykle rozciąga się on na dowolny port hosta. Ale niektóre klienty dopasowują ściśle po porcie. Przy niestandardowych portach wewnętrznych serwisów warto to sprawdzić.
Insight z praktyki: dziewięć na dziesięć incydentów zerwanej komunikacji wewnętrznej po wdrożeniu proxy to niekompletne NO_PROXY. Zrób wzorcową listę dla swojego klastra raz, wyciągnij ją do wspólnego ConfigMapa i używaj ponownie we wszystkich deploymentach. To oszczędza dziesiątki godzin debugowania.
Czego NIE podchwytują zmienne środowiskowe
Naiwne przekonanie "ustawiłem HTTP_PROXY, więc cały ruch poszedł przez proxy" w klastrze jest błędne podwójnie. Istnieje cała klasa komponentów, które po prostu ignorują te zmienne. Trzeba je znać zawczasu.
kubelet i komponenty systemowe
Zmienne środowiskowe kontenera są widoczne tylko dla procesów wewnątrz tego kontenera. kubelet - agent węzła, który pobiera obrazy, uruchamia kontenery i komunikuje się z serwerem API - żyje na poziomie węzła, nie poda. Nie czyta env z manifestu. Jeśli chcesz, żeby kubelet pobierał obrazy przez proxy, konfiguracja robi się na poziomie serwisu systemd kubelet lub konfiguracji container runtime, a nie w specyfikacji poda.
# /etc/systemd/system/kubelet.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://gate.proxeon.net:8080"
Environment="HTTPS_PROXY=http://gate.proxeon.net:8080"
Environment="NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,.svc,.cluster.local"To samo dotyczy container runtime - containerd lub CRI-O. Pobieranie obrazów idzie przez ich własny kontekst sieciowy. Konfiguracja proxy dla rejestru obrazów ustawia się w konfiguracji runtime, i to osobna historia od podów.
Obrazy z własną konfiguracją
Wiele popularnych obrazów ma wbudowane ustawienia sieci, które nadpisują zmienne środowiskowe. Na przykład menedżery pakietów, serwery WWW czy narzędzia proxy wewnątrz obrazu mogą czytać własny plik konfiguracyjny, a nie środowisko. Jeśli aplikacja wewnątrz kontenera używa, powiedzmy, pliku ustawień z jawnie wpisanymi parametrami połączenia, twoje zmienne env po prostu nie zostaną zauważone.
Osobna kategoria to aplikacje, w których klient sieciowy inicjalizuje się przed odczytem środowiska albo buforuje ustawienia przy starcie. Zmiana zmiennej bez restartu procesu nic tu nie da.
Poszczególne SDK i runtime'y językowe
To najbardziej zdradliwa grupa. Poszanowanie HTTP_PROXY to konwencja, a nie standard. Ktoś ją respektuje, ktoś nie.
- Go. Standardowy http.Client przez ProxyFromEnvironment respektuje zmienne. Ale jeśli kod tworzy transport z jawnym Proxy nil, zmienne są ignorowane.
- Python. Biblioteka requests domyślnie czyta środowisko. A niskopoziomowe sockety, niektóre klienty gRPC i biblioteki asynchroniczne - nie.
- Java. JVM używa własnych właściwości systemowych http.proxyHost i https.proxyHost, a zmiennych środowiskowych domyślnie w ogóle nie czyta. Trzeba je przekazać przez JAVA_TOOL_OPTIONS.
- Node.js. Wbudowany moduł http nie respektuje zmiennych środowiskowych. Potrzebne są zewnętrzne agenty, które je czytają.
- gRPC. Część implementacji czyta specjalną zmienną grpc_proxy, a nie standardowe.
Wniosek prosty: nie można zakładać, że skoro zmienna jest zadeklarowana, cały ruch przez nią pójdzie. Każdy runtime sprawdzaj osobno. Dla JVM na przykład manifest wygląda tak:
env:
- name: JAVA_TOOL_OPTIONS
value: "-Dhttp.proxyHost=gate.proxeon.net -Dhttp.proxyPort=8080 -Dhttps.proxyHost=gate.proxeon.net -Dhttps.proxyPort=8080 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.svc|*.cluster.local"Zwróć uwagę: w JVM separatorem wyjątków jest pionowa kreska, a nie przecinek, a wzorce używają gwiazdki. Kolejna pułapka dla tych, którzy mechanicznie kopiują NO_PROXY.
Sekrety: dane logowania proxy przez Secret, a nie w manifeście
Jeśli twoje proxy wymaga uwierzytelniania, pojawia się pytanie o przechowywanie loginu i hasła. Pokusa jest duża: wpisać je wprost w URL wewnątrz wartości env. Tak nie wolno robić i oto dlaczego.
Manifesty deploymentów prawie zawsze leżą w systemie kontroli wersji. Hasło jawnym tekstem w env to wyciek danych logowania do historii Git, dostępnej dla wszystkich z dostępem do repozytorium. Poza tym wartości env widzi każdy, kto może wykonać kubectl describe pod. To bezpośrednie naruszenie zasady najmniejszych uprawnień.
Właściwa droga to obiekt Secret. Przechowuje wrażliwe dane osobno, z możliwością ograniczenia dostępu przez RBAC i włączenia szyfrowania w magazynie etcd.
apiVersion: v1
kind: Secret
metadata:
name: proxeon-credentials
type: Opaque
stringData:
proxy-user: my_account
proxy-pass: s3cr3t_token_valueNastępnie podłączamy wartości z sekretu do zmiennych środowiskowych kontenera przez secretKeyRef, a sam URL proxy składamy tak, aby dane logowania nie świeciły w samym manifeście:
env:
- name: PROXY_USER
valueFrom:
secretKeyRef:
name: proxeon-credentials
key: proxy-user
- name: PROXY_PASS
valueFrom:
secretKeyRef:
name: proxeon-credentials
key: proxy-pass
- name: HTTPS_PROXY
value: "http://$(PROXY_USER):$(PROXY_PASS)@gate.proxeon.net:8080"
- name: NO_PROXY
value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.cluster.local"Kubernetes podstawia wartości z wcześniej zadeklarowanych zmiennych przez składnię z dolarem i nawiasami. To pozwala złożyć URL z danymi logowania w runtime, nie umieszczając hasła wprost w tekście manifestu. Uwzględnij jednak, że złożona wartość HTTPS_PROXY i tak będzie widoczna w env uruchomionego kontenera, więc dodatkowo ogranicz, kto może wykonywać exec i describe dla tych podów.
Dobre praktyki pracy z sekretami
- Włącz szyfrowanie etcd at rest - inaczej sekrety są przechowywane tylko w base64, co nie jest zabezpieczeniem.
- Ogranicz dostęp do sekretów przez RBAC: pod powinien widzieć tylko swój sekret.
- Rotuj dane logowania proxy regularnie i automatyzuj restart podów po rotacji.
- Rozważ zewnętrzne menedżery sekretów z podłączeniem przez sterownik CSI, żeby dane logowania nie trafiały do etcd w ogóle.
- Nigdy nie loguj złożonego URL-a proxy - hasło wycieknie do systemu zbierania logów.
Podejście sidecar: kiedy się opłaca i co daje
Nazwaliśmy już sidecar jednym z trzech poziomów. Teraz przyjrzyjmy się mu jako samodzielnej strategii, bo to najbardziej elastyczne narzędzie, jeśli gotów jesteś zapłacić za nie zasobami.
Kiedy sidecar się opłaca
- Aplikacja nie umie czytać HTTP_PROXY i przepisanie jej kodu jest niemożliwe lub drogie.
- Potrzebna jednolita polityka proxowania bez edycji każdej aplikacji.
- Wymagane przezroczyste przechwytywanie ruchu, w tym protokołów non-HTTP.
- Potrzebna obserwowalność: metryki połączeń wychodzących, tracing, audyt.
- Wymagane polityki ponownych prób, limitów czasu, przerywania łańcucha na poziomie połączeń.
Co daje sidecar poza proxowaniem
Tu kryje się prawdziwa wartość. Sidecar to nie tylko przekierowywacz pakietów. Odpowiednio skonfigurowany staje się punktem kontroli i obserwacji całego ruchu wychodzącego poda.
- Metryki. Ile żądań wyszło na zewnątrz, do jakich hostów, z jakim opóźnieniem, ile błędów. Bez sidecara te dane trzeba by zbierać osobno w każdej aplikacji.
- Tracing. Rozproszone ślady wywołań wychodzących, powiązane z żądaniami przychodzącymi.
- Polityki niezawodności. Automatyczne ponowne próby żądań idempotentnych, limity czasu, ograniczanie równoczesnych połączeń.
- Jednolity TLS. Sidecar może centralnie terminować i ustanawiać zabezpieczone połączenia.
- Audyt i zgodność. Pełny dziennik tego, dokąd konkretnie łączyła się aplikacja - niezastąpione dla compliance.
Przykład poda z sidecarem proxy
Poniżej uproszczony szablon, w którym kontener główny kieruje wychodzące żądania HTTP na lokalny sidecar, a ten już proxuje je dalej przez Proxeon. Dla aplikacji adresem proxy jest localhost, co automatycznie wyklucza ruch między serwisami z zewnętrznego proxowania, jeśli aplikacja łączy się z sąsiadami bezpośrednio.
apiVersion: v1
kind: Pod
metadata:
name: app-with-proxy-sidecar
labels:
app: billing
spec:
containers:
- name: app
image: registry.example.com/billing:2.1.0
env:
- name: HTTP_PROXY
value: "http://127.0.0.1:3128"
- name: HTTPS_PROXY
value: "http://127.0.0.1:3128"
- name: NO_PROXY
value: "localhost,127.0.0.1,10.0.0.0/8,.svc,.cluster.local"
- name: egress-proxy
image: registry.example.com/egress-agent:1.0.0
ports:
- containerPort: 3128
env:
- name: UPSTREAM_PROXY
value: "http://gate.proxeon.net:8080"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128MiNative sidecar i kolejność uruchamiania
Klasyczny problem sidecara - wyścig przy starcie. Jeśli aplikacja główna startuje i wykonuje pierwsze żądanie wychodzące zanim sidecar jest gotowy przyjmować połączenia, żądanie pada. Od Kubernetes 1.28 pojawiło się wsparcie natywnych kontenerów sidecar: deklaruje się je w bloku initContainers z restartPolicy Always i gwarantowanie startują oraz pozostają żywe do momentu kontenera głównego. To rozwiązuje problem wyścigu czysto, bez protez w rodzaju opóźnień i ponownych prób przy starcie.
spec:
initContainers:
- name: egress-proxy
image: registry.example.com/egress-agent:1.0.0
restartPolicy: Always
ports:
- containerPort: 3128Brama egress: stabilny adres wychodzący dla klastra
Trzeci poziom zasługuje na osobną rozmowę, bo właśnie on rozwiązuje zadanie, dla którego najczęściej w ogóle przychodzi się do proxy w klastrze: przewidywalny IP wychodzący.
Po co stały adres wychodzący
Zewnętrzni partnerzy, bramy płatności, dostawcy danych nierzadko działają po białej liście adresów. Mówią: przyjmujemy żądania tylko z tych IP. W zwykłym klastrze adres wychodzący poda to adres węzła, na którym pod akurat się znalazł. Węzłów jest wiele, skalują się, są wymieniane, dodawane przy autoskalowaniu. Adres jest nieprzewidywalny. Partner nie może wpisać na białą listę całej puli węzłów, która zresztą się zmienia.
Brama egress rozwiązuje to: cały ruch wychodzący zbiera się w jednym punkcie ze stałym adresem. Partner wpisuje na białą listę jeden-dwa stabilne IP - i wszystko działa. Używając Proxeon w roli punktu wyjścia, otrzymujesz stabilny adres zewnętrzny, który podajesz partnerom raz.
Jak routowany jest ruch na bramę egress
Mechanizm zależy od CNI. Niektóre pluginy CNI wspierają obiekt polityki egress, gdzie opisujesz: ruch od podów z takimi a takimi etykietami, idący na zewnątrz, ma wychodzić przez taki węzeł lub adres. Plugin automatycznie konfiguruje odpowiednie reguły SNAT i routingu. Koncepcyjnie wygląda to tak:
apiVersion: policy.example.io/v1
kind: EgressPolicy
metadata:
name: billing-egress
spec:
selector:
matchLabels:
egress: proxeon
egressIP: 203.0.113.10
destinationCIDRs:
- 0.0.0.0/0Dokładna składnia różni się między CNI, więc sprawdzaj dokumentację swojego pluginu. Idea jest niezmienna: oznaczyć pody, których ruch kierowany jest przez bramę, i ustawić stabilny adres wyjściowy.
Niezawodność bramy egress
Skoro brama to jeden punkt, to też jeden punkt awarii. Zasady przetrwania:
- Zapewnij redundancję: minimum dwa węzły-bramy z automatycznym przełączaniem.
- Monitoruj dostępność wyjścia osobnym syntetycznym żądaniem na zewnątrz co minutę.
- Pilnuj przepustowości: całe północ-południe płynie przez bramę, wąskie gardło odbije się na wszystkim.
- Rozdzielaj polityki: ruch krytyczny i tło lepiej rozdzielić na różne ścieżki, żeby obciążenie tła nie przeszkadzało temu ważnemu.
Diagnostyka ruchu wychodzącego w klastrze
Kiedy coś idzie nie tak - a pójdzie - potrzebny jest arsenał sprawdzeń. Zbierzmy praktyczny zestaw komend i podejść.
Krok 1: poznać rzeczywisty adres wychodzący z poda
Pierwsze, co sprawdzamy: z jakiego adresu pod jest widoczny dla świata zewnętrznego. Uruchamiamy efemeryczny kontener diagnostyczny albo exec do istniejącego poda i łączymy się z serwisem zwracającym twój publiczny adres.
kubectl run nettest --rm -it --image=registry.example.com/nettools:1.0 --restart=Never -- sh
# wewnątrz kontenera
curl -s https://api.ipify.org
curl -s -x http://gate.proxeon.net:8080 https://api.ipify.orgPierwsze żądanie pokazuje adres bez proxy, drugie - przez proxy jawnie. Jeśli oba są takie same, a spodziewałeś się różnych, znaczy proxy nie jest stosowane. Jeśli drugie zwraca oczekiwany adres Proxeon, proxy działa i rzecz w konfiguracji aplikacji.
Krok 2: sprawdzić, czy aplikacja widzi zmienne
kubectl exec deploy/worker -c app -- env | grep -i proxyJeśli wyjście jest puste - zmienne nie dotarły do kontenera. Sprawdzaj manifest i to, czy pod został odtworzony po zmianie. Przypominam: zmiana env wymaga odtworzenia poda, w locie nie jest podchwytywana.
Krok 3: sprawdzić rozwiązywanie DNS od środka
Wiele błędów maskuje się jako problem z proxy, a w rzeczywistości to DNS. Sprawdzamy rozwiązywanie nazwy wewnętrznej i zewnętrznej:
kubectl exec deploy/worker -c app -- nslookup orders.default.svc.cluster.local
kubectl exec deploy/worker -c app -- nslookup api.external-partner.comJeśli nazwa wewnętrzna się nie rozwiązuje - problem w CoreDNS albo w tym, że żądanie poszło na zewnętrzne proxy z powodu braku sufiksu w NO_PROXY. To bezpośrednie potwierdzenie, że lista wyjątków jest niekompletna.
Krok 4: gdzie patrzeć na odmowy
- Logi aplikacji. Szukaj błędów połączenia, limitów czasu, odmowy uwierzytelnienia proxy (zwykle kod 407).
- Logi sidecara. Jeśli jest, widać tam, jakie żądania przyjął i dokąd skierował.
- Metryki bramy egress. Wzrost odmów i opóźnień na bramie.
- Zdarzenia poda. kubectl describe pod pokaże problemy ze startem, w tym wyścig sidecara.
- NetworkPolicy. Sprawdź, czy polityka sieciowa nie blokuje ruchu wychodzącego - częsta przyczyna cichych odmów.
Typowe sygnatury problemów
- Kod 407 od proxy - błędne lub brakujące dane logowania. Sprawdzaj Secret.
- Limit czasu przy łączeniu z wewnętrznym serwisem po włączeniu proxy - niekompletne NO_PROXY.
- Różne adresy wychodzące przy kolejnych żądaniach - ruch nie idzie przez bramę egress, działa zwykły SNAT węzła.
- Pierwsze żądania po starcie padają, potem wszystko działa - wyścig sidecara, przejdź na native sidecar.
- Aplikacja Java ignoruje proxy - zapomniano o JAVA_TOOL_OPTIONS i właściwościach systemowych.
Typowe błędy, których warto unikać
Zbierzmy w jednym miejscu grabie, na które nadeptuje się najczęściej. Sprawdź się po tej liście przed, a nie po incydencie.
Niekompletne NO_PROXY
Absolutny lider. Zapomniano Service CIDR, zapomniano sufiks .svc, nie uwzględniono adresu metadanych chmury - i otrzymano kaskadę zagadkowych odmów. Zawsze wychodź od pełnej wzorcowej listy dla swojego klastra.
Hasło proxy jawnym tekstem w manifeście
Wyciek do Git i do wyjścia describe. Tylko Secret, tylko ograniczony dostęp.
Założenie, że wszystkie runtime'y respektują zmienne
JVM, Node.js, część klientów gRPC ignorują HTTP_PROXY. Sprawdzaj każdy stos osobno i konfiguruj natywnie.
Ignorowanie kubelet i runtime
Pobieranie obrazów przez proxy konfiguruje się na poziomie węzła, a nie poda. Nie dziw się, że obrazy się nie pobierają, jeśli ustawiłeś tylko env w manifeście.
Wyścig przy starcie sidecara
Aplikacja startuje przed proxy i przewraca pierwsze żądania. Rozwiązanie - native sidecar containers.
Brama egress bez redundancji
Jeden punkt awarii bez duplikacji kładzie cały ruch wychodzący. Zapewnij redundancję i monitoring.
Mieszanie formatów CIDR
Ustawiłeś 10.0.0.0/8 dla klienta, który nie rozumie CIDR. Sprawdź, jaki format je twoja biblioteka, i daj alternatywę.
Brak odtworzenia podów po zmianie env
Zmieniłeś zmienną w deploymentcie, ale stare pody działają dalej ze starymi wartościami do restartu. Zapewnij rollout.
Zapomniane małe litery zmiennych
Część bibliotek czyta tylko http_proxy małymi literami. Duplikuj nazwy w obu wariantach.
Narzędzia i zasoby
Co trzymać pod ręką do pracy z ruchem wychodzącym w klastrze.
Diagnostyczne
- kubectl exec i kubectl debug - podstawa sprawdzeń od środka poda.
- Efemeryczne kontenery diagnostyczne - pozwalają podłączyć zestaw narzędzi sieciowych do działającego poda bez przebudowywania obrazu.
- Obraz z narzędziami sieciowymi - curl, dig, nslookup, traceroute, zebrane w jednym kontenerze do szybkich sprawdzeń.
- Serwis zwracający publiczne IP - sprawdzenie rzeczywistego adresu wychodzącego.
Infrastrukturalne
- ConfigMap z wzorcowym NO_PROXY - jedno źródło prawdy dla wszystkich deploymentów.
- Secret i zewnętrzne menedżery sekretów - dla danych logowania proxy.
- Service mesh - jeśli potrzebny sidecar z obserwowalnością od razu.
- Polityki egress CNI - dla routingu na bramę.
- Proxeon (proxeon.net) - infrastruktura proxy dla stabilnego adresu wychodzącego i uwierzytelnionego dostępu.
Monitoring
- Metryki połączeń wychodzących z sidecara lub bramy egress.
- Syntetyczne sprawdzenia dostępności adresów zewnętrznych z klastra.
- Alerte na wzrost kodów 407 i limitów czasu żądań wychodzących.
- Dashboard z rozkładem ruchu wychodzącego po hostach docelowych.
Case'y i rezultaty
Przeanalizujmy trzy uogólnione scenariusze pokazujące, jak wybór poziomu wpływa na wynik.
Case 1: integracja płatnicza i biała lista IP
Zespół integrował zewnętrzną bramę płatniczą, która przyjmuje żądania tylko z uzgodnionych adresów. Najpierw spróbowano zmiennych środowiskowych na poziomie kontenera - i natrafiono na to, że przy autoskalowaniu pody rozjeżdżały się po nowych węzłach z nowymi adresami, a adres wychodzący i tak pozostawał adresem węzła, nie proxy. Część żądań zaczęła być odrzucana.
Rozwiązanie: przenieśli serwis płatniczy na egress przez Proxeon ze stałym adresem wychodzącym. Partner wpisał jedno IP na białą listę. Odmowy z powodu nieznanego adresu zniknęły. Dodatkowy efekt - scentralizowany dziennik wszystkich odwołań do bramy płatniczej dla audytu.
Case 2: zerwanie komunikacji między serwisami po wdrożeniu proxy
Firma dodała HTTP_PROXY do wszystkich deploymentów za jednym zamachem przez wspólny szablon. Po kilku minutach posypały się błędy: serwisy przestały się widzieć. Wywołania wewnętrzne szły na zewnętrzne proxy, które nie mogło ich rozwiązać.
Diagnostyka zajęła czas, dopóki nie sprawdzono rozwiązywania od środka poda i nie zobaczono, że nazwa .svc.cluster.local idzie na proxy. Przyczyna - NO_PROXY zawierał tylko localhost. Ułożono pełną wzorcową listę z Pod CIDR, Service CIDR i wszystkimi sufiksami DNS, wyciągnięto ją do ConfigMapa, podłączono we wszystkich podach. Komunikacja wewnętrzna wróciła. Od tego czasu wzorcowe NO_PROXY to obowiązkowa część szablonu deploymentu.
Case 3: obserwowalność ruchu wychodzącego przez sidecar
Wymóg bezpieczeństwa: wiedzieć, dokąd konkretnie łączy się na zewnątrz każda aplikacja, z pełnym dziennikiem. Aplikacje były w różnych językach, część nie umiała czytać HTTP_PROXY. Edycja kodu dziesiątek serwisów - zbyt droga.
Wdrożono sidecar proxy w szablonie poda. Aplikacje kierują ruch wychodzący na lokalnego agenta, ten proxuje przez Proxeon i zapisuje metryki: host docelowy, opóźnienie, kod odpowiedzi. Pojawił się dashboard odwołań wychodzących dla każdego serwisu. Dodatkowo skonfigurowano limity czasu i ponowne próby na poziomie sidecara, co zmniejszyło liczbę kaskadowych awarii przy krótkotrwałej niedostępności zewnętrznych API. Ceną były dodatkowe zasoby na sidecar, ale zysk w obserwowalności i niezawodności to uzasadnił.
FAQ: częste pytania inżynierów
Dlaczego ruch do sąsiedniego poda idzie przez zewnętrzne proxy, choć to oczywiście adres wewnętrzny?
Bo klient HTTP nie wie, że adres jest wewnętrzny. Widzi nazwę lub adres i jeśli ten nie wchodzi w NO_PROXY, kieruje żądanie na proxy zgodnie z regułami. Klient sam nie rozróżnia wschód-zachód i północ-południe - robi to za niego lista wyjątków. Dodaj wewnętrzne sufiksy i podsieci do NO_PROXY.
Czy trzeba konfigurować proxy także dla pobierania obrazów?
Tak, ale nie przez env poda. Pobieranie obrazów wykonuje container runtime i kubelet na poziomie węzła. Proxy dla rejestru konfiguruje się w konfiguracji runtime lub w unicie systemd kubelet. Zmienne środowiskowe kontenera w ogóle na to nie wpływają.
Co ważniejsze dla stabilnego adresu wychodzącego - sidecar czy brama egress?
Brama egress. Sidecar proxuje ruch pojedynczego poda, ale adres wychodzący i tak określa to, dokąd dalej idzie połączenie. Dla gwarantowanie stałego adresu, który przyjmie zewnętrzny partner, potrzebna jest albo brama egress, albo wyjście przez zewnętrzny punkt o stałym adresie, na przykład przez Proxeon.
Dlaczego aplikacja Java ignoruje HTTP_PROXY?
JVM z przyczyn historycznych nie czyta zmiennych środowiskowych dla proxy. Używa własnych właściwości systemowych http.proxyHost, https.proxyHost i wyjątków http.nonProxyHosts. Przekazuj je przez JAVA_TOOL_OPTIONS. I pamiętaj: separatorem wyjątków w JVM jest pionowa kreska, a wzorce używają gwiazdki, a nie CIDR.
Jak bezpiecznie przechowywać hasło proxy?
W obiekcie Secret z ograniczeniem dostępu przez RBAC i włączonym szyfrowaniem etcd. Podłączaj wartości przez secretKeyRef. Nie pisz hasła jawnym tekstem w manifeście, inaczej wycieknie do Git i do wyjścia describe. Przy wyższych wymaganiach użyj zewnętrznego menedżera sekretów przez CSI.
Zmieniłem zmienną środowiskową, a zachowanie się nie zmieniło - dlaczego?
Zmienne środowiskowe są ustalane przy starcie kontenera. Dopóki pod nie zostanie odtworzony, działa ze starymi wartościami. Zaktualizuj deployment tak, żeby przeszedł rollout podów. Sprawdź też, czy aplikacja w ogóle czyta środowisko, a nie buforuje ustawień przy inicjalizacji.
Jak poznać, jaki format NO_PROXY potrzebuje właśnie moja biblioteka?
Empirycznie: skonfiguruj proxy, połącz się z wewnętrzną nazwą od środka poda i sprawdź, czy żądanie poszło na proxy, czy bezpośrednio. Jeśli poszło na proxy - format nie został rozpoznany. Próbuj wariant z wiodącą kropką i bez, dodawaj osobne adresy zamiast CIDR, sprawdzaj wielkość liter nazwy zmiennej. Dokumentacja konkretnego klienta to najlepszy punkt odniesienia.
Czy warto zawsze używać service mesh dla proxowania?
Nie. Service mesh to potężne narzędzie z obserwowalnością i politykami, ale niesie ze sobą zauważalny narzut i złożoność operacyjną. Jeśli zadaniem jest tylko skierowanie ruchu wychodzącego przez proxy, lekki agent sidecar lub brama egress będą tańsze. Wdrażaj mesh, kiedy naprawdę potrzebujesz jego możliwości w całości.
Co zrobić z ruchem, który w ogóle nie jest HTTP?
Zmienne HTTP_PROXY działają tylko dla klientów HTTP i HTTPS, które je respektują. Dla dowolnych połączeń TCP potrzebne jest przezroczyste przechwytywanie na poziomie sidecara lub bramy egress z odpowiednimi regułami routingu. Tu zmienne środowiskowe są bezsilne.
Czy można ustawić różną politykę proxy dla różnych podów?
Tak. Na poziomie kontenera - przez różne env w różnych deploymentach. Na poziomie egress - przez selektory etykiet w politykach egress, kierując oznaczone pody na właściwe wyjście. Łączenie daje elastyczność: podstawowe wyjście przez bramę plus indywidualne ustawienia dla pojedynczych serwisów.
Zakończenie: składamy checklistę wdrożenia
Przeszliśmy drogę od tego, dlaczego konfiguracja "jak na zwykłym serwerze" nie działa w klastrze, do trzech poziomów wpinania proxy, niuansów NO_PROXY, sekretów, sidecara, bramy egress i diagnostyki. Główny wniosek: w Kubernetes ruch wychodzący to nie jedna zmienna, ale warstwowy system, w którym najważniejsze jest nie to, jak włączyć proxy, ale jak nie zepsuć nim komunikacji wewnętrznej.
Zbierzmy finałową checklistę wdrożenia, którą warto mieć przed oczami przy każdym wdrożeniu.
Checklista wdrożenia proxy w klastrze
- Określiliśmy poziom. Wybraliśmy kontener, sidecar lub bramę egress świadomie, pod konkretne zadanie.
- Ułożyliśmy pełne NO_PROXY.