Inheaden недавно помогла одному из своих клиентов подключить IoT-устройства к облаку с помощью Kubernetes Kosmos. Кристиан Хайн, директор по информационным технологиям в Inheaden, расскажет нам, как им это удалось, какие проблемы им пришлось преодолеть и какие обходные пути им пришлось предпринять, чтобы добраться туда, где они должны были быть. Но мы позволим Крису рассказать вам о деталях.
Наш клиент в сфере Интернета вещей (IoT) производит устройства IoT, которые отправляют данные, такие как положение GPS, состояние батареи, данные датчиков и т. д. Связь с устройствами IoT осуществляется через UDP, а проприетарный микросервис затем собирает эти UDP. пакеты и декодирует их.
Требование было довольно простым: серверная часть должна иметь возможность отправлять пакет на одно устройство IoT, которое включает или выключает функцию. Хотя это звучит довольно просто, это не сразу стало возможным из-за специфического способа настройки инфраструктуры. Нам нужен был реальный IP-адрес IoT-устройства для управления функцией при прямом соединении, но по разным причинам, о которых мы поговорим ниже, IP-адрес не сохранился при передаче.
В конце концов мы нашли решение, используя Kubernetes Kosmos и Envoy от Scaleway. Но давайте немного вернемся назад, чтобы увидеть, с чего мы начали и как мы туда попали.
Подключение устройств IoT с мобильным соединением и ограниченной пропускной способностью
Мобильное соединение устройств IoT устанавливается с помощью радиотехнологии LPWAN. Этот стандарт радиосвязи, разработанный для межмашинной связи (M2M), является энергоэффективным, может проникать в здания и надежно передавать данные. Недостатком этой технологии является то, что у нас ограниченная пропускная способность.
Из-за этого перегруженные протоколы, такие как TCP, — не лучший способ отправки данных с устройства на серверную часть. Поскольку HTTP/1 и HTTP/2 основаны на TCP, использование HTTPS для соединения также нежелательно.
Так как же эти устройства безопасно передают данные?
В области стандарта радиосвязи LPWAN имя частной точки доступа (частное APN) от оператора мобильной сети (MNO) используется для передачи данных с устройств IoT в сеть компании с использованием устаревшего VPN-подключения. Это устаревшее VPN-подключение заканчивается на виртуальной машине (ВМ). Каждое устройство получает частный IP-адрес из CIDR сети частного класса, например, 10.200.0.0/16.
Смена облачного провайдера
У предыдущего провайдера нашего клиента инфраструктура была настроена таким образом, что устаревший туннель VPN был напрямую подключен к соответствующим серверам для рабочей среды и среды предварительного просмотра.
После перехода на Scaleway несколько лет назад мы настроили его таким образом, чтобы пакеты UDP от устройств IoT поступали на рабочий сервер через туннель Wireguard VPN. На рабочей и промежуточной ВМ у нас был статический внутренний IP-адрес, на который устройства IoT отправляли свои данные.
Затем входящие данные на производственном и промежуточном серверах передавались в кластер Kubernetes.
Наша задача: сохранить IP-адрес IoT-устройства
Проблема, связанная со старой инфраструктурой, заключалась в том, что клиентский IP-адрес IoT-устройства (например, 10.200.21.22) не сохранялся — мы не могли видеть реальный IP-адрес устройства, потому что у нас было несколько уровней NAT между ними.
Даже когда мы пытались полностью маршрутизировать пакеты, у нас все еще был уровень NAT на последнем этапе, где UDP-пакеты поступали в кластер Kubernetes через nodePort. Здесь IP-адрес источника был изменен на внутренний IP-адрес узла Kubernetes (подробнее о работе сети Kubernetes).
Почти два года не было необходимости видеть IP-адрес IoT-устройства, так как UDP-пакеты приходили на коннектор декодирования и передавали информацию об IMEI и IMSI устройства, которые затем можно было присвоить в бэкенде.
Так было до тех пор, пока наш клиент не захотел, чтобы серверная часть могла отправлять пакет на устройство IoT для включения или выключения функции. Для этого нам нужен был реальный IP-адрес IoT-устройства. И это не сработает с промежуточными слоями NAT. Мы смогли ответить на пакеты с UDP-устройства, но не более того.
Как бы мы решили эту проблему?
Прокси-протокол в помощь?
После некоторых исследований мы обнаружили, что протокол прокси ( HA Proxy — Proxy Protocol ) может помочь нам сохранить IP-адрес клиента. Многие реализации прокси-протокола имеют дело с обычными HTTP-соединениями. Но, как было сказано выше, HTTP у нас не работает. Нам нужна была реализация, которая работает с UDP.
И там возможные решения снова начали таять — мы смогли найти только несколько возможных подходов, которые могли бы соответствовать нашим потребностям.
Подход udppp/mmproxy
mmproxy — это инструмент, разработанный Cloudflare для сохранения IP-адресов клиентов в среде UDP. В дополнение к mmproxy мы также используем небольшой инструмент под названием udppp. udppp работает на локальном порту, на который отправляются исходные пакеты UDP.
udppp добавляет к пакету заголовок Proxy Protocol и отправляет его на IP-адрес, указанный вами в командной строке. Затем mmproxy берет этот пакет, удаляет заголовок Proxy Protocol и создает новый пакет с IP_TRANSPARENTопцией волшебного сокета. Подробнее о режиме IP Transparent читайте в этом блоге от Cloudflare.
С помощью записи в блоге Энди Смита о сохранении клиентских IP-адресов в Kubernetes мы реализовали sidecar-контейнер на микросервисе декодирования. После некоторых тестов мы увидели, что этот подход работает — IP-адрес клиента сохраняется.
Хотя подход был успешным, на некоторые вопросы все еще не были даны ответы:
- Как пакеты возвращаются на устройство IoT без прохождения уровня NAT?
- Насколько масштабируемо это решение?
Чтобы решить первую проблему, мы попробовали несколько хаков iptables для отправки пакетов обратно, но в конечном итоге этот подход не удался — обратный маршрут был невозможен.
Подход Wireguard Pod
Как упоминалось ранее, мы использовали Wireguard для подключения старых серверов к серверу шлюза. Поэтому мы подумали: «Почему бы нам не попробовать разместить соединение в кластере?»
С этой идеей мы создали Wireguard Pod, который установил безопасное VPN-соединение с нашим сервером-шлюзом. После некоторых взломов iptables и нескольких ошибок мы обнаружили, что этот подход также не работает, потому что сеть Wireguard не известна узлу Kubernetes напрямую. В этом случае внутренняя маршрутизация кластера невозможна. Маршрутизация на основе политик тоже не работала. Обратного пути тоже не было.
Нам было трудно понять, что эти два подхода вообще не работают. Поэтому я решил сделать шаг назад и покопаться в голове, чтобы найти подход. Затем я понял, что читал кое-что о пирах в документации для Kubernetes CNI Kilo, которая лежит в основе настоящего многооблачного предложения Scaleway, Kubernetes Kosmos.
Kubernetes Kosmos и Envoy спешат на помощь!
Прочитав некоторую документацию о пирах Kilo, я опробовал подход с ресурсом пиров Kilo:
apiVersion: kilo.squat.ai/v1alpha1
kind: Peer
metadata:
name: gw-peer
spec:
allowedIPs:
- 10.4.0.99/32 # Single IP for the peer
- 192.168.0.0/24 # Device testing subnet
publicKey: <PublicKey of the Peer>
persistentKeepalive: 10
С помощью инструмента командной строки kgctl мы получили конфигурацию Wireguard для шлюза. После добавления этой конфигурации настал волнующий момент, и мы протестировали наш подход на поде. Мы задавались вопросом, сохранились ли у нас как клиентские IP-адреса, так и двунаправленное соединение с тестовой установкой. К счастью, ответ на оба вопроса был «да» — это сработало! Мы приступили к подключению тестовой установки к нашему кластеру Kosmos.
Маленький шаг назад
Для распределения трафика IoT-устройств нам нужна была возможность балансировки нагрузки входящих UDP-пакетов. Самым простым решением было использование IP сервиса Kubernetes.
Мы попробовали этот подход, но потом увидели внутренний Kilo IP узла. Мы обнаружили, что входящие пакеты на исходный IP-адрес Kubernetes получают исходный NAT (SNAT). Мы создали issue в репозитории kilo на Github, и сопровождающий проекта подтвердил, что Kubernetes будет SNAT-пакеты в текущей реализации.
К сожалению, эта проблема нарушает реализацию, чтобы сохранить исходный IP-адрес устройства IoT. А без исходного IP-адреса мы все равно не сможем обратиться к IoT-устройству.
Последняя часть головоломки: Посланник
Для решения задач, с которыми мы столкнулись, необходимо было сработать следующие моменты:
- Нам нужна балансировка нагрузки UDP-пакетов на наши модули Kubernetes.
- Поскольку IP-адрес модуля меняется каждый раз, системе необходимо обновлять существующие новые IP-адреса модуля.
- Исходный IP-адрес IoT-устройства должен быть сохранен.
После некоторого исследования подходящих прокси, которые могли бы решить нашу проблему, мы наткнулись на Envoy. После некоторых попыток мы разработали конфиг, который поддерживал все указанные пункты:
- Envoy поддерживает балансировку нагрузки с пакетами UDP.
- Envoy может разрешать измененные IP-адреса подов с помощью dns_resolversопции — понадобилось только создание безголового сервиса в Kubernetes.
- С помощью опции use_original_src_ip: trueмы смогли сохранить исходный IP-адрес устройства IoT.
- После того, как все критерии были выполнены, мы настроили производственную среду, в которой сервер с VPN-туннелем был подключен к кластеру в качестве узла Kilo. После тщательного тестирования все заработало, как и ожидалось.
Как только мы узнали, как заставить его работать с одним устройством, мы подключили к облаку более 250 000 устройств IoT с помощью решения Scaleways Kubernetes Kosmos.