В облаке Амвера микросервисы и базы данных пользователей крутятся в кластере 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:
Из него видно, что когда идет подключение к обычному приложению из примера, то на 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
onegreyonewhite
А можно просто Service и Metallb (если не облачный кубер). А если облачный, то скорее всего просто Service.
maxcoolmakarov Автор
Service Вам обеспечит доступ внутри кластера, вместе с Metallb Вы сможете конечно выдать IP из какого-нибудь локального пула адресов, но мы решали задачку, когда доступ нужно получить из глобальной сети Интернет к нескольким СУБД по одному и тому-же белому IP адресу.
onegreyonewhite
Вам бы матчасть подтянуть...
А можно не из локального, BGP'шного. Так, кстати, внешний адрес трушно балансируется по нодам.
Если прям нужно один адрес, то можно использовать аннотацию:
С другой стороны, вы же как-то уже на прокси разные балансите трафик. Тут либо nodeport, либо Klipper, либо что-то вроде metallb. Ну либо calico в одной сети с NAT-маршрутизатором.
Просто для такой простой задачи разные прокси просто избыточны и часто добавляют только задержку. Если уж брать, то лучше haproxy хотя бы.
maxcoolmakarov Автор
Вы кажется, не до конца разобрались в том, какую задачу мы решали. Нам нужно обеспечить одновременно доступ к нескольким СУБД из внешней сети, имея только один белый IP. Обычный nginx ингресс выбирает куда проксировать трафик на основе хоста в HTTP заголовке, к которому идет обращение. В том варианте, который Вы предлагаете, за счет чего будет производиться выбор, куда проксировать трафик? На основе порта? Не все будут рады, что у них postgres висит на 9090 порту например.
onegreyonewhite
Вы не просто один адрес хотите использовать, но и один порт тогда уж.
Даже с учётом что вы себе сами этим геморрой придумали на ровном месте, возьмите haproxy. Там где traefik захлебнется на 100 соединений в 4 vCPU, haproxy легко прожуёт это всё на одном ядре в десятикратном размере с меньшими задержками.
Traefik очень удобный, когда у вас дофигаллион разных источников для сервисов (не самих сервисов), например, кубер+docker swarm+какая-то кастомная БД+ещё какой-то HTTP-сервис. И вам нужно централизовано управлять не только подключениями, но и сертификатами, middleware и т.п.
В вашем примере с таким же успехом можно было Istio использовать. Но зачем?
Поэтому с учётом придуманной проблемы с портами (безопасники, будь у вас наоборот всё на разных нестандартных портах, обрадовались бы), хотя бы посмотрите в сторону более адекватных инструментов для маршрутизации типа haproxy.
Ну и не забудьте протюнить ядро по сетевому стеку, буферам и файловым дескрипторам, чтоб не удивиться потом. Но рано или поздно всё равно в сеть упрётесь.