Como gerenciar volumes e artefatos de deploy no Kubernetes

Quando se usa docker, trabalhar com volumes parece fácil. Basta vincular uma coisa aqui, outra acolá e pronto: seu contêiner já tem acesso à persistência. A coisa muda um pouco de figura quando se trabalha com Kubernetes. Afinal, seus pods podem estar, em qualquer momento, em qualquer um dos nós do seu cluster. E como fica o gerenciamento de disco? E se eu precisar que meus pods tenham identidade? É por isso que hoje vamos entender melhor como gerenciar volumes e artefatos de deploy no Kubernetes.

Pra começar a nossa conversa, o que são esses tais de volumes?

Volumes

Não estamos falando dos decibéis emitidos pela sua aplicação. Se você já trabalhou com docker, deve estar familiarizado com o conceito de volume. Se não, volumes nada mais são do que “pedaços” do seu HD que você diz para o contêiner: “Toma aqui: pode usar como quiser”.

Vez ou outra você vai me ver falando em “disco” ou “HD” ou storage. Todos são termos que, de certa forma, remetem a mesma ideia: um espaço de armazenamento setorizado. O seu disco (mesmo o SSD) está divido em setores minúsculos, onde são armazenados os dados do seu computador. Como uma lista ligada em que um nó aponta para o próximo. E para ler toda a lista, você precisa apenas saber onde ela começa.

Essa definição é relevante porque é assim que os storages são tratados no mundo cloud: blocos de informação. E se você encara seus dados com esse nível de abstração, qualquer coisa que permita leitura-escrita pode ser encarado como um disco. E até mesmo o próprio disco pode ser encarado como um banco de dados.

Toda essa “viagem” apenas para que as nossas aplicações possam guardar estado. E por falar nisso, já ouviu falar em stateless e stateful?

Stateless x stateful

A maior parte das aplicações que desenvolvemos hoje são stateless, ou “sem estado”. Isso quer dizer que essa aplicação não se preocupa em fazer a gestão de como os dados são armazenados, além de não guardar nenhuma informação de quem a invocou na sua memória. Em geral, essas tarefas são delegadas a outro sistema; como a um banco de dados, por exemplo.

Aplicações stateful, por outro lado, guardam estado ou são afetadas pela mudança desse estado. Uma API, por exemplo, que guarda dados da sessão do usuário em memória (ainda que seja uma má prática) é considerada como stateful.

Como você percebeu as aplicações possuem comportamentos e necessidades diferentes. O que exige que o cluster as trate de forma distinta também. É aí que entram o deployment e o statefulSet

Deployment vs StatefulSet

Você já deve ter visto um Deployment de perto em algum lugar. Basicamente, deployment é um artefato que diz ao Kubernetes, o quê, onde e como deve ser publicado.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:stable
          ports:
            - containerPort: 80

O exemplo acima diz ao Kubernetes que você precisa ter uma instância do nginx rodando em algum lugar, expondo a porta 80. Muito mais informações podem ser definidas no deployment – como probes, limites dos recursos de cada pod, afinidade com pods e assim por diante. Uma característica importante do deployment é que a identidade do pod e onde ele está não é importante. Assim descartá-lo e construir um novo “onde der” é o comportamento padrão do Kubernetes.

Agora vamos conhecer o statefulSet

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx-statefulset
  labels:
    app: nginx
spec:
  serviceName: nginx
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:stable
          ports:
            - containerPort: 80
          volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: www
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi

O statefulSet é muito parecido com o deployment, como você pode ver. A grande diferença é que o statefulSet garante duas coisas: O nome dos pods jamais irá mudar. E cada pod terá sempre um volume próprio e não-compartilhado. Essas duas propriedades são interessantes para cenários em que precisamos ter previsibilidade no nome – mantendo um DNS específico e imutável para um pod – e que as diversas réplica não irão compartilhar o mesmo volume.

Resumindo: Se você está querendo entregar aplicações stateless – como APIs, por exemplo – e pode usar um ingress ou um loadbalance para acessar as diversas instâncias, utilize um deployment. Quando você for entregar uma aplicação que gerencia o acesso aos dados, que cada instância mantém estados diferentes, não acessando o mesmo volume e precisam ser acessadas distintamente, vá de statefulSet.

Até aqui nós já entendemos os conceitos de stateless e stateful, de deployment e statefulSet e também de volumes. Mas como essas coisas funcionam no Kubernetes?

Como o Kubernetes gerencia volumes?

O esquema acima nos mostra a cadeia de dependências entre os diversos artefatos que garantem acesso aos dados. O pod se conecta a um Persistence Volume (PV), que foi criado através de uma Persistence Volume Claim (PVC) baseada nas restrições especificadas por uma Storage Class (StgC) que se comunica com o blob através de uma implementação do driver CSI (Container Storage Interface). Mas o que é cada uma dessas coisas?

PV e PVC

Entenda o PVC como a solicitação de um pedaço do disco que está disponível. Nele você consegue definir modos de disponibilidade do recurso. Por exemplo, você pode dizer que apenas um pod é capaz de operar esse volume (RWO), ou que vários pods podem ler e escrever (RWX). Além disso, você pode definir nome, tamanho, construir os dados baseando em outro disco, a storage class pra quem você está solicitando o disco.

Ou seja, com o PVC estamos tentando provisionar um PV. E sim, você pode criar PV manualmente. Mas sinceramente, não há razão para isso.

Veja um exemplo de manifesto que cria um PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-longhorn
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 20Gi

E aqui um deployment de um nginx utilizando o PVC recém-criado.

apiVersion: v1
kind: Pod
metadata:
  name: pod-com-volume
spec:
  containers:
    - name: app
      image: nginx:stable
      volumeMounts:
        - name: dados
          mountPath: /usr/share/nginx/html
  volumes:
    - name: dados
      persistentVolumeClaim:
        claimName: pvc-longhorn

Storage Class e CSI

Imagine comigo: Se o Kubernetes fosse o responsável direto por fazer a gestão de acesso aos storages, ele teria que construir diversas implementações, certo? Afinal tem os discos SSD, NVMe, Azure Blob Storage, Amazon S3, Pendrive, MicroSD… Então o que ele faz? A boa e velha inversão de dependência! Uma definição/interface padrão foi criada para que os vendors tornem as suas soluções compatíveis com o Kubernetes. Isso é o CSI: Container Storage Interface.

Cada CSI vai prover funcionalidades diferentes. Alguns drivers podem não suportar RWX ou replicação, por exemplo, enquanto com outros drivers isso é possível. Também pode ser que você queira segmentar os discos, vinculando a determinadas tags de seleção. A definição de quais features serão utilizadas para um volume está atrelada ao Storage Class.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: driver.longhorn.io
allowVolumeExpansion: true
reclaimPolicy: Delete
volumeBindingMode: Immediate
parameters:
  numberOfReplicas: "3"
  staleReplicaTimeout: "30"
  fsType: ext4

Observe que em provisioner nós especificamos que essa storageClass utiliza o driver CSI do Longhorn. E também outros parâmetros que definem até mesmo o sistema de arquivos que utilizado nos volumes criados à partir dessa storage class.

Para o Longhorn, no objeto parameters, você pode definir, por exemplo, seletores, indicando em qual disco os volumes serão criados. Veja o exemplo à seguir:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: longhorn-fast
provisioner: driver.longhorn.io
allowVolumeExpansion: true
reclaimPolicy: "Delete"
volumeBindingMode: Immediate
parameters:
  numberOfReplicas: "1"
  staleReplicaTimeout: "30"
  fsType: "ext4"
  diskSelector: "ssd"
  nodeSelector: "ssd"

As PVC que utilizarem o storageClass longhorn-fast apenas serão agendados nas máquinas com a tag “ssd” e nos discos com a tag “ssd”.

Esse manifesto já é um spoiler do que vem à seguir, quando instalarmos o Longhorn no nosso cluster. Te vejo lá!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.