Немного воды

Всем читателям, привет! Хочется поделиться своим опытом по созданию Nginx reverse proxy для интересного кейса. Не судите строго но критике и предложениям буду рад.

Начало

Поступил вызов о необходимости реализации следующего кейса:

  • Есть 1 ip и на него нацелено n доменов

  • Есть n серверов (за NAT)

  • Когда пользователь заходит на домен_1 попадает на сервер_1

  • Когда пользователь заходит на домен_2 попадает на сервер_2

  • Когда пользователь заходит на домен_n попадает на сервер_n

Казалось бы, тривиальная задача... Но не с инфраструктурой которая есть

Немного о инфраструктуре

Из за отсутствия необходимости реверсивного прокси схема инфраструктуры выглядела так как на рис.1.

рис.1
рис.1

Естественно, для реализации кейса, необходимо было встроить Nginx в эту схему. Итоговая схема представлена на рис.2

В целом такая конфигурация не предоставляет собой ничего сложного. Однако если istio настроен и присутствуют политики авторизации будут тонкости но об этом дальше.

Настройка Nginx reverse proxy

Немного вводных:

  1. ОС виртуализации Centos7

  2. Iptables настроен на полный запрет иных портов кроме 443

  3. Istio нацелен на доменное имя которое соответствует ВМ

Так как у меня ОС Centos7 конфиги http и server будут совмещены. Но важно понимать что если у вас раздельные конфиги как в Ubuntu это так же будет работать. Главное поместить нужный код в нужные места ;)

Итоговый конфиг для nginx выглядит так:

worker_processes 10;
    events {
    worker_connections 1024;
}
http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    #ВМ_1
    server {
        server_name test1.domain.ru;
        listen       443 ssl;
        listen [::]:443 ssl;
        ssl_certificate      /home/CERT.crt;
        ssl_certificate_key  /home/CERT.key;
        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
        ssl_protocols        TLSV1.1 TLSV1.2 TLSV1.3;
        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;
        location / {
            proxy_pass https://192.168.0.21;
            proxy_http_version  1.1;
            proxy_cache_bypass  $http_upgrade;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-Host  $host;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Port  $server_port;
        }
    #ВМ_2
    server {
        server_name test2.domain.ru;
        listen       443 ssl;
        listen [::]:443 ssl;
        ssl_certificate      /home/CERT.crt;
        ssl_certificate_key  /home/CERT.key;
        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
        ssl_protocols        TLSV1.1 TLSV1.2 TLSV1.3;
        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;
        location / {
            proxy_pass https://192.168.0.21;
            proxy_http_version  1.1;
            proxy_cache_bypass  $http_upgrade;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-Host  $host;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Port  $server_port;
        }
    }
    #ВМ_n
    server {
        server_name testn.domain.ru;
        listen       443 ssl;
        listen [::]:443 ssl;
        ssl_certificate      /home/CERT.crt;
        ssl_certificate_key  /home/CERT.key;
        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
        ssl_protocols        TLSV1.1 TLSV1.2 TLSV1.3;
        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;
        location / {
            proxy_pass https://192.168.0.x;
            proxy_http_version  1.1;
            proxy_cache_bypass  $http_upgrade;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-Host  $host;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Port  $server_port;
        }
    }
}

Теперь давайте немного разберем основные параметры.

Общий блок:

  • worker_processes - Колличество возможных рабочих процессов при подключении к одному IP. Как правило, на ядро запускается 1 рабочий процесс;

  • worker_processes - Колличество пользователей могут одновременно обслуживаться Nginx;

Блок http {

  • keepalive_timeout - Максимальное время поддержания keepalive-соединения, в случае, если пользователь по нему ничего не запрашивает.

Блок http { server {

  • server_name - Ожидаемое доменное имя по которому будет доступен сервер. Например testn.domain.ru

  • listen - Указывает на порт по которому будет доступен сервер. В случае с https/443 необходимо контролировать наличие подписи ssl в строке.

  • ssl_certificate/ssl_certificate_key - доменнные сертификаты

  • ssl_session_cache - Кеширование сессий. Необходимо для повторного использования ключей, чтобы не повторять хэндшейк.

  • ssl_protocols - Определение возможных TLS протоколов для работы с сервером.

Блок http { server { location / {

ВАЖНО
В случае если у вас есть фронт с url testn.domain.ru/front не получится использовать другие location кроме как корневой (/). Связано с передачей url от nginx на сервер во время proxy. istio будет отбивать такие запросы и rewrite не поможет

  • proxy_pass - Сервер на которое осуществляется проксирование домена, указанного в server_name блока server. 

  • proxy_http_version - Позволяет использовать HTTP/1.1 вместо дефолтного параметра HTTP/1.0

  • proxy_cache_bypass - В нашем случае не отдаются данные об изменении http запроса ($http_upgrade)

  • proxy_set_header - Записывает заголовки для передачи на сервер назначения proxy_pass

Подробнее можете почитать в доках nginx.

Должен отметить что по незнанию к такому конфигу я шел достаточно долго. 3 рабочих дня если быть точным.
Я долго не мог заставить дружить istio и nginx и перелопатил кучу информации. И что в итоге? Самый важный параметр всего этого конфига - это proxy_cache_bypass.

В чем же суть этого proxy_cache_bypass?
proxy_cache_bypass
 — Задает условия, при которых nginx не станет брать ответ из кэша, а сразу перенаправит запрос на бэкенд. Если хотя бы один из параметров не пустой и не равен “0”. Подробнее почитайте вот тут

В итоге у нас получается достаточно гибкая конфигурация прокси сервера которую легко аадминистрировать. Основной изменяемый/удаляемый/добавляемый блок - это блок http { server {

    #ВМ_n
    server {
        server_name testn.domain.ru;
        listen       443 ssl;
        listen [::]:443 ssl;
        ssl_certificate      /home/CERT.crt;
        ssl_certificate_key  /home/CERT.key;
        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
        ssl_protocols        TLSV1.1 TLSV1.2 TLSV1.3;
        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;
        location / {
            proxy_pass https://192.168.0.x;
            proxy_http_version  1.1;
            proxy_cache_bypass  $http_upgrade;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-Host  $host;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Port  $server_port;
        }
    }

Итоги

На мой взгляд получилась достаточно гибкая конфигурация отвечающая всем условиям кейса. Надеюсь будет полезно. Спасибо за внимание.

Комментарии (12)


  1. by_awkward
    05.10.2023 12:27

    ssl_certificate /home/CERT.crt;

    ssl_certificate_key /home/CERT.key;

    ssl_session_cache shared:SSL:1m;

    ssl_session_timeout 5m;

    ssl_protocols TLSV1.1 TLSV1.2 TLSV1.3;

    ssl_ciphers HIGH:!aNULL:!MD5;

    ssl_prefer_server_ciphers on;

    и

    proxy_http_version 1.1;

    proxy_cache_bypass $http_upgrade;

    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    proxy_set_header X-Real-IP $remote_addr;

    proxy_set_header X-Forwarded-Host $host;

    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_set_header X-Forwarded-Port $server_port;

    можно вынести в файлы типа ssl.conf и proxy.conf и подключить через include. Значительно улучшится читаемость и снизится вероятность ошибки при переконфигурировании. Настройки конкретных серверов проще вынести в отдельные файлы как это сделано в дебиане/убунте - так проще выключить более ненужный сервер/подключить нужный без ошибок удалив/создав симлинк.


    1. ToomIm Автор
      05.10.2023 12:27

      Да, всё верно. Я как раз и описал в статье что это можно разделить. Как для быстрого решения конечно подойдет и монолитный конфиг, который в дальнейшем можно будет оптимизировать.
      В процессе эксплуатации такой системы я бы даже настоятельно советовал использовать подход с разделенными конфигами. Уже были прецеденты с участием неопытных админов когда были забыты закрывающие знаки.

      "Но важно понимать что если у вас раздельные конфиги как в Ubuntu это так же будет работать. Главное поместить нужный код в нужные места ;)"


  1. Konstantinus
    05.10.2023 12:27

    А Nginx выполняеться непосредственно на сервере виртуализации?
    Подсткажите, если не трудно, как быть в ситуации когда есть веб-сервер с N доменами, и один из доменов надо перенаправить на другой сервер?


    1. by_awkward
      05.10.2023 12:27

      server {
              listen 168.119.???.???:80;
              listen 168.119.???.???:443 ssl;
              server_name clamav.xxx.ru;
              root /var/www/clamav;
              access_log /var/log/nginx/clamav__access.log;
              index index.html;
              location / {
                      # First attempt to serve request as file, then
                      # as directory, then fall back to displaying a 404.
                      try_files $uri $uri/ =404;
              }
      }

      для домена и

      server {
              listen 168.119.???.???:80;
              listen 168.119.???.???:443 ssl;
              server_name ns0.xxx.ru;
              root /var/www/html/ns0;
              access_log /var/log/nginx/ns0__access.log;
              index index.html;
              location / {
                      proxy_set_header Host $host;
                      proxy_set_header X-Real-IP $remote_addr;
                      proxy_set_header X-Scheme https;
                      proxy_set_header X-Forwarded-Proto https;
                      proxy_buffering off;
                      proxy_ssl_verify off;
                      proxy_redirect https://127.0.0.1:443/ /;
                      proxy_pass https://127.0.0.1:443;
                      # First attempt to serve request as file, then
                      # as directory, then fall back to displaying a 404.
                      #try_files $uri $uri/ =404;
              }
      }

      для реверс-прокси. Обе могут быть в одном блоке http{}.


  1. vesper-bot
    05.10.2023 12:27

    Не пойму, зачем nginx перед istio ставить? вроде и то и другое — прокси-серверы в этой схеме, причем istio ещё и маршрутизирует запросы по урлам.


    1. ToomIm Автор
      05.10.2023 12:27

      Объясню почему в этом случает так.

      Мы разрабатываем коробочное решение и в это коробочное решение входит вся инфраструктура (проще говоря мы передаём ВМ). По этому не трогаем "коробочную" инфру

      Решение из статьи - быстрый выход из ситуации когда необходимо держать какое то множество пред-прод серверов в своей инфраструктуре с внешним доступом. Специфично но имеет место быть :)


  1. darkrin
    05.10.2023 12:27
    +1

    Зачем указывать полные имена URL?

    Достаточно server_name *.xxx.ru

    Istio сам разберётся какой запрос куда.

    Единственное, сертификат с *. xxx.ru не распространяется на *. yyy.xxx.ru - только на *. xxx.ru

    Если конечно все сайты в пределах одного домена.


    1. ToomIm Автор
      05.10.2023 12:27

      Про server_name *.xxx.ru в таком ключе не экспериментировал. Если есть статейка на почитать буду благодарен.

      > Istio сам разберётся какой запрос куда.
      Тут не очень понял. Как istio будет разбираться какой запрос куда если 3 инстанса kubernetes имеют не связныt с собой istio


  1. s_korobeiko
    05.10.2023 12:27

    Почему сделан такой акцент на proxy_cache_bypass, когда в статье приведен конфиг без кэшей и даже нет упоминания, что настроено кэширование? Разве nginx по умолчанию что-то кэширует?


    1. ToomIm Автор
      05.10.2023 12:27

      Ответа однозначного в силу отсутствия достаточного опыта по nginx не смогу дать, НО!

      Во время настройки взаимосвязи nginx -> istio я получал ошибки от istio "426 Upgrade Required" . На просторах интернета нашел единственное подходящее объяснение где предлагалось добавлять proxy_cache_bypass $http_upgrade;

      Однако сейчас, для того что бы сделать скрин ошибки и показать вам, я закоммитил эти строки в конфиге и не получил 426 ошибки.


      1. SignFinder
        05.10.2023 12:27
        +1

        Видимо хост работает на вебсокетах, поддержку которых нужно включить в nginx


        1. ToomIm Автор
          05.10.2023 12:27

          Нет, у нас вебсоккетов нет вообще нигде