Co stoi między przeglądarką a twoją aplikacją

Wpisz w przeglądarce example.com, a żądanie nigdy nie dotrze bezpośrednio do procesu, który uruchamia twój kod. Najpierw trafia na coś innego: serwer, który odczytuje żądanie, decyduje, który backend ma odpowiedzieć, przekazuje je tam i odsyła odpowiedź tak, jakby sam ją wygenerował. Ten serwer to reverse proxy.

Nginx, Caddy, Traefik i HAProxy robią dokładnie to samo, podobnie jak load balancer stojący przed klastrem Kubernetes. Układ się nie zmienia: klienci rozmawiają wyłącznie z proxy. Prawdziwe serwery zostają za nim, niezależnie od tego, ile ich jest i gdzie działają.

Jak reverse proxy obsługuje żądanie

Klient otwiera połączenie z adresem IP i portem reverse proxy, tak samo jak z każdym innym serwerem. Proxy kończy to połączenie, odczytuje linię żądania i nagłówki, po czym porównuje je z własnymi regułami — zwykle nagłówkiem Host i ścieżką URL. Na tej podstawie otwiera drugie, osobne połączenie do backendu, przekazuje żądanie dalej, czeka na odpowiedź i odsyła ją z powrotem przez pierwotne połączenie klienta.

Dwa niezależne połączenia zszyte razem. Z perspektywy klienta wygląda to identycznie jak bezpośrednia rozmowa z backendem. Z perspektywy backendu każde żądanie wygląda, jakby pochodziło z jednej maszyny, proxy, a nie z internetu w ogóle.

1
2
klient  --- żądanie --->  reverse proxy  --- żądanie --->  serwer backend
klient  <-- odpowiedź ---  reverse proxy  <-- odpowiedź ---  serwer backend

Ponieważ proxy widzi każde żądanie, zanim trafi ono do backendu, może je sprawdzić, zmodyfikować, zcache’ować albo odrzucić. Właśnie ta pozycja sprawia, że przejmuje robotę, której nikt nie chce trzymać wewnątrz aplikacji: terminowanie TLS, kompresję odpowiedzi, cache’owanie statycznej treści, ograniczanie nadużywających klientów i kierowanie różnych ścieżek do różnych usług — /api do jednego backendu, / do drugiego, /static prosto z dysku.

Reverse proxy vs forward proxy

Oba stoją w środku połączenia. Różnica polega na tym, dla kogo pracują.

Forward proxyReverse proxy
Działa w imieniuKlientaSerwera
Klient wie, że istniejeZwykle tak (skonfigurowany jawnie)Nie (wygląda jak prawdziwy serwer)
Co ukrywaTożsamość klienta przed serweremTożsamość serwera przed klientem
Typowe zastosowanieFiltrowanie ruchu wychodzącego w firmie, omijanie geoblokad, anonimowe przeglądanieRouting, terminowanie TLS, cache, ochrona serwerów źródłowych

Forward proxy to skrzynka, którą pracodawca stawia w biurze, żeby cały ruch wychodzący przechodził przez jeden filtrowany punkt: strona, którą odwiedzasz, zapisuje adres proxy, nie twojego laptopa. Reverse proxy stawia właściciel strony, żeby ukryć to, co stoi po drugiej stronie. Z zewnątrz nie da się stwierdzić, czy za example.com stoi jeden serwer, czy czterdzieści.

Reverse proxy vs load balancer

Te dwa terminy są używane zamiennie, głównie dlatego, że jeden proces Nginx albo HAProxy zwykle robi obie rzeczy naraz.

Reverse proxyLoad balancer
Główne pytanie, na które odpowiadaCo powinno się stać z tym żądaniem?Który serwer powinien obsłużyć to żądanie?
Działa z jednym backendem?Tak, nadal się przydaje (TLS, cache, routing po ścieżce)Nie, potrzebuje co najmniej dwóch do balansowania
Typowe dodatkowe funkcjeCache, przepisywanie nagłówków, terminowanie TLS, routing po ścieżceHealth checki, ważone rozłożenie ruchu, przywiązanie sesji

Reverse proxy przed jednym backendem wciąż się przydaje: terminuje TLS, żeby twoja aplikacja nie musiała tego robić, ukrywa prawdziwy adres backendu, dokłada gzip. Load balancer nie ma nic do roboty, dopóki nie ma co najmniej dwóch backendów do wyboru. Etykieta, której używasz, opisuje więc napisaną konfigurację, nie zainstalowane oprogramowanie.

Jak skonfigurować Nginx jako reverse proxy

Nasłuchuj na porcie, dopasuj hostname, przekaż do backendu. To całe minimum:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

proxy_pass to linia, która ma znaczenie: wszystko, co złapie location, trafia do http://127.0.0.1:3000. Cztery linie proxy_set_header istnieją, bo bez nich backend widziałby proxy jako klienta, a każde żądanie wyglądałoby, jakby pochodziło z 127.0.0.1. X-Real-IP i X-Forwarded-For przenoszą dalej prawdziwy adres klienta. X-Forwarded-Proto mówi backendowi, czy oryginalne żądanie było przez HTTPS, bo TLS kończy się na proxy, a kolejny skok idzie już zwykłym HTTP.

Jeden szczegół regularnie psuje konfiguracje: końcowy ukośnik w URI proxy_pass zmienia przekazywaną ścieżkę. proxy_pass http://backend; (bez ścieżki) przekazuje oryginalne URI bez zmian. proxy_pass http://backend/; (z końcowym ukośnikiem) usuwa przed przekazaniem tę część URI, którą złapał location. Przy location /api/ i proxy_pass http://backend/; żądanie do /api/users trafia do backendu jako /users. Pomyl te dwa warianty, a każda trasa zwróci 404 po stronie backendu, podczas gdy curl uderzający w proxy będzie wyglądał, jakby działał poprawnie.

Reverse proxy przed aplikacją w Dockerze

Tutaj ten wzorzec pojawia się najczęściej w codziennej pracy: Nginx w jednym kontenerze, twoja aplikacja w drugim, oba w tej samej sieci Docker, dzięki czemu Nginx dociera do aplikacji po nazwie kontenera — to zachowanie DNS opisane w co daje sieć Docker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# nginx.conf
server {
    listen 80;

    location / {
        proxy_pass http://app:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# compose.yaml
services:
  app:
    build: .
    expose:
      - "3000"

  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app

app udostępnia port 3000 w sieci Compose, ale niczego nie publikuje na hoście. Robi to wyłącznie Nginx, na porcie 80. Nazwa app:3000 w konfiguracji Nginx jest rozwiązywana przez wewnętrzny DNS Dockera, ten sam mechanizm opisany w przewodniku po Docker Compose. Uruchom docker compose up, a żądania do localhost trafiają do Nginx, który przekazuje je do kontenera aplikacji. Sama aplikacja nigdy nie jest dostępna spoza hosta — to równie celowe, jak sam routing.

Ile kosztuje reverse proxy

Dodaje dodatkowy skok sieciowy i jeszcze jedno miejsce, w którym żądanie może się wywalić. Źle skonfigurowany proxy_pass albo zgubiony nagłówek wygląda jak błąd w aplikacji, chociaż usterka siedzi warstwę wyżej. To także domyślnie pojedynczy punkt awarii: zabij ten jeden proces Nginx, a każdy backend za nim staje się nieosiągalny. Środowiska produkcyjne albo uruchamiają go nadmiarowo, albo oddają to zadanie czemuś, co z góry zakłada awarię — cloudowemu load balancerowi albo kontrolerowi Ingress w Kubernetesie opartemu na stojących pod nim Deploymentach i Podach.

Reverse proxy sam z siebie nie sprawia też, że aplikacja jest szybsza czy lepiej się skaluje. Potrafi cache’ować i kompresować, ale wolny backend zostaje wolny; proxy tylko przekazuje tę powolność o jeden skok dalej. I nie jest to ani uwierzytelnianie, ani walidacja wejścia. Blokowanie jawnie zniekształconych żądań na brzegu to bonus, nigdy powód, dla którego aplikacja miałaby przestać sprawdzać to, co dostaje.

Wdrażanie tej konfiguracji na produkcję

Weź compose.yaml z powyższego przykładu, zamień app na prawdziwą usługę i dodaj ssl_certificate oraz ssl_certificate_key do bloku server w Nginx, gdy już masz certyfikat. To jest różnica między lokalnym ćwiczeniem a czymś, co postawisz przed prawdziwym ruchem. Jeśli kierujesz ruch do więcej niż jednego backendu, zamień pojedynczy adres na blok upstream z kilkoma liniami server i skieruj proxy_pass na ten blok: ta sama konfiguracja staje się wtedy load balancerem.

Powiązane artykuły