---
# System prepended metadata

title: Multipass e Kubespray
tags: [linux, setup, k8s]

---

# Multipass e Kubespray

O [multipass](https://multipass.run/) é uma ferramenta excelente para realizar testes que precisam simular máquinas virtuais, pois é mais simples e rápido de inicializar em comparação com o VirtualBox ou lxc/lxd.

Nesse documento vamos utilizar o Multipass para realizar a instalação de um cluster kubernetes utilizando o [kubespray](https://kubespray.io/#/).

## Multipass

### Instalação do multipass

Se estiver usando um ambiente baseado em arch-linux, pode utilizar o comando abaixo. Para outras opções, acesse a [documentação oficial](https://multipass.run/install).

```shell=
yay -S canonical-multipass
```

Após a instalação, provavelmente será necessário habilitar e inicializar o serviço.

```shell=
# Execute o comando abaixo para verificar se o serviço está ativo e rodando, provavelmente não estará
$ systemctl status multipassd
○ multipassd.service - Multipass is a mini-cloud on your workstation using native hypervisors
     Loaded: loaded (/usr/lib/systemd/system/multipassd.service; disabled; preset: disabled)
     Active: inactive (dead)
     
# Habilite e inicie o serviço:
$ sudo systemctl enable --now multipassd
Created symlink /etc/systemd/system/multi-user.target.wants/multipassd.service → /usr/lib/systemd/system/multipassd.service.

# Execute o status novamente pra validar se está ok
$ systemctl status multipassd
● multipassd.service - Multipass is a mini-cloud on your workstation using native hypervisors
     Loaded: loaded (/usr/lib/systemd/system/multipassd.service; enabled; preset: disabled)
     Active: active (running) since Fri 2024-05-31 15:28:24 -03; 8s ago
   Main PID: 32822 (multipassd)
      Tasks: 45 (limit: 77040)
     Memory: 120.7M (peak: 121.4M)
        CPU: 424ms
     CGroup: /system.slice/multipassd.service
             ├─32822 /usr/bin/multipassd
             └─32934 /usr/bin/dnsmasq --keep-in-foreground --strict-order --bind-interfaces --pid-file --domain=multipass --local=/multipass/ --except-interface=lo --interface=mpqemubr0 --listen-address=10.183.57.1 --dhcp-no-overri>

mai 31 15:28:24 roberto-desktop dnsmasq-dhcp[32934]: read /var/lib/multipassd/.local/share/multipassd/network/dnsmasq.hosts
mai 31 15:28:24 roberto-desktop multipassd[32822]: # Warning: iptables-legacy tables present, use iptables-legacy to see them
mai 31 15:28:24 roberto-desktop multipassd[32822]: # Warning: iptables-legacy tables present, use iptables-legacy to see them
mai 31 15:28:24 roberto-desktop multipassd[32822]: # Warning: iptables-legacy tables present, use iptables-legacy to see them
mai 31 15:28:24 roberto-desktop multipassd[32822]: # Warning: iptables-legacy tables present, use iptables-legacy to see them
mai 31 15:28:24 roberto-desktop multipassd[32822]: Using iptables-legacy for firewall rules.
mai 31 15:28:24 roberto-desktop multipassd[32822]: gRPC listening on unix:/run/multipass_socket
mai 31 15:28:24 roberto-desktop multipassd[32822]: Starting Multipass 1.13.0-dev.0+g55d5eed55
mai 31 15:28:24 roberto-desktop multipassd[32822]: Daemon arguments: /usr/bin/multipassd
```

Execute o comando abaixo para validar se está tudo ok.

```shell=
$ multipass list
```

### Criação do cluster

Agora nós vamos criar um cluster com 3 nós para simular um ambiente de alta disponibilidade do kubernetes.
Para que o cluster seja inicializado sem problemas, é necessário configurar pelo menos 8GB de memória RAM para cada instância.

```shell=
multipass launch 24.04 --name mp1 --memory 10G --disk 30G --cpus 4
multipass launch 24.04 --name mp2 --memory 10G --disk 30G --cpus 4
multipass launch 24.04 --name mp3 --memory 10G --disk 30G --cpus 4
```

### Criação do usuário kubespray

Vamos criar um usuário específico para o kubespray realizar a instalação. Como esse usuário precisará executar comandos de root, vamos adicioná-lo no grupo sudoers.

```shell=
multipass exec mp1 -- \
sudo useradd --base-dir /home \
--shell /bin/bash \
--gid 0 \
--create-home \
--password $(perl -e 'print crypt(kubespray, "password")') \
kubespray

multipass exec mp2 -- \
sudo useradd --base-dir /home \
--shell /bin/bash \
--gid 0 \
--create-home \
--password $(perl -e 'print crypt(kubespray, "password")') \
kubespray

multipass exec mp3 -- \
sudo useradd --base-dir /home \
--shell /bin/bash \
--gid 0 \
--create-home \
--password $(perl -e 'print crypt(kubespray, "password")') \
kubespray

# Adiciona o usuário no grupo sudo
multipass exec mp1 -- \
sudo usermod -a -G sudo kubespray

multipass exec mp2 -- \
sudo usermod -a -G sudo kubespray

multipass exec mp3 -- \
sudo usermod -a -G sudo kubespray
```

```shell=
# Talvez precise adicionar o usuário kubespray no sudoers para permitir comandos root sem pedir a senha
$ echo "kubespray ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/91-kubespray
```

### Configuração do ssh

O kubespray (ansible) acessa as máquinas via ssh, por isso é necessário configurar o acesso a partir do usuário `kubespray` que criamos no passo anterior.

```shell=
# mp1
multipass exec mp1 -- sudo sed -ri 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config.d/60-cloudimg-settings.conf
multipass exec mp1 -- sudo sed -ri 's/#PasswordAuthentication yes/PasswordAuthentication yes/g' /etc/ssh/sshd_config
multipass exec mp1 -- sudo systemctl reload ssh

# mp2
multipass exec mp2 -- sudo sed -ri 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config.d/60-cloudimg-settings.conf
multipass exec mp2 -- sudo sed -ri 's/#PasswordAuthentication yes/PasswordAuthentication yes/g' /etc/ssh/sshd_config
multipass exec mp2 -- sudo systemctl reload ssh

# mp3
multipass exec mp3 -- sudo sed -ri 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config.d/60-cloudimg-settings.conf
multipass exec mp3 -- sudo sed -ri 's/#PasswordAuthentication yes/PasswordAuthentication yes/g' /etc/ssh/sshd_config
multipass exec mp3 -- sudo systemctl reload ssh

#PermitRootLogin prohibit-password
```

Para testar a conexão ssh, execute o comando abaixo

```shell=
# mp1
ssh kubespray@$(multipass info --format json mp1 | jq -r .info.[].ipv4[0])

# mp2
ssh kubespray@$(multipass info --format json mp2 | jq -r .info.[].ipv4[0])

# mp3
ssh kubespray@$(multipass info --format json mp3 | jq -r .info.[].ipv4[0])
```

Todos os comandos executas acima foram concentrados no arquivo abaixo para facilitar a execução.

```shell=
$ cat multipass-create.sh
#!/bin/bash

name=${1}

if [[ -z ${name} ]]; then
  echo "Invalid name: '${name}'"
  exit 1
fi

echo "Creating multipass with name ${name}"

# multipass launch
multipass launch 24.04 --name ${name} --memory 10G --disk 30G --cpus 4

# kubespray user
multipass exec ${name} -- \
sudo useradd --base-dir /home \
--shell /bin/bash \
--gid 0 \
--create-home \
--password $(perl -e 'print crypt(kubespray, "password")') \
kubespray

# add kubespray user to sudoers
multipass exec ${name} -- sudo usermod -a -G sudo kubespray

# Configure ssh to allow ssh with password
multipass exec ${name} -- sudo sed -ri 's/PasswordAuthentication no/PasswordAuthentication yes/g' /etc/ssh/sshd_config.d/60-cloudimg-settings.conf
multipass exec ${name} -- sudo sed -ri 's/#PasswordAuthentication yes/PasswordAuthentication yes/g' /etc/ssh/sshd_config
multipass exec ${name} -- sudo systemctl reload ssh

# Connect via ssh
ssh kubespray@$(multipass info --format json ${name} | jq -r .info.[].ipv4[0])
```

Execute passando o nome da instância, por exemplo:
```shell=
./multipass-create.sh mp1
```

## Kubespray

O kubespray pode ser executado a partir de qualquer local que possua acesso as máquinas que serão utilizadas para a instalação. Para mais detalhes [veja](https://kubespray.io/#/docs/getting_started/setting-up-your-first-cluster?id=set-up-kubespray).

```shell=
git clone https://github.com/kubernetes-sigs/kubespray.git
cd kubespray

# Veja a página de releases para pegar a última versão
# https://github.com/kubernetes-sigs/kubespray/releases
git checkout release-2.25
```

<!-- Vamos criar um ambiente virtual python para instalar as dependências.

```shell=
conda create --name tce2-kubespray python=3.9 --yes
conda activate tce2-kubespray
pip install -r requirements.txt
``` -->

Agora nós vamos criar nosso arquivo de configuração baseado no arquivo `sample`.

```shell=
cp -rfp inventory/sample inventory/multipass
```

O comando abaixo vai configurar os IPs no arquivo `inventory/multipass/hosts.yaml`.

```shell=
declare -a IPS=$(multipass info --format json | jq -r .info.[].ipv4[0] | grep -v null | tr "\n" " ")
CONFIG_FILE=inventory/multipass/hosts.yaml python contrib/inventory_builder/inventory.py $IPS[@]
```

Observe que o comando acima não funciona com o `fish`, caso esteja usando, execute o comando `bash` antes do `declare`.
Faça um cat no arquivo `inventory/multipass/hosts.yaml` pra ver como ficou, abaixo está um exemplo.

```shell=
$ cat inventory/multipass/hosts.yaml
all:
  hosts:
    node1:
      ansible_host: 10.183.57.178
      ip: 10.183.57.178
      access_ip: 10.183.57.178
    node2:
      ansible_host: 10.183.57.7
      ip: 10.183.57.7
      access_ip: 10.183.57.7
    node3:
      ansible_host: 10.183.57.85
      ip: 10.183.57.85
      access_ip: 10.183.57.85
  children:
    kube_control_plane:
      hosts:
        node1:
        node2:
    kube_node:
      hosts:
        node1:
        node2:
        node3:
    etcd:
      hosts:
        node1:
        node2:
        node3:
    k8s_cluster:
      children:
        kube_control_plane:
        kube_node:
    calico_rr:
      hosts: {}
```

<!-- Nós precisamos editar o arquivo `inventory/multipass/group_vars/k8s_cluster/k8s-cluster.yml` para adicionar os IPs dos controllers para que depois seja possível conectar no cluster a partir de outros nós.

Abaixo é um exemplo de como ficou essa configuração.
```yaml=
## Supplementary addresses that can be added in kubernetes ssl keys.
## That can be useful for example to setup a keepalived virtual IP
supplementary_addresses_in_ssl_keys: [10.183.57.178, 10.183.57.7, 10.183.57.85]
``` -->

### sshpass

Antes de executar o playbook do ansible, garanta que o utilitário `sshpass` está instalado.

```shell=
yay -S sshpass
```

Comando para fazer a instalação. Observe o parâmetro `--extra-vars`, que nós passamos a senha criada para o usuário `kubespray`.

```shell=
# Não se esqueça de habilitar o environment que foi usado para instalar o kubespray/ansible
$ conda activate tce2-kubespray

$ ansible-playbook --inventory inventory/multipass/hosts.yaml \
--user kubespray \
--become --verbose \
--extra-vars "ansible_password=kubespray ansible_sudo_pass=kubespray" \
cluster.yml
```

## Após instalação

Para poder acessar o cluster, é necessário utilizar o arquivo de configuração, o comando abaixo disponibiliza um arquivo no local padrão.

```shell=
# Entre em algum node do multipass
$ multipass exec mp1 -- bash

mkdir ~/.kube
sudo cp /etc/kubernetes/admin.conf ~/.kube/config
sudo chown $USER ~/.kube/config
```

Lembrando que esse arquivo só estará disponível nos servidores que são do tipo `control-plane`.
Execute o comando abaixo para validar o cluster.

```shell=
alias k=kubectl

$ k get nodes -o wide
NAME    STATUS   ROLES           AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE           KERNEL-VERSION     CONTAINER-RUNTIME
node1   Ready    control-plane   40m   v1.29.5   10.183.57.178   <none>        Ubuntu 24.04 LTS   6.8.0-31-generic   containerd://1.7.16
node2   Ready    control-plane   39m   v1.29.5   10.183.57.7     <none>        Ubuntu 24.04 LTS   6.8.0-31-generic   containerd://1.7.16
node3   Ready    <none>          39m   v1.29.5   10.183.57.85    <none>        Ubuntu 24.04 LTS   6.8.0-31-generic   containerd://1.7.16
```

Por padrão, o `container runtime` utilizado é o cri-o, portanto o utilitário de linha de comando é o `crictl`. Você pode configurar um alias para o comando docker, caso esteja mais habituado a ele.

```shell=
# É necessário dar permissão ao socket do containerd.
$ sudo chmod 777 /var/run/containerd/containerd.sock

$ crictl ps
CONTAINER           IMAGE               CREATED             STATE               NAME                      ATTEMPT             POD ID              POD
3e8c29870f5a4       e579eb50f57be       32 minutes ago      Running             kube-scheduler            2                   51e864b9b391f       kube-scheduler-node1
372695cc2813b       2242ad7f7c41a       32 minutes ago      Running             kube-controller-manager   2                   291a7d7168628       kube-controller-manager-node1
4ad009ded79e3       59d295ba73230       42 minutes ago      Running             node-cache                0                   5bfe47d64d882       nodelocaldns-8xr4z
8ccaa17273a96       cbb01a7bd410d       42 minutes ago      Running             coredns                   0                   860bc98cee928       coredns-69db55dd76-vkxbx
d33407fe62c4d       5c6ffd2b2a1d0       42 minutes ago      Running             calico-node               0                   b8b6941557a32       calico-node-x75kn
93de5e2096273       2019bbea5542a       42 minutes ago      Running             kube-proxy                0                   f78c312d7e10f       kube-proxy-6gbsk
26c7d93f61e2f       b36112597a5f1       43 minutes ago      Running             kube-apiserver            0                   d1a7d37df66bb       kube-apiserver-node1

$ alias docker=crictl

$ docker ps
CONTAINER           IMAGE               CREATED             STATE               NAME                      ATTEMPT             POD ID              POD
3e8c29870f5a4       e579eb50f57be       32 minutes ago      Running             kube-scheduler            2                   51e864b9b391f       kube-scheduler-node1
372695cc2813b       2242ad7f7c41a       32 minutes ago      Running             kube-controller-manager   2                   291a7d7168628       kube-controller-manager-node1
4ad009ded79e3       59d295ba73230       42 minutes ago      Running             node-cache                0                   5bfe47d64d882       nodelocaldns-8xr4z
8ccaa17273a96       cbb01a7bd410d       42 minutes ago      Running             coredns                   0                   860bc98cee928       coredns-69db55dd76-vkxbx
d33407fe62c4d       5c6ffd2b2a1d0       42 minutes ago      Running             calico-node               0                   b8b6941557a32       calico-node-x75kn
93de5e2096273       2019bbea5542a       43 minutes ago      Running             kube-proxy                0                   f78c312d7e10f       kube-proxy-6gbsk
26c7d93f61e2f       b36112597a5f1       44 minutes ago      Running             kube-apiserver            0                   d1a7d37df66bb       kube-apiserver-node1
```

Abaixo tem alguns comandos para iniciar um deployment e um service para validar melhor o funcionamento.
Esses comandos foram extraídos da [documentação oficial](https://kubernetes.io/docs/tutorials/hello-minikube/) do k8s.

```shell=
# Cria um deployment
$ kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080
deployment.apps/hello-node created

# Lista os pods
$ kubectl get pods -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP            NODE
hello-node-ccf4b9788-xzc2z   1/1     Running   0          13s   10.233.71.3   node3

# Cria um service
$ kubectl expose deployment hello-node --port=8080
service/hello-node exposed

# Lista os services
$ kubectl get svc
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
hello-node   ClusterIP   10.233.42.89   <none>        8080/TCP   24s
kubernetes   ClusterIP   10.233.0.1     <none>        443/TCP    50m

# Abaixo estão todas as possibilidades de acesso ao serviço
# A partir do nome do serviço
$ curl -w '\n' http://hello-node:8080
NOW: 2024-06-01 22:21:41.489750998 +0000 UTC m=+258.170006187

# A partir do IP do service
$ curl -w '\n' http://10.233.42.89:8080
NOW: 2024-06-01 22:22:27.379604952 +0000 UTC m=+304.059860151

# A partir do IP do pod
$ curl -w '\n' http://10.233.71.3:8080
NOW: 2024-06-01 22:23:32.921088652 +0000 UTC m=+369.601343851
```

É possível acessar o serviço de fora do cluster kubernetes, vamos testar utilizando o serviço do tipo `NodePort`.

```shell=
# Vamos recriar o service com o novo tipo
$ kubectl delete svc hello-node
service "hello-node" deleted

$ kubectl expose deployment hello-node --type=NodePort --port=8080
service/hello-node exposed

$ kubectl get svc hello-node
NAME         TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
hello-node   NodePort   10.233.27.128   <none>        8080:32047/TCP   18s
```

Observe que o service agora está listado com type=NodePort e a coluna port(s) contém um outro valor após o `8080`, nesse caso a porta 32047, essa porta será utilizada para acessar o serviço de fora do cluster.

```shell=
$ curl -w '\n' http://10.183.57.85:32047
NOW: 2024-06-01 22:28:36.225014115 +0000 UTC m=+672.905269304
```

O IP utilizado acima é o do `node3` do cluster.

### etcd

Caso você queira validar o cluster de etcd, utilize o comando abaixo como exemplo:
```shell=
etcdctl member list \
--endpoints 10.183.57.141:2379 \ # IP do pod que tem o etcd
--cert /etc/kubernetes/ssl/etcd/server.crt \
--key /etc/kubernetes/ssl/etcd/server.key \
--cacert /etc/kubernetes/ssl/etcd/ca.crt
```

## Clean up

```shell=
$ multipass delete mp1 mp2 mp3
$ multipass purge
```