No Kubernetes, o gerenciamento de armazenamento é um dos pilares para executar aplicações com estado, como bancos de dados, sistemas de arquivos e serviços que precisam preservar dados entre reinicializações. Diferente de contêineres efêmeros, que perdem seus dados ao serem destruídos, o Kubernetes oferece uma abstração robusta para persistência, permitindo que os dados sobrevivam ao ciclo de vida dos Pods. Nesta aula, vamos explorar os principais componentes de armazenamento: Volumes, PersistentVolumes (PVs), PersistentVolumeClaims (PVCs) e StorageClasses, além de discutir o conceito de estado em aplicações distribuídas.

Compreender essas abstrações é essencial para qualquer profissional de DevOps que deseja operar clusters Kubernetes em produção, pois aplicações reais frequentemente exigem armazenamento durável, compartilhado entre réplicas e com políticas de retenção adequadas. Vamos começar com os fundamentos e, ao final, você terá uma visão clara de como projetar soluções de armazenamento eficientes e escaláveis no Kubernetes.

Volumes

Um Volume no Kubernetes é um diretório acessível aos contêineres em um Pod, que pode ser preenchido com dados de diversas fontes. A principal diferença entre um Volume e o sistema de arquivos do contêiner é que o Volume tem um ciclo de vida ligado ao Pod, não ao contêiner. Isso significa que, se um contêiner reiniciar, o Volume persiste, mas se o Pod for excluído, o Volume também é removido (a menos que seja um PersistentVolume).

O Kubernetes suporta uma ampla variedade de tipos de volumes, desde diretórios locais no nó (como emptyDir e hostPath) até integrações com sistemas de armazenamento em nuvem (como awsElasticBlockStore, gcePersistentDisk, azureDisk) e sistemas distribuídos (como nfs, cephfs, glusterfs). Cada tipo tem suas particularidades e casos de uso. Os volumes são especificados na seção spec.volumes do Pod e montados nos contêineres através de volumeMounts.

Vamos ver um exemplo simples de um Pod que usa um volume do tipo emptyDir, que é um diretório temporário criado no nó e compartilhado entre os contêineres do Pod:

apiVersion: v1
kind: Pod
metadata:
  name: meu-pod
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: dados
      mountPath: /usr/share/nginx/html
  volumes:
  - name: dados
    emptyDir: {}

Nesse exemplo, o diretório /usr/share/nginx/html no contêiner nginx é montado a partir do volume dados, que é um diretório temporário no nó. Se houvesse um segundo contêiner no mesmo Pod, ele também poderia montar o mesmo volume e compartilhar os dados. O emptyDir é útil para dados temporários, como caches e arquivos de log, mas não para persistência de longo prazo.

Outro tipo comum é o hostPath, que monta um diretório do nó dentro do Pod. Isso é útil para acessar arquivos do sistema host, como logs do Docker, mas deve ser usado com cautela, pois amarra o Pod a um nó específico e pode causar problemas de portabilidade.

Para aplicações que precisam de persistência real, o Kubernetes oferece os PersistentVolumes, que veremos a seguir.

PersistentVolumes e Claims

PersistentVolumes (PVs) e PersistentVolumeClaims (PVCs) são as abstrações que permitem o gerenciamento de armazenamento persistente de forma independente do ciclo de vida dos Pods. Um PV é um recurso do cluster que representa um pedaço de armazenamento provisionado por um administrador ou dinamicamente por uma StorageClass. Ele é como um volume físico que pode ser consumido por um ou mais Pods.

Um PVC é uma solicitação de armazenamento feita por um usuário ou aplicação. O Kubernetes tenta vincular o PVC a um PV que atenda aos requisitos de capacidade, modo de acesso e classe de armazenamento. Essa separação permite que os desenvolvedores solicitem armazenamento sem precisar conhecer os detalhes da infraestrutura subjacente.

Vamos criar um exemplo prático. Primeiro, definimos um PersistentVolume do tipo hostPath (para demonstração, mas em produção você usaria soluções como NFS, EBS, etc.):

apiVersion: v1
kind: PersistentVolume
metadata:
  name: meu-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  hostPath:
    path: "/mnt/dados"

Agora, criamos um PVC que solicita 1Gi de armazenamento com acesso ReadWriteOnce:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: meu-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

Após aplicar esses manifestos, o PVC será vinculado ao PV, e podemos usá-lo em um Pod:

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

Os modos de acesso definem como o volume pode ser montado nos nós: ReadWriteOnce (RWO) permite leitura e escrita em um único nó; ReadOnlyMany (ROX) permite leitura em vários nós; ReadWriteMany (RWX) permite leitura e escrita em vários nós. A escolha do modo depende do tipo de armazenamento e dos requisitos da aplicação.

Os PVs também possuem políticas de reivindicação (reclaim policy): Retain, Recycle (deprecated) e Delete. A política Delete remove o armazenamento subjacente quando o PVC é excluído, enquanto Retain mantém os dados para que possam ser recuperados manualmente.

StorageClasses (visão geral)

StorageClasses são um recurso que permite o provisionamento dinâmico de PersistentVolumes. Em vez de um administrador criar PVs manualmente, a StorageClass define um provisionador que cria automaticamente o volume quando um PVC é criado. Isso é especialmente útil em ambientes de nuvem, onde o armazenamento pode ser provisionado sob demanda.

Uma StorageClass especifica o provisionador (por exemplo, kubernetes.io/aws-ebs, azure-disk, gce-pd), parâmetros de configuração (como tipo de disco, zona, etc.) e a política de recuperação padrão. Quando um PVC não especifica uma classe, a classe padrão (default) é usada.

Vamos ver um exemplo de StorageClass para AWS EBS:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  fsType: ext4
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

No exemplo, a classe fast usa o provisionador da AWS para criar volumes gp3. O parâmetro volumeBindingMode define quando o volume é criado e vinculado: Immediate (assim que o PVC é criado) ou WaitForFirstConsumer (somente quando um Pod que usa o PVC é agendado). O modo WaitForFirstConsumer é útil para garantir que o volume seja criado na mesma zona que o nó que executará o Pod.

Para usar essa StorageClass, basta criar um PVC referenciando-a:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: meu-pvc-dinamico
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: fast
  resources:
    requests:
      storage: 10Gi

Quando o PVC é criado, o provisionador da StorageClass cria um volume EBS de 10Gi automaticamente, e o PVC é vinculado a ele. Isso simplifica muito a gestão de armazenamento em clusters grandes, eliminando a necessidade de pré-provisionar volumes.

Estado

O termo "estado" em Kubernetes refere-se à capacidade de uma aplicação de manter dados persistentes e consistentes entre execuções. Aplicações sem estado (stateless) não armazenam dados localmente, enquanto aplicações com estado (stateful) precisam de armazenamento persistente, como bancos de dados, sistemas de mensageria e serviços de arquivos.

Para gerenciar aplicações com estado, o Kubernetes oferece o recurso StatefulSet, que fornece identidades estáveis e armazenamento persistente para cada réplica. Diferente de um Deployment, que cria Pods efêmeros com nomes aleatórios, um StatefulSet cria Pods com nomes sequenciais (ex.: meu-app-0, meu-app-1) e cada Pod pode ter seu próprio PersistentVolumeClaim, garantindo que os dados sejam preservados mesmo se o Pod for recriado.

Um exemplo típico é um StatefulSet para um banco de dados PostgreSQL:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: "postgres"
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15
        volumeMounts:
        - name: dados
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: dados
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

O volumeClaimTemplates cria um PVC para cada réplica automaticamente, resultando em volumes separados para cada Pod. Isso é essencial para bancos de dados com replicação, onde cada instância precisa de seu próprio armazenamento.

Entender o estado é crucial para projetar aplicações resilientes. Muitas vezes, você pode manter a maior parte da aplicação sem estado, mas os componentes que exigem persistência devem ser isolados e gerenciados com StatefulSets e PersistentVolumes.

Boas Práticas e Observações Finais

Ao trabalhar com armazenamento no Kubernetes, considere as seguintes boas práticas: sempre prefira StorageClasses para provisionamento dinâmico, use PersistentVolumeClaims em vez de volumes locais para dados que precisam persistir, e escolha os modos de acesso adequados à sua aplicação. Além disso, monitore o consumo de armazenamento e configure alertas para evitar esgotamento de disco. Para ambientes de produção, evite hostPath e prefira soluções de armazenamento distribuído ou em nuvem.

Lembre-se de que o armazenamento é um recurso crítico e caro; dimensionar corretamente e usar políticas de retenção adequadas pode reduzir custos e melhorar a confiabilidade. A abstração de volumes do Kubernetes é poderosa, mas exige planejamento cuidadoso.

Referências

Exercícios

  1. Exercício 1: Crie um manifesto YAML para um Pod que usa um volume emptyDir e monte-o em um contêiner nginx no caminho /cache. Inclua também um comando que escreva um arquivo de teste no volume.
  2. ✓ Resposta:
    apiVersion: v1
    kind: Pod
    metadata:
      name: pod-empty-dir
    spec:
      containers:
      - name: app
        image: nginx
        command: ["/bin/sh", "-c", "echo 'teste' > /cache/arquivo.txt && sleep 3600"]
        volumeMounts:
        - name: cache
          mountPath: /cache
      volumes:
      - name: cache
        emptyDir: {}
  3. Exercício 2: Crie um PersistentVolume de 2Gi com acesso ReadWriteOnce usando hostPath no caminho /mnt/pv e um PersistentVolumeClaim solicitando 1Gi. Verifique se o PVC consegue ser vinculado ao PV (justifique).
  4. ✓ Resposta:
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: pv-hostpath
    spec:
      capacity:
        storage: 2Gi
      accessModes:
      - ReadWriteOnce
      hostPath:
        path: /mnt/pv
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: pvc-hostpath
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi

    O PVC será vinculado porque o PV oferece capacidade suficiente (2Gi) e o modo de acesso é compatível. O Kubernetes vincula o PVC a um PV que atenda aos requisitos de capacidade e acesso.

  5. Exercício 3: Crie uma StorageClass chamada fast que use o provisionador kubernetes.io/no-provisioner e explique por que isso é útil para armazenamento local.
  6. ✓ Resposta:
    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: fast
    provisioner: kubernetes.io/no-provisioner
    volumeBindingMode: WaitForFirstConsumer

    O provisionador kubernetes.io/no-provisioner indica que a StorageClass não cria volumes dinamicamente. Em vez disso, ela é usada para permitir que administradores criem PersistentVolumes localmente (por exemplo, usando local volumes) e os vinculem a PVCs. Isso é útil para armazenamento local de alta performance, como SSDs, onde o provisionamento dinâmico não é viável.

  7. Exercício 4: Escreva um manifesto de StatefulSet para um banco de dados Redis com 2 réplicas, usando um template de volume que solicita 5Gi de armazenamento com acesso ReadWriteOnce.
  8. ✓ Resposta:
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: redis
    spec:
      serviceName: "redis"
      replicas: 2
      selector:
        matchLabels:
          app: redis
      template:
        metadata:
          labels:
            app: redis
        spec:
          containers:
          - name: redis
            image: redis:7
            ports:
            - containerPort: 6379
            volumeMounts:
            - name: dados
              mountPath: /data
      volumeClaimTemplates:
      - metadata:
          name: dados
        spec:
          accessModes: [ "ReadWriteOnce" ]
          resources:
            requests:
              storage: 5Gi
  9. Exercício 5: Explique a diferença entre emptyDir e um PersistentVolumeClaim. Em que cenário você usaria cada um?
  10. ✓ Resposta:

    emptyDir é um volume temporário criado no nó e excluído quando o Pod é removido. É útil para dados efêmeros, como caches, logs temporários e compartilhamento de dados entre contêineres do mesmo Pod. Um PVC, por outro lado, solicita armazenamento persistente que sobrevive ao ciclo de vida do Pod, permitindo que os dados sejam preservados mesmo se o Pod for recriado. Use PVC para dados que precisam ser duráveis, como bancos de dados, arquivos de upload e configurações persistentes.