Запускаем HAProxy Kubernetes Ingress Controller вне кластера для уменьшения сетевой задержки и числа хопов

Содержание:

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

Обычно вы можете запустить HAProxy Kubernetes Ingress Controller как под внутри Kubernetes-кластера. Как под, он имеет доступ к другим подам, потому что они используют внутреннюю сеть Kubernetes-кластера. Это дает возможность управлять маршрутизацией и балансировать трафик к приложениям, запущенным в кластере. Но возникает проблема, как передать внешний трафик во внутренний Ingress Controller.

Как и другие поды, Ingress Controller существует внутри изолированной среды Kubernetes, поэтому клиенты не получат к нему доступ, пока его не пробросят с помощью Service. Когда вы создаете сервис типа NodePort или LoadBalancer, Kubernetes открывает доступ к поду из внешнего мира.

В облаках более распространен вариант LoadBalancer, потому что он говорит облачному провайдеру развернуть один из его облачных балансировщиков нагрузки (например, AWS Network Load Balancer) и поместить его перед Ingress Controller. В случае self hosted Kubernetes обычно используют NodePort и затем вручную устанавливают балансировщик нагрузки. Так что практически в каждом случае балансировщик нагрузки размещается перед вашим Ingress Controller, что означает наличие двойного проксирования, через которые должен пройти трафик для достижения приложений.

В обычной схеме внешний балансировщик нагрузки (Load Balancer) посылает трафик на один из воркеров, а затем Kubernetes передает его на узел, на котором запущен под с Ingress Controller:

Начиная с версии 1.5 HAProxy Ingress Controller, у вас есть возможность запустить его снаружи вашего Kubernetes-кластера, что избавляет от необходимости дополнительного балансировщика перед Ingress Controller. Это дает доступ к физической сети и позволяет достучаться до кластера внешним клиентам. Проблема в том, что теперь Ingress Controller не запущен как под и находится вне кластера. Поэтому ему нужен доступ к внутренней сети кластера каким-то другим способом.

Для решения этой проблемы мы установим Calico в качестве сетевого плагина в Kubernetes и настроим маршрутизацию с помощью протокола BGP. В продакшне BGP будет работать на третьем уровне сети, но для демонстрации этого мы используем в качестве роутера демона BIRD, установленного на той же VM, что Ingress Controller.

Диаграмма поясняет схему, где Ingress Controller находится снаружи Kubernetes-кластера и использует BIRD для взаимодействия с сетью кластера:

Для самостоятельного повторения примеров загрузите демо-проект из GitHub. В нем используются VirtualBox и Vagrant для создания тестового окружения с 4 виртуальными машинами. Одна VM запускает Ingress Controller и BIRD, а 3 остальные формируют Kubernetes-кластер. Вызовите vagrant up из директории проекта для создания виртуальных машин.

Мы пошагово разберем, как запустить внешний HAProxy Kubernetes Ingress Controller и как установить Kubernetes-кластер с Calico.

Kubernetes-кластер

В демо-проекте используются Vagrant и VirtualBox для создания виртуальных машин. 3 из них составляют Kubernetes-кластер:

  • один мастер-узел, который управляет кластером 

  • два рабочих узла для запуска подов

Vagrant автоматизирует большую часть установки через запуск Bash-скрипта, который запускает необходимые команды для установки Kubernetes и Calico. Если вы задаетесь вопросом, зачем нам понадобилось устанавливать Calico, напомню, что Kubneretes — модульный фреймворк. Некоторые его компоненты можно заменить другими при условии, что они будут реализовывать нужный интерфейс. Сетевые плагины должны реализовывать CNI. Среди популярных вариантов Calico, Flannel, Cilium и Weave Net. BGP-пиринг в Calico делает его хорошим вариантом конкретно в нашем случае.

Чтобы посмотреть, как настраивается мастер-узел, посмотрите Bash-скрипт проекта setup_kubernetes_control_plane.sh. Поверхностно этот процесс делится на такие этапы:

  1. Установка Docker

  2. Установка утилит kubeadm и kubectl

  3. Вызов kubeadm init для для начальной установки Kubernetes-кластера 

  4. Копирование файл kubeconfig в домашнюю директорию пользователя, чтобы при подключении по SSH к этой виртуальной машине этот пользователь мог запускать kubectl-команды

  5. Установка Calico в качестве сетевого плагина с включенным BGP

  6. Установка утилиты calicoctl

  7. Создание ConfigMap-объекта в Kubernetes haproxy-kubernetes-ingress, который требуется для Ingress Controller

Два рабочих узла инициализируются с помощью Bash-скрипта setup_kubernetes_worker.sh. Он выполняет следующие шаги:

  1. Установка Docker

  2. Установка kubeadm и kubectl

  3. Вызов kubeadm join для присоединения к кластеру в качестве воркера

Особенно интересна та часть, в которой мы установим Calico на мастер. Скрипт сначала устанавливает оператор Calico, а затем создает Kubernetes-объект Installation, который включает BGP и назначает диапазон IP-адресов для сети:

apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
 name: default
spec:
 # Configures Calico networking.
 calicoNetwork:
   bgp: Enabled
    
   # Note: The ipPools section cannot be modified post-install.
   ipPools:
   - blockSize: 26
     cidr: 172.16.0.0/16
     encapsulation: IPIP
     natOutgoing: Enabled
     nodeSelector: all()

После этого скрипт устанавливает calicoctl и использует его для создания BGPConfiguration и объекта BGPPeer в Kubernetes.

apiVersion: projectcalico.org/v3
kind: BGPConfiguration
metadata:
 name: default
spec:
 logSeverityScreen: Info
 nodeToNodeMeshEnabled: true
 asNumber: 65000

---
apiVersion: projectcalico.org/v3
kind: BGPPeer
metadata:
 name: my-global-peer
spec:
 peerIP: 192.168.50.21
 asNumber: 65000

Объект BGPConfiguration устанавливает уровень логирования для BGP-коннекторов, включает режим full mesh network и назначает номер Autonomous System (AS) для Calico. Объект BGPPeer получает IP-адрес виртуальной машины, где находится Ingress Controller, где будет запущен BIRD, который установит номер AS для BIRD-маршрутизатора. Я выбрал номер AS 65000. Так Calico расшарит сетевые маршруты BIRD, чтобы их мог использовать Ingress Controller.

Ingress Controller и BIRD

В демо-проекте мы устанавливаем HAProxy Kubernetes Ingress Controller и BIRD на одну и ту же виртуальную машину. Эта VM существует вне Kubernetes-кластера, где BIRD получает IP-маршруты от Calico, и Ingress Controller использует их для передачи клиентского трафика на поды.

Vagrant вызывает Bash-скрипт setup_ingress_controller.sh для выполнения шагов:

  1. Устанавливает HAProxy, при этом дизейблит сервис 

  2. Вызывает команду setcap для разрешения HAProxy слушать привилегированные порты 80 и 443

  3. Загружает HAProxy Kubernetes Ingress Controller и копирует его в /usr/local/bin

  4. Настраивает Systemd для запуска Ingress Controller

  5. Копирует в корень домашней директории пользователя файл kubeconfig, который необходим кластеру для отслеживания Ingress-объектов и смены сервисов

  6. Устанавливает BIRD

Ingress Controller должен быть уже запущен, так как он настроен как сервис Systemd. Он выполняет команду:

/usr/local/bin/haproxy-ingress-controller \
  --external \
  --configmap=default/haproxy-kubernetes-ingress \
  --program=/usr/sbin/haproxy \
  --disable-ipv6 \
  --ipv4-bind-address=0.0.0.0 \
  --http-bind-port=80

Аргумент --external позволяет Ingress Controller запуститься вне Kubernetes. Также он может общаться с кластером и конфигурировать HAProxy, потому что у него есть файл kubeconfig в /root/.kube/config. Однако запросы будут падать, пока мы не сделаем сеть 172.16.0.0/16 доступной.

Calico может взаимодействовать с BIRD, так что она может посылать информацию о внутренней сети. BIRD заполняет таблицу маршрутизации на сервере, где он запущен, что делает IP-адреса подов доступными для Ingress Controller. Перед тем как показать, как настроить BIRD для получения маршрутов от Calico, будет полезно увидеть, как назначаются маршруты со стороны Kubernetes.

Calico и BGP-обмен

Чтобы дать вам представление о том, как это работает, давайте познакомимся с демонстрационной средой. Виртуальные машины получили следующие IP-адреса в сети 192.168.50.0/24:

  • узел Ingress Controller = 192.168.50.21

  • мастер-узел = 192.168.50.22

  • worker 1 = 192.168.50.23

  • worker 2 = 192.168.50.24

Сначала подключимся по SSH к мастер-узлу Kubernetes, используя команду vagrant ssh:

$ vagrant ssh controlplane

Вызовем kubectl get nodes, чтобы увидеть, что все узлы подняты и готовы:

$ kubectl get nodes

NAME           STATUS   ROLES                  AGE     VERSION
controlplane   Ready    control-plane,master   4h7m    v1.21.1
worker1        Ready    <none>                 4h2m    v1.21.1
worker2        Ready    <none>                 3h58m   v1.21.1

Вызовем calicoctl node status для проверки, какая из VM делится маршрутом через BGP:

$ sudo calicoctl node status

Calico process is running.

IPv4 BGP status
+---------------+-------------------+-------+----------+--------------------------------+
| PEER ADDRESS  |     PEER TYPE     | STATE |  SINCE   |              INFO              |
+---------------+-------------------+-------+----------+--------------------------------+
| 192.168.50.21 | global            | start | 00:10:03 | Active Socket: Connection      |
|               |                   |       |          | refused                        |
| 192.168.50.23 | node-to-node mesh | up    | 00:16:11 | Established                    |
| 192.168.50.24 | node-to-node mesh | up    | 00:20:08 | Established                    |
+---------------+-------------------+-------+----------+--------------------------------+

Последние две строки с адресами 192.168.50.23 и 192.168.50.24 показывают, что рабочие узлы установили BGP-подключение и совместно используют маршруты. Однако на первой строке с адресом 192.168.50.21 мы видим, что подключение виртуальной машины Ingress Controller не выполнено, потому что мы пока не настроили BIRD.

Calico назначила каждому из воркеров Kubernetes-кластера IP адреса из подмножества более крупной сети 172.16.0.0/16, которая работает поверх сети 192.168.50.0/24. Вы можете вызвать kubectl describe blockaffinities, чтобы увидеть назначенные сетевые диапазоны.

$ kubectl describe blockaffinities | grep -E "Name:|Cidr:"

Name: controlplane-172-16-49-64-26
 Cidr: 172.16.49.64/26
Name: worker1-172-16-171-64-26
 Cidr: 172.16.171.64/26
Name: worker2-172-16-189-64-26
 Cidr: 172.16.189.64/26

Здесь мы видим, что worker 1 получил диапазон 172.16.171.64/26, а worker 2 — 172.16.171.64/26. Нам нужно отправить эту информацию в BIRD, чтобы: 

  • запрос клиента ушел в worker 1, если включает IP в диапазоне 172.16.171.64/26

  • запрос клиента ушел в worker 2, если включает IP в диапазоне 172.16.189.64/26

Мастер Kubernetes запускает поды на один из этих узлов.

Чтобы настроить конфигурацию BIRD, подключитесь по SSH к той виртуальной машине, на которой находится Ingress Controller.

$ vagrant ssh ingress

Откройте файл /etc/bird/bird.conf и добавьте protocol bgp для каждого воркера. Обратите внимание, что секция import filter говорит BIRD, что ему следует принимать только те IP-диапазоны, которые назначены этому узлу, а все остальные блокировать:

router id 192.168.50.21;

log syslog all;

# мастер-узел
protocol bgp {
   local 192.168.50.21 as 65000;
   neighbor 192.168.50.22 as 65000;
   direct;
   import filter {
     if ( net ~ [ 172.16.0.0/16{26,26} ] ) then accept;
   };
   export none;
}

# worker1
protocol bgp {
   local 192.168.50.21 as 65000;
   neighbor 192.168.50.23 as 65000;
   direct;
   import filter {
     if ( net ~ [ 172.16.0.0/16{26,26} ] ) then accept;
   };
   export none;
}

# worker2
protocol bgp {
   local 192.168.50.21 as 65000;
   neighbor 192.168.50.24 as 65000;
   direct;
   import filter {
     if ( net ~ [ 172.16.0.0/16{26,26} ] ) then accept;
   };
   export none;
}

protocol kernel {
   scan time 60;
   #import none;
   export all;   # insert routes into the kernel routing table
}

protocol device {
   scan time 60;
}

Перезапустите BIRD, чтобы принять изменения:

$ sudo systemctl restart bird

Давайте убедимся, что все работает. Вызовите birdc show protocols чтобы увидеть со стороны BIRD, что BGP-обмен установлен:

$ sudo birdc show protocols

BIRD 1.6.8 ready.
name     proto    table    state  since       info
bgp1     BGP      master   up     23:18:17    Established   
bgp2     BGP      master   up     23:18:17    Established   
bgp3     BGP      master   up     23:18:59    Established   
kernel1  Kernel   master   up     23:18:15    
device1  Device   master   up     23:18:15 

Вы также можете вызвать birdc show route protocol для проверки соответствия сетей IP-адресам виртуальных машин и сети подов:

$ sudo birdc show route protocol bgp2

BIRD 1.6.8 ready.
172.16.171.64/26   via 192.168.50.23 on enp0s8 [bgp2 23:18:18] * (100) [i]

$ sudo birdc show route protocol bgp3

BIRD 1.6.8 ready.
172.16.189.64/26   via 192.168.50.24 on enp0s8 [bgp3 23:19:00] * (100) [i]

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

$ route

Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         _gateway        0.0.0.0         UG    100    0        0 enp0s3
10.0.2.0        0.0.0.0         255.255.255.0   U     0      0        0 enp0s3
_gateway        0.0.0.0         255.255.255.255 UH    100    0        0 enp0s3
172.16.49.64    192.168.50.22   255.255.255.192 UG    0      0        0 enp0s8
172.16.171.64   192.168.50.23   255.255.255.192 UG    0      0        0 enp0s8
172.16.189.64   192.168.50.24   255.255.255.192 UG    0      0        0 enp0s8
192.168.50.0    0.0.0.0         255.255.255.0   U     0      0        0 enp0s8

Если вы вернетесь к виртуальной машине мастера и запустите calicoctl node status, вы увидите, что Calico уже зафиксировала установку BGP-обмена с виртуальной машиной Ingress Controller, 192.168.50.21:

$ sudo calicoctl node status

Calico process is running.

IPv4 BGP status
+---------------+-------------------+-------+----------+-------------+
| PEER ADDRESS  |     PEER TYPE     | STATE |  SINCE   |    INFO     |
+---------------+-------------------+-------+----------+-------------+
| 192.168.50.21 | global            | up    | 00:32:13 | Established |
| 192.168.50.23 | node-to-node mesh | up    | 00:16:12 | Established |
| 192.168.50.24 | node-to-node mesh | up    | 00:20:09 | Established |
+---------------+-------------------+-------+----------+-------------+

Добавляем Ingress

Благодаря обмену BGP-маршрутами между Kubernetes-кластером и сервером Ingress Controller мы готовы приступить к работе. Давайте добавим объект Ingress, чтобы убедиться, что все работает. Следующий YAML раскладывает 5 экземпляров приложения и создает Ingress-объект:

apiVersion: apps/v1
kind: Deployment
metadata:
 labels:
   run: app
 name: app
spec:
 replicas: 5
 selector:
   matchLabels:
     run: app
 template:
   metadata:
     labels:
       run: app
   spec:
     containers:
     - name: app
       image: jmalloc/echo-server
       ports:
       - containerPort: 8080

---       
apiVersion: v1
kind: Service
metadata:
 labels:
   run: app
 name: app
spec:
 selector:
   run: app
 ports:
 - name: port-1
   port: 80
   protocol: TCP
   targetPort: 8080

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
 name: test-ingress
 namespace: default
spec:
 rules:
 - host: test.local
   http:
     paths:
     - path: /
       pathType: Prefix
       backend:
         service:
           name: app
           port:
             number: 80

Ingress-объект настраивает Ingress Controller так, чтобы отправлять любой запрос от test.local к приложению, которое вы только что развернули. Вам понадобится обновить файл /etc/hosts на вашем хосте для сопоставления test.local с IP-адресом виртуальной машины Ingress Controller, 192.168.50.21.

Разложите объекты через kubectl:

$ kubectl apply -f app.yaml

Откройте test.local в вашем браузере, и вас встретит приложение, которое просто напечатает подробности вашего HTTP-запроса. Поздравляю! Вы запустили HAProxy Kubernetes Ingress Controller снаружи Kubernetes-кластера! Вам больше не нужно использовать двойное проксирование.

Чтобы увидеть созданный файл harpoxy.cfg, откройте /tmp/haproxy-ingress/etc/haproxy.cfg. Он сгенерировал backend с именем default-app-port-1, который содержит строку server для каждого из запущенных в приложении подов. Конечно, IP-адреса в каждой строке server теперь доступны. Вы можете масштабировать приложение в большую или меньшую сторону, и Ingress Controller автоматически настроит соответствующую конфигурацию.

backend default-app-port-1
  mode http
  balance roundrobin
  option forwardfor
  server SRV_1 172.16.171.67:8080 check weight 128
  server SRV_2 172.16.171.68:8080 check weight 128
  server SRV_3 172.16.189.68:8080 check weight 128
  server SRV_4 172.16.189.69:8080 check weight 128
  server SRV_5 172.16.189.70:8080 check weight 128

Заключение

В этой статье мы разобрали, как запустить HAProxy Kubernetes Ingress Controller снаружи Kubernetes-кластера. Это избавляет от необходимости запуска дополнительного  балансировщика нагрузки. Такой подход позволит снизить время ожидания клиента, поскольку требует меньшего количества сетевых переходов. Для обеспечения высокой доступности вы дополнительно можете настроить Keepalive.

От переводчиков

В эту среду, 1-го декабря в 19:00, мы в KTS проведем открытый вебинар на тему:
«От кода до продакшена: деплоим приложение в Kubernetes с нуля»

Приходите посмотреть и послушать:

→ Почитать про вебинар и зарегистрироваться

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


  1. identw
    29.11.2021 09:00

    В случае self hosted Kubernetes обычно используют NodePort и затем вручную устанавливают балансировщик нагрузки. Так что практически в каждом случае балансировщик нагрузки размещается перед вашим Ingress Controller, что означает наличие двойного проксирования, через которые должен пройти трафик для достижения приложений.

    Вы же можете публиковать pod'ы ingress controller'а с помощью опции hostNetwork: true, получать трафик напрямую без лишних проксирований. Нет никакой необходимости иметь внешний балансировщик или вытаскивать ingress controller из кластера.


    1. igorcoding
      29.11.2021 12:45
      +3

      Все это в целом может работать, но начинаются проблемы, если узлы кластера все в приватной сети и нужно как-то извне пробросить доступ к нему. Необходим какой-либо edge узел, способный принять трафик, но дальше он должен все же прийти на узел кластера (желательно на любой) и попасть на ingress-контроллер. Получаем двойное проксирование, чего можно избежать например с подобным решением как в статье


      1. identw
        29.11.2021 20:16
        +1

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

        Делаете такие узлы воркерами кластера. Шедулите туда pod'ы ingress-controller'а с hostNentwork: true, и никаких двойных проксирований не будет. pod'у будет досупна как сеть внутри кластера, так и сеть хоста (то есть сеть edge узла).

        Эта схема ничем принципиально не отличается от того что вы сделали. Только ingress-controller будет у вас в кластере, и деплоить вы его будете с помощью абстракций k8s. Любой pod k8s может иметь доступ в сеть хоста. На мой взгляд нет никаких причин выносить ingress-controller из куба. Вы сделали тоже самое решение что и с hostNetwork: true, только деплой реализовали сами (вместо того чтобы деплоить с помощью k8s).


  1. DarkHost
    29.11.2021 10:47

    Очень странное решение. Во-первых, как написали выше, hostNetwork прекрасно работает. Во-вторых, лично мое мнение, лучше всего для ingress в self-hosted кластере смотрелся бы hostNetwork+DaemonSet, если нужен внешний доступ. Таким образом мы еще и избавляемся от единой точки отказа в кластере.


    1. igorcoding
      29.11.2021 12:50
      +2

      Как написал выше, что зачастую кластера находятся во внутренней сети и просто так трафик не пропустить в кластер. Хочется еще отметить, что в вашем предложении с DaemonSet+hostnetwork есть проблема - а как в итоге клиенту выбрать на какой узел попасть? Помещать в dns все узлы кластера плохой путь - при добавлении/удалении узла из кластера нужно вносить изменения в dns, а они распространяются не моментально. Поэтому желательно иметь какую-то одну точку входа, желательно отказоустойчивую.
      С подходом внешнего ингресс контроллера можно как избежать лишних проксирований, так и запустить несколько балансировщиков в HA режиме, например с помощью keepalived.


  1. emashev
    29.11.2021 11:08

    MetalLB разве не тоже самое делает?


    1. igorcoding
      29.11.2021 12:55
      +2

      Не совсем, metallb лишь выдаст доступный ip-адрес из пула, узел, которого должен привлекать трафик в кластер. Уже логика на этом узле должна как-то запроксировать трафик к нужным подам.


      1. dnbstd
        29.11.2021 18:09

        У metallb есть bgp режим.

        https://metallb.universe.tf/concepts/bgp/


  1. vainkop
    30.11.2021 00:56

    Автор, наверное, отлично поупражнялся, но ставить такое кому-то точно не стоит, да и себе зачем, если существует как минимум несколько описанных выше в комментариях более "нативных" для куба решений, которыми гораздо удобнее управлять и они более гибкие.