Jak zachować oryginalny IP źródła żądania po równoważeniu obciążenia w klastrze K8s

Wstęp

Wdrożenie aplikacji nie zawsze polega na prostym instalowaniu i uruchamianiu, czasami trzeba też rozważyć problemy sieciowe. Niniejszy artykuł opisuje, jak w klastrze k8s sprawić, aby usługa mogła uzyskać źródłowy IP żądania.

Aplikacje świadczące usługi zazwyczaj zależą od informacji wejściowych. Jeśli informacje wejściowe nie zależą od pięciokrotki (źródłowy IP, źródłowy port, docelowy IP, docelowy port, protokół), to taka usługa ma niskie powiązanie z siecią i nie musi przejmować się szczegółami sieciowymi.

Dlatego dla większości osób nie ma potrzeby czytania tego artykułu. Jeśli interesujesz się sieciami lub chcesz poszerzyć horyzonty, możesz kontynuować czytanie, aby dowiedzieć się więcej o scenariuszach usług.

Artykuł oparty na k8s v1.29.4. W niektórych opisach mieszam pojęcia pod i endpoint – w kontekście tego artykułu można je traktować jako równoważne.

Jeśli zauważysz błędy, chętnie przyjmę poprawki, poprawię je niezwłocznie.

Dlaczego informacje o źródłowym IP są tracone?

Najpierw wyjaśnijmy, czym jest źródłowy IP. Gdy A wysyła żądanie do B, a B przekazuje je do C, to chociaż C widzi źródłowy IP protokołu IP jako IP B, w tym artykule IP A traktujemy jako źródłowy IP.

Głównie dwie klasy zachowań powodują utratę informacji o źródle:

  1. Translacja adresów sieciowych (NAT), w celu oszczędzania publicznych IPv4, równoważenia obciążenia itp. Powoduje, że serwer widzi źródłowy IP jako IP urządzenia NAT, a nie prawdziwy źródłowy IP.
  2. Proxy, odwrotne proxy (RP, Reverse Proxy) i równoważenie obciążenia (LB, Load Balancer) należą do tej klasy, poniżej określane zbiorczo jako serwery proxy. Te serwery proxy przekazują żądania do usług backendowych, ale zastępują źródłowy IP swoim własnym IP.
  • NAT to w skrócie wymiana przestrzeni portów na przestrzeń IP. Adresy IPv4 są ograniczone, jeden adres IP może mapować 65535 portów. W większości przypadków te porty nie są w pełni wykorzystane, więc wiele podsieci IP może współdzielić jeden publiczny IP, rozróżniając usługi po portach. Forma użycia: publiczny IP:publiczny port -> prywatny IP_1:prywatny port. Więcej informacji znajdziesz w Translacja adresów sieciowych.
  • Serwery proxy służą do ukrywania lub eksponowania. Przekazują żądania do usług backendowych, jednocześnie zastępując źródłowy IP swoim własnym IP, co ukrywa prawdziwy IP usług backendowych i chroni ich bezpieczeństwo. Forma użycia: IP klienta -> IP proxy -> IP serwera. Więcej informacji znajdziesz w Proxy.

NAT i serwery proxy są bardzo powszechne, większość usług nie może uzyskać źródłowego IP żądania.

To dwie powszechne metody modyfikacji źródłowego IP. Inne sugestie mile widziane.

Jak zachować źródłowy IP?

Oto przykład żądania HTTP:

PoleDługość (bajty)Przesunięcie bitoweOpis
Nagłówek IP
Źródłowy IP40-31Adres IP nadawcy
Docelowy IP432-63Adres IP odbiorcy
Nagłówek TCP
Źródłowy port20-15Numer portu nadawcy
Docelowy port216-31Numer portu odbiorcy
Numer sekwencyjny432-63Służy do identyfikacji strumienia bajtów danych wysyłanych przez nadawcę
Numer potwierdzenia464-95Jeśli ustawiona flaga ACK, to następny oczekiwany numer sekwencyjny
Przesunięcie danych496-103Liczba bajtów od początku nagłówka TCP do danych
Zarezerwowane4104-111Pole zarezerwowane, nieużywane, ustawione na 0
Flagi2112-127Różne flagi kontrolne, takie jak SYN, ACK, FIN itp.
Rozmiar okna2128-143Ilość danych, jaką odbiorca może przyjąć
Suma kontrolna2144-159Służy do wykrywania błędów w transmisji danych
Wskaźnik pilny2160-175Pozycja pilnych danych, które nadawca chce, aby odbiorca przetworzył jak najszybciej
OpcjeZmienne176-…Może zawierać znaczniki czasu, maksymalny rozmiar segmentu itp.
Nagłówek HTTP
Wiersz żądaniaZmienne…-…Zawiera metodę żądania, URI i wersję HTTP
Pola nagłówkaZmienne…-…Zawiera różne pola nagłówka, takie jak Host, User-Agent itp.
Pusta linia2…-…Służy do oddzielenia nagłówka od ciała
CiałoZmienne…-…Opcjonalne ciało żądania lub odpowiedzi

Przeglądając powyższą strukturę żądania HTTP, widać, że opcje TCP, wiersz żądania, pola nagłówka, ciało są zmienne. Przestrzeń opcji TCP jest ograniczona i generalnie nie służy do przekazywania źródłowego IP. Wiersz żądania ma stałą informację i nie można go rozszerzać. Ciało HTTP po zaszyfrowaniu nie można modyfikować. Tylko pola nagłówka HTTP nadają się do rozszerzenia w celu przekazania źródłowego IP.

W nagłówku HTTP można dodać pole X-REAL-IP, aby przekazać źródłowy IP. Ta operacja jest zazwyczaj wykonywana na serwerze proxy, a następnie serwer proxy przekazuje żądanie do usługi backendowej, która może uzyskać źródłowy IP z tego pola.

Uwaga: Należy zapewnić, że serwer proxy znajduje się przed urządzeniem NAT, aby uzyskać prawdziwy źródłowy IP żądania whoami. W produktach Alibaba Cloud widzimy kategorię towaru Load Balancer, której pozycja w sieci różni się od zwykłych serwerów aplikacji.

Instrukcja操作 K8S

Przykład wdrożenia z projektem whoami.

Tworzenie Deployment

Najpierw utwórz usługę:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
        - name: whoami
          image: docker.io/traefik/whoami:latest
          ports:
            - containerPort: 8080

To utworzy Deployment zawierający 3 Pody, każdy pod zawiera jeden kontener uruchamiający usługę whoami.

Tworzenie Service

Możesz utworzyć usługę typu NodePort lub LoadBalancer dla dostępu zewnętrznego lub usługę typu ClusterIP tylko dla dostępu wewnętrznego w klastrze, a następnie dodać usługę Ingress do ekspozycji dostępu zewnętrznego.

NodePort można uzyskać zarówno przez NodeIP:NodePort, jak i przez usługę Ingress, co ułatwia testowanie. W tej sekcji używamy usługi NodePort.

apiVersion: v1
kind: Service
metadata:
  name: whoami-service
spec:
  type: NodePort
  selector:
    app: whoami
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 30002

Po utworzeniu usługi, dostęp curl whoami.example.com:30002 pokaże IP jako NodeIP, a nie źródłowy IP żądania whoami.

Uwaga: To nie jest poprawny IP klienta, to wewnętrzne IP klastra. Oto co się dzieje:

  • Klient wysyła pakiet do node2:nodePort
  • node2 zastępuje źródłowy IP pakietu swoim własnym adresem IP (SNAT)
  • node2 zastępuje docelowy IP pakietu IP Pod
  • Pakiet jest routowany do node1, potem do endpointu
  • Odpowiedź Pod jest routowana z powrotem do node2
  • Odpowiedź Pod jest wysyłana z powrotem do klienta

Ilustracja graficzna:

Konfiguracja externalTrafficPolicy: Local

Aby uniknąć tej sytuacji, Kubernetes ma funkcję zachowującą źródłowy IP klienta. Jeśli ustawisz service.spec.externalTrafficPolicy na Local, kube-proxy będzie proxyować żądania tylko do lokalnych endpointów, bez przekazywania ruchu do innych węzłów.

apiVersion: v1
kind: Service
metadata:
  name: whoami-service
spec:
  type: NodePort
  externalTrafficPolicy: Local
  selector:
    app: whoami
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 30002

Testuj za pomocą curl whoami.example.com:30002. Gdy whoami.example.com mapuje się na IP wielu węzłów klastra, istnieje pewna szansa na brak dostępu. Upewnij się, że rekord DNS zawiera tylko IP węzła z endpointem (podem).

Ta konfiguracja ma swoją cenę: traci się zdolność równoważenia obciążenia w klastrze. Klient uzyska odpowiedź tylko po dostępie do węzła z wdrożonym endpointem.

Ograniczenie ścieżki dostępu

Gdy klient uzyska dostęp do Node 2, nie będzie odpowiedzi.

Tworzenie Ingress

Większość usług oferowanych użytkownikom używa http/https, forma https://ip:port może być obca dla użytkowników. Zazwyczaj używa się Ingress, aby załadować usługę NodePort utworzoną powyżej na port 80/443 domeny.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami-ingress
  namespace: default
spec:
  ingressClassName: external-lb-default
  rules:
    - host: whoami.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: whoami-service
                port:
                  number: 80

Po zastosowaniu, testuj dostęp curl whoami.example.com. ClientIP zawsze będzie IP Poda Ingress Controller na węźle endpointu.

root@client:~# curl whoami.example.com
...
RemoteAddr: 10.42.1.10:56482
...

root@worker:~# kubectl get -n ingress-nginx pod -o wide
NAME                                       READY   STATUS    RESTARTS   AGE    IP           NODE          NOMINATED NODE   READINESS GATES
ingress-nginx-controller-c8f499cfc-xdrg7   1/1     Running   0          3d2h   10.42.1.10   k3s-agent-1   <none>           <none>

Użycie Ingress jako odwrotnego proxy dla usługi NodePort oznacza dwie warstwy service przed endpointem. Poniższy rysunek pokazuje różnicę.

graph LR
    A[Client] -->|whoami.example.com:80| B(Ingress)
    B -->|10.43.38.129:32123| C[Service]
    C -->|10.42.1.1:8080| D[Endpoint]
graph LR
    A[Client] -->|whoami.example.com:30001| B(Service)
    B -->|10.42.1.1:8080| C[Endpoint]

W ścieżce 1, przy dostępie zewnętrznym do Ingress, pierwszym endpointem jest Ingress Controller, potem endpoint whoami.
Ingress Controller to w istocie usługa LoadBalancer,

kubectl -n ingress-nginx get svc

NAMESPACE   NAME             CLASS   HOSTS                       ADDRESS                                              PORTS   AGE
default     echoip-ingress   nginx   ip.example.com       172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707   80      18h
default     whoami-ingress   nginx   whoami.example.com   172.16.0.57,2408:4005:3de:8500:4da1:169e:dc47:1707   80      16h

Dlatego można ustawić wspomniane wcześniej externalTrafficPolicy w Ingress Controller, aby zachować źródłowy IP.

Jednocześnie należy ustawić use-forwarded-headers na true w configmap ingress-nginx-controller, aby Ingress Controller mógł rozpoznać pola X-Forwarded-For lub X-REAL-IP.

apiVersion: v1
data:
  allow-snippet-annotations: "false"
  compute-full-forwarded-for: "true"
  use-forwarded-headers: "true"
  enable-real-ip: "true"
  forwarded-for-header: "X-Real-IP" # X-Real-IP or X-Forwarded-For
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.10.1
  name: ingress-nginx-controller
  namespace: ingress-nginx

Różnica między usługą NodePort a ingress-nginx-controller polega na tym, że backend NodePort zazwyczaj nie jest wdrażany na każdym węźle, podczas gdy backend ingress-nginx-controller zazwyczaj jest wdrażany na każdym węźle eksponowanym na zewnątrz.

W przeciwieństwie do usługi NodePort, gdzie ustawienie externalTrafficPolicy powoduje brak odpowiedzi dla żądań między węzłami, Ingress może najpierw ustawić nagłówek, a potem przekazać żądanie, realizując zarówno zachowanie źródłowego IP, jak i równoważenie obciążenia.

Podsumowanie

  • Translacja adresów (NAT), Proxy, odwrotne proxy (Reverse Proxy), równoważenie obciążenia (Load Balance) powodują utratę źródłowego IP.
  • Aby zapobiec utracie źródłowego IP, serwer proxy może ustawić prawdziwy IP w polu nagłówka HTTP X-REAL-IP podczas przekazywania. Przy wielu warstwach proxy używa się pola X-Forwarded-For, które w formie stosu rejestruje źródłowy IP i listę IP ścieżki proxy.
  • Ustawienie externalTrafficPolicy: Local w usłudze NodePort klastra zachowuje źródłowy IP, ale traci zdolność równoważenia obciążenia.
  • ingress-nginx-controller wdrożony w formie daemonset na wszystkich węzłach roli loadbalancer, z ustawieniem externalTrafficPolicy: Local, zachowuje źródłowy IP i zdolność równoważenia obciążenia.

Referencje