[Перевод] Приключения с домашним Kubernetes-кластером
Прим. перев.: Автор статьи — Marshall Brekka — занимает позицию директора по проектированию систем в компании Fair.com, предлагающей своё приложение для лизинга автомобилей. В свободное же от работы время он любит применять свой обширный опыт для решения «домашних» задач, которые вряд ли удивят любого гика (посему вопрос «Зачем?» — применительно к описанным дальше действиям — априори опущен). Итак, в своей публикации Marshall делится результатами недавнего развёртывания Kubernetes на… ARM-платах.
Как и у многих других гиков, за прошедшие годы у меня накопились разнообразные платы для разработки вроде Raspberry Pi. И как и у многих гиков, они пылились на полках с мыслью, что когда-нибудь пригодятся. И вот для меня этот день наконец-то настал!
Во время зимних каникул появились несколько недель вне работы, в рамках которых было достаточно времени для того, чтобы инвентаризировать всё накопленное железо и решить, что с ним делать. Вот что у меня было:
- RAID-корпус на 5 дисков с подключением по USB3;
- Raspberry Pi Model B (модель OG);
- CubbieBoard 1;
- Banana Pi M1;
- нетбук HP (2012 года?).
Из 5 перечисленных железных компонентов я использовал разве что RAID и нетбук в качестве временного NAS. Однако из-за отсутствия поддержки USB3 в нетбуке у RAID’а был задействован не весь скоростной потенциал.
Жизненные цели
Поскольку работа с RAID не была оптимальной при использовании нетбука, я задался следующими целями для получения лучшей конфигурации:
- NAS с USB3 и гигабитным ethernet’ом;
- лучший способ управления программным обеспечением на устройстве;
- (бонус) возможность потокового вещания мультимедийного контента с RAID на Fire TV.
Поскольку ни одно из имевшихся в наличии устройств не поддерживало USB3 и гигабитный ethernet, к сожалению, пришлось сделать дополнительные покупки. Выбор пал на плату ROC-RK3328-CC. Она обладала всеми нужными спецификациями и достаточной поддержкой операционных систем.
Решив свои аппаратные потребности (и ожидая прибытия этого решения), я переключился на вторую цель.
Управление софтом на устройстве
Отчасти мои прошлые проекты, связанные с платами для разработки, провалились по причине недостаточного внимания к вопросам воспроизводимости и документирования. При создании очередной конфигурации под свои текущие потребности я не утруждал себя записывать ни предпринятые шаги, ни ссылки на публикации в блогах, которым следовал. И когда, спустя месяцы или годы, что-то шло не так и я пытался исправить проблему, у меня не было понимания, как всё изначально устроено.
Поэтому я сказал себе, что уж на этот раз всё будет иначе!
И обратился к тому, что достаточно хорошо знаю, — к 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, хранимые в том же репозитории, так что получить необходимую конфигурацию с нуля теперь очень просто.
В перспективе же мне хотелось бы добиться следующего:
- сделать мастер-узлы высокодоступными;
- добавить мониторинг/уведомления, чтобы знать о сбоях в каких-либо компонентах;
- сменить DCHP-настройки роутера на использование DNS-сервера из кластера, чтобы упростить обнаружение приложений (кто хочет помнить внутренние IP-адреса?);
- запустить MetalLB для проброса сервисов кластера в частную сеть (DNS и т.п.).
P.S. от переводчика
Читайте также в нашем блоге: