Kubernetes completo do zero no Raspberry Pi

Kubernetes completo do zero no Raspberry Pi

Recentemente, uma empresa famosa anunciou que estava transferindo sua linha de notebooks para a arquitetura ARM. Ao ouvir essa notícia, lembrei que, ao verificar novamente os preços do EC2 na AWS, prestei atenção nos Gravitons com um preço muito interessante. A pegadinha, claro, era que se tratava de ARM. Naquela época, eu nem imaginava que ARM era algo sério...

Para mim, essa arquitetura sempre foi associada a dispositivos móveis e outras coisinhas IoT. Servidores ‘de verdade’ em ARM pareciam um pouco incomuns, quase estranhos... No entanto, uma nova ideia se alojou na minha cabeça, e então, em um dos fins de semana, decidi verificar o que realmente poderia ser executado em ARM hoje. Para isso, decidi começar com algo próximo e familiar: o cluster Kubernetes. E não apenas qualquer ‘cluster’, mas tudo ‘de forma profissional’, para que fosse o mais parecido possível com o que estou acostumado a ver em produção.

Na minha visão, o cluster deveria ser acessível pela internet, executar um certo aplicativo web e pelo menos ter um monitoramento. Para realizar essa ideia, seria necessário um par (ou mais) de Raspberry Pis, não inferiores ao modelo 3B+. A AWS poderia ser um bom local para a experiência, mas eu estava realmente interessado nas ‘frambuesas’ (que de qualquer forma estavam parada). Portanto, vamos implementar um cluster Kubernetes com Ingress, Prometheus e Grafana.

Preparação das ‘frambuesas’

Instalação do SO e SSH

Quanto à escolha do SO para instalação, não me preocupei muito: simplesmente peguei o mais recente Raspberry Pi OS Lite de site oficial. Também está disponível a documentação de instalação, cujas ações precisam ser executadas em todos os nós do futuro cluster. Em seguida, será necessário realizar as seguintes manipulações (também em todos os nós).

Conectando um monitor e teclado, é necessário pré-configurar a rede e o SSH:

  1. Para o funcionamento do cluster, o mestre deve ter um endereço IP estático, e nos nós de trabalho, isso fica a critério. Eu preferi endereços estáticos em todos os lugares por questões de conveniência de configuração.
  2. Um endereço estático pode ser configurado no SO (no arquivo /etc/dhcpcd.conf há um exemplo adequado) ou fixando o lease no servidor DHCP do roteador usado (no meu caso, o doméstico).
  3. O ssh-server é ativado simplesmente no raspi-config (opções de interface → ssh).

Após isso, já é possível fazer login via SSH (por padrão, o login é pi, e a senha é raspberry ou aquela que foi alterada) e continuar as configurações.

Outras configurações

  1. Vamos configurar o nome do host. No meu exemplo, usaremos pi-control e pi-worker.
  2. Vamos verificar se o sistema de arquivos está expandido para todo o disco (df -h /). Caso seja necessário, ele pode ser expandido utilizando o raspi-config.
  3. Vamos alterar a senha do usuário padrão no raspi-config.
  4. Desativaremos o arquivo de swap (isso é um requisito do Kubernetes; se você está interessado em mais detalhes sobre este assunto, veja issue #53533):
    dphys-swapfile swapoff
    systemctl disable dphys-swapfile
  5. Vamos atualizar os pacotes para as versões mais recentes:
    apt-get update && apt-get dist-upgrade -y
  6. Vamos instalar o Docker e pacotes adicionais:
    apt-get install -y docker docker.io apt-transport-https curl bridge-utils iptables-persistent

    Ao instalar iptables-persistent é necessário salvar as configurações do iptables para ipv4, e no arquivo /etc/iptables/rules.v4 — adicionar regras à cadeia FORWARD, assim:

    # 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. Agora, só precisamos reiniciar.

Agora tudo está pronto para a instalação do cluster Kubernetes.

Instalação do Kubernetes

Neste ponto, deixei de lado todas as minhas e nossas implementações corporativas de automação para a instalação e configuração do cluster K8s. Em vez disso, utilizaremos a documentação oficial com kubernetes.io (ligeiramente complementada com comentários e abreviações).

Vamos adicionar o repositório 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

Em seguida, a documentação sugere instalar o CRI (interface de tempo de execução de contêiner). Como o Docker já está instalado, vamos em frente e instalamos os componentes principais:

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

Na etapa de instalação dos componentes principais, eu já acrescentei kubernetes-cni, que é necessário para o funcionamento do cluster. E aqui há um ponto importante: o pacote kubernetes-cni por algum motivo não cria o diretório padrão para as configurações dos interfaces CNI, então eu tive que criá-lo manualmente:

mkdir -p /etc/cni/net.d

Para o funcionamento do backend de rede, sobre o qual falaremos mais adiante, é necessário instalar o plugin para CNI. Eu escolhi o plugin portmap que já estou acostumado e que me é familiar (veja a lista completa em documentação):

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

Configuração do Kubernetes

Nó com control plane

A instalação do próprio cluster é bastante simples. E para acelerar esse processo e verificar se as imagens do Kubernetes estão disponíveis, podemos executar previamente:

kubeadm config images pull

Agora vamos para a instalação propriamente dita — inicializamos o control plane do cluster:

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

Observe que as sub-redes para serviços e pods não devem se sobrepor entre si e com as redes existentes.

No final, será exibida uma mensagem informando que tudo está bem e também serão fornecidas instruções sobre como conectar os nós de trabalho ao control plane:

Seu control-plane do Kubernetes foi inicializado com sucesso!
Para começar a usar seu cluster, você precisa executar o seguinte como um usuário comum:
 mkdir -p $HOME/.kube
 sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
 sudo chown $(id -u):$(id -g) $HOME/.kube/config
Agora você deve implantar uma rede de pods no cluster.
Execute "kubectl apply -f [podnetwork].yaml" com uma das opções listadas em:
 https://kubernetes.io/docs/concepts/cluster-administration/addons/
Você pode agora juntar qualquer número de nós do control-plane executando o seguinte comando em cada um como root:
 kubeadm join 192.168.88.30:6443 --token a485vl.xjgvzzr2g0xbtbs4 
   --discovery-token-ca-cert-hash sha256:9da6b05aaa5364a9ec59adcc67b3988b9c1b94c15e81300560220acb1779b050 
   --control-plane --certificate-key 72a3c0a14c627d6d7fdade1f4c8d7a41b0fac31b1faf0d8fdf9678d74d7d2403
Por favor, note que a chave do certificado dá acesso a dados sensíveis do cluster, mantenha-a em segredo!
Como medida de segurança, os certificados carregados serão excluídos em duas horas; Se necessário, você pode usar
"kubeadm init phase upload-certs --upload-certs" para recarregar os certificados posteriormente.
Em seguida, você pode juntar qualquer número de nós de trabalho executando o seguinte em cada um como root:
kubeadm join 192.168.88.30:6443 --token a485vl.xjgvzzr2g0xbtbs4 
   --discovery-token-ca-cert-hash sha256:9da6b05aaa5364a9ec59adcc67b3988b9c1b94c15e81300560220acb1779b050

Vamos seguir as recomendações para adicionar a configuração para o usuário. E também recomendo adicionar a conclusão automática para kubectl:

 kubectl completion bash > ~$HOME/.kube/completion.bash.inc
 printf "
 # Completação para o shell do Kubectl
 source '$HOME/.kube/completion.bash.inc'
 " >> $HOME/.bash_profile
 source $HOME/.bash_profile

Nesta etapa, já é possível ver o primeiro nó no cluster (embora ele ainda não esteja pronto):

root@pi-control:~# kubectl get no
NAME         STATUS     ROLES    AGE   VERSION
pi-control   NotReady   master   29s   v1.18.6

Configuração da rede

Em seguida, como mencionado na mensagem após a instalação, será necessário configurar a rede no cluster. A documentação sugere opções como Calico, Cilium, contiv-vpp, Kube-router e Weave Net… Aqui, eu me afastei da instrução oficial e escolhi uma opção mais familiar e compreensível para mim: flannel no modo host-gw (mais detalhes sobre os backends disponíveis em documentação do projeto).

Instalá-lo no cluster é bastante simples. Para começar — faça o download dos manifests:

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

Em seguida, altere nas configurações o tipo de vxlan para host-gw:

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

… e a sub-rede dos pods — de seu valor padrão para o que foi especificado durante a inicialização do cluster:

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

Depois disso, crie os recursos:

kubectl create -f kube-flannel.yml

Pronto! Em pouco tempo, o primeiro nó K8s mudará para o status Pronto:

NAME         STATUS   ROLES    AGE   VERSION
pi-control   Ready    master   2m    v1.18.6

Adicionando um nó de trabalho

Agora é possível adicionar um worker. Para isso, após a instalação do próprio Kubernetes conforme descrito acima, basta executar o comando obtido anteriormente:

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

A partir daqui, podemos considerar que o cluster está pronto:

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

Eu só tinha duas Raspberry Pi à disposição, então não queria ceder uma delas apenas para o control plane. Portanto, removi o taint instalado automaticamente do nó pi-control, executando:

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

… e removi as linhas:

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

Preenchendo o cluster com o mínimo necessário

Primeiro, precisaremos de Helm. Claro, é possível fazer tudo sem ele, mas o Helm permite configurar alguns componentes à sua maneira sem editar arquivos. E, na verdade, é apenas um arquivo binário que "não pede nada".

Então, vamos ao site helm.sh na seção docs/installation e executamos o comando de lá:

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

Depois disso, adicionamos o repositório de charts:

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

Agora vamos instalar os componentes de infraestrutura conforme planejado:

  • Ingress controller;
  • Prometheus;
  • Grafana;
  • cert-manager.

Ingress controller

O primeiro componente — Ingress controller — é instalado de forma bastante simples e está pronto para uso "pronto para uso". Para isso, basta acessar a seção bare-metal no site e executar o comando de instalação de lá:

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

No entanto, neste momento, a "framboesa" começou a ficar sobrecarregada e a esbarrar no IOPS do disco. O fato é que junto com o Ingress controller, muitos recursos são instalados, muitas requisições ao API são feitas e, consequentemente, muitos dados são gravados no etcd. Em suma, ou o cartão SD de classe 10 não é tão eficiente, ou os cartões SD não são suficientes para essa carga. De qualquer forma, após cerca de 5 minutos, tudo iniciou.

Um namespace foi criado e nele apareceu o controller e tudo que ele precisava:

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

Os dois componentes a seguir são bastante simples de instalar via Helm a partir do repositório de charts.

Encontramos Prometheus, criamos o namespace e instalamos nele:

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 padrão, o Prometheus solicita 2 discos: um para os dados do próprio Prometheus e outro para os dados do AlertManager. Como a storage class não foi criada no cluster, os discos não serão solicitados e os pods não iniciarão. Para instalações bare metal do Kubernetes, costumamos usar Ceph rbd, porém, no caso do Raspberry Pi, isso é um exagero.

Portanto, criaremos um simples armazenamento local no hostpath. Os manifestos PV (persistent volume) para prometheus-server e prometheus-alertmanager estão juntos no arquivo prometheus-pv.yaml em Os repositórios Git com exemplos para o artigo. O diretório para o PV deve ser criado previamente no disco do nó ao qual queremos vincular o Prometheus: no exemplo, está especificado nodeAffinity pelo hostname pi-worker e nele foram criados os diretórios /data/localstorage/prometheus-server e /data/localstorage/prometheus-alertmanager.

Baixamos (clonamos) o manifesto e adicionamos ao Kubernetes:

kubectl create -f prometheus-pv.yaml

Nesse estágio, enfrentei pela primeira vez o problema da arquitetura ARM. O kube-state-metrics, que é instalado por padrão no chart do Prometheus, se recusou a iniciar. Ele apresentou o erro:

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"

O problema é que o kube-state-metrics usa uma imagem do projeto CoreOS, que não é construída 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

Tive que pesquisar um pouco e encontrei, por exemplo, esta imagem. Para utilizá-la, atualizaremos a versão, especificando qual imagem usar para o 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 se tudo subiu:

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 e cert-manager

Para gráficos e dashboards, instalamos Grafana:

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

No final da saída, vamos mostrar como obter a senha de acesso:

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

Para solicitar certificados, vamos instalar o cert-manager. Para sua instalação, vamos nos referir ao documentação, que oferece os comandos correspondentes para o 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 autoassinados em uso doméstico, isso é mais que suficiente. No entanto, se precisar obter o mesmo Let’s Encrypt, será necessário configurar também o cluster issuer. Detalhes sobre isso podem ser encontrados em nosso artigo "Certificados SSL do Let’s Encrypt com cert-manager no Kubernetes».

Eu optei pela opção do exemplo na documentação, decidindo que a versão staging do LE seria suficiente. Alteramos no exemplo o e-mail, salvamos no arquivo e adicionamos ao cluster (cert-manager-cluster-issuer.yaml):

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

Agora pode-se solicitar um certificado, por exemplo, para o Grafana. Para isso, será necessário um domínio e acesso ao cluster externamente. Eu tenho o domínio, e configurei o tráfego redirecionando as portas 80 e 443 no meu roteador doméstico de acordo com o serviço criado do ingress-controller:

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

A porta 80, neste caso, é transmitida para 31303, enquanto a 443 é transmitida para 30498. (As portas são geradas aleatoriamente, portanto, as suas serão diferentes.)

Aqui está um exemplo 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

Adicionamos-no ao cluster:

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

Depois disso, aparecerá o recurso Ingress, pelo qual a validação será feita pelo 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

Após a validação, veremos que o recurso certificate está pronto, e no segredo mencionado acima, grafana-tls está o certificado e a chave. Pode-se verificar imediatamente quem emitiu o 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

Vamos voltar ao Grafana. Precisamos fazer algumas correções em seu release do Helm, alterando as configurações para TLS de acordo com o certificado criado.

Para isso, baixamos o chart, editamos e atualizamos a partir do diretório local:

helm pull --untar stable/grafana

Editamos o arquivo grafana/values.yaml as configurações de TLS:

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

Aqui também podemos configurar imediatamente o Prometheus instalado como datasource:

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

Agora, atualizamos o chart do Grafana a partir do diretório local:

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

Verificamos se no Ingress grafana foi adicionado a porta 443 e se o acesso via HTTPS está disponível:

root@pi-control:~# kubectl -n monitoring get ing grafana
NOME CLASSE HOSTS ENDEREÇO PORTAS IDADE
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
data: Ter, 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 demonstrar o Grafana em ação, podemos baixar e adicionar um dashboard para kube-state-metrics. Veja como isso fica:

Kubernetes completo do zero no Raspberry Pi

Recomendo também adicionar um dashboard para o node exporter: ele mostrará detalhadamente o que está acontecendo com as "malinas" (carga da CPU, uso de memória, rede, disco, etc.).

Após isso, considero que o cluster está pronto para receber e executar aplicativos!

Nota sobre a construção

Para construir aplicativos para a arquitetura ARM, existem pelo menos duas opções. Primeiro, você pode compilar em um dispositivo ARM. No entanto, olhando para a utilização atual de duas Raspberry Pi, percebi que elas não suportariam a compilação. Portanto, encomendei uma nova Raspberry Pi 4 (ela é mais poderosa e tem 4 GB de memória) — planejo compilar nela.

A segunda opção é a construção de uma imagem Docker multi-arquitetura em uma máquina mais potente. Para isso, existe a extensão docker buildx. Se a aplicação estiver em um linguagem compilada, será necessário fazer a cross-compilação para ARM. Não vou descrever todas as configurações para esse caminho, pois isso renderia um artigo separado. Com essa abordagem, é possível alcançar imagens "universais": o Docker, rodando em uma máquina ARM, irá automaticamente baixar a imagem correspondente à arquitetura.

Conclusão

O experimento realizado superou todas as minhas expectativas: [no mínimo] o Kubernetes "vanilla" com a base necessária se comporta bem em ARM, e durante sua configuração surgiram apenas alguns detalhes.

Os próprios Raspberry Pi 3B+ conseguem lidar com a carga da CPU, no entanto, seus cartões SD são um claro gargalo. Colegas sugeriram que em algumas versões há a possibilidade de inicializar a partir de USB, onde é possível conectar um SSD: então, provavelmente, a situação melhorará.

Aqui está um exemplo de carga da CPU durante a instalação do Grafana:

Kubernetes completo do zero no Raspberry Pi

Para experimentos e "para testar", na minha opinião, um cluster Kubernetes em Raspberry Pi transmite muito melhor a sensação de operação do que o mesmo Minikube, pois todos os componentes do cluster são instalados e funcionam "de maneira real".

A longo prazo, há a ideia de adicionar todo o ciclo de CI/CD ao cluster, totalmente implementado em Raspberry Pi. Eu também ficaria feliz se alguém compartilhasse sua experiência na configuração do K8s em AWS Graviton.

P.S. Sim, o "produção" pode estar mais perto do que eu pensava:

Kubernetes completo do zero no Raspberry Pi

P.P.S.

Confira também em nosso blog:

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster