В облаке Амвера микросервисы и базы данных пользователей крутятся в кластере Kuberneres. Для доступа к приложениям обычно достаточно использовать nginx ngress controller который чудесно работает с HTTP(S) трафиком и позволяет получить доступ к сотням сервисов с использованием только одного внешнего IP адреса. Но, что если пользователь хочет получить доступ к развернутой СУБД не только изнутри кластера, но и извне? Конечно, мы могли бы выдавать каждой СУБД свой белый IP и создать ClusterIP, но это привело бы к дополнительным затратам на аренду адресов. В этой статье я бы хотел поделиться элегантным методом проксирования TCP трафика на основе SNI сообщений, который позволяет использовать один белый IP на сотни СУБД.

Постановка задачи

Имеется кластер Kubernetes в котором развернуто некоторое количество СУБД различного типа: Postgres, MongoDB, Redis итд.

Требуется:

  • Предоставить возможность удаленного подключения к этим базам данных.

  • Включение/отключение внешнего доступа для конкретной СУБД не должно затрагивать другие работающие соединения.

  • Количество выделенных белых IP адресов существенно меньше количества СУБД.

Рассмотренные варианты решения

  • Nginx

    Сам по себе nginx по умолчанию работает только с HTTP(S) трафиком, соответсвенно просто развернуть nginx ingress controller у Вас не получится, так как СУБД используют свои протоколы прикладного уровня и нам придется работать на транспортном уровне с TCP пакетами. В отличие от протоколов более верхнего уровня тут мы не знаем ничего про хост, к которому хотим подключиться и проксирование на основе доменного имени в классическом смысле сделать не выйдет.

    Nginx поддерживает проксирование TCP пакетов, но настраивается это дело на уровне общей конфигурации. Т.е. если нужно будет добавить внешний доступ к новой СУБД придется вносить изменние в ConfigMap, что приведет к перезагрузке контроллера и разрыву установленных TCP соединений. Более того, придется каждому приложению выдавать свой собственный уникальный порт, что может повлечь за собой переконфигурацию firewall.

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: udp-services
      namespace: ingress-nginx
    data:
      8001: "my-db/postgres-1:8001"
      8002: "my-db/postgres-2:8002"
  • Проксирование на основе SNI с определением имени хоста

    Server Name Indication (SNI) — это расшерение протокола TLS, который позволяет клиенту по время установки защищенного соединения явно передать имя хоста, с которым он хочет соединиться. Т.е. если мы используем защищенное соединение, то благодаря SNI мы можем вписать домен, к которому хотим достучаться. К слову, большая часть современных СУБД клиентов поддерживает установку соединения по SSL.

Kong Proxy

Спойлер: Данный метод не подошел для работы с PostgreSQL.

Kong умеет работать с TCP трафиком двумя способами:

  • На основании порта - весь трафик, который пришел на данный порт будет перенаправлен на указанный Kubernetes Service. TCP соединения при этом будут распределяться на все запущенные поды.

  • Используя SNI - Kong принимает шифрованный TLS поток и перенаправляет трафик в Kubernetes Service на основании SNI сообщения, которое было получено во время TLS рукопожатия. Стоит отметить, что за установкой защищенного соединения занимается именно Kong и далее в приложение уходит уже дешифрованный TCP поток.

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

Отметим, однако, что получить доступ к СУБД используя только PGAdmin со включенным SSL у нас не получилось. Если сначала настроить защищенный тунель через stunel, а потом через него выполнить подключение, то PGAdmin успешно его устанавливает. Чтобы лучше разобраться в происходящем, стоит взглянуть на сетевой дамп из Wireshark:

дамп сети из Wireshark
дамп сети из Wireshark

Из него видно, что когда идет подключение к обычному приложению из примера, то на 3-м шаге явно видно ClientHello сообщение, в то время как при подключении к СУБД (нижние строчки) такого сообщения в явном виде нет. Изучив форумы, стало понятно, что это особенности установления защищенного соединения PostgreSQL, а именно проблема кроется в STARTTLS.

Traefik

Наконец-то мы подобрались к тому решению, которое в итоге и было взято на вооружение.

Traefik в отличие от Kong справился с работой с СУБД через SNI без использования stunel. Для установки к кластер использовался Helm чарт со следующими значениями:

ingressClass:
      enabled: true
      isDefaultClass: false
    additionalArguments:
      - "--entryPoints.postgres.address=:5432/tcp" 
    providers:
      kubernetesCRD:
        enabled: true
        allowCrossNamespace: true
    listeners:
      postgres:
        port: 5432
        protocol: HTTP
    ports:
      postgres:
        port: 5432
        expose:
          default: true
        exposedPort: 5432
        protocol: TCP

А чтобы создать доступ для конкретной СУБД Postgres, развернутой через оператор CNPG достаточно применить следующий TCPIngress:

apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
  name: my-postgress-ingress
  namespace: dbs
spec:
  entryPoints:
  - postgres
  routes:
  - match: HostSNI(`my-postgres.mydomain.ru`)
    services:
    - name: my-postgress-service-rw
      port: 5432
  tls:
    secretName: tlsSecretName

Стоит отметить, что в entryPoints указано то название, которое прописано в listeners у чарта. Так, если нам нужно будет добавить mongoDB на другом порту, то мы пропишем его в listeners и ports в чарте и в entryPoints укажем mongoDB.

В серете tlsSecretName хранится обычный самоподписанный сертификат, выданный на указанный домен. Который можно получить через команду:

openssl req -new -x509 -keyout cert-key.pem -out cert.pem -days 365 -nodes

Добавив его в Kubernetes через kubectl:

kubectl create secret tls tlsSecretName --cert=./cert.pem --key=./cert-key.pem

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


  1. onegreyonewhite
    24.09.2024 20:03

    А можно просто Service и Metallb (если не облачный кубер). А если облачный, то скорее всего просто Service.


    1. maxcoolmakarov Автор
      24.09.2024 20:03

      Service Вам обеспечит доступ внутри кластера, вместе с Metallb Вы сможете конечно выдать IP из какого-нибудь локального пула адресов, но мы решали задачку, когда доступ нужно получить из глобальной сети Интернет к нескольким СУБД по одному и тому-же белому IP адресу.


      1. onegreyonewhite
        24.09.2024 20:03

        Service Вам обеспечит доступ внутри кластера

        Вам бы матчасть подтянуть...

        вместе с Metallb Вы сможете конечно выдать IP из какого-нибудь локального пула адресов

        А можно не из локального, BGP'шного. Так, кстати, внешний адрес трушно балансируется по нодам.

        мы решали задачку, когда доступ нужно получить из глобальной сети Интернет к нескольким СУБД по одному и тому-же белому IP адресу.

        Если прям нужно один адрес, то можно использовать аннотацию:

        metallb.universe.tf/allow-shared-ip: "true"

        С другой стороны, вы же как-то уже на прокси разные балансите трафик. Тут либо nodeport, либо Klipper, либо что-то вроде metallb. Ну либо calico в одной сети с NAT-маршрутизатором.

        Просто для такой простой задачи разные прокси просто избыточны и часто добавляют только задержку. Если уж брать, то лучше haproxy хотя бы.


        1. maxcoolmakarov Автор
          24.09.2024 20:03

          Вы кажется, не до конца разобрались в том, какую задачу мы решали. Нам нужно обеспечить одновременно доступ к нескольким СУБД из внешней сети, имея только один белый IP. Обычный nginx ингресс выбирает куда проксировать трафик на основе хоста в HTTP заголовке, к которому идет обращение. В том варианте, который Вы предлагаете, за счет чего будет производиться выбор, куда проксировать трафик? На основе порта? Не все будут рады, что у них postgres висит на 9090 порту например.


          1. onegreyonewhite
            24.09.2024 20:03
            +1

            Вы не просто один адрес хотите использовать, но и один порт тогда уж.

            Даже с учётом что вы себе сами этим геморрой придумали на ровном месте, возьмите haproxy. Там где traefik захлебнется на 100 соединений в 4 vCPU, haproxy легко прожуёт это всё на одном ядре в десятикратном размере с меньшими задержками.

            Traefik очень удобный, когда у вас дофигаллион разных источников для сервисов (не самих сервисов), например, кубер+docker swarm+какая-то кастомная БД+ещё какой-то HTTP-сервис. И вам нужно централизовано управлять не только подключениями, но и сертификатами, middleware и т.п.

            В вашем примере с таким же успехом можно было Istio использовать. Но зачем?

            Поэтому с учётом придуманной проблемы с портами (безопасники, будь у вас наоборот всё на разных нестандартных портах, обрадовались бы), хотя бы посмотрите в сторону более адекватных инструментов для маршрутизации типа haproxy.

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