Как мы использовали Telekube для удаленной отладки приложений в Kubernetes

Привет, Хабр! В этой статье я расскажу о способе, который мы в Just AI придумали и реализовали для локальной разработки и отладки сервиса, работающего в Kubernetes.

Краткое описание проблемы

У нас есть некий компонент (ядро системы), который обычно запускается в kubernetes и имеет множество взаимосвязей с другими сервисами. У компонента два сетевых интерфейса, которыми активно пользуются другие части системы, также развёрнутые в Kubernetes. Наша задача — научиться запускать его в IDE на своем ноутбуке в режиме отладки, чтобы максимально удобно и быстро отлаживать этот компонент. Telekube предоставляет возможность это сделать.

Предыстория

Этот раздел содержит описание специфики нашего проекта и рассуждения о том, зачем нам потребовалось изобретать такое решение. Если хотите сократить время чтения, можете его пропустить или оставить на конец.

Наша команда разрабатывает платформу для хостинга и публикации ML-сервисов, Caila. Система изначально ориентирована на развертывание в Kubernetes и, кроме того, использует Kubernetes в качестве сервиса и динамически создает в нем новые деплойменты для запуска ML-сервисов. 

Архитектура нашего проекта сейчас выглядит примерно так:

aa85857552b34f383ec57efad5e8a37f.png

Обратите внимание на компонент caila-core — он имеет множество соединений с другими компонентами, из которых надо выделить:  

  • Два внешних интерфейса: HTTP и GRPC. Причём к GRPC интерфейсу обращается множество клиентов (ML-сервисов) внутри кластера;

  • S3 — объектное хранилище, которое используется и caila-core, и ML-сервисами. Caila-core создаёт в minio бакеты, настраивает ключи авторизации и передает их в ML-сервисы. Далее ML-сервисы уже читают и пишут информацию в эти бакеты.

Такое количество взаимосвязей создает две проблемы:

1. Настройка локального окружения для разработки становится очень трудоёмкой из-за большого количества зависимостей;

2. Отладка end-to-end сценариев все равно невозможна без Kubernetes, т.к. основной сценарий использования состоит в том, что caila-core сначала создает один или несколько новых Deployment«ов, а потом взаимодействует с ними.

В общем, запускать caila-core не так, как в kubernetes, для которого у нас есть стандартная конфигурация для развертывания, практически невозможно. Небольшая оговорка: теперь у нас есть специальный вариант поставки решения без Kubernetes, но он имеет функциональные ограничения, и для разработки мы такую конфигурацию не используем. А если запускать программу можно только внутри кубер-кластера, то встаёт вопрос —, а как же её отлаживать?

Варианты:

  • Использовать mock’и и разрабатываться на IT-тестах. Неподходящий вариант, потому что:
    1. Cоздание таких тестов довольно трудоемкая задача;
    2. Очень часто проблемы кроются в нюансах поведения систем, с которыми мы взаимодействуем и которые мы не предусмотрели, потому что не знали о них. А если мы о них не знали, то и соответствующий mock мы бы написать не смогли.

  • Использовать удалённую отладку. Неплохо, но:
    1. Тормозит из-за сетевого взаимодействия (если только кубер-кластер не развернут у вас же на локал-хосте). На практике при задержках удалённой отладки стек вызовов на некоторых брейкпоинтах будет подгружаться около минуты.
    2. При удалённой отладке весьма неудобно применять экспериментальные изменения в коде. Если hot-reload не хватает, то придётся пересобирать контейнер, перезапускать деплоймент, заново подключать отладчик. Все эти факторы заставляют искать решение получше.

  • Телепортация. А что, если мы каким-то образом возьмем и запустим наш отлаживаемый компонент на локальном компьютере (прямо в любимой IDE в режиме отладки)? Перенаправим сетевой трафик так, чтобы другие компоненты приложения обращались к кубер-сервису caila-core, как обычно, а обрабатывал бы эти запросы инстанс, запущенный у нас на локал-хосте?

Варианты для «телепортации»:

  • Telepresence — сервис для решения именно такой проблемы. Мы пробовали использовать Telepresence, но:
    1. Он платный в случае, если у нашего сервиса более одного интерфейса; 2. Нестабильный. По невыясненной причине соединения проброшенные через Telepresence разрывались через несколько часов работы, что доставляло большие неудобства, потому что не всегда сразу было понятно, что именно сломалось. Потому мы пошли искать счастье дальше.

  • Telekube. А что, если мы попробуем решить проблему «в лоб»? Есть такая штука — ssh port forwarding, которая позволяет «телепортировать» любые порты куда-угодно и как угодно. Да, настроить port forwarding непросто и изучение того, как работает этот механизм, может отнять время, но больше проблем в этом подходе нет. А вся сложность настройки прекрасно упаковывается в один deployment для кубера и bash-файл для установки клиентского подключения.

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

Telekube

Telekube — название не продукта, а методики использования ssh port-forwarding для того, чтобы настроить удобный процесс локальной разработки компонента, выполняющегося в Kubernetes и тесно взаимодействующего с другими сервисами.  Суть решения — задеплоить в Kubernetes контейнер sshd со включенным port-forwarding, прокинуть в обе стороны все порты, которые использует приложение и сделать вид, будто бы сервер ssh — это наше настоящее приложение, а не прокси. Поясню суть на схеме.

Обычный деплоймент
Компонент caila-core выставляет два порта 10600 (http) и 10601 (grpc) и использует два сервиса DB и S3.

Схема с Telekube
Вместо деплоймента нашего приложения (caila-core) устанавливается другой деплоймент, который по сути является ssh-сервером. У нас именно этот деплоймент и получил название Telekube. У этого деплоймента прописываются точно такие же описания containerPort, какие были у оригинального caila-core (чтобы соседние компоненты могли пользоваться теми же самыми сервисами). И для того, чтобы к этому ssh-серверу можно было приконнектится извне, создаётся NodePort.

6b217c309df86aae933c37a4892fc353.png

А теперь посмотрим в код.

Докер-контейнер для telekube-деплоймента

Сначала нам надо собрать докер-образ с sshd сервером. Рядом с докер-файлом должен лежать файл client_rsa.pub. Он пакуется внутрь докер-контейнера и используется для аутентификации клиента.

FROM ubuntu:20.04
# Устанавливаем необходимые пакеты
RUN apt update
ENV DEBIAN_FRONTEND=noninteractive
RUN apt install ssh -y
RUN apt install vim curl net-tools -y
RUN apt install iputils-ping dnsutils iproute2 -y
WORKDIR /root
# Копируем авторизационный ssh-ключ
RUN mkdir .ssh && chmod 700 .ssh
COPY client_rsa.pub .ssh/authorized_keys
RUN chmod 600 .ssh/authorized_keys
# Включаем порт-форвардинг
RUN echo "GatewayPorts yes" > /etc/ssh/sshd_config.d/telekube.conf
# И запускаем ssh сервер
RUN echo "#!/bin/sh \n\
/etc/init.d/ssh start \n\
tail -f /dev/null \n\
" > container.sh && chmod +x container.sh
EXPOSE 22
ENTRYPOINT ["/bin/sh", "container.sh"]

В этом докер-файле можно многое улучшить:

  • Использовать базовый образ меньшего размера;

  • Оптимизировать команды установки пакетов, не ставить вспомогательные и отладочные пакеты (в нормальном режиме работы, например, vim и curl не нужны).

  • И, конечно, можно заморочится с тем, чтобы ходить не под root«ом, а под отдельным пользователем. Если кому-то это будет актуально — напишите, пожалуйста, доработанный вариант в комментариях. Но у нас необходимости в доработках не возникло.

Деплоймент для Telekube

Затем этот контейнер надо поместить в Kubernetes и сконфигурировать порты — ssh и все другие, что были у оригинального компонента.

apiVersion: apps/v1
         kind: Deployment
         metadata:
           name: "telekube-app"
         spec:
           strategy:
             type: Recreate
           selector:
             matchLabels:
               app: "telekube-app"
           template:
             metadata:
               labels:
                 app: "telekube-app"
             spec:
               containers:
                 - name: "telekube-container"
                   image: "docker-registry.my.com/components/telekube"
                   imagePullPolicy: Always
                   ports:
			# Порт, по которому будет подключаться ssh-клиент
                     - containerPort: 22
                       name: ssh
			# Здесь перечисляем порты, которые слушает приложение
                     - containerPort: 10600
                       name: http
                     - containerPort: 10601
                       name: grpc

NodePort, через который мы будем подключаться к sshd с рабочего ноутбука

Затем выставим наружу NodePort для подключения по ssh. В значенииnodePortнадо указать любой свободный порт:

apiVersion: v1
kind: Service
metadata:
 name: telekube-port
 labels:
   name: telekube-port
spec:
 type: NodePort
 ports:
   - port: 22
     nodePort: 30201
     name: ssh
 selector:
   app: telekube-app

Если по каким-то причинам NodePort создать не представляется возможным, то можно использовать любой другой способ проброса порта. Например port-forwarding через Lens или kubectl.

Описание сервиса, который мы телепортируем

И, наконец, сам сервис, через который к поду подключаются другие сервисы нашей системы. Сервис имеет точно такое же название, как и оригинальный, телепортируемый сервис и такие же порты. От оригинального сервиса отличается только selector.

apiVersion: v1
kind: Service
metadata:
 name: "caila-core-service"
 labels:
   app: "caila-core-service"
spec:
 type: ClusterIP
 selector:
   app: "telekube-app"
 ports:
   - protocol: TCP
     port: 10600
     targetPort: 10600
     name: http
   - protocol: TCP
     port: 10601
     targetPort: 10601
     name: grpc

Скрипт создания клиентского подключения

Теперь можно попробовать подключиться!

#!/bin/bash

ssh -R 0.0.0.0:10600:localhost:10600 \
   -R 0.0.0.0:10601:localhost:10601 \
   -L 49000:minio.ig.svc.cluster.local:9000 \
   -L 49090:prometheus-operated.monitoring.svc.cluster.local:9090 \
   -L 47017:mongodb.ig.svc.cluster.local:27017 \
   -o ServerAliveInterval=10 \
   -T -S /tmp/.ssh-telekube \
   -i ~/.ssh/id_rsa_telekube \
   root@dev-server.my.com -p 30201 \
   -f -N -T

Разберём эту команду по частям:

-R — определяют порты, которые мы пробрасываем с локалхоста в кубер;

-L — определяет порты, которые мы пробрасываем из кубера на локалхост. То есть, установив соединение с localhost:49000, мы, по сути, устанавливаем соединение с minio.ig.svc.cluster.local:9000;

-О — ServerAliveInterval=10 — без этой опции не работает;

-S — создание управляющего сокета. Нужно, чтобы подключение можно было остановить;

-i — Определяет ssh-ключ для авторизации. Тот самый, публичную часть которого мы упаковали в контейнер;

-p — Номер порта. Тот номер, для которого мы создали NodePort;

root@dev-server.my.com — адрес сервера, на котором опубликован NodePort;

-f — Запуск в background режиме;

-N — Не выполнять команды, только пробрасывать порты;

-T — Отключает создание псевдотерминала. Нам он не нужен в режиме portforwarding.

Скрипт для остановки клиентского подключения

#!/bin/bash

ssh -S  /tmp/.ssh-telekube -O exit dev-server.my.com

Он отправляет команду остановки в управляющий сокет sshd.

Заключение

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

© Habrahabr.ru