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.
| |
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 proxy | Reverse proxy | |
|---|---|---|
| Działa w imieniu | Klienta | Serwera |
| Klient wie, że istnieje | Zwykle tak (skonfigurowany jawnie) | Nie (wygląda jak prawdziwy serwer) |
| Co ukrywa | Tożsamość klienta przed serwerem | Tożsamość serwera przed klientem |
| Typowe zastosowanie | Filtrowanie ruchu wychodzącego w firmie, omijanie geoblokad, anonimowe przeglądanie | Routing, 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 proxy | Load balancer | |
|---|---|---|
| Główne pytanie, na które odpowiada | Co 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 funkcje | Cache, przepisywanie nagłówków, terminowanie TLS, routing po ścieżce | Health 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:
| |
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.
| |
| |
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.