Dominando Persistent Volumes em Kubernetes: PV, PVC e Storage Classes em Projetos Reais Já leu

Entendendo Armazenamento Persistente em Kubernetes Quando você trabalha com Kubernetes em produção, rapidamente percebe que os dados precisam sobreviver ao ciclo de vida dos pods. Diferentemente de um container tradicional, um pod no Kubernetes é efêmero — ele nasce, executa e morre. Se você armazenar dados dentro do pod, tudo será perdido. É aqui que entram os Persistent Volumes (PVs), Persistent Volume Claims (PVCs) e Storage Classes. O modelo de armazenamento persistente em Kubernetes funciona como um mecanismo de desacoplamento entre a infraestrutura e a aplicação. Você não precisa saber onde os dados serão armazenados (local, cloud, SAN) — basta declarar que precisa de armazenamento com certas características, e o Kubernetes cuida do resto. Esse padrão segue o princípio de abstração que torna Kubernetes portável entre diferentes ambientes. Persistent Volumes (PV) — O Recurso de Infraestrutura Um Persistent Volume é um recurso a nível de cluster que representa uma unidade de armazenamento física ou virtual. Ele existe independentemente de qualquer

Entendendo Armazenamento Persistente em Kubernetes

Quando você trabalha com Kubernetes em produção, rapidamente percebe que os dados precisam sobreviver ao ciclo de vida dos pods. Diferentemente de um container tradicional, um pod no Kubernetes é efêmero — ele nasce, executa e morre. Se você armazenar dados dentro do pod, tudo será perdido. É aqui que entram os Persistent Volumes (PVs), Persistent Volume Claims (PVCs) e Storage Classes.

O modelo de armazenamento persistente em Kubernetes funciona como um mecanismo de desacoplamento entre a infraestrutura e a aplicação. Você não precisa saber onde os dados serão armazenados (local, cloud, SAN) — basta declarar que precisa de armazenamento com certas características, e o Kubernetes cuida do resto. Esse padrão segue o princípio de abstração que torna Kubernetes portável entre diferentes ambientes.

Persistent Volumes (PV) — O Recurso de Infraestrutura

Um Persistent Volume é um recurso a nível de cluster que representa uma unidade de armazenamento física ou virtual. Ele existe independentemente de qualquer pod. Um administrador é responsável por criar e gerenciar os PVs, definindo qual tecnologia de storage será usada (NFS, iSCSI, cloud storage, etc).

Pense no PV como um "pedaço de disco" que você disponibiliza para o cluster. Ele possui capacidade, modo de acesso e outras propriedades. Quando um pod termina, o PV continua lá, pronto para ser usado por outro pod.

Veja um exemplo prático de um PV usando NFS:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs-001
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  nfs:
    server: 192.168.1.100
    path: "/exports/kubernetes"
  persistentVolumeReclaimPolicy: Retain

Aqui, criamos um PV de 10GB usando NFS. O accessModes define como o volume pode ser montado. As opções disponíveis são:

  • ReadWriteOnce (RWO): pode ser montado como leitura-escrita por um único nó
  • ReadOnlyMany (ROX): pode ser montado como apenas leitura por múltiplos nós
  • ReadWriteMany (RWX): pode ser montado como leitura-escrita por múltiplos nós

O persistentVolumeReclaimPolicy define o que acontece quando o PVC é deletado. Com Retain, o volume é mantido (você precisa deletá-lo manualmente). Outras opções são Delete (deleta automaticamente) e Recycle (limpa o conteúdo — obsoleto).

Ciclo de Vida do PV

Um PV passa por diferentes fases: Available (disponível para ser reclamado), Bound (vinculado a um PVC), Released (o PVC foi deletado, mas o PV ainda existe) e Failed (ocorreu um erro).

Persistent Volume Claims (PVC) — A Solicitação da Aplicação

Enquanto o PV é um recurso de infraestrutura, o PVC é uma solicitação de armazenamento feita pela aplicação ou desenvolvedor. Um PVC "reclama" um PV existente ou dispara a criação automática de um (quando usar Storage Classes, que veremos adiante).

O PVC é namespaced, ou seja, existe dentro de um namespace específico. A aplicação (pod) se vincula ao PVC, não diretamente ao PV. Essa separação permite que desenvolvedores solicitem armazenamento sem conhecer detalhes de infraestrutura.

Veja um exemplo de PVC que reclama o PV que criamos anteriormente:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: meu-pvc
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  volumeName: pv-nfs-001

Neste exemplo, solicitamos 5GB de armazenamento em modo ReadWriteOnce e especificamos explicitamente que queremos usar o PV chamado pv-nfs-001. Após criar este PVC, o status muda para "Bound" e ele fica pronto para ser usado.

Agora, veja como um pod usa o PVC:

apiVersion: v1
kind: Pod
metadata:
  name: app-com-storage
spec:
  containers:
  - name: minha-aplicacao
    image: nginx:latest
    volumeMounts:
    - name: dados
      mountPath: /data
  volumes:
  - name: dados
    persistentVolumeClaim:
      claimName: meu-pvc

O pod monta o PVC no caminho /data. Qualquer arquivo salvo nesse diretório será persistido no NFS. Se o pod morrer e um novo pod montar o mesmo PVC, os dados estarão lá.

Storage Classes — Provisionamento Dinâmico

Até agora, criamos PVs manualmente. Em ambientes reais, especialmente em cloud, você não quer fazer isso. Storage Classes permite provisionamento dinâmico: quando um PVC é criado, a Storage Class automaticamente cria um PV correspondente.

Uma Storage Class define o tipo de storage disponível e como os PVs devem ser criados. Cada cloud provider (AWS, Azure, GCP) possui suas próprias Storage Classes, assim como tecnologias on-premises como Ceph ou Longhorn.

Veja um exemplo de Storage Class usando o provisionador padrão do Kubernetes em um cluster local (usando hostPath — apenas para desenvolvimento):

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
allowVolumeExpansion: true

Este é um Storage Class que usa o provisionador no-provisioner, adequado para volumes estáticos. Aqui está um exemplo mais realista com um provisionador dinâmico (como em um cluster AWS):

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-gp3
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"
  throughput: "125"
  encrypted: "true"
allowVolumeExpansion: true
reclaimPolicy: Delete

Este Storage Class usa o provisionador EBS da AWS, cria volumes GP3 com 3000 IOPS, throughput de 125 MB/s e criptografia habilitada.

Usando Storage Class com PVC

Quando você cria um PVC referenciando uma Storage Class, o PV é criado automaticamente:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dados-app
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ebs-gp3
  resources:
    requests:
      storage: 20Gi

Observe que agora usamos storageClassName em vez de volumeName. O Kubernetes procura um Storage Class chamado ebs-gp3, usa seu provisionador para criar um novo PV e vincula automaticamente o PVC a ele.

Expansão Dinâmica de Volumes

Uma característica poderosa é a allowVolumeExpansion: true na Storage Class. Isso permite aumentar o tamanho de um volume existente editando o PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: dados-app
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: ebs-gp3
  resources:
    requests:
      storage: 50Gi  # aumentado de 20Gi

Dependendo do tipo de storage e do sistema de arquivos, essa expansão pode acontecer sem downtime. Isso é especialmente útil em produção quando um volume fica cheio e você precisa aumentar sua capacidade rapidamente.

Um Exemplo Prático Completo

Para consolidar o aprendizado, vamos criar um cenário real: um banco de dados PostgreSQL persistente em Kubernetes.

Primeiro, a Storage Class:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: postgres-storage
provisioner: kubernetes.io/no-provisioner
allowVolumeExpansion: true

O PVC para o PostgreSQL:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: postgres-storage
  resources:
    requests:
      storage: 50Gi

E um StatefulSet que usa este PVC:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: default
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15-alpine
        env:
        - name: POSTGRES_PASSWORD
          value: "senha-forte"
        - name: POSTGRES_USER
          value: "admin"
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        ports:
        - containerPort: 5432
          name: postgres
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
      volumes:
      - name: postgres-storage
        persistentVolumeClaim:
          claimName: postgres-pvc

Neste exemplo, usamos um StatefulSet porque o PostgreSQL precisa de identidade estável. O container monta o PVC em /var/lib/postgresql/data, garantindo que os dados persistam mesmo se o pod for reiniciado.

Para verificar o status, use:

kubectl get pv
kubectl get pvc
kubectl get storageclass
kubectl describe pvc postgres-pvc

Conclusão

Você aprendeu que armazenamento persistente em Kubernetes funciona em três camadas: infraestrutura (PV), solicitação (PVC) e provisionamento automático (Storage Class). O PV é estático e gerenciado pelo administrador, o PVC é dinâmico e gerenciado pela aplicação, e a Storage Class automatiza tudo. Na prática, em produção, você raramente criará PVs manualmente — trabalhará com Storage Classes e deixará o provisionador fazer seu trabalho.

Outro ponto crucial é entender os accessModes e escolher o tipo correto de storage para seu caso de uso. Um banco de dados precisa de ReadWriteOnce; uma aplicação que serve conteúdo estático para múltiplos nós precisa de ReadWriteMany ou ReadOnlyMany. E finalmente, lembre-se de definir uma política de reciclagem apropriada — em produção, você quase sempre usará Delete com Storage Classes, evitando acúmulo de volumes órfãos.

Referências


Artigos relacionados