Полноценный Kubernetes с нуля на Raspberry Pi
Совсем недавно одна известная компания объявила, что переводит линейку своих ноутбуков на 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):
dphys-swapfile swapoff systemctl disable dphys-swapfile
- Обновим пакеты до последних версий:
apt-get update && apt-get dist-upgrade -y
- Установим Docker и дополнительные пакеты:
apt-get install -y docker docker.io apt-transport-https curl bridge-utils iptables-persistent
При установке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 <
Далее в документации предлагается установить CRI (container runtime interface). Поскольку Docker уже установлен, двигаемся дальше и инсталлируем основные компоненты:
sudo apt-get install -y kubelet kubeadm kubectl kubernetes-cni
На шаге установки основных компонентов я сразу добавил 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 кластера:
kubeadm init --pod-network-cidr=10.1.0.0/16 --service-cidr=10.2.0.0/16 --upload-certs
Обратите внимание, что подсети для сервисов и 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:
kubectl completion bash > ~/.kube/completion.bash.inc
printf "
# Kubectl shell completion
source '$HOME/.kube/completion.bash.inc'
" >> $HOME/.bash_profile
source $HOME/.bash_profile
На данном этапе уже можно увидеть первый узел в кластере (правда, он еще не готов):
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 (подробнее о доступных бэкендах см. в документации проекта).
Установить его в кластер довольно просто. Для начала — скачиваем манифесты:
wget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
Затем меняем в настройках тип с vxlan
на host-gw
:
sed -i 's/vxlan/host-gw/' kube-flannel.yml
… и подсеть 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 по сценарию, описанному выше, — нужно просто выполнить ранее полученную команду:
kubeadm join 192.168.88.30:6443 --token a485vl.xjgvzzr2g0xbtbs4 \
--discovery-token-ca-cert-hash sha256:9da6b05aaa5364a9ec59adcc67b3988b9c1b94c15e81300560220acb1779b050
На этом можно считать, что кластер готов:
root@pi-control:~# kubectl get no
NAME STATUS ROLES AGE VERSION
pi-control Ready master 28m v1.18.6
pi-worker Ready 2m8s v1.18.6
У меня под рукой было всего две Raspberry Pi, так что отдавать одну из них только под control plane мне не хотелось. Поэтому я снял автоматически установленный taint с узла pi-control, запустив:
root@pi-control:~# kubectl edit node pi-control
… и удалив строки:
- effect: NoSchedule
key: node-role.kubernetes.io/master
Наполнение кластера необходимым минимумом
В первую очередь нам понадобится Helm. Конечно, можно все делать и без него, но Helm позволяет буквально без правки файлов настраивать некоторые компоненты по своему усмотрению. И по факту это просто бинарный файл, который «хлеба не просит».
Итак, заходим на helm.sh в раздел docs/installation и выполняем команду оттуда:
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
После этого добавляем репозиторий чартов:
helm repo add stable https://kubernetes-charts.storage.googleapis.com/
Теперь установим инфраструктурные компоненты в соответствии с задумкой:
- Ingress controller;
- Prometheus;
- Grafana;
- cert-manager.
Ingress controller
Первый компонент — Ingress controller — устанавливается довольно просто и готов к использованию «из коробки». Для этого достаточно зайти в раздел bare-metal на сайте и выполнить команду установки оттуда:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.34.1/deploy/static/provider/baremetal/deploy.yaml
Однако в этот момент «малина» начала напрягаться и упираться в дисковый 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 и устанавливаем в него:
helm search repo stable | grep prometheus
kubectl create ns monitoring
helm install prometheus --namespace monitoring stable/prometheus --set server.ingress.enabled=True --set server.ingress.hosts={"prometheus.home.pi"}
По умолчанию 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:
helm upgrade prometheus --namespace monitoring stable/prometheus --set server.ingress.enabled=True --set server.ingress.hosts={"prometheus.home.pi"} --set kube-state-metrics.image.repository=carlosedp/kube-state-metrics --set kube-state-metrics.image.tag=v1.9.6
Проверяем, что все запустилось:
root@pi-control:~# kubectl -n monitoring get po
NAME READY STATUS RESTARTS AGE
prometheus-alertmanager-df65d99d4-6d27g 2/2 Running 0 5m56s
prometheus-kube-state-metrics-5dc5fd89c6-ztmqr 1/1 Running 0 5m56s
prometheus-node-exporter-49zll 1/1 Running 0 5m51s
prometheus-node-exporter-vwl44 1/1 Running 0 4m20s
prometheus-pushgateway-c547cfc87-k28qx 1/1 Running 0 5m56s
prometheus-server-85666fd794-z9qnc 2/2 Running 0 4m52s
Grafana и cert-manager
Для графиков и dashboard’ов ставим Grafana:
helm install grafana --namespace monitoring stable/grafana --set ingress.enabled=true --set ingress.hosts={"grafana.home.pi"}
В конце вывода нам покажут, как получить пароль для доступа:
kubectl get secret --namespace monitoring grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
Для заказа сертификатов установим cert-manager. Для его установки обратимся к документации, которая предлагает соответствующие команды для Helm:
helm repo add jetstack https://charts.jetstack.io
helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--version v0.16.0 \
--set installCRDs=true
Для самоподписанных сертификатов в домашнем использовании этого вполне достаточно. Если же нужно получать тот же Let’s Encrypt, то необходимо настроить еще cluster issuer. Подробности об этом можно найти в нашей статье «SSL-сертификаты от Let’s Encrypt с cert-manager в Kubernetes».
Сам я остановился на варианте из примера в документации, решив, что staging-варианта LE будет достаточно. Изменяем в примере e-mail, сохраняем в файл и добавляем в кластер (cert-manager-cluster-issuer.yaml):
kubectl create -f cert-manager-cluster-issuer.yaml
Теперь можно заказать сертификат, например, для 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 80:31303/TCP,443:30498/TCP 23d
80-й порт в данном случае транслируется в 31303, а 443 — в 30498. (Порты генерируются случайным образом, поэтому у вас они будут другие.)
Вот пример сертификата (cert-manager-grafana-certificate.yaml):
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: grafana
namespace: monitoring
spec:
dnsNames:
- grafana.home.pi
secretName: grafana-tls
issuerRef:
kind: ClusterIssuer
name: letsencrypt-staging
Добавляем его в кластер:
kubectl create -f cert-manager-grafana-certificate.yaml
После этого появится ресурс Ingress, через который будет происходить валидация Let’s Encrypt’ом:
root@pi-control:~# kubectl -n monitoring get ing
NAME CLASS HOSTS ADDRESS PORTS AGE
cm-acme-http-solver-rkf8l grafana.home.pi 192.168.88.31 80 72s
grafana grafana.home.pi 192.168.88.31 80 6d17h
prometheus-server 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:
tls:
- secretName: grafana-tls
hosts:
- grafana.home.pi
Здесь же можно сразу настроить установленный Prometheus в качестве datasource
:
datasources:
datasources.yaml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus-server:80
access: proxy
isDefault: true
Теперь из локальной директории обновляем чарт Grafana:
helm upgrade grafana --namespace monitoring ./grafana --set ingress.enabled=true --set ingress.hosts={"grafana.home.pi"}
Проверяем, что в Ingress grafana
добавился 443 порт и есть доступ по HTTPS:
root@pi-control:~# kubectl -n monitoring get ing grafana
NAME CLASS HOSTS ADDRESS PORTS AGE
grafana grafana.home.pi 192.168.88.31 80, 443 63m
root@pi-control:~# curl -kI https://grafana.home.pi
HTTP/2 302
server: nginx/1.19.1
date: Tue, 28 Jul 2020 19:01:31 GMT
content-type: text/html; charset=utf-8
cache-control: no-cache
expires: -1
location: /login
pragma: no-cache
set-cookie: redirect_to=%2F; Path=/; HttpOnly; SameSite=Lax
x-frame-options: deny
strict-transport-security: max-age=15724800; includeSubDomains
Для демонстрации Grafana в действии можно скачать и добавить dashboard для kube-state-metrics. Вот как это выглядит:
Еще рекомендую добавить 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» может быть ближе, чем я думал:
P.P. S.
Читайте также в нашем блоге: