Armazenamento no Kubernetes
Esta aula aborda os conceitos fundamentais de armazenamento no Kubernetes, incluindo Volumes, PersistentVolumes, PersistentVolumeClaims e StorageClasses. Você aprenderá como gerenciar dados persistentes em contêineres, a diferença entre armazenamento efêmero e persistente, e como provisionar armazenamento de forma dinâmica ou estática no cluster.
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: 1GiApó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-pvcOs 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: WaitForFirstConsumerNo 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: 10GiQuando 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: 10GiO 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
- Kubernetes Documentation: Volumes
- Kubernetes Documentation: Persistent Volumes
- Kubernetes Documentation: Storage Classes
- Kubernetes Documentation: StatefulSets
- Kubernetes Tasks: Configure Persistent Volume Storage
- Kubernetes Documentation: PVC DataSource
Exercícios
- Exercício 1: Crie um manifesto YAML para um Pod que usa um volume
emptyDire monte-o em um contêiner nginx no caminho/cache. Inclua também um comando que escreva um arquivo de teste no volume. - Exercício 2: Crie um PersistentVolume de 2Gi com acesso ReadWriteOnce usando
hostPathno caminho/mnt/pve um PersistentVolumeClaim solicitando 1Gi. Verifique se o PVC consegue ser vinculado ao PV (justifique). - Exercício 3: Crie uma StorageClass chamada
fastque use o provisionadorkubernetes.io/no-provisionere explique por que isso é útil para armazenamento local. - 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.
- Exercício 5: Explique a diferença entre
emptyDire um PersistentVolumeClaim. Em que cenário você usaria cada um?
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: {}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: 1GiO 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.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumerO 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.
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: 5GiemptyDir é 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.