[Перевод] Приключения с домашним Kubernetes-кластером

Прим. перев.: Автор статьи — Marshall Brekka — занимает позицию директора по проектированию систем в компании Fair.com, предлагающей своё приложение для лизинга автомобилей. В свободное же от работы время он любит применять свой обширный опыт для решения «домашних» задач, которые вряд ли удивят любого гика (посему вопрос «Зачем?» — применительно к описанным дальше действиям — априори опущен). Итак, в своей публикации Marshall делится результатами недавнего развёртывания Kubernetes на… ARM-платах.

ulnjdoyysctwv-34jhuyn-wvsp8.png

Как и у многих других гиков, за прошедшие годы у меня накопились разнообразные платы для разработки вроде Raspberry Pi. И как и у многих гиков, они пылились на полках с мыслью, что когда-нибудь пригодятся. И вот для меня этот день наконец-то настал!

Во время зимних каникул появились несколько недель вне работы, в рамках которых было достаточно времени для того, чтобы инвентаризировать всё накопленное железо и решить, что с ним делать. Вот что у меня было:

  • RAID-корпус на 5 дисков с подключением по USB3;
  • Raspberry Pi Model B (модель OG);
  • CubbieBoard 1;
  • Banana Pi M1;
  • нетбук HP (2012 года?).


Из 5 перечисленных железных компонентов я использовал разве что RAID и нетбук в качестве временного NAS. Однако из-за отсутствия поддержки USB3 в нетбуке у RAID’а был задействован не весь скоростной потенциал.

Жизненные цели


Поскольку работа с RAID не была оптимальной при использовании нетбука, я задался следующими целями для получения лучшей конфигурации:

  1. NAS с USB3 и гигабитным ethernet’ом;
  2. лучший способ управления программным обеспечением на устройстве;
  3. (бонус) возможность потокового вещания мультимедийного контента с RAID на Fire TV.


Поскольку ни одно из имевшихся в наличии устройств не поддерживало USB3 и гигабитный ethernet, к сожалению, пришлось сделать дополнительные покупки. Выбор пал на плату ROC-RK3328-CC. Она обладала всеми нужными спецификациями и достаточной поддержкой операционных систем.

Решив свои аппаратные потребности (и ожидая прибытия этого решения), я переключился на вторую цель.

Управление софтом на устройстве


Отчасти мои прошлые проекты, связанные с платами для разработки, провалились по причине недостаточного внимания к вопросам воспроизводимости и документирования. При создании очередной конфигурации под свои текущие потребности я не утруждал себя записывать ни предпринятые шаги, ни ссылки на публикации в блогах, которым следовал. И когда, спустя месяцы или годы, что-то шло не так и я пытался исправить проблему, у меня не было понимания, как всё изначально устроено.

Поэтому я сказал себе, что уж на этот раз всё будет иначе!

dmvbivkoa65wfd1ve5mo5wh5jdc.jpeg

И обратился к тому, что достаточно хорошо знаю, — к Kubernetes.

Хоть K8s и является слишком тяжелым решением достаточно простой проблемы, после почти трёх лет управления кластерами с помощью разных средств (собственных, kops и т.п.) на основной работе я очень хорошо знаком с этой системой. К тому же, развернуть K8s вне облачного окружения, да ещё и на ARM-устройствах — всё это представлялось интересной задачей.

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

Kubernetes на ARM


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

В качестве операционной системы был выбран Raspbian, поскольку он славится лучшей поддержкой имеющихся у меня плат.

Я нашёл хорошую статью по настройке Kubernetes на Raspberry Pi с использованием HypriotOS. Поскольку не был уверен в доступности HypriotOS для всех своих плат, я адаптировал эти инструкции под Debian/Raspbian.

Необходимые компоненты


Для начала потребовалась установка следующих инструментов:

  • Docker,
  • kubelet,
  • kubeadm,
  • kubectl.


Docker должен быть установлен с помощью специального скрипта — convenience script (так указано для случая использования Raspbian).

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh


После этого я установил компоненты Kubernetes по инструкциям из блога Hypriot, проведя их адаптацию с тем, чтобы для всех зависимостей использовались конкретные версии:

curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" > /etc/apt/sources.list.d/kubernetes.list
apt-get update
apt-get install -y kubelet=1.13.1-00 kubectl=1.13.1-00 kubeadm=1.13.1-00


Raspberry Pi B


Первая же сложность возникла при попытке bootstrap’а кластера на Raspberry Pi B:

$ kubeadm init
Illegal instruction


Выяснилось, что в Kubernetes убрали поддержку ARMv6. Ну что ж, у меня есть ещё CubbieBoard и Banana Pi.

Banana Pi


Изначально казалось, что такая же последовательность действий для Banana Pi будет успешнее, однако команда kubeadm init завершилась таймаутом при попытке дождаться рабочего состояния control plane:

error execution phase wait-control-plane: couldn't initialize a Kubernetes cluster


Выясняя с помощью docker ps, что происходило с контейнерами, я увидел, что и kube-controller-manager, и kube-scheduler работали уже не менее 4–5 минут, а вот kube-api-server поднялся всего 1–2 минуты назад:

$ docker ps
CONTAINER ID   COMMAND                  CREATED              STATUS           
de22427ad594   "kube-apiserver --au…"   About a minute ago   Up About a minute
dc2b70dd803e   "kube-scheduler --ad…"   5 minutes ago        Up 5 minutes     
60b6cc418a66   "kube-controller-man…"   5 minutes ago        Up 5 minutes     
1e1362a9787c   "etcd --advertise-cl…"   5 minutes ago        Up 5 minutes


Очевидно, api-server умирал или же стронний процесс убивал и перезапускал его.

Проверяя логи, я увидел весьма стандартные процедуры по запуску — была запись о начале прослушивания безопасного порта и продолжительная пауза перед появлением многочисленных ошибок в TLS-рукопожатиях:

20:06:48.604881  naming_controller.go:284] Starting NamingConditionController
20:06:48.605031  establishing_controller.go:73] Starting EstablishingController
20:06:50.791098  log.go:172] http: TLS handshake error from 192.168.1.155:50280: EOF
20:06:51.797710  log.go:172] http: TLS handshake error from 192.168.1.155:50286: EOF
20:06:51.971690  log.go:172] http: TLS handshake error from 192.168.1.155:50288: EOF
20:06:51.990556  log.go:172] http: TLS handshake error from 192.168.1.155:50284: EOF
20:06:52.374947  log.go:172] http: TLS handshake error from 192.168.1.155:50486: EOF
20:06:52.612617  log.go:172] http: TLS handshake error from 192.168.1.155:50298: EOF
20:06:52.748668  log.go:172] http: TLS handshake error from 192.168.1.155:50290: EOF


И вскоре после этого сервер завершает свою работу. Гугление привело к такой проблеме, указывающей на возможную причину в медленной работе криптографических алгоритмов на некоторых ARM-устройствах.

Я пошёл дальше и подумал, что, возможно, api-server получает слишком много повторяющихся запросов от scheduler и controller-manager.

Вынос этих файлов из директории с манифестами скажет kubelet’у остановить выполнение соответствующих pod’ов:

mkdir /etc/kubernetes/manifests.bak
mv /etc/kubernetes/manifests/kube-scheduler.yaml /etc/kubernetes/manifests.bak/
mv /etc/kubernetes/manifests/kube-controller-mananger.yaml /etc/kubernetes/manifests.bak/


Просмотр последних логов api-server показал, что теперь процесс пошёл дальше, однако всё равно умирал примерно через 2 минуты. Тогда мне вспомнилось, что манифест мог содержать liveness-пробу с таймаутами, имеющими слишком низкие значения для такого медлительного устройства.

Поэтому проверил /etc/kubernetes/manifests/kube-api-server.yaml — и в нём, конечно же…

livenessProbe:
  failureThreshold: 8
  httpGet:
    host: 192.168.1.155
    path: /healthz
    port: 6443
    scheme: HTTPS
  initialDelaySeconds: 15
  timeoutSeconds: 15


Pod убивался через 135 секунд (initialDelaySeconds + timeoutSeconds * failureThreshold). Повышаем значение initialDelaySeconds до 120…

Успех! Ну, ошибки в рукопожатиях всё ещё происходят (предположительно от kubelet), однако запуск всё равно состоялся:

20:06:54.957236  log.go:172] http: TLS handshake error from 192.168.1.155:50538: EOF
20:06:55.004865  log.go:172] http: TLS handshake error from 192.168.1.155:50384: EOF
20:06:55.118343  log.go:172] http: TLS handshake error from 192.168.1.155:50292: EOF
20:06:55.252586  cache.go:39] Caches are synced for autoregister controller
20:06:55.253907  cache.go:39] Caches are synced for APIServiceRegistrationController controller
20:06:55.545881  controller_utils.go:1034] Caches are synced for crd-autoregister controller
...
20:06:58.921689  storage_rbac.go:187] created clusterrole.rbac.authorization.k8s.io/cluster-admin
20:06:59.049373  storage_rbac.go:187] created clusterrole.rbac.authorization.k8s.io/system:discovery
20:06:59.214321  storage_rbac.go:187] created clusterrole.rbac.authorization.k8s.io/system:basic-user


Когда api-server поднялся, я переместил YAML-файлы для контроллера и планировщика обратно в директорию манифестов, после чего они уже тоже нормально стартовали.

Теперь пора удостовериться, что загрузка будет успешно проходить, если оставить все файлы в исходной директории: достаточно ли одного только изменения допустимой задержки в инициализации у livenessProbe?

20:29:33.306983  reflector.go:134] k8s.io/client-go/informers/factory.go:132: Failed to list *v1.Service: Get https://192.168.1.155:6443/api/v1/services?limit=500&resourceVersion=0: dial tcp 192.168.1.155:6443: i/o timeout
20:29:33.434541  reflector.go:134] k8s.io/client-go/informers/factory.go:132: Failed to list *v1.ReplicationController: Get https://192.168.1.155:6443/api/v1/replicationcontrollers?limit=500&resourceVersion=0: dial tcp 192.168.1.155:6443: i/o timeout
20:29:33.435799  reflector.go:134] k8s.io/client-go/informers/factory.go:132: Failed to list *v1.PersistentVolume: Get https://192.168.1.155:6443/api/v1/persistentvolumes?limit=500&resourceVersion=0: dial tcp 192.168.1.155:6443: i/o timeout
20:29:33.477405  reflector.go:134] k8s.io/client-go/informers/factory.go:132: Failed to list *v1beta1.PodDisruptionBudget: Get https://192.168.1.155:6443/apis/policy/v1beta1/poddisruptionbudgets?limit=500&resourceVersion=0: dial tcp 192.168.1.155:6443: i/o timeout
20:29:33.493660  reflector.go:134] k8s.io/client-go/informers/factory.go:132: Failed to list *v1.PersistentVolumeClaim: Get https://192.168.1.155:6443/api/v1/persistentvolumeclaims?limit=500&resourceVersion=0: dial tcp 192.168.1.155:6443: i/o timeout
20:29:37.974938  controller_utils.go:1027] Waiting for caches to sync for scheduler controller
20:29:38.078558  controller_utils.go:1034] Caches are synced for scheduler controller
20:29:38.078867  leaderelection.go:205] attempting to acquire leader lease  kube-system/kube-scheduler
20:29:38.291875  leaderelection.go:214] successfully acquired lease kube-system/kube-scheduler


Да, всё работает, хотя такие старые устройства, по всей видимости, не предназначались для запуска control plane, поскольку повторяющиеся TLS-подключения вызывают значительные тормоза. Так или иначе — рабочая инсталляция K8s на ARM получена! Поехали дальше…

Монтирование RAID’а


Поскольку SD-карты не подходят для записи в долгосрочной перспективе, для самых изменчивых частей файловой системы я решил использовать более надёжное хранилище — в данном случае это RAID. На нём были выделены 4 раздела:

  • 50 Гб;
  • 2 × 20 Гб;
  • 3,9 Тб.


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

В файле /etc/fstab для раздела с 50 Гб точка монтирования была указана как /mnt/root, а для 3,9 Тб — /mnt/raid. После этого я примонтировал директории с etcd и docker к разделу с 50 Гб:

UUID=655a39e8-9a5d-45f3-ae14-73b4c5ed50c3 /mnt/root ext4 defaults,rw,user,auto,exec 0 0
UUID=0633df91-017c-4b98-9b2e-4a0d27989a5c /mnt/raid ext4 defaults,rw,user,auto 0 0
/mnt/root/var/lib/etcd /var/lib/etcd none defaults,bind 0 0
/mnt/root/var/lib/docker /var/lib/docker none defaults,bind 0 0


Прибытие ROC-RK3328-CC


Когда новая плата была доставлена, я установил на ней необходимые компоненты для K8s (см. в начале статьи) и запустил kubeadm init. Несколько минут ожидания — успех и вывод команды join для запуска на других узлах.

Отлично! Никакой возни с таймаутами.

А поскольку на этой плате тоже будет использоваться RAID, снова потребуется настройка mount’ов. Подытожу все шаги:

1. Монтирование дисков в /etc/fstab

UUID=655a39e8-9a5d-45f3-ae14-73b4c5ed50c3 /mnt/root ext4 defaults,rw,user,auto,exec 0 0
UUID=0633df91-017c-4b98-9b2e-4a0d27989a5c /mnt/raid ext4 defaults,rw,user,auto 0 0
/mnt/root/var/lib/etcd /var/lib/etcd none defaults,bind 0 0
/mnt/root/var/lib/docker /var/lib/docker none defaults,bind 0 0


2. Установка бинарников Docker и K8s

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" > /etc/apt/sources.list.d/kubernetes.list
apt-get update
apt-get install -y kubelet=1.13.1-00 kubectl=1.13.1-00 kubeadm=1.13.1-00


3. Настройка уникального имени хоста (важно, т.к. добавляется множество узлов)

hostnamectl set-hostname k8s-master-1


4. Инициализация Kubernetes


Опускаю фазу с control plane, поскольку хочу иметь возможность планирования нормальных pod’ов и на этом узле:

kubeadm init --skip-phases mark-control-plane


5. Установка сетевого плагина


Информация об этом в статье Hypriot была немного устаревшей, поскольку сетевой плагин Weave теперь тоже поддерживается на ARM:

export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')"


6. Добавление лейблов узла


На этом узле я собираюсь запустить сервер NAS, поэтому помечу его лейблами для возможности дальнейшего использования в планировщике:

kubectl label nodes k8s-master-1 marshallbrekka.raid=true
kubectl label nodes k8s-master-1 marshallbrekka.network=gigabit


Подключение других узлов к кластеру


Настройка других устройств (Banana Pi, CubbieBoard) была так же проста. Для них нужно повторить первые 3 шага (изменив настройки для монтирования дисков/flash-носителей в зависимости от их доступности) и выполнить команду kubeadm join вместо kubeadm init.

Поиск Docker-контейнеров для ARM


Сборка большей части нужных Docker-контейнеров нормально проходит на Mac’е, однако для ARM всё несколько сложнее. Найдя множество статей о том, как использовать для этих целей QEMU, я всё же пришёл к тому, что большинство нужных мне приложений уже есть в собранном виде, и многие из них доступны на linuxserver.

Следующие шаги


Всё ещё не получив начальную конфигурацию устройств в настолько автоматизированном/заскриптованном виде, как хотелось бы, я хотя бы составил набор основных команд (mount’ы, вызовы docker и kubeadm) и и задокументировал их в Git-репозитории. Остальные используемые приложения тоже получили YAML-конфигурации для K8s, хранимые в том же репозитории, так что получить необходимую конфигурацию с нуля теперь очень просто.

В перспективе же мне хотелось бы добиться следующего:

  1. сделать мастер-узлы высокодоступными;
  2. добавить мониторинг/уведомления, чтобы знать о сбоях в каких-либо компонентах;
  3. сменить DCHP-настройки роутера на использование DNS-сервера из кластера, чтобы упростить обнаружение приложений (кто хочет помнить внутренние IP-адреса?);
  4. запустить MetalLB для проброса сервисов кластера в частную сеть (DNS и т.п.).

P.S. от переводчика


Читайте также в нашем блоге:

© Habrahabr.ru