Совсем недавно одна известная компания объявила, что переводит линейку своих ноутбуков на ARM-архитектуру. Услышав эту новость, я вспомнил: просматривая в очередной раз цены на EC2 в AWS, обратил внимание на Graviton’ы с очень вкусной ценой. Подвох, конечно же, был в том, что это ARM. Тогда мне и в голову не приходило, что ARM — это довольно серьезно…
Для меня эта архитектура всегда была уделом мобильных и прочих IoT-штучек. «Настоящие» серверы на ARM — как-то необычно, в чем-то даже дико… Однако новая мысль засела в голову, поэтому в один из выходных решил проверить, что вообще можно сегодня запустить на ARM. И для этого решил начать с близкого и родного — кластера Kubernetes. Причем не просто какого-то условного «кластера», а всё «по-взрослому», чтобы он был максимально таким же, каким я привык его видеть в production.
По моей задумке, кластер должен быть доступным из интернета, в нём должно выполняться некоторое веб-приложение и еще должен быть как минимум мониторинг. Для реализации этой идеи понадобится пара (или больше) Raspberry Pi не ниже модели 3B+. Площадкой для экспериментов могла бы стать и AWS, но мне были интересны именно «малины» (которые всё равно стояли без дела). Итак, мы развернём на них кластер Kubernetes с Ingress, Prometheus и Grafana.
Подготовка «малин»
Установка ОС и SSH
С выбором ОС для установки я сильно не заморачивался: просто взял самый свежий Raspberry Pi OS Lite с официального сайта. Там же доступна документация по установке, все действия из которой нужно выполнить на всех узлах будущего кластера. Далее потребуется произвести следующие манипуляции (тоже на всех узлах).
Подключив монитор и клавиатуру, необходимо предварительно настроить сеть и SSH:
Для работы кластера на мастере обязательно должен быть статический IP-адрес, а на рабочих узлах — по усмотрению. Я предпочел статичные адреса везде из соображений удобства настройки.
Статический адрес можно сконфигурировать в ОС (в файле /etc/dhcpcd.conf есть подходящий пример) или путем фиксации lease в DHCP-сервере используемого (в моём случае — домашнего) маршрутизатора.
ssh-server просто включается в raspi-config (interfacing options → ssh).
После этого можно уже залогиниться по SSH (по умолчанию логин — pi, а пароль — raspberry или тот, на который поменяли) и продолжить настройки.
Другие настройки
Установим имя хоста. В моём примере будут использоваться pi-control и pi-worker.
Проверим, что файловая система расширена на весь диск (df -h /). При необходимости её можно расширить с помощью raspi-config.
Изменим пароль пользователя по умолчанию в raspi-config.
Выключим swap-файл (таково требование Kubernetes; если вам интересны подробности по этой теме, см. issue #53533):
При установке iptables-persistent потребуется сохранить настройки iptables для ipv4, а в файле /etc/iptables/rules.v4 — добавить правила в цепочку FORWARD, вот так:
# Generated by xtables-save v1.8.2 on Sun Jul 19 00:27:43 2020
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A FORWARD -s 10.1.0.0/16 -j ACCEPT
-A FORWARD -d 10.1.0.0/16 -j ACCEPT
COMMIT
Осталось только перезагрузиться.
Теперь все готово к установке кластера Kubernetes.
Инсталляция Kubernetes
На этом этапе я специально отложил все свои и наши корпоративные наработки по автоматизации установки и конфигурации кластера K8s. Вместо этого, воспользуемся официальной документацией с kubernetes.io (слегка дополненной комментариями и сокращениями).
Добавим репозиторий Kubernetes:
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
sudo apt-get update
Далее в документации предлагается установить CRI (container runtime interface). Поскольку Docker уже установлен, двигаемся дальше и инсталлируем основные компоненты:
На шаге установки основных компонентов я сразу добавил kubernetes-cni, который необходим для работы кластера. И тут есть важный момент: пакет kubernetes-cni по каким-то причинам не создает директорию по умолчанию для настроек CNI-интерфейсов, поэтому мне пришлось создать ее вручную:
mkdir -p /etc/cni/net.d
Для работы network-бэкенда, речь о котором пойдет ниже, необходимо доустановить плагин для CNI. Я выбрал привычный и понятный мне плагин portmap (полный их список см. в документации):
curl -sL https://github.com/containernetworking/plugins/releases/download/v0.7.5/cni-plugins-arm-v0.7.5.tgz | tar zxvf - -C /opt/cni/bin/ ./portmap
Настройка Kubernetes
Узел с control plane
Установка самого кластера делается довольно просто. А для ускорения этого процесса и проверки того, что образы Kubernetes доступны, можно предварительно выполнить:
kubeadm config images pull
Теперь проводим саму установку — инициализируем control plane кластера:
Обратите внимание, что подсети для сервисов и pod’ов не должны пересекаться между собой и с существующими сетями.
В конце нам покажут сообщение о том, что все хорошо, и заодно подскажут, как присоединить рабочие узлы к control plane:
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/
You can now join any number of the control-plane node running the following command on each as root:
kubeadm join 192.168.88.30:6443 --token a485vl.xjgvzzr2g0xbtbs4
--discovery-token-ca-cert-hash sha256:9da6b05aaa5364a9ec59adcc67b3988b9c1b94c15e81300560220acb1779b050
--contrl-plane --certificate-key 72a3c0a14c627d6d7fdade1f4c8d7a41b0fac31b1faf0d8fdf9678d74d7d2403
Please note that the certificate-key gives access to cluster sensitive data, keep it secret!
As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use
"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join 192.168.88.30:6443 --token a485vl.xjgvzzr2g0xbtbs4
--discovery-token-ca-cert-hash sha256:9da6b05aaa5364a9ec59adcc67b3988b9c1b94c15e81300560220acb1779b050
Выполним рекомендации по добавлению конфига для пользователя. А заодно рекомендую сразу добавить автодополнение для kubectl:
На данном этапе уже можно увидеть первый узел в кластере (правда, он еще не готов):
root@pi-control:~# kubectl get no
NAME STATUS ROLES AGE VERSION
pi-control NotReady master 29s v1.18.6
Конфигурация сети
Далее, как было сказано в сообщении после установки, потребуется установить сеть в кластер. В документации предлагают выбор из Calico, Cilium, contiv-vpp, Kube-router и Weave Net… Здесь я отступил от официальной инструкции и выбрал более привычный и понятный мне вариант: flannel в режиме host-gw (подробнее о доступных бэкендах см. в документации проекта).
Установить его в кластер довольно просто. Для начала — скачиваем манифесты:
… и подсеть pod’ов — со значения по умолчанию на ту, которая указана при инициализации кластера:
sed -i 's#10.244.0.0/16#10.1.0.0/16#' kube-flannel.yml
После этого создаем ресурсы:
kubectl create -f kube-flannel.yml
Готово! Через некоторое время первый узел K8s перейдет в статус Ready:
NAME STATUS ROLES AGE VERSION
pi-control Ready master 2m v1.18.6
Добавление рабочего узла
Теперь можно добавить worker’а. Для этого на нем — после установки собственно Kubernetes по сценарию, описанному выше, — нужно просто выполнить ранее полученную команду:
root@pi-control:~# kubectl get no
NAME STATUS ROLES AGE VERSION
pi-control Ready master 28m v1.18.6
pi-worker Ready <none> 2m8s v1.18.6
У меня под рукой было всего две Raspberry Pi, так что отдавать одну из них только под control plane мне не хотелось. Поэтому я снял автоматически установленный taint с узла pi-control, запустив:
В первую очередь нам понадобится Helm. Конечно, можно все делать и без него, но Helm позволяет буквально без правки файлов настраивать некоторые компоненты по своему усмотрению. И по факту это просто бинарный файл, который «хлеба не просит».
Итак, заходим на helm.sh в раздел docs/installation и выполняем команду оттуда:
Теперь установим инфраструктурные компоненты в соответствии с задумкой:
Ingress controller;
Prometheus;
Grafana;
cert-manager.
Ingress controller
Первый компонент — Ingress controller — устанавливается довольно просто и готов к использованию «из коробки». Для этого достаточно зайти в раздел bare-metal на сайте и выполнить команду установки оттуда:
Однако в этот момент «малина» начала напрягаться и упираться в дисковый IOPS. Дело в том, что вместе с Ingress-контроллером устанавливается большое количество ресурсов, выполняется много запросов к API и, соответственно, много данных записывается в etcd. В общем, либо карта памяти 10 класса не очень производительна, либо SD-карты в принципе не хватает для такой нагрузки. Тем не менее, через минут 5 все запустилось.
Был создан namespace и в нем появился контроллер и всё ему необходимое:
root@pi-control:~# kubectl -n ingress-nginx get pod
NAME READY STATUS RESTARTS AGE
ingress-nginx-admission-create-2hwdx 0/1 Completed 0 31s
ingress-nginx-admission-patch-cp55c 0/1 Completed 0 31s
ingress-nginx-controller-7fd7d8df56-68qp5 1/1 Running 0 48s
Prometheus
Следующие два компонента довольно просто установить через Helm из chart repo.
Находим Prometheus, создаем namespace и устанавливаем в него:
По умолчанию Prometheus заказывает 2 диска: под данные самого Prometheus и под данные AlertManager. Поскольку в кластере не создан storage class, диски не закажутся и pod’ы не запустятся. Для bare metal-инсталляций Kubernetes мы обычно используем Ceph rbd, однако в случае с Raspberry Pi это явный перебор.
Поэтому создадим простой local storage на hostpath. Манифесты PV (persistent volume) для prometheus-server и prometheus-alertmanager объединены в файле prometheus-pv.yaml в Git-репозитории с примерами для статьи. Директорию для PV необходимо заранее создать на диске того узла, к которому хотим привязать Prometheus: в примере прописан nodeAffinity по hostname pi-worker и на нем созданы директории /data/localstorage/prometheus-server и /data/localstorage/prometheus-alertmanager.
Скачиваем (клонируем) манифест и добавляем в Kubernetes:
kubectl create -f prometheus-pv.yaml
На этом этапе я впервые столкнулся с проблемой ARM-архитектуры. Kube-state-metrics, который по умолчанию устанавливается в чарте Prometheus, отказался запускаться. Он выдавал ошибку:
root@pi-control:~# kubectl -n monitoring logs prometheus-kube-state-metrics-c65b87574-l66d8
standard_init_linux.go:207: exec user process caused "exec format error"
Дело в том, что для kube-state-metrics используется образ проекта CoreOS, который не собирают под ARM:
kubectl -n monitoring get deployments.apps prometheus-kube-state-metrics -o=jsonpath={.spec.template.spec.containers[].image}
quay.io/coreos/kube-state-metrics:v1.9.7
Пришлось слегка погуглить и найти, например, вот этот образ. Чтобы им воспользоваться, обновим релиз, указав, какой образ использовать для kube-state-metrics:
Для самоподписанных сертификатов в домашнем использовании этого вполне достаточно. Если же нужно получать тот же Let’s Encrypt, то необходимо настроить еще cluster issuer. Подробности об этом можно найти в нашей статье «SSL-сертификаты от Let’s Encrypt с cert-manager в Kubernetes».
Теперь можно заказать сертификат, например, для Grafana. Для этого потребуется домен и доступ в кластер извне. Домен у меня есть, а трафик я настроил пробросом портов 80 и 443 на домашнем маршрутизаторе в соответствии с созданным сервисом ingress-controller’a:
kubectl -n ingress-nginx get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller NodePort 10.2.206.61 <none> 80:31303/TCP,443:30498/TCP 23d
80-й порт в данном случае транслируется в 31303, а 443 — в 30498. (Порты генерируются случайным образом, поэтому у вас они будут другие.)
После этого появится ресурс Ingress, через который будет происходить валидация Let’s Encrypt’ом:
root@pi-control:~# kubectl -n monitoring get ing
NAME CLASS HOSTS ADDRESS PORTS AGE
cm-acme-http-solver-rkf8l <none> grafana.home.pi 192.168.88.31 80 72s
grafana <none> grafana.home.pi 192.168.88.31 80 6d17h
prometheus-server <none> prometheus.home.pi 192.168.88.31 80 8d
После того, как валидация пройдет, мы увидим, что ресурс certificate готов, а в указанном выше секрете grafana-tls — сертификат и ключ. Можно сразу проверить, кто выпустил сертификат:
root@pi-control:~# kubectl -n monitoring get certificate
NAME READY SECRET AGE
grafana True grafana-tls 13m
root@pi-control:~# kubectl -n monitoring get secrets grafana-tls -ojsonpath="{.data['tls.crt']}" | base64 -d | openssl x509 -issuer -noout
issuer=CN = Fake LE Intermediate X1
Вернемся к Grafana. Нам потребуется немного исправить её Helm-релиз, изменив настройки для TLS в соответствии с созданным сертификатом.
Для этого скачиваем чарт, правим и обновляем из локальной директории:
helm pull --untar stable/grafana
Редактируем в файле grafana/values.yaml параметры TLS:
Еще рекомендую добавить dashboard для node exporter: он детально покажет, что происходит с «малинами» (нагрузка CPU, использование памяти, сети, диска и т.д.).
После этого считаю, что кластер готов принимать и запускать приложения!
Примечание про сборку
Для сборки приложений под ARM-архитектуру есть как минимум два варианта. Во-первых, можно собирать на ARM-устройстве. Однако, посмотрев на текущую утилизацию двух Raspberry Pi, я понял, что еще и сборку они не выдержат. Поэтому заказал себе новую Raspberry Pi 4 (она помощнее и в ней есть аж 4 GB памяти) — планирую собирать на ней.
Второй вариант — сборка мультиархитектурного образа Docker на более мощной машине. Для этого есть расширение docker buildx. Если приложение на компилируемом языке, то потребуется кросс-компиляция для ARM. Описывать все настройки для такого пути не буду, т.к. это потянет на отдельную статью. При реализации такого подхода можно добиться «универсальных» образов: Docker, запущенный на ARM-машине, сам будет автоматически загружать соответствующий архитектуре образ.
Заключение
Проведенный эксперимент превзошел все мои ожидания: [как минимум] «ванильный» Kubernetes с необходимой базой неплохо себя чувствует на ARM, а при его конфигурации возникла лишь пара нюансов.
Сами Raspberry Pi 3B+ держат нагрузку на CPU, однако их SD-карты — явное бутылочное горлышко. Коллеги подсказали, что в каких-то версиях есть возможность загружаться с USB, куда можно подключить SSD: тогда скорее всего ситуация станет получше.
Вот пример загрузки CPU при установке Grafana:
Для экспериментов и «на попробовать», на мой взгляд, Kubernetes-кластер на «малинах» гораздо лучше передаёт ощущения от эксплуатации, чем тот же самый Minikube, потому что все компоненты кластера и устанавливаются, и работают «по-взрослому».
В перспективе есть идея добавить к кластеру весь цикл CI/CD, реализованный полностью на Raspberry Pi. А также я буду рад, если кто-то поделится своим опытом по настройке K8s на AWS Graviton’ах.
P.S. Да, «production» может быть ближе, чем я думал: