Desenhando o Kubernetes

Esse texto é uma adaptação livre do texto em inglês do Jérôme Petazzoni. Resolvi reescrever o texto do meu jeito e em português pela simplicidade que o Jérôme mostrou os componentes do kubernetes se comunicando e trabalhando em conjunto e que de alguma forma eu possa guardar tudo do meu jeito.

Esse artigo não é para clusters em produção, é somente um "desenho" de como os componentes do Kubernetes funcionam.

Diagrama de funcionamento do kubernetes

A primeira coisa que eu fiz foi criar um servidor Ubuntu 16 usando o VirtualBox e executei todos os passos desse artigo nesse servidor.

Pegando os binarios

O Kubernetes é um conjunto de diversos componentes. Vamos precisar:

  • Kubernetes, especificamente vamos usar o hyperkube que é um all-in-one de todos os componentes da plataforma
  • Container Runtime, para não sair do padrão vamos usar o Docker, mas existem outros como o cri-o ou o containerd
  • etcd, banco de dados distribuído de chave-valor.

Eu baixei todos os binários na minha home, e apesar do conselho do Jérôme de deixar eles no "bin", eles funcionaram bem dessa forma e ficam mais visuais para mim.

etcd

Precisamos somente dos binarios do etcd e do etcdctl e para isso vamos extrair ele do tar de instalação.

$ ETCD_VER=v3.3.12
$ curl -L https://storage.googleapis.com/etcd/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o etcd-${ETCD_VER}-linux-amd64.tar.gz | tar xzvf etcd-v3.3.12-linux-amd64.tar.gz -C . --strip-components=1 --wildcards -zx '*/etcd' '*/etcdctl'
etcd-v3.3.12-linux-amd64/etcdctl
etcd-v3.3.12-linux-amd64/etcd

Kubernetes

Conforme ja falei antes vamos usar o hyperkube que é um binario all-in-one de todos os componentes do kubernetes que vamos precisar, do estilo do busybox.

$ curl -L https://dl.k8s.io/v1.14.1/kubernetes-server-linux-amd64.tar.gz | tar --strip-components=3 -zx kubernetes/server/bin/hyperkube

Uma boa maneira de usar o kyperkube é criar links simbólicos para que ele seja utilizado para executar as aplicações que vamos precisar.

$ for BINARY in kubectl kube-apiserver kube-scheduler kube-controller-manager kubelet kube-proxy;
do
ln -s hyperkube $BINARY
done

Docker

O Docker é o container runtime mais utilizado para o Kubernetes. Foi o primeiro a popularizar o modelo de desenvolver microserviços em containers e é largamente utilizado pela comunidade, o que significa que tem um monte de documentação sobre ele disponível na internet.

$ curl -L https://download.docker.com/linux/static/stable/x86_64/docker-18.09.5.tgz | tar --strip-components=1 -zx

Começando a brincadeira

Vou executar tudo como root do servidor, não é a melhor pratica, mas para o teste é a forma mais fácil de fazer tudo acontecer.

Iniciando o etcd.

$ ./etcd

Com esse simples comando ja temos um cluster de um nodo do etcd rodando no servidor.

Agora precisamos inicializar o Kubernetes API, componente responsável por toda a comunicação do Kubernetes e persistir as informações no etcd.

$ ./kube-apiserver --etcd-servers http://localhost:2379

Legal, agora ja temos o mínimo necessário para executar os comandos kubectl para inspecionar o nosso cluster.

Verificando a versão do cluster.

$ ./kubectl version --short
Client Version: v1.14.1
Server Version: v1.14.1

Usando um get all para ver um panorama mais geral.

$ ./kubectl get all
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 5m6s

Verificando o status dos componentes:

$ ./kubectl get componentstatuses
NAME STATUS MESSAGE ERROR
scheduler Unhealthy Get http://127.0.0.1:10251/healthz: dial tcp 127.0.0.1:10251: connect: connection refused
controller-manager Unhealthy Get http://127.0.0.1:10252/healthz: dial tcp 127.0.0.1:10252: connect: connection refused
etcd-0 Healthy {"health":"true"}

Como podemos ver o Kubernetes sabe que esta faltando dois componentes necessários que não foram iniciados ainda.

Agora vamos criar o primeiro deployment usando uma imagem simples do nginx.

$ ./kubectl create deployment web --image=nginx
deployment.apps/web created

Depois vamos rodar um kubectl get all para ver o que aconteceu:

$ ./kubectl get all
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/web 0/1 0 0 75s

Podemos ver que ele criou o deployment e nada mais, isso porque esta faltando os componentes necessários para que ele continue para as próximas etapas.

Vamos iniciar o controller-manager, mas antes precisamos criar uma chave publica para que ele possa assinar as API tokens para os service account.

$ openssl ecparam -name secp521r1 -genkey -noout -out sa.key
$ ./kube-controller-manager --master http://localhost:8080 \ 
--service-account-private-key-file certs/sa.key

Agora se executar vermos os eventos vamos ver um ele criando o replicaset:

$ ./kubectl get events
LAST SEEN TYPE REASON OBJECT MESSAGE
16s Normal SuccessfulCreate replicaset/web-5bc9bd5b8d Created pod: web-5bc9bd5b8d-954wd
16s Normal ScalingReplicaSet deployment/web Scaled up replica set web-5bc9bd5b8d to 1

Replicaset criado com sucesso a partir do deployment.

$ ./kubectl get all
NAME READY STATUS RESTARTS AGE
pod/web-5bc9bd5b8d-954wd 0/1 Pending 0 7m32s
NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web 0/1 1 0 8m10s
NAME                             DESIRED   CURRENT   READY   AGE
replicaset.apps/web-5bc9bd5b8d 1 1 0 7m32s

Legal, agora falta o pod ser inicializado. Para isso precisamos de mais alguns componentes.

A partir desse momento faltam 2 coisas, ainda não temos o scheduler executando e não temos um node. O Kubernetes funciona com servidores masters e nodes, os masters rodam aplicações que controlam o cluster como o api, controller manager e o scheduler, e os nodes são onde propriamente dito vamos executar nossas aplicações e eles executam o kubelet e o container runtime (Docker).

Criando o Nodo

Vamos iniciar o Docker, e se você, como eu, deixou todos os binários na home do teu usuário é necessário exportar para o PATH para o docker encontrar os binários do containerd.

$ export PATH=$PATH:/home/seu-usuario-aqui
$ ./dockerd

Para testar se o docker funcionou só executar:

$ ./docker run alpine echo hello
Unable to find image 'alpine:latest' locally
latest: Pulling from library/alpine
bdf0201b3a05: Pull complete
Digest: sha256:28ef97b8686a0b5399129e9b763d5b7e5ff03576aa5580d6f4182a49c5fe1913
Status: Downloaded newer image for alpine:latest
hello

E agora precisamos iniciar o kubelet, que infelizmente não é tão simples quando o controller onde basta apontar a url do api server. Aqui é necessário um arquivo de kubeconfig que pode ser criado através do kubectl:

$ ./kubectl --kubeconfig kubeconfig.kubelet config set-cluster localhost --server http://localhost:8080
$ ./kubectl --kubeconfig kubeconfig.kubelet config set-context localhost --cluster localhost
$ ./kubectl --kubeconfig kubeconfig.kubelet config use-context localhost

O arquivo deve ficar parecido com isso:

$ cat kubeconfig.kubelet
apiVersion: v1
clusters:
- cluster:
server: http://localhost:8080
name: localhost
contexts:
- context:
cluster: localhost
user: ""
name: localhost
current-context: localhost
kind: Config
preferences: {}
users: []

Uma coisa antes é desabilitar a swap pois o Kubelet não suporta, depois é iniciar com o arquivo de inicialização que acabamos de criar.

$ swapoff -a
$ ./kubelet --kubeconfig kubeconfig.kubelet

Ae, agora temos nosso primeiro nodo ready.

$ ./kubectl get nodes
NAME STATUS ROLES AGE VERSION
cirolini-ubuntu Ready <none> 72s v1.14.1

Mas o nosso pod continua em pending.

$ ./kubectl get pods
NAME READY STATUS RESTARTS AGE
web-5bc9bd5b8d-954wd 0/1 Pending 0 32m

Scheduler

Isso acontece pq não tem o scheduler para decidir onde o pode deve ser iniciado, para iniciar ele é bem simples:

$ ./kube-scheduler --master http://localhost:8080
$ ./kubectl get pods
NAME READY STATUS RESTARTS AGE
web-5bc9bd5b8d-954wd 1/1 Running 0 162m

Para testar se esta tudo ok, pegue o IP do pod e de um curl para ver a tela de welcome do nginx.

$ ./kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-5bc9bd5b8d-954wd 1/1 Running 0 163m 172.17.0.2 cirolini-ubuntu <none> <none>
$ curl http://172.17.0.2/

Criando a camada de Serviço

A camada de serviços do Kubernetes é uma abstração para um conjunto de pods que fornece um endereço de IP que não vai ser alterado conforme os pods nascem e morrem. Funciona de uma forma muito parecida com um balanceador de carga, ou um HA Proxy.

$ ./kubectl expose deployment web --port=80
service/web exposed
root@cirolini-ubuntu:/home/cirolini/tmp# ./kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 20h
web ClusterIP 10.0.0.246 <none> 80/TCP 5s

Agora vamos testar o service:

$ curl -v http://10.0.0.246/
* Trying 10.0.0.246...
* TCP_NODELAY set
* connect to 10.0.0.246 port 80 failed: Connection timed out
* Failed to connect to 10.0.0.246 port 80: Connection timed out
* Closing connection 0
curl: (7) Failed to connect to 10.0.0.246 port 80: Connection timed out

O que precisa ser feito é iniciar o kube-proxy componente responsável por criar as regras dos services efetivamente nos nodes.

Inicializado o kube-proxy

$ ./kube-proxy --master http://localhost:8080
$ curl 10.0.0.246
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Agora olhando um pouco do que o kube-proxy faz no netfilter.

$ iptables -t nat -L OUTPUT
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
KUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */
DOCKER all -- anywhere !localhost/8 ADDRTYPE match dst-type LOCAL
$ iptables -t nat -L KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
target prot opt source destination
KUBE-SVC-NPX46M4PTMTKRN6Y tcp -- anywhere 10.0.0.1 /* default/kubernetes:https cluster IP */ tcp dpt:https
KUBE-SVC-BIJGBSD4RZCCZX5R tcp -- anywhere 10.0.0.246 /* default/web: cluster IP */ tcp dpt:http
KUBE-NODEPORTS all -- anywhere anywhere /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
# iptables -t nat -L KUBE-SEP-OGNOLD2JUSLFPOMZ
Chain KUBE-SEP-OGNOLD2JUSLFPOMZ (1 references)
target prot opt source destination
KUBE-MARK-MASQ all -- cirolini-ubuntu anywhere
DNAT tcp -- anywhere anywhere tcp to:10.0.2.15:6443

Agora aumentando o numero de pods para 4 e vendo como fica no iptables.

$ ./kubectl scale deployment web --replicas=4
$ iptables -t nat -L KUBE-SVC-BIJGBSD4RZCCZX5R
Chain KUBE-SVC-BIJGBSD4RZCCZX5R (1 references)
target prot opt source destination
KUBE-SEP-ESTRSP6725AF5NCN all -- anywhere anywhere statistic mode random probability 0.25000000000
KUBE-SEP-VEPQL5BTFC5ANBYK all -- anywhere anywhere statistic mode random probability 0.33332999982
KUBE-SEP-7G72APUFO7T3E33L all -- anywhere anywhere statistic mode random probability 0.50000000000
KUBE-SEP-ZSGQYP5GSBQYQECF all -- anywhere anywhere

Finalizando

Com tudo isso temos um cluster all-in-one funcionando com o mínimo necessário funcionando.

Os próximos passos são:

  • Para poder adicionar mais nodos precisamos instalar um plugin de rede, como por exemplo o calico ou flannel
  • Configuração dos certificados para TLS
  • Executar todos os processos que iniciamos acima em containers sem o root
  • Criar um cluster High Avaliable