Запуск Flannel & kube-proxy отдельно от кластера

от автора

Всем привет! В сегодняшнем материале разберемся, как сделать внутренние сетевые ресурсы кластера k8s доступными напрямую с внешнего хоста. Спойлер: в этом нам поможет запуск Flannel в связке с kube-proxy на этом самом хосте.

Так мы получим доступ к приложениям, запущенным в Kubernetes без использования NodePort, LoadBalancer и Ingress Controller.

Мы в hh.ru уже используем это решение для поднятия окружения в разработке и тестировании, решили поделиться с комьюнити. Поехали!

Зачем оно нужно

В hh.ru персональные тестовые стенды есть у каждого разработчика и тестировщика, кроме того присутствует и выделенный пул для запуска e2e тестов. Сегодня их общее количество насчитывает более 200 хостов.

Каждый стенд представляет собой KVM-виртуальную машину с запущенными в ней Docker контейнерами. Фактически это — микро-production с каждым сервисом в единственном экземпляре. KVM изолирует стенды между собой и позволяет поселить несколько хостов на один железный сервер, а Docker изолирует запущенные приложения в пределах одной виртуальной машины. Основной способ доставки артефактов в production у нас — это docker images.

На Хабре уже давно лежит подробная, пусть и немного устаревшая статья о том, как был устроен наш тестовый стенд, так что за подробностями можно сходить туда. При переходе на модную микросервисную архитектуру количество запущенных сервисов, а значит и docker-контейнеров, на стенде стало быстро расти. Поэтому возникла довольно острая проблема: стенд начал упираться в выделенные ресурсы по памяти и CPU.

Это, как вы понимаете, ведет к нестабильности, OOM, проблемам с автотестами и многочисленным неудобствам в разработке. Кроме того, мы познакомились с проблемой фрагментации ресурсов, когда, например, три виртуалки на сервер не помещаются, а две недостаточно его нагружают.

Тогда для решения этих проблем мы предприняли вот что:

  • Некоторые сервисы сделали общими для всех стендов и вынесли на отдельную машину.

  • Сервисы on-demand стали запускаться только если нужны для конкретной задачи.

  • Внедрили так называемые «upstream-стенды». Это когда какой-то проект, состоящий из группы сервисов, поднимается на “легком” стенде, и использует бэкенд hh.ru. При этом в один upstream могут ходить несколько легких стендов. Аналогичный upstream-стенд может использоваться в том числе для мобильных приложений.

Эти шаги позволили нам относительно безбедно прожить еще какое-то время, но из-за постоянного добавления новых сервисов проблема вернулась. На тот момент, а это было начало славного 2020 года, на стенде нужно было запустить > 100 сервисов. Сегодня их количество выросло до ~160-180.  Мы решили действовать радикально: сделать стенд распределенным, а в качестве инструмента взять Kubernetes. Он показался нам самым подходящим и наиболее интересным, поскольку до этого мы с ним еще никогда не работали. 

Однако после разворачивания тестового кластера появилось пара новых вопросов.

К сожалению, господин Друзь на эти вопросы ответить на сможет
К сожалению, господин Друзь на эти вопросы ответить на сможет

Во-первых, переезд в k8s хотелось сделать максимально плавным и переводить сервисы постепенно. При этом нам была нужна сетевая связь в обе стороны — между стендом и кластером.

Во-вторых, требовалось поднять N (по количеству стендов) инстансов сервиса так, чтобы порт, на котором слушает сервис, был неуникальный, но с каждого стенда была бы возможность ходить в свой инстанс. Например, иметь N rabbitmq на порту 5672, и чтобы они не конфликтовали между собой и были доступны извне.

C учетом этих двух требований базовые решения в виде выставления NodePort или Ingress Controller не особо помогают. Еще одна альтернатива — кластер под каждый стенд. Но  здесь был страх, что 200+ кластеров потребуют дополнительного времени на обслуживание и разбор инцидентов. 

В процессе обсуждения возникла идея: каждый стенд нужно сделать частью внутренней сети кластера. В таком случае, если запущенное приложение убрать за ClusterIP Service, к нему можно обращаться по уникальному имени <myapp>.<namespace>.svc.cluster.local:<port> . Имя может резолвиться в ClusterIP сервиса, либо напрямую в IP пода, если использовать Headless Service.

При этом порт в пределах кластера и даже одной Node будет неуникальным. Собственно, в этом предложении и заключается наша основная задача. А дальше пойдет техническая реализация.

В качестве CNI плагина для тестового кластера был выбран Flannel, потому что, во-первых, это простое и популярное решение, а во-вторых, с чего-то надо было начинать. Flannel запускается на каждой ноде, отвечает за распределение адресов Pod-ов в рамках одной Node и сетевую связь между Pod’ами, расположенными на разных нодах.

По умолчанию Flannel использует сеть 10.244.0.0/16 для кластера и 10.244.X.0/24 для каждой ноды. Мы оставили эту конфигурацию для Node на виртуальных машинах и сделали более широкую маску /21 для Node на железе, чтобы иметь возможность на такую ноду зашедулить более 254 Pod-ов. 

У Flannel есть разные бэкенды, а мы использовали VXLAN. Он упаковывает L2-фреймы в UDP-датаграммы и передает их между Node поверх существующей сети. Еще он обеспечивает маршрутизацию между нодами и использует etcd кластера для хранения состояния.

В нашем случае задача сводится к тому, чтобы поднять Flannel и kube-proxy на тестовом стенде и сказать k8s api серверу, что тестовый стенд — это теперь Node. Эта нода всегда будет в статусе Not Ready, и на ней не смогут подниматься Pod-ы, поэтому мы называем ее “Fake Node”, чтобы отличать от реальных Worker Node. 

Как это сделать

Шаг 1. Запуск Flannel на стенде

Бинарник можно собрать самостоятельно, либо использовать предсозданные c github.  Для запуска нужен минимальный конфиг /etc/net-conf.json

{    "Network": "10.244.0.0/16",  "Backend": {  "Type": "vxlan",  "VNI": 101,  "DirectRouting": false } }

Как  описано выше, 10.244.0.0/16 — это сеть всего кластера, VNI — цифровой идентификатор VXLAN. Поскольку у нас используется несколько кластеров в одной физической сети, у каждого из них уникальный VNI. DirectRotung = false в нашем случае — это борьба с вот чем. Если ноды L2 связаны, то при значении true не будет инкапсуляции в UDP-пакеты, а значит сеть должна работать быстрее. Но мы выбираем надежность.

Также для работы Flannel в составе сети кластера k8s потребуется kubeconfig файл. Так как flannel устанавливается при разворачивании кластера, все необходимые данные — flannel serviceAccount и secret — для конфига уже есть. 

Еще для запуска на стенде сделали systemd юнит:

[Unit] Description=Flannel network agent Documentation=https://github.com/coreos/flannel After=network.target After=network-online.target Wants=network-online.target  [Service] Type=notify Restart=always RestartSec=5 Environment=NODE_NAME=<имя Fake Node (hostname стенда)> ExecStart=/usr/local/bin/flanneld-amd64 \   --kube-subnet-mgr=true \   --kubeconfig-file=<Путь до kubeconfig > \   --v=5 \   --ip-masq=true \ #по желанию   --iface=eth0  [Install] WantedBy=multi-user.target

Шаг 2. Kube-Proxy

Если мы хотим использовать kube-proxy, его тоже придется запустить на стенде. Kube-proxy понадобится, если использовать обычный, а не Headless Service. Запущенный kube-proxy будет создавать на тестовом стенде цепочки правил в iptables или виртуальные сервера в IPVS для доступа к Pod-ам через виртуальный IP Service. Бинарник kube-proxy проще взять готовый, например здесь, но не забудьте подставить свою версию. Также можно запустить его в докер-контейнере, мы используем именно этот способ. Образы можно взять здесь.

Сначала мы использовали обычные Service, но из-за количества запущенных приложений размер таблиц с правилами был таков, что kube-proxy обновлял их за несколько десятков секунд. Это порождало проблемы с доступностью приложений: api server и readiness probe считают, что Pod успешно запущен, но при этом со стенда он еще не доступен, так как kube-proxy в процессе синхронизации. Переход на Headless Services помог решить эту проблему.

Для запуска kube-proxy также требуется kubeconfig и собственный конфиг. Пример конфига:

apiVersion: kubeproxy.config.k8s.io/v1alpha1 bindAddress: 0.0.0.0 clientConnection:   acceptContentTypes: ""   burst: 0   contentType: ""   kubeconfig: /etc/kube-proxy/kubeconfig   qps: 0 clusterCIDR: 10.244.0.0/16 configSyncPeriod: 0s conntrack:   maxPerCore: null   min: null   tcpCloseWaitTimeout: null   tcpEstablishedTimeout: null enableProfiling: false healthzBindAddress: "" hostnameOverride: "" iptables:   masqueradeAll: false   masqueradeBit: null   minSyncPeriod: 0s   syncPeriod: 0s ipvs:   excludeCIDRs: null   minSyncPeriod: 0s   scheduler: ""   strictARP: false   syncPeriod: 0s kind: KubeProxyConfiguration metricsBindAddress: "" mode: "" nodePortAddresses: null oomScoreAdj: null portRange: "" udpIdleTimeout: 0s winkernel:   enableDSR: false   networkName: ""   sourceVip: ""

Конфиг практически без изменений взят из рабочего Pod-а kube-proxy в кластере.

Шаг 3. Инициализация “Fake” Node

Чтобы всё заработало, нужно сказать API-серверу, что наш стенд теперь тоже Worker Node. Для этого необходимо применить манифест примерно такого содержания:

apiVersion: v1 kind: Node metadata: name: <имя ноды/стенда> annotations: flannel.alpha.coreos.com/backend-type: vxlan      flannel.alpha.coreos.com/kube-subnet-manager: "true"      flannel.alpha.coreos.com/public-ip: <публичный IP стенда> spec: podCIDR: 10.244.XXX.0/YY

Если не указать явно podCIDR, по умолчанию для “Fake” Node резервируется блок /24. Чтобы сэкономить адреса для настоящих Worker Node и не выйти за рамки дефолтной сети 10.244.0.0/16, мы делаем для стендов блоки /28, резервируя только 16 адресов на каждый стенд.

Шаг 4. Почти всё

Выставляем coredns наружу кластера. Для этого достаточно запатчить Service coredns, применив такой патч командой kubectl patch (можно и отдельный манифест сделать):

spec:   ports:   - name: dns     nodePort: 32053     port: 53     protocol: UDP     targetPort: 53

Таким образом мы получаем: стенд, который умеет ходить в кластер, и сущности кластера, которые умеют ходить в стенд. Великолепно!

Шаг 5. Финальный

Дополнительно на стенде установлен nginx, работающий как tcp proxy. Он нужен, чтобы спроксировать запрос в кластер. Так для каждого сервиса в кластере в nginx будет прописан примерно следующий конфиг:

server {     listen 0.0.0.0:1234;     proxy_pass app1.<namespace>.svc.cluster.local:1234; }

А в качестве резолвера добавлен coredns из шага 4:

stream {     resolver <внешний IP кластера/балансировщика>:32053; }

Теперь для приложения в кластере на стенде открыт порт, и запрос идет через tcp proxy. Можно отключить прокси и запустить приложение в докере с тем же портом в host network, не меняя конфигурации остальных приложений.

Пример работы: на стенде запущено приложение в dоcker App1, которое ходит в App2 в кластере. В конфиге App1 указано, что-то вроде

external_service = <внешний IP тестового стенда>:1234

Запрос <внешний IP тестового стенда>:1234 попадает в nginx, далее nginx в соответствии с конфигом, резолвит app2.<namespace>.svc.cluster.local:1234 в IP адрес Pod-a с помощью coredns. Поскольку стенд «Fake» Node находится в одной сети с Pod-ами, запрос благополучно доходит до сервиса в кластере. Если перед Pod-ом стоит обычный, а не Headless, Service, то app2.<namespace>.svc.cluster.local сначала резолвится в виртуальный IP Service’a, а затем пакет по правилам iptables/IPVS(за которые отвечает kube-proxy) доходит до Pod’a.

Постепенно наши сервисы переходят на Consul и service discovery, и необходимость в nginx tcp proxy отпадает, так как в Consul можно явно задать IP адрес Pod-a.

Выводы

Это, слегка велосипедное, решение:

  • Достаточно приемлемая платформа для создания распределенного стенда.

  • Позволяет постепенно переводить сервисы в тестовом окружении в кластер k8s.

  • Не требует дополнительных инструментов.

  • Не подойдет для использования в продакшне.

  • Скорее всего не сработает или потребует допиливания, если кластер в облаке.

А вот и немного цифр:

  • В таком виде тестовое окружение работает с 2020 года. Сегодня на стенде запущено ~60 контейнеров и ~100 Pod-ов в кластере.

  • У нас 4 тестовых кластера, разделенные по ролям: 2 для разработчиков и тестировщиков, 1 для стендов релизного пула, и еще 1 тестовый-совсем тестовый, чтобы ничего не сломать и обкатать новые решения.

  • В первых двух кластерах запущено больше 20к подов в сумме, все это работает на более чем 100 Node. Некоторых из них — виртуальные машины KVM, а другие полноценные железки.

На этом, пожалуй, всё. Пишите в комментах про ваш опыт, может вы нашли более изящное решение. Будет здорово, если поделитесь!


ссылка на оригинал статьи https://habr.com/ru/company/hh/blog/680084/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *