Kubernetes completo desde cero en Raspberry Pi

Kubernetes completo desde cero en Raspberry Pi

Recientemente, una conocida empresa anunció que trasladará su línea de laptops a la arquitectura ARM. Al escuchar esta noticia, recordé que al revisar los precios de EC2 en AWS, noté los Graviton con un precio muy atractivo. La trampa, por supuesto, era que era ARM. En ese momento, no se me pasó por la cabeza que ARM es algo bastante serio...

Para mí, esta arquitectura siempre había sido territorio de dispositivos móviles y otros gadgets de IoT. Los "servidores reales" en ARM son un poco inusuales, incluso un poco extraños... Sin embargo, esta nueva idea se me quedó en la cabeza, así que en uno de los fines de semana decidí comprobar qué se puede ejecutar hoy en ARM. Y para ello decidí comenzar con algo cercano y familiar: un clúster de Kubernetes. No simplemente algún "clúster" circunstancial, sino uno "serio", para que fuera lo más similar posible a cómo estoy acostumbrado a verlo en producción.

En mi idea, el clúster debe ser accesible desde Internet, debe ejecutar alguna aplicación web y también debe tener al menos monitoreo. Para realizar esta idea, se necesitarán un par (o más) de Raspberry Pi al menos del modelo 3B+. El entorno para experimentos podría ser AWS, pero me interesaba especialmente la "frambuesa" (que de todos modos estaba sin uso). Entonces, desplegaremos en ellas un clúster de Kubernetes con Ingress, Prometheus y Grafana.

Preparación de las "frambuesas"

Instalación del SO y SSH

No me compliqué mucho con la elección del SO para la instalación: simplemente tomé la versión más reciente de Raspberry Pi OS Lite con el sitio oficial. Allí también está disponible la documentación de instalación, todas las acciones que deben realizarse en todos los nodos del futuro clúster. A continuación, serán necesarias las siguientes manipulaciones (también en todos los nodos).

Conectando un monitor y teclado, es necesario configurar previamente la red y SSH:

  1. Para que el clúster funcione, el maestro debe tener una dirección IP estática, y en los nodos de trabajo, a consideración. Preferí direcciones estáticas en todas partes por razones de conveniencia en la configuración.
  2. La dirección estática se puede configurar en el SO (en el archivo /etc/dhcpcd.conf hay un ejemplo adecuado) o fijando el lease en el servidor DHCP del enrutador usado (en mi caso, doméstico).
  3. El servidor SSH se activa simplemente en raspi-config (opciones de interfacing → ssh).

Después de esto, ya se puede iniciar sesión por SSH (por defecto, el nombre de usuario es pi, y la contraseña es raspberry o el que se cambió) y continuar con la configuración.

Otras configuraciones

  1. Establezcamos el nombre del host. En mi ejemplo se utilizarán pi-control y pi-worker.
  2. Verificaremos que el sistema de archivos está expandido a todo el disco (df -h /). Si es necesario, se puede ampliar usando raspi-config.
  3. Cambiaremos la contraseña del usuario por defecto en raspi-config.
  4. Desactivaremos el archivo swap (esto es un requisito de Kubernetes; si te interesa más sobre este tema, consulta issue #53533):
    dphys-swapfile swapoff
    systemctl disable dphys-swapfile
  5. Actualizaremos los paquetes a las últimas versiones:
    apt-get update && apt-get dist-upgrade -y
  6. Instalaremos Docker y paquetes adicionales:
    apt-get install -y docker docker.io apt-transport-https curl bridge-utils iptables-persistent

    Al instalar iptables-persistent será necesario guardar la configuración de iptables para ipv4, y en el archivo /etc/iptables/rules.v4 — añadir reglas a la cadena FORWARD, de esta manera:

    # 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. Solo queda reiniciar.

Ahora todo está listo para la instalación del clúster de Kubernetes.

Instalación de Kubernetes

En esta etapa, dejé deliberadamente de lado todo mi trabajo previo y el de nuestra empresa para automatizar la instalación y configuración del clúster K8s. En su lugar, usaremos la documentación oficial de kubernetes.io (ligeramente complementada con comentarios y abreviaciones).

Agregaremos el repositorio de 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

A continuación, la documentación sugiere instalar el CRI (interfaz de tiempo de ejecución de contenedores). Dado que Docker ya está instalado, avanzamos y instalamos los componentes principales:

sudo apt-get install -y kubelet kubeadm kubectl kubernetes-cni

En la etapa de instalación de los componentes principales, añadí de inmediato kubernetes-cni, que es necesario para el funcionamiento del clúster. Y hay un punto importante: el paquete kubernetes-cni por alguna razón no crea el directorio por defecto para la configuración de interfaces CNI, así que tuve que crearlo manualmente:

mkdir -p /etc/cni/net.d

Para el funcionamiento del backend de red, del que hablaremos más adelante, es necesario instalar un plugin para CNI. Elegí el plugin portmap, que me resulta familiar y comprensible. (puedes ver la lista completa en la documentación):

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

Configuración de Kubernetes

Nodo con el plano de control

La instalación del propio clúster se realiza de manera bastante sencilla. Y para acelerar este proceso y verificar que las imágenes de Kubernetes están disponibles, se puede ejecutar previamente:

kubeadm config images pull

Ahora realizamos la instalación en sí: inicializamos el plano de control del clúster:

kubeadm init --pod-network-cidr=10.1.0.0/16 --service-cidr=10.2.0.0/16 --upload-certs

Tenga en cuenta que las subredes para los servicios y los pod no deben cruzarse entre sí ni con las redes existentes.

Al final, se mostrará un mensaje indicando que todo está bien y además se sugerirá cómo unir nodos de trabajo al plano de control:

¡Su plano de control de Kubernetes se ha inicializado correctamente!
Para comenzar a usar su clúster, necesita ejecutar lo siguiente como un usuario normal:
 mkdir -p $HOME/.kube
 sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
 sudo chown $(id -u):$(id -g) $HOME/.kube/config
Ahora debe implementar una red de pod en el clúster.
Ejecute "kubectl apply -f [podnetwork].yaml" con una de las opciones listadas en:
 https://kubernetes.io/docs/concepts/cluster-administration/addons/
Ahora puede unir cualquier número de nodos del plano de control ejecutando el siguiente comando en cada uno como root:
 kubeadm join 192.168.88.30:6443 --token a485vl.xjgvzzr2g0xbtbs4 
   --discovery-token-ca-cert-hash sha256:9da6b05aaa5364a9ec59adcc67b3988b9c1b94c15e81300560220acb1779b050 
   --contrl-plane --certificate-key 72a3c0a14c627d6d7fdade1f4c8d7a41b0fac31b1faf0d8fdf9678d74d7d2403
Tenga en cuenta que el certificate-key otorga acceso a datos sensibles del clúster, ¡manténgalo en secreto!
Como medida de seguridad, los certificados subidos se eliminarán en dos horas; si es necesario, puede usar
"kubeadm init phase upload-certs --upload-certs" para volver a cargar los certificados después.
Luego puede unir cualquier número de nodos de trabajo ejecutando lo siguiente en cada uno como root:
kubeadm join 192.168.88.30:6443 --token a485vl.xjgvzzr2g0xbtbs4 
   --discovery-token-ca-cert-hash sha256:9da6b05aaa5364a9ec59adcc67b3988b9c1b94c15e81300560220acb1779b050

Sigamos las recomendaciones para agregar la configuración para el usuario. Y también recomiendo agregar la autocompletación para kubectl desde ya:

 kubectl completion bash > ~/.kube/completion.bash.inc
 printf "
 # Autocompletado en shell para Kubectl
 source '$HOME/.kube/completion.bash.inc'
 " >> $HOME/.bash_profile
 source $HOME/.bash_profile

En esta etapa, ya se puede ver el primer nodo en el clúster (aunque aún no está listo):

root@pi-control:~# kubectl get no
NOMBRE       ESTATUS    ROLES    EDAD   VERSIÓN
pi-control   NotReady   maestro   29s   v1.18.6

Configuración de red

A continuación, como se mencionó en el mensaje después de la instalación, será necesario instalar la red en el clúster. La documentación ofrece elegir entre Calico, Cilium, contiv-vpp, Kube-router y Weave Net… Aquí me desvié de la instrucción oficial y elegí una opción que me resulta más familiar y comprensible: flannel en modo host-gw (más detalles sobre los backends disponibles en la documentación del proyecto).

Instalarlo en el clúster es bastante sencillo. Primero, descargamos los manifiestos:

wget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

Luego cambiamos en la configuración el tipo de vxlan en host-gw:

sed -i 's/vxlan/host-gw/' kube-flannel.yml

… y la subred de los pod — del valor por defecto al que se especificó durante la inicialización del clúster:

sed -i 's#10.244.0.0/16#10.1.0.0/16#' kube-flannel.yml

Después de eso, creamos los recursos:

kubectl create -f kube-flannel.yml

¡Listo! En un momento, el primer nodo K8s cambiará a estado Listo:

NOMBRE       ESTADO   ROLES      EDAD   VERSIÓN
pi-control   Listo    master     2m     v1.18.6

Agregar un nodo de trabajo

Ahora podemos añadir un worker. Para ello, en él — después de instalar Kubernetes según el guion descrito anteriormente — solo hay que ejecutar el comando obtenido anteriormente:

kubeadm join 192.168.88.30:6443 --token a485vl.xjgvzzr2g0xbtbs4 
    --discovery-token-ca-cert-hash sha256:9da6b05aaa5364a9ec59adcc67b3988b9c1b94c15e81300560220acb1779b050

Con esto, podemos considerar que el clúster está listo:

root@pi-control:~# kubectl get no
NOMBRE       ESTADO   ROLES      EDAD    VERSIÓN
pi-control   Listo    master     28m    v1.18.6
pi-worker    Listo      2m8s   v1.18.6

Solo tenía dos Raspberry Pi a mano, así que no quería entregar una de ellas solo para el control plane. Por eso, quité el taint automáticamente instalado del nodo pi-control, ejecutando:

root@pi-control:~# kubectl edit node pi-control

… y eliminé las siguientes líneas:

 - effect: NoSchedule
   key: node-role.kubernetes.io/master

Llenando el clúster con lo mínimo necesario

En primer lugar, necesitaremos Helm. Claro, se puede hacer todo sin él, pero Helm permite configurar algunos componentes a su gusto sin modificar archivos. Y de hecho, es simplemente un archivo binario que "no pide mucho".

Así que, entramos en helm.sh en la sección docs/installation y ejecutamos el comando de allí:

curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

Después de esto, añadimos el repositorio de charts:

helm repo add stable https://kubernetes-charts.storage.googleapis.com/

Ahora instalaremos los componentes de infraestructura de acuerdo con lo planeado:

  • Controlador de Ingress;
  • Prometheus;
  • Grafana;
  • cert-manager.

Controlador de Ingress

El primer componente — Controlador de Ingress — se instala bastante fácilmente y está listo para usar "de inmediato". Para ello, es suficiente con acceder a la sección bare-metal en el sitio web y ejecutar el comando de instalación de allí:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.34.1/deploy/static/provider/baremetal/deploy.yaml

Sin embargo, en ese momento, la "frambuesa" comenzó a estresarse y a llegar al límite de IOPS del disco. La cuestión es que junto con el controlador de Ingress se instalan una gran cantidad de recursos, se realizan muchas solicitudes a la API y, por lo tanto, se escriben muchos datos en etcd. En resumen, o la tarjeta de memoria de clase 10 no es muy eficiente, o las tarjetas SD simplemente no son suficientes para tal carga. Sin embargo, después de unos 5 minutos, todo se puso en marcha.

Se creó un namespace y en él apareció el controlador y todo lo que necesitaba:

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

Los siguientes dos componentes son bastante fáciles de instalar a través de Helm desde el repositorio de charts.

Buscamos Prometheus, creamos el namespace y lo instalamos en él:

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"}

Por defecto, Prometheus solicita 2 discos: uno para los datos de Prometheus y otro para los datos de AlertManager. Como no se ha creado una clase de almacenamiento en el clúster, los discos no se solicitarán y los pod no se iniciarán. Para instalaciones bare metal de Kubernetes, generalmente utilizamos Ceph rbd, sin embargo, para Raspberry Pi esto es evidentemente excesivo.

Por lo tanto, crearemos un simple almacenamiento local en hostpath. Los manifiestos PV (volumen persistente) para prometheus-server y prometheus-alertmanager están combinados en el archivo prometheus-pv.yaml en Los repositorios de Git con ejemplos para el artículo. La carpeta para el PV debe ser creada de antemano en el disco del nodo al que queremos vincular Prometheus: en el ejemplo se especifica nodeAffinity por hostname pi-worker y se han creado directorios en él /data/localstorage/prometheus-server y /data/localstorage/prometheus-alertmanager.

Descargamos (clonamos) el manifiesto y lo añadimos a Kubernetes:

kubectl create -f prometheus-pv.yaml

En esta etapa me encontré por primera vez con el problema de la arquitectura ARM. Kube-state-metrics, que se instala por defecto en el chart de Prometheus, se negó a iniciarse. Mostraba el error:

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"

El problema es que para kube-state-metrics se utiliza una imagen del proyecto CoreOS, que no se compila para 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

Tuve que buscar un poco y encontrar, por ejemplo, esta imagen. Para usarla, actualizaremos la versión, especificando qué imagen usar para 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

Verificamos que todo se haya iniciado:

root@pi-control:~# kubectl -n monitoring get po
NAME                                             READY   STATUS              RESTARTS   AGE
prometheus-alertmanager-df65d99d4-6d27g          2/2     En ejecución       0          5m56s
prometheus-kube-state-metrics-5dc5fd89c6-ztmqr   1/1     En ejecución       0          5m56s
prometheus-node-exporter-49zll                   1/1     En ejecución       0          5m51s
prometheus-node-exporter-vwl44                   1/1     En ejecución       0          4m20s
prometheus-pushgateway-c547cfc87-k28qx           1/1     En ejecución       0          5m56s
prometheus-server-85666fd794-z9qnc               2/2     En ejecución       0          4m52s

Grafana y cert-manager

Para gráficos y dashboards instalamos Grafana:

helm install grafana --namespace monitoring stable/grafana  --set ingress.enabled=true --set ingress.hosts={"grafana.home.pi"}

Al final de la salida nos mostrarán cómo obtener la contraseña de acceso:

kubectl get secret --namespace monitoring grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo

Para solicitar certificados, instalaremos cert-manager. Para su instalación, consultamos a la documentación, que ofrece los comandos correspondientes para 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

Para certificados autofirmados en uso doméstico, esto es suficiente. Sin embargo, si se necesita obtener el mismo Let’s Encrypt, es necesario configurar también el cluster issuer. Puede encontrar detalles sobre esto en nuestro artículo «Certificados SSL de Let’s Encrypt con cert-manager en Kubernetes».

Yo elegí la opción del ejemplo en la documentación, decidiendo que la opción de staging de LE sería suficiente. Alteramos el correo electrónico en el ejemplo, lo guardamos en un archivo y lo añadimos al clúster (cert-manager-cluster-issuer.yaml):

kubectl create -f cert-manager-cluster-issuer.yaml

Ahora se puede solicitar un certificado, por ejemplo, para Grafana. Para ello se requiere un dominio y acceso externo al clúster. Tengo un dominio y configuré el tráfico a través de los puertos 80 y 443 en mi enrutador doméstico en consonancia con el servicio ingresado creado:

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

El puerto 80 en este caso se mapea a 31303, y el 443 a 30498. (Los puertos se generan aleatoriamente, por lo que los suyos serán diferentes.)

Aquí hay un ejemplo de certificado (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

Lo añadimos al clúster:

kubectl create -f cert-manager-grafana-certificate.yaml

Después de eso, aparecerá el recurso Ingress, a través del cual se realizará la validación de 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

Después de que la validación se complete, veremos que el recurso certificate está listo, y en el secreto mencionado anteriormente grafana-tls — certificado y clave. Se puede verificar inmediatamente quién emitió el certificado:

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

Volvamos a Grafana. Necesitaremos hacer algunos ajustes en su lanzamiento de Helm, modificando la configuración para TLS de acuerdo con el certificado creado.

Para ello, descargamos el chart, lo editamos y actualizamos desde un directorio local:

helm pull --untar stable/grafana

Editamos en el archivo grafana/values.yaml los parámetros de TLS:

  tls:
    - secretName: grafana-tls
      hosts:
        - grafana.home.pi

Aquí también podemos configurar Prometheus ya instalado como datasource:

datasources:
  datasources.yaml:
    apiVersion: 1
    datasources:
    - name: Prometheus
      type: prometheus
      url: http://prometheus-server:80
      access: proxy
      isDefault: true

Ahora actualizamos el chart de Grafana desde el directorio local:

helm upgrade grafana --namespace monitoring ./grafana --set ingress.enabled=true --set ingress.hosts={"grafana.home.pi"}

Verificamos que en el Ingress grafana se añadió el puerto 443 y hay acceso por 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=; Path=/; HttpOnly; SameSite=Lax
x-frame-options: deny
strict-transport-security: max-age=15724800; includeSubDomains

Para demostrar Grafana en acción, puedes descargar y agregar un dashboard para kube-state-metrics. Así es como se ve:

Kubernetes completo desde cero en Raspberry Pi

También recomiendo agregar un dashboard para node exporter: mostrará detalladamente lo que está sucediendo con las "frambuesas" (carga de CPU, uso de memoria, red, disco, etc.).

Después de esto, considero que el clúster está listo para recibir y ejecutar aplicaciones.

Nota sobre la instalación

Para construir aplicaciones para la arquitectura ARM, hay al menos dos opciones. Primero, se puede compilar en un dispositivo ARM. Sin embargo, después de observar la actual utilización de dos Raspberry Pi, me di cuenta de que ni siquiera soportarían la compilación. Por eso, me pedí una nueva Raspberry Pi 4 (es más potente y tiene nada menos que 4 GB de memoria) — planeo compilar en ella.

La segunda opción es crear una imagen Docker multicardiaca en una máquina más potente. Para ello, existe la extensión docker buildx. Si la aplicación está escrita en un lenguaje compilado, será necesaria la compilación cruzada para ARM. No describiré todas las configuraciones para este enfoque, ya que eso requeriría un artículo aparte. Al implementar este método, se pueden lograr imágenes "universales": Docker, ejecutándose en una máquina ARM, automáticamente descargará la imagen correspondiente a la arquitectura.

Conclusión

El experimento realizado superó todas mis expectativas: [como mínimo] el Kubernetes "vainilla" con la base necesaria se siente bastante bien en ARM, y durante su configuración surgieron solo un par de inconvenientes.

Las propias Raspberry Pi 3B+ soportan la carga de la CPU, sin embargo, sus tarjetas SD son un evidente cuello de botella. Mis colegas me hicieron notar que en algunas versiones hay la posibilidad de arrancar desde USB, donde se puede conectar un SSD: entonces, probablemente, la situación mejorará.

Aquí hay un ejemplo de carga de CPU al instalar Grafana:

Kubernetes completo desde cero en Raspberry Pi

Para experimentos y «para probar», en mi opinión, un clúster de Kubernetes en las "frambuesas" transmite una mejor sensación de uso que Minikube, porque todos los componentes del clúster se instalan y funcionan "de verdad".

En el futuro, hay una idea de agregar a el clúster todo el ciclo de CI/CD, totalmente implementado en Raspberry Pi. También estaré encantado si alguien comparte su experiencia en la configuración de K8s en AWS Graviton.

P.D. Sí, la "producción" puede estar más cerca de lo que pensaba:

Kubernetes completo desde cero en Raspberry Pi

P.P.D.

También puedes leer en nuestro blog:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster