Nginx в работе DevOps/Администратора. Тёмная сторона силы

  • Tutorial

В работе DevOps/Администраторов зачастую возникают моменты, в которые необходимо куда-то кому-то срочно предоставить доступ. Будь то инстанс докера, один из многочисленных контейнеров или какой-то внутренний сервис.


Все знают о возможностях nginx с точки зрения проксирования трафика, балансировки нагрузки между серверами и прочих полезных вещей, помогающих объединять разрозненные сервисы. Однако задача разрешения проблем возникающих в процессе разработки намного обширнее.
Основной посыл данной статьи — показать нестандартный подход к казалось бы простым вещам, таким как предоставление временного доступа внутрь закрытого сегмента.


Начнём с простого.


У вас есть закрытая зона, внутри которой крутятся различные инстансы баз данных, веб сервисов и прочая прочая. Обычно перед всем этим стоит nginx, который "рулит" трафиком.
Рассмотрим например Galera Cluster для MySQL или MariaDB.


Galera Cluster это система блочной online репликации нескольких инстансов MySQL или MariaDB.
Инструкций по настройке кластера довольно много, и в каждой из них присутствует nginx с модулем stream. Классический вариант конфигурационного файла примерно такой.


stream {
    error_log /var/log/nginx/mariadb-balancer-error.log info;
    upstream db {
        server 172.16.100.21:3306;
        server 172.16.100.22:3306;
        server 172.16.100.23:3306;
        server 172.16.100.24:3306 backup;
        server 172.16.100.25:3306 down;
    }

    server {
        listen 3306;
        proxy_pass db;
        proxy_connect_timeout 1s; # detect failure quickly
    }
}

На хосте где установлен nginx открывается listen socket и входящие соединения в этот сокет обслуживаются встроенным балансировщиком nginx'а. Как мы все понимаем в системах где используется докер или kubernetes, нет жёсткой привязки сервиса к хосту на котором выполняется тот или иной контейнер. Тогда в дело вступает DNS резолвинг и всё это прекрасно работает до тех пор, пока не возникает необходимость подключиться на конкретную ноду и конкретный инстанс MySQL(MariaDB) минуя балансировщик. И в данном случае статичный конфиг является очень узким местом.


К чему я веду.


Рассмотрим вариант, когда у нас внутри контура находятся различные сервисы, но они должны быть доступны по одному и тому же порту открытому на firewall и nginx. Скажем у нас есть три ноды на которых крутятся три независимых инстанса: Prod, Dev и Test со своей связкой ssh, mysql, nginx, apache и т.д.


С точки зрения HTTP/HTTPS проблем нет никаких. Сделал пачку виртуальных серверов, забиндил на каждый нужное доменное имя — запрос пришёл — из запроса определилось имя сервера — соединение пошло в нужный инстанс. Profit!


Сервисы ssh, mysql, postgresql и прочие не умеют в виртуальность и зачастую, чтобы организовать подключение внутрь какого-либо инстанса необходимо устроить танец с бубном по открытию портов на firewall, организацию forwarding'а пакетов, multihome source routing и прочая прочая.
Хотя само решение лежит на поверхности. Forward пакетов дело хорошее, но править в режиме активной разработки руками правила Firewall на ноде, которая обслуживает входящий трафик, не очень удобно.


Большинство механизмов nginx работают в location и не работают в stream.


Однако нашлась небольшая лазейка, которая позволяет оптимизировать процесс динамического подключения к ресурсам с минимальными трудозатратами. Для этого нужен локальный dns резолвер имеющий в качестве database backend базу memcached или redis.


Суть в следующем. При инициировании соединения со стороны клиента к серверу, nginx попытается отрезолвить IP адрес клиента в зоне ssh.local или mysql.local на локальном резолвере 127.0.0.1, слушающем на порту 5353. Ответом dns сервера должен являться адрес сервера внутри контура на который необходимо пробросить соединение. Всю остальную работу nginx сделает сам.


stream {
    error_log /var/log/nginx/ssh-forward-error.log debug;
    resolver 127.0.0.1:5353 valid=10s;
    map $remote_addr $sshtarget {
        default $remote_addr.ssh.local:22;
    }

    map $remote_addr $mysqltarget {
        default $remote_addr.mysql.local:3306;
    }

    server {
        listen 2223;
        proxy_pass $sshtarget;
        proxy_connect_timeout 1s; # detect failure quickly
    }

    server {
        listen 33306;
        proxy_pass $mysqltarget;
        proxy_connect_timeout 1s; # detect failure quickly
    }

}

Что отдавать по умолчанию, если адрес клиента не найден в таблицах соответствия — решать администратору системы. Можно вернуть какой-нибудь несуществующий адрес 127.0.0.2 и т.д.
Хочется обратить Ваше внимание, что подключение к несуществующему адресу будет происходить от имени nginx, поэтому при использовании данной схемы подключения необходимо быть осторожным. На чём написать dns сервер или использовать готовый — каждый решает сам. Можно взять сервер на питоне и парой строк добавить в него запрос в базу данных или взять мой старый проект на перле на котором тестировалась эта конфигурация.


Управление содержимым базы данных можно реализовать на чём угодно. Главное понимать кто куда должен ходить в какой момент времени.


Собственно почему "Тёмная сторона силы"? По сути данная схема позволяет обеспечивать отображение различной информации в различные промежутки времени с различных контуров предоставления этой информации с одной единственной точкой входа. Вы никогда не будете знать какой блок информации из какого контура будет показан в конкретный момент времени.


Установленный keepalive SSL туннель обеспечит непрерывное соединение, а пропавшая, в режиме автоочистки, из dns базы запись не позволит подключиться к вашим ресурсам другому хосту с аналогичным IP.


Скажем закрытый корпоративный портал для тех, кто предварительно авторизовался в системе управления или система администрирования внутренними ресурсами компании, проброс любого порта на внутренний сервер. У меня данная схема используется для временного проброса rdp соединения на конкретный компьютер в сети, согласно авторизации пользователя, без использования RDGateway. Иногда клиентам нужно попасть на определённый контур внутри закрытой зоны, и данная схема временного доступа прекрасно работает.


Я не призываю никого использовать данную схему на постоянной основе, т.к. она всё-таки недостаточно безопасна, но в критические моменты у вас будет возможность повлиять на ситуацию внутри контура находящегося за nginx.


© Aborche 2017
Aborche

  • +16
  • 10,8k
  • 8
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 8
  • 0

    а nginx-proxy не рассматривали?

    • 0
      а чем оно поможет в случае stream?
      я так понимаю ТС и service-discovery умеет, но в данном случае это не поможет.
      Хотя опять же, с каким-нить consul etcd можно тупо клиенту отдавать dns имя сервиса, аля postgre-for-vasya.service, но только через через впн какой-нить
      • 0

        через него можно генерить сразу готовую nginx-конфигурацию с IP адресами под сервисы и тогда локальный dns-resolver не понадобится. пример — можете расширить конфигурацию и генерить наборы location для ваших сервисов. Причём конфигурация динамически меняется под количество доступных контейнеров. Тут больше документации

        • 0
          повторюсь, но по-другому, переставив слова, stream не умеет виртуальные хосты, в публикации это написано, оно только tcp или udp на интерфейсе.
          Как вы предполагаете подключать пользователя к RDP/mysql/ssh/redis/e.t.c. сервису, если фронт, утрируя ситуацию, имеет только один ip в настройке server у nginx, и вы не можете в зависимости от имени сервера перекинуть на другой сервис?
    • 0
      и в каждой из них присутствует nginx с модулем
      Про https://mariadb.com/products/technology/maxscale слышали?
      • 0
        > На чём написать dns сервер или использовать готовый — каждый решает сам. Можно взять сервер на питоне и…
        Может чего не понял. А просто hosts файл не пойдет, не?
        • 0
          expire в hosts bash скриптами? :)
          • +2
            Можно dnsmasq'у скормить свой файл hosts, а демон запускать с параметром --min-cache-ttl=0, тогда у всех запросов TTL будет 0, т.е. с отключённым кэшированием.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.