Fazendo seu primeiro deploy no Kubernetes

Kubernetes pode parecer um bicho de sete cabeças pra quem está começando (e talvez realmente seja), mas levantar aplicações e operar no dia a dia não é lá tão complicado pra um programador. Nesse tutorial mostro como levantar duas simples aplicações, um worker (dummy-logger) e uma aplicação web (visit-counter).

Ao finalizar esse tutorial você será capaz de fazer configurações básicas de Pods e Deployments, escalar aplicações, visualizar logs e lidar com roteamento interno de tráfego.

Pré-requisitos

Para esse laboratório você precisará de:

  1. Docker
  2. kubectl
  3. Minikube
  4. Uma conta no Docker Hub.

Vou assumir que você já conhece o Docker.

Kubectl é o utilitário de linha de comando que nos possibilita interagir com o cluster Kubernetes.
Minikube é um utilitário de linha de comando que simplifica a interação com uma máquina virtual com o cluster Kubernetes instalado.
Docker Hub É o local onde hospedaremos as imagens Docker para que o Kubernetes consiga acessá-las.

Primeiros passos

Primeiro faça login no Docker Hub pela linha de comando: docker login. Isso é importante para que possamos enviar nossas imagens para lá.

Uma vez que o Minikube estiver instalado, rode o seguinte comando:

minikube start

Uma máquina virtual será iniciada e o cluster Kubernetes estará disponível. Rodando comando minikube ip você consegue visualizar o IP dessa máquina.

Como é possível termos acessos a diversos clusters, é necessário configurar o comando kubectl para que ele interaja especificamente com o cluster instalado pelo Minikube. Para isso execute o seguinte comando:

kubectl config use-context minikube

Assim você já pode interagir com o cluster. Se executar o comando kubectl cluster-info você verá algumas informações sobre o cluster. O comando kubectl get nodes mostrará todos os nodes disponíveis.

Rodando nossa primeira aplicação

Criaremos uma aplicação em Ruby que faz logging no stdout.

Em um novo diretório crie um arquivo chamado app.rb:

loop do
  puts 'Hello world!'
  STDOUT.flush
  sleep 1
end

Em seguida crie o sequinte Dockerfile:

FROM ruby:2.6-alpine

COPY . /app
WORKDIR /app

CMD ["ruby", "app.rb"]

Faça o build da imagem e envie para o Docker Hub:

# Substitua "<username>" pelo seu username no Docker Hub
docker build . -t <username>/dummy-logger:1.0
docher push <username>/dummy-logger:1.0

Agora crie seu primeiro arquivo de configuração do Kubernetes chamado pod-1.yaml (esse arquivo pode ter qualquer nome):

apiVersion: v1
kind: Pod
metadata:
  name: dummy-logger
spec:
  containers:
  - name: logger-container
    image: <username>/dummy-logger:1.0

Não se esqueça de substituir seu usuário no campo spec.containers[].image!
Em seguida aplique essa configuração no Kubernetes:

kubectl create -f pod-1.yaml

Pra saber se tudo funcionou corretamente, execute o comando kubectl get pods. Um output similar a esse deve aparecer:

NAME          READY   STATUS    RESTARTS   AGE
dummy-logger  1/1     Running   0          2m

Pra ver os logs da aplicação:

# --follow (ou -f) para acompanhar os logs
kubectl logs --follow dummy-logger

Se você consegue visualizar os logs, isso significa que acabou de deployar sua primeira aplicação no Kubernetes com sucesso!

Entendendo tudo que aconteceu

Vamos separar o que fizemos em três partes:

  1. Desenvolvimento da aplicação.
  2. Criação/hospedagem da imagem Docker.
  3. Rodar a aplicação no Kubernetes.

Desenvolvimento da aplicação

Essa é a parte que você já deve dominar, então nem vou perder muito tempo aqui. Você pode desenvolver uma aplicação web, um worker, um script etc. Dependendo do seu objetivo, você pode usar um ou outro objeto no Kubernetes pra encapsular sua aplicação.

Criação/hospedagem da imagem Docker

Se você já trabalhou com Docker, deve estar acostumado com esse fluxo. Criar imagens e enviá-las a um registry é um fluxo normal pra quem trabalha com essa tecnologia. Como estou assumindo que você sabe trabalhar com Docker, também não vou perder aqui explicando como isso funciona.

Rodar a aplicação no Kubernetes

Até aqui podemos dividir essa interação com o Kubernetes em duas partes: Criar arquivo de configuração e enviá-lo ao Kubernetes. Esse é basicamente o fluxo que sempre utilizaremos, seja manualmente (dando kubectl create/apply direto da nossa máquina), seja através de um servidor de integração contínua.

Overview do Kubernetes

Arquitetura do Kubernetes

Nó no Kubernetes (também conhecido como worker node) é o local onde as aplicações rodam. Na imagem acima vemos diversos Pods, que são a menor unidade no Kubernetes. Esses Pods podem conter um ou mais contêineres Docker. Apesar de podermos criar diretamente os Pods, é comum usarmos Deployments pra isso, pois simplificam escalar aplicações se baseando em um template de Pod (veremos isso mais à frente).

O master node é responsável por fazer todo o trabalho pesado do Kubernetes. Ele tem basicamente quatro componentes. O Api Server, Controller Manager, Scheduler e etcd.

Api Server é o componente central de toda comunicação no cluster. É com ele que falamos quando executamos o comando kubectl por exemplo.

Controller Manager é responsável, entre outras coisas, por garantir que o estado informado nos arquivos yaml estejam vigentes.

Scheduler é quem decide onde (em qual nó) um Pod irá rodar.

etcd é a camada de persistência onde o Kubernetes mantém todos os dados necessários pro cluster.

Exercício 1

Sua missão agora é escalar a aplicação dummy-logger para que rode duas instâncias. Para diferenciá-lo do primeiro, o novo Pod deve ter o nome dummy-logger-2, porém deve usar a mesma imagem do Pod anterior. Volte aqui quando tiver dois Pods rodando no Kubernetes.

Resolução do exercício 1

Primeiro criei o arquivo pod-2.yaml com o seguinte conteúdo:

apiVersion: v1
kind: Pod
metadata:
  name: dummy-logger-2
spec:
  containers:
    - name: logger-container
      image: <username>/dummy-logger:1.0

Depois deployei esse Pod:

kubectl apply -f pod-2.yaml

executando o comando kubectl get pods temos um resultado semelhante a esse:

NAME             READY   STATUS    RESTARTS   AGE
dummy-logger     1/1     Running   0          3m
dummy-logger-2   1/1     Running   0          2s

Exercício 2

Agora vamos atualizar nossa aplicação para printar Hello world from V2!. No arquivo app.rb, altere a string que fazemos puts:

loop do
  puts 'Hello world from V2!'
  STDOUT.flush
  sleep 1
end

Gere uma nova imagem Docker, dessa vez com a tag 2.0, e a envie pro Docker Hub:

# Substitua "<username>" pelo seu username no Docker Hub
docker build . -t <username>/dummy-logger:2.0
docher push <username>/dummy-logger:2.0

Daqui pra frente é com você. Sua missão é fazer a versão 2.0 rodar no Kubernetes.

Resolução do exercício 2

Pra subir uma versão nova, basta alterar a imagem Docker nos arquivos pod-1.yaml e pod-2.yaml:

image: <username>/dummy-logger:2.0

E em seguidar enviá-los para o Kubernetes:

kubectl apply -f pod-1.yaml
# pod/dummy-logger configured

kubectl apply -f pod-2.yaml
# pod/dummy-logger-2 configured

Para ver o status dos Pods com a nova versão da nossa aplicação, vamos ver o output do comando kubectl get pods:

NAME             READY   STATUS    RESTARTS   AGE
dummy-logger     1/1     Running   1          19m
dummy-logger-2   1/1     Running   1          18m

Repare que a coluna RESTARTS aparece com o valor 1, ou seja, o Kubernetes simplesmente reiniciou o Pod já existente trazendo a nova imagem Docker. Pra garantir que a aplicação realmente foi atualizada, vamos ver os logs através do comando kubectl logs --tail 3 dummy-logger. Você deve ter o seguinte resultado na sua linha de comando:

Hello, world from V2!
Hello, world from V2!
Hello, world from V2!

Parabéns, você acabou de deployar uma nova versão da sua aplicação!

Simplificando o ato de escalar

Criar um novo Pod sempre que quiser levantar uma nova instância da aplicação não parece muito prático. O Kubernetes fornece uma maneira mais simples de alcançar esse mesmo objetivo através do objeto Deployment.

Crie um novo arquivo chamado deployment.yaml com o seguinte conteúdo:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dummy-logger-deployment
spec:
  selector:
    matchLabels:
      app: dummy-logger-deployment
  template:
    metadata:
      labels:
        app: dummy-logger-deployment
    spec:
      containers:
        - name: my-container
          image: ematos/dummy-logger:2.0

Repare que os campos spec.selector.matchLabels.app tem o mesmo valor de spec.template.metadata.labels.app. Essa é a forma do Deployment saber quais Pods pertencem a ele, ganhando assim o poder de iniciar novos Pods ou removê-los conforme julgar necessário.

Aplique esse arquivo no Kubernetes e em seguida consulte os Pods que estão rodando:

kubectl apply -f deployment.yaml
# deployment.apps/dummy-logger-deployment created

kubectl get pods
# NAME                                       READY   STATUS    RESTARTS   AGE
# dummy-logger                               1/1     Running   1          34h
# dummy-logger-2                             1/1     Running   1          32m
# dummy-logger-deployment-6b57cdcbdc-457fv   1/1     Running   0          18s

Vemos exatamente 1 novo Pod rodando. Agora vamos informar no arquivo de configuração que queremos quatro instâncias desse Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dummy-logger-deployment
spec:
  replicas: 4
  selector:
    matchLabels:
      app: dummy-logger-deployment
  template:
    metadata:
      labels:
        app: dummy-logger-deployment
    spec:
      containers:
        - name: my-container
          image: ematos/dummy-logger:2.0

Em seguida podemos enviar essa nova configuração pro Kubernetes e verificar se realmente temos quatro Pods rodando:

kubectl apply -f deployment.yaml
# deployment.apps/dummy-logger-deployment configured

kubectl get pods
# NAME                                       READY   STATUS    RESTARTS   AGE
# dummy-logger                               1/1     Running   1          40m
# dummy-logger-2                             1/1     Running   1          39m
# dummy-logger-deployment-6b57cdcbdc-457fv   1/1     Running   0          7s
# dummy-logger-deployment-6b57cdcbdc-7h7sn   1/1     Running   0          7s
# dummy-logger-deployment-6b57cdcbdc-89rsh   1/1     Running   0          7s
# dummy-logger-deployment-6b57cdcbdc-hdvzl   1/1     Running   0          7s

Pra ver os logs de todos os Pods, podemos fazer um filtro pela label que definimos no yaml:

kubectl logs --follow --tail 0 --selector="app=dummy-logger-deployment"

É possível ver que os logs rodam muito mais rápido agora, o que faz sentido já que estamos coletando logs de quatro instâncias ao mesmo tempo. Se quiser também pode ver o log de um Pod específico passando diretamente o nome do Pod para o comando kubectl logs ao invés de usar a flag --selector.

Exercício 3

Uma grande vantagem de usar Deployment é de que ele garante a quantidade de réplicas configuradas, ou seja, se por um acaso um Pod petencente ao Deployment morrer, ele levanta outro no lugar.

Sua missão aqui é matar Pods. Para isso você pode usar o comando kubectl delete pod <nome do pod>. Primeiro faça isso pros Pods que criamos no início do tutorial (dummy-logger e dummy-logger-2). O que acontece quando consulta os Pods novamente através do comando kubectl get pods?

Em seguida escolha qualquer Pod pertencente ao Deployment e mate-o. O que acontece quando consulta os Pods novamente através do comando kubectl get pods?

Lançando uma aplicação web

Até aqui desenvolvemos uma aplicação conhecida como worker, isto é, uma aplicação que roda em background indefinidamente e não recebe tráfego. Agora desenvolveremos uma aplicação web, que diferentemente do worker, poderá ser acessada diretamente através do nosso navegador.

Nesse tutorial criaremos um simples contador de visitas. Uma aplicação que a cada visita irá exibir na tela quantas vezes já o visitamos.

Arquitetura do Visit Counter

Essa aplicação terá somente dois elementos, uma aplicação web e um banco de dados. Por pura simplicidade usaremos o Redis como camada de persistência. O fluxo é bem simples: O Visit Counter recebe uma requisição, incrementa um contador no Redis, faz uma consulta para pegar o número total de visitas e retorna este valor como resposta.

Arquitetura do Visit Counter

Arquitetura no Kubernetes

No Kubernetes precisaremos de um Deployment para a aplicação web, o que nos permite escalar com facilidade, e um Pod rodando o Redis.

Arquitetura do Visit Counter

Pods são uma caixa preta no cluster, isto é, não é possível acessá-los diretamente. A maneira “Kubernetes” de expor um Pod pro cluster é usando um Service. Services podem expor uma ou mais portas de Pods. Pensando em nossa aplicação, iremos expor a porta 6379 do Redis, e a porta 8000 do Visit Counter.

Deployando o Redis

Começamos com o Redis pois ele vive independentemente da existência do Visit Counter. Como já existe uma imagem oficial do Redis no Docker Hub, não precisamos nos preocupar com a criação de um Dockerfile, e podemos ir diretamente pra difinição do Pod.

Criamos um arquivo redis-pod.yaml com a seguinte configuração:

apiVersion: v1
kind: Pod
metadata:
  name: redis-for-visit-counter
  labels:
    app: redis-for-visit-counter
spec:
  containers:
    - name: redis
      image: redis

Agora precisamos expor o redis para o restante do cluster, e usaremos um Service pra isso. Vamos criar um arquivo chamado redis-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: redis-service
spec:
  selector:
    app: redis-for-visit-counter
  ports:
    - port: 9000
      targetPort: 6379

O campo spec.selector.label no Service é usado pelo Kubernetes para saber para quais Pods ele deve rotear tráfego. Nesse caso o Service redis-service informa ao Kubernetes que deve ser roteado tráfego para Pods que tenham a label app=redis-for-visit-counter, por isso spec.selector.label no Service tem o mesmo valor de metadata.labels.app no Pod.

Este Service expõe somente uma porta, mas poderiam ser várias (repare no yaml que spec.ports é um array). targetPort é a porta onde o Redis roda dentro do Pod. port é a porta que ficará visível pro restante do cluster, e basicamente fará redirect pra porta 6379 no Pod.

Agora basta aplicar esses arquivos usando kubectl apply -f:

kubectl apply -f redis.yaml
# pod/redis-for-visit-counter created
kubectl apply -f redis-service.yaml
# service/redis-service created

Podemos ver se realmente o Pod e o Service está de pé:

kubectl get pods,services
# NAME                          READY   STATUS    RESTARTS   AGE
# pod/redis-for-visit-counter   1/1     Running   0          50s
#
# NAME                    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
# service/kubernetes      ClusterIP   10.96.0.1      <none>        443/TCP    18h
# service/redis-service   ClusterIP   10.98.84.252   <none>        9000/TCP   47s

Uma forma de testarmos o Redis é o acessando direto de nossa máquina. Para isso é necessário que redirecionemos uma porta específica para nossa máquina através do comando kubectl port-forward:

kubectl port-forward svc/redis-service 7000:9000
# Forwarding from 127.0.0.1:7000 -> 6379
# Forwarding from [::1]:7000 -> 6379

Assumindo que você tenha redis-cli instalado localmente, é possível acessar o Redis que está no cluster:

redis-cli -h localhost -p 7000 ping
# PONG

Codando o Visit Counter

Novamente iremos desenvolver em Ruby. Usaremos a gem redis para nos comunicar com o Redis e a gem sinatra pra expor uma interface web. Num novo diretório criamos um arquivo chamado Gemfile com o conteúdo abaixo:

source 'https://rubygems.org'

gem 'redis'
gem 'sinatra'

Agora basta escrever o contador em si:

require 'redis'
require 'sinatra'

set(:port, 8000)
set(:bind, '0.0.0.0')

redis = Redis.new(
  host: ENV.fetch('REDIS_HOST', 'localhost'),
  port: ENV.fetch('REDIS_PORT', '6379'),
)

get '/' do
  redis.incr('visits')
  "Visits: #{redis.get('visits')}"
end

Como sempre, precisamos dockerizar nossa aplicação:

FROM ruby:2.6-alpine

COPY . /app
WORKDIR /app

RUN bundle install

CMD ["ruby", "app.rb"]

Para que esta imagem fique disponível pro Kubernetes, vamos fazer o build e enviá-la pro Docker Hub:

# Substituir <username> pelo seu usuário no Docker Hub
docker build . -t <username>/visit-counter:1.0
docker push <username>/visit-counter:1.0

Agora podemos criar o Deployment (visit-counter-deployment.yaml) que enviaremos pro Kubernetes:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: visit-counter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: visit-counter
  template:
    metadata:
      labels:
        app: visit-counter
    spec:
      containers:
        - name: visit-counter
          image: <username>/visit-counter:1.0
          env:
            - name: "REDIS_HOST"
              value: "redis-service"
            - name: "REDIS_PORT"
              value: "9000"

Lembre que precisamos criar um Service a fim de expor o Visit Counter para o restante do cluster. Num arquivo chamado visit-counter-service.yaml crie a seguinte configuração:

apiVersion: v1
kind: Service
metadata:
  name: visit-counter-service
spec:
  selector:
    app: visit-counter
  ports:
    - port: 3000
      targetPort: 8000

Agora basta enviar essas configurações pro Kubernetes:

kubectl apply -f visit-counter-deployment.yaml
# deployment.apps/visit-counter created
kubectl apply -f visit-counter-service.yaml
# service/visit-counter-service created

Vamos ver como andam os Pods e Services:

kubectl get pods,svc
# NAME                                 READY   STATUS             RESTARTS   AGE
# pod/redis-for-visit-counter          1/1     Running            0          5m
# pod/visit-counter-5b4574bf85-dpsd5   0/1     InvalidImageName   0          34s
#
# NAME                            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
# service/kubernetes              ClusterIP   10.96.0.1        <none>        443/TCP    18h
# service/redis-service           ClusterIP   10.111.208.210   <none>        9000/TCP   5m
# service/visit-counter-service   ClusterIP   10.98.48.238     <none>        3000/TCP   31s

Podemos usar novamente o comando kubectl port-forward, mas dessa vez pra acessar o Visit Counter:

kubectl port-forward svc/visit-counter-service 2000:3000
# Forwarding from 127.0.0.1:2000 -> 8000
# Forwarding from [::1]:2000 -> 8000

Acessando http://localhost:2000 no navegador, teremos um output semelhante a esse:

Screenshot Visit Counter usando port-forward

Parabéns, você acabou de deployar uma aplicação no Kubernetes que acessa um banco de dados e armazena estado! \o/

Permitindo tráfego externo

O comando kubectl port-forward é útil para termos acesso rapidamente a um Pod ou Service que não está exposto para a internet, e só isso. Para que usuários tenham acesso através da internet nós precisamos configurar um objeto chamado Ingress.

Um detalhe importante é que o cluster criado pelo Minikube disponibiliza somente um nó, porém um cluster em produção invariavelmente vai ter diversos nós, então além do Ingress será necessário configurar um load balancer de modo que todo tráfego passe por ele antes de chegar ao Ingress.

A grosso modo, uma requisição num cluster com mais de um nó segue o seguinte fluxo:

Arquitetura do Kubernetes

Nesse tutorial não temos que nos preocupar com o load balancer, o que vai simplificar um pouco a nossa vida.

Configurando o Ingress

O Ingress pode ser dividido em duas partes: Configuração e Controller. A configuração é o yaml responsável pela configuração das rotas. Ele diz pra qual Service uma requisição deve ser roteada. O controller é um Deployment que roda uma aplicação responsável por fazer o roteamento de fato. Nginx e Traefik são duas opções. Nesse tutorial usaremos o Nginx Ingress Controller.

Para instalar esse controller, basta seguir as instruções em sua documentação. Na data de publicação desse tutorial para rodar o Nginx Ingress Controller no Minikube só precisamos executar o seguinte comando:

minikube addons enable ingress 

Uma vez que o Ingress Controller estiver instalado, basta configurarmos as rotas:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - http:
        paths:
          - path: /visit-counter
            backend:
              serviceName: visit-counter-service
              servicePort: 3000

Aplicando no Kubernetes:

kubectl apply -f my-ingress.yaml
# ingress.extensions/my-ingress created

Para acessar o Visit Counter, precisamos saber em qual IP o minikube está rodando. Para isso podemos usar o seguinte comando:

minikube ip

Supondo que o IP seja 192.168.99.100, se deu tudo certo podemos acessar http://192.168.99.100/visit-counter no navegador e ver nosso contador de visitas funcionando perfeitamente.

Screenshot Visit Counter usando port-forward

Parabéns, você acabou de levantar uma aplicação web no Kubernetes!

Conclusão

Nesse tutorial você conseguiu fazer interações básicas com um cluster Kubernetes. Pode parecer pouco (ou não), mas o que você viu compreende boa parte do que um programador precisa saber quando estiver operando um cluster em produção. Existem muitas outras coisas importantes no Kubernetes, mas ficam mais na resposabilidade de um admin do cluster do que como responsabilidade de um programador, como manutenção dos nós, criação de discos persistentes, atualização do cluster etc.

Um workshop deu origem a esse tutorial, e você pode acessar uma versão pronta do código no meu Github.