Полноценный Kubernetes с нуля на Raspberry Pi

zfxxqxpx87oqbinmn4pwsrubg-e.png

Совсем недавно одна известная компания объявила, что переводит линейку своих ноутбуков на 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:

  1. Для работы кластера на мастере обязательно должен быть статический IP-адрес, а на рабочих узлах — по усмотрению. Я предпочел статичные адреса везде из соображений удобства настройки.
  2. Статический адрес можно сконфигурировать в ОС (в файле /etc/dhcpcd.conf есть подходящий пример) или путем фиксации lease в DHCP-сервере используемого (в моём случае — домашнего) маршрутизатора.
  3. ssh-server просто включается в raspi-config (interfacing options → ssh).


После этого можно уже залогиниться по SSH (по умолчанию логин — pi, а пароль — raspberry или тот, на который поменяли) и продолжить настройки.

Другие настройки


  1. Установим имя хоста. В моём примере будут использоваться pi-control и pi-worker.
  2. Проверим, что файловая система расширена на весь диск (df -h /). При необходимости её можно расширить с помощью raspi-config.
  3. Изменим пароль пользователя по умолчанию в raspi-config.
  4. Выключим swap-файл (таково требование Kubernetes; если вам интересны подробности по этой теме, см. issue #53533):
    dphys-swapfile swapoff
    systemctl disable dphys-swapfile
  5. Обновим пакеты до последних версий:
    apt-get update && apt-get dist-upgrade -y
  6. Установим 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
  7. Осталось только перезагрузиться.


Теперь все готово к установке кластера 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. Вот как это выглядит:

kbvxpmpmcviyijk6w_sblobotmg.png

Еще рекомендую добавить 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:

m6nxhrqrz6ae_wj1hhrywfo_z7a.png

Для экспериментов и «на попробовать», на мой взгляд, Kubernetes-кластер на «малинах» гораздо лучше передаёт ощущения от эксплуатации, чем тот же самый Minikube, потому что все компоненты кластера и устанавливаются, и работают «по-взрослому».

В перспективе есть идея добавить к кластеру весь цикл CI/CD, реализованный полностью на Raspberry Pi. А также я буду рад, если кто-то поделится своим опытом по настройке K8s на AWS Graviton’ах.

P.S. Да, «production» может быть ближе, чем я думал:

hojgfhu2-egnp-hkyijkydcydfw.jpeg

P.P. S.


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

© Habrahabr.ru