Usando Ngnix-Controller com MetalLB no meu Home Lab
Published: 2023-11-19 17:12:29

Fala pessoal! Estou estudando Kubernetes (Fazendo o sensacional PICK da LinuxTips) e resolvi compartilhar o setup que preparei para o meu home lab.

Meu objetivo é ter o Nginx Controller balanceando as requisições para os meus serviços e diponível num FQDN (Full Qualified Domain Name) que eu separei pra ele.

Os manifestos usados na configuração podem ser achados [aqui.](https://github.com/Isadoramenezes/metallb-lab)

Esse post está divido em:

-  Setup Inicial
-  Conceitos Importantes
-  Tipos de Serviço
-  O que é ingress (com foco no Nginx Controller)
-  O que é Metal LB
-  Configurando tudo

## Setup Inicial

Inicialmente meu cluster tem 1 Control Plane e 2 Workers, os três estão rodando na AWS, usando `Ubuntu 22.04`, em instâncias `t3.medium`e possuem IP's públicos efêmeros (eles mudam toda vez que eu reincio essas máquinas).

Eu segui uma instalação básica do K8S, e me guiei principalmente pelo [Descomplicando o Kubernetes](https://livro.descomplicandokubernetes.com.br/pt/day_five/#instala%C3%A7%C3%A3o-de-um-cluster-kubernetes).

## Conceitos Importantes
Disclaimer: Se, assim como eu você está começando a estudar o Kubernetes, ou se quer refrescar alguns conceitos, os próximos tópicos são pra você. Caso você já tenha uma boa ideia de toda a base, eu sugiro pular para o tópico "Configurando Tudo".


### Tipos de Serviço

Antes de mais nada, um serviço no Kubernetes é uma forma de expor uma aplicação que esteja rodando num Pod na rede, de forma que se um Pod for recriado, ou se mais Pods de uma aplicação forem criados, todas as instâncias sejam acessíveis através do mesmo "canal".

Quando um serviço é criado (seja através do kubectl create svc ou do kubectl expose deploy), um objeto chamado "Endpoint" é criado junto. Esse objeto mantém a lista com os IPs de todos os Pods ativos rodando aquele determinado serviço. Quando um Pod é criado ou destruído a lista é atualizada.

Por exemplo, na imagem abaixo 2 Pods com App-A e App-B estão rodando. É importante relembrar que as aplicaçoes dentro desses Pods **compartilham recursos** como o endereço IP. Ao criar um serviço para expor a App-A, o Kubernetes identifica os endereços de todos os Pods rodando a aplicação (através das labels e selectors) e cria o serviço apontando para esses Pods. Para acessar a App-A de dentro do cluster nesse caso, é possível executar um `curl http://10.100.100.100` e a requisição será encaminhada para qualquer dos Pods disponívels.


![Diagrama com um retangulo para representar o Service em cima de dois retangulos para representar os Pods. O retangulo do Service tem as informações: APP-A-SVC, ClusterIP 10.100.100.100,Endpoints [10.5.6.7:80, 10.5.6.8:80]. O primeiro retangulo para representar um Pod tem Pod 1, IP: 10.5.6.7 e dois quadrados com "App-A, porta 80" e "App-B, porta 5555". O segundo retângulo para representar um Pod contem Pod 2, IP: 10.5.6.8 e dois quadrados com "App-A, porta 80" e "App-B, porta 5555"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0drrpi8erg2ga0vezlj.png)

Finalmente, os serviços podem ser de quatro tipos:

1. **Cluster IP**: Esse é o tipo padrão de serviços, ele expõe a aplicação apenas dentro do cluster.
2. **Load Balancer**: Expõe a aplicação através de um load balancer, com IP fixo fora do cluster.
3. **Node Port**: Torna a aplicação acessível atravpes de uma porta específica em cada nó do cluster. Permite o acesso externo ao cluster.
4. **External Name**: É uma exceção aos demais, invés de usar labels para definir os endpoints, usa DNS. É usado para apontar para serviços externos ao cluster.


### O que é um Ingress?

O Ingress é um objeto no Kubernetes que ajuda no gerenciamento de acesso aos serviços. Através dele é possível definir regras de acesso, rotas, SSL certificados (com integrações) e outras coisas mais.

Para funcionar, o Ingress requer um Ingress Controller, que não é criado na instalação padrão no Kubernetes. É através do controller que toda a configuração do Ingress será feita, e há várias opções disponíveis. Aqui [nessa página](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) você pode checar a lista completa.

Um dos controllers mais utilizados (e um dos três suportados oficialmente projeto Kubernetes) é o [Nginx-Controller](https://docs.nginx.com/). Ele é uma implementação do Nginx e pode fazer o balancemaento de requisições WebSocket, gRPC, TCP e UDP, além de suportar o redirecionamento de rotas e terminação TLS/SSL. 

### O que é MetalLB:

O MetalLB é uma implementação de LoadBalancer para Kubernetes que rodam em bare-metal (fora da cloud). Ele é capaz de alocar IP's para serviços do tipo LoadBalancer e essa é a principal característica que eu vou utilizar no meu HomeLab.

É importante ressaltar que o MetalLB não _cria_ os IP's, ele apenas faz a alocação de IP's baseado em ranges definidos pelo usuário.


## Configurando tudo

Eu sugiro começar a configuração pelo MetalLB

Minha configuração final é a seguinte:

O guia completo da Instalação e Configuração pode ser encontrado [aqui](https://metallb.universe.tf/installation/).

O primeiro passo para a instalação é habilitar o modo 'strict ARP':

```
kubectl edit configmap -n kube-system kube-proxy
``` 

```
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
  strictARP: true
```

Após sair salvando (`:wq!`), é hora de fazer a instalção do MetalLB:


```
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml
```

Esse comando vai criar o namespace `metallb-system` com um controller no control-plane e um speaker em cada nó do cluster. Após todos os pods estarem rodando, alguma configuração é necessária:

ipaddrpool.yml
```yml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: k8s-lab
  namespace: metallb-system
spec:
  addresses:
  #No meu caso esses são os IPs que eu tenho disponíveis, caso queira aplicar no seu ambiente, atualize esses IPs
  - 54.152.233.228/32 
  - 54.167.93.192/32

```
Como eu estou usando a configuração via ARP, também é necessário criar um L2Advertsiment:

L2Advertsiment.yml
```yml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: k8s-lab
  namespace: metallb-system
spec:
  ipAddressPools:
  - k8s-lab

```
E então:

```
k apply -f ipaddrpool.yml
k apply -f L2Advertsiment.yml
```
Checando se tudo está criado:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/46cjhk5lg3sfpy0edfv3.png)

A configuração do MetalLB está feita, para validar, precisamos criar um serviço. Optei pelo bom e velho Nginx:

nginx-deployment.yml
```yml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 0.5
            memory: 256Mi
          requests:
            cpu: 0.3
            memory: 64Mi
```
```
k apply -f nginx-deployment.yml
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z1ah8rp8dgw7yk0nuq3v.png)


A forma mais rápida de validar se o MetalLB está sendo capaz de fornecer IPs para os serviços, é expondo o deploy que acabamos de criar utilizando o serviço do tipo loadBalancer:

```
kubectl expose deploy nginx --port 80 --target-port 80 --type LoadBalancer
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxifddzqd9i0iihcpwtc.png)

E com um `curl 54.167.93.192` recebemos a mensagem padrão do Nginx.


Okay, o MetalLB está funcionando e é possível acessar o serviço através do IP de dentro do cluster, mas eu quero que esse serviço esteja disponível no meu FQDN `k8s-lab.ibmenezes.com`. É hora de installar o nginx-controller.

O getting started dessa instalação pode ser encontrado [aqui](https://kubernetes.github.io/ingress-nginx/deploy/).

Como estamos em uma instalação 'bare-metal', a instalação será:
```
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/baremetal/deploy.yaml
```

Após alguns segundos (talvez minutos) temos o pod do ingress-controller rodando no Control Plane. Além disso, o serviço do ingress-controler é criado no namespace `ingress-nginx`, e o External IP é atribuído pelo MetalLB:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xj99xjzghf9hcmid4a80.png)

Nesse momento já é possível acessar o ingress controller no browser com https://54.152.233.228:31333/. Esse acesso retorna um 503 porque não existe nenhum service associado a ingress nesse momento.

Para corrigir isso, a criação do ingress:

ingress.yml
```yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: k8s-lab
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/issuer: "letsencrypt-staging"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - k8s-lab.ibmenezes.com
    secretName: "k8s-lab-tls"
  rules:
  - host: k8s-lab.ibmenezes.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx
            port:
              number: 80
```

Aqui algumas coisas são importantes:
A primeira annotation `nginx.ingress.kubernetes.io/rewrite-target: /` determina que todas as vezes que uma requisição for encaminhada para a `/`, ela será redirecionada ao path (que falaremos mais abaixo).

A segunda anotação, `cert-manager.io/issuer: "letsencrypt-staging"` é associada à criação de certificados SSL usando o letsencrypt. Nesse setup não iremos a fundo nisso, na próxima sessão explico os motivos. O bloco `tls` também está associado ao certificado.

Já nas especificações, o bloco de rules vai determinar:
```
- host: k8s-lab.ibmenezes.com -> O FQDN onde o ingress espera ouvir a requisição 
http.paths.path -> O caminho da applicação para onde o trafego será redirecionado quando o usuário digitar `/` (da annotation)
http.paths.pathType ->  O tipo de path, esse valor é requerido quando o tipo é Prefix ou Exact
http.paths.backend -> as configurações do serviço para o qual o ingress estará apontando
```

Quase tudo pronto, alguns detalhes finais:

É importante remover o serviço que criamos anteriormente:

```
kubectl delete svc nginx
```

Isso vai liberar o IP anteriormente atribuido ao serviço e prevenir que a gente valide o serviço errado. Antes de criar o ingress é importante criar o serviço definido em http.paths.backend. Como eu defini o meu se chamando 'nginx', me basta:

```
kubectl expose deploy nginx --port 80
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/99m2sbv42ho54hy9a9ld.png)

Para criar o ingress a partir do manifesto criado anteriormente basta executar: 

```
kubectl apply -f ingress.yml
```

Com um describe é possível verificar que o ingress recebe o mesmo IP do ingress-controller recebeu anteriormente.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pt50bhqsgqx1n0bfbkku.png)

A última configuração necessária é no provedor de DNS, no meu caso eu criei uma entrada tipo A apontando para o IP do ingress.

Para validar o balanceamento, editei o index.html de cada Pod:

```
k exec -it nginx-f888bbf65-j94fk /bin/bash
echo "Eu sou o pod nginx-f888bbf65-j94fk" > /usr/share/nginx/html/index.html
```

```
k exec -it nginx-f888bbf65-tcmvz/bin/bash
echo "Eu sou o pod nginx-f888bbf65-tcmvz" > /usr/share/nginx/html/index.html
```

E, acessando 'https://k8s-lab.ibmenezes.com:31333' pelo browser temos:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gopryzq377l71qusan5k.png)



Resumindo como tudo funciona de forma superficial:

1. Eu tenho uma entrada DNS 'k8s-lab.ibmenezes.com' do tipo A, aonde eu precisei atualizar o IP de destino;
2. A lista de IP's disponíveis para a virtualização foi feita a partir dos IP's públicos das minhas máquinas na AWS e passada para o MetalLB;
3. O MetalLB forneceu um dos IP's disponíveis para o nginx-controller
4. Ta pronto o sorvetinho! Agora eu consigo acessar meus serviços externamente.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d4ozti37ywbzo6razney.png)