StatefulSets e Armazenamento Persistente: Rodando Dados no Kubernetes Sem Perder o Sono
Guia técnico de StatefulSets, PV, PVC e StorageClass: identidade estável, provisionamento dinâmico, access modes e as armadilhas de rodar estado no Kubernetes.
Neste artigo
O Kubernetes nasceu para cargas efêmeras. A promessa original é sedutora justamente porque nada importa individualmente: um Pod é gado, não bicho de estimação, e se ele morre outro sobe no lugar sem que ninguém perceba. Esse modelo funciona lindamente para uma API stateless, onde qualquer réplica atende qualquer requisição e a identidade de cada instância é irrelevante. O problema aparece no dia em que você precisa rodar algo que se lembra — um banco de dados, uma fila, um cache com persistência, um cluster de consenso. De repente, "qualquer Pod serve" deixa de ser verdade. A réplica que tem os dados mais recentes é diferente da que acabou de subir vazia. A instância que é a líder do cluster não pode ser tratada como intercambiável. Identidade passa a importar, e persistência de dados passa a ser questão de vida ou morte.
É para esse mundo que existem os StatefulSets e a família de objetos de armazenamento persistente do Kubernetes. Eles não são "um Deployment com disco". São uma abstração diferente, com garantias diferentes, feita para cargas onde ordem, identidade estável e dados que sobrevivem ao Pod são requisitos, não luxos. Este guia percorre como essas peças se encaixam e onde elas costumam morder quem trata estado com o mesmo descaso com que trata um serviço sem memória.
Por que um Deployment não basta#
Num Deployment, os Pods são anônimos e intercambiáveis. Seus nomes são aleatórios (api-7d9f8-x2k4l), eles sobem em qualquer ordem, e o Kubernetes pode matar qualquer um a qualquer momento durante um rollout ou reescalonamento. Se todos compartilham o mesmo volume ou nenhum tem volume, tudo bem. Mas dê a cada réplica o seu próprio disco e o modelo desmorona: quando o Pod api-7d9f8-x2k4l morre e um novo sobe com outro nome, quem garante que ele reencontra o mesmo disco de antes? Nada garante, porque o Deployment não foi projetado para amarrar identidade a armazenamento.
O StatefulSet resolve exatamente isso com três garantias que o Deployment não oferece:
- Identidade de rede estável. Os Pods recebem nomes ordinais previsíveis:
postgres-0,postgres-1,postgres-2. Esse nome não muda quando o Pod é recriado. Opostgres-0de hoje é opostgres-0de amanhã. - Armazenamento estável por Pod. Cada Pod ganha o seu próprio volume, amarrado ao seu ordinal. Quando o
postgres-1morre e é recriado, ele reencontra exatamente o disco que era dele — não o de outro Pod, não um disco vazio. - Ordem garantida. Os Pods são criados em sequência (
0, depois1, depois2), e destruídos na ordem inversa. Escalar para cima ou para baixo respeita essa ordem, o que importa muito para bancos que exigem que o primário esteja de pé antes das réplicas.
O DNS estável e o headless Service#
A identidade de rede do StatefulSet só é útil se outros conseguem encontrar cada Pod pelo nome. É aqui que entra o headless Service — um Service com clusterIP: None. Diferente de um Service normal, que dá um IP virtual único e balanceia entre os Pods de trás, o headless não balanceia nada. Ele existe para criar registros de DNS individuais, um por Pod, no formato postgres-0.postgres.namespace.svc.cluster.local. Assim, uma réplica consegue dizer explicitamente "quero replicar do postgres-0", e não "quero replicar de qualquer um por trás desse IP" — o que seria desastroso num cluster de banco.
```yaml apiVersion: v1 kind: Service metadata: name: postgres spec: clusterIP: None # headless: sem IP virtual, só DNS por Pod selector: app: postgres ports:
- port: 5432
```
Esse par — StatefulSet mais headless Service — é o padrão canônico. Um dá identidade e ordem; o outro dá endereço fixo para cada identidade.
volumeClaimTemplates: o disco que segue o Pod#
A peça que amarra armazenamento à identidade é o volumeClaimTemplates. Em vez de você declarar um volume manualmente, o StatefulSet usa esse template para fabricar um pedido de disco para cada réplica, com um nome derivado do ordinal. O resultado é um PersistentVolumeClaim por Pod: dados-postgres-0, dados-postgres-1, e assim por diante.
```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: postgres spec: serviceName: postgres # aponta para o headless Service replicas: 3 selector: matchLabels: { app: postgres } template: metadata: labels: { app: postgres } spec: containers:
- name: postgres
image: postgres:16 ports: [{ containerPort: 5432 }] volumeMounts:
- name: dados
mountPath: /var/lib/postgresql/data volumeClaimTemplates:
- metadata:
name: dados spec: accessModes: ["ReadWriteOnce"] storageClassName: standard resources: requests: storage: 20Gi ```
O detalhe que muita gente não percebe na primeira vez: esses PVCs não são apagados quando você deleta o StatefulSet, nem quando você reduz o número de réplicas. Isso é proposital e é uma proteção. Se escalar de 3 para 2 apagasse o disco do postgres-2, um simples erro de digitação destruiria dados. O Kubernetes prefere te deixar com um PVC órfão a te deixar sem dados. A consequência é que a limpeza é responsabilidade sua, e voltaremos a isso nas armadilhas.
O ciclo PV, PVC e StorageClass#
Para entender de verdade o que acontece quando o volumeClaimTemplates roda, é preciso destrinchar os três objetos de armazenamento do Kubernetes e como eles conversam.
- O PersistentVolume (PV) é o disco em si — uma peça de armazenamento real no cluster, seja um volume de nuvem, um LUN de storage, um diretório de rede. É um recurso de nível de cluster, com capacidade, modos de acesso e uma política de reciclagem.
- O PersistentVolumeClaim (PVC) é um pedido de disco feito por uma aplicação. Ele diz "quero 20Gi, com acesso ReadWriteOnce, desta classe". O PVC é a abstração que o Pod monta; ele não sabe nem quer saber qual PV específico vai atendê-lo.
- A StorageClass é a ponte entre pedido e disco. Ela descreve como provisionar armazenamento de determinado tipo — qual driver usar, quais parâmetros passar. É o que permite o provisionamento dinâmico: em vez de um administrador pré-criar PVs à mão, o PVC referencia uma StorageClass, e um provisionador cria o PV sob demanda, no exato tamanho pedido.
O fluxo, então, é: o StatefulSet emite um PVC via template; o PVC referencia uma StorageClass; o provisionador daquela classe fala com o driver CSI (Container Storage Interface, a interface padronizada que desacopla o Kubernetes de qualquer fornecedor de storage) e cria um volume real; esse volume vira um PV; o PV é ligado (bound) ao PVC; o Pod monta o PVC. Tudo automático, contanto que você tenha declarado uma StorageClass funcional.
Reclaim policy: o que acontece com o disco quando o PVC some#
Todo PV tem uma persistentVolumeReclaimPolicy, e ela decide o destino do disco quando o PVC que o usa é apagado. Delete significa "quando o PVC for embora, destrua o volume real também" — cômodo para ambientes descartáveis, perigoso para dados que importam. Retain significa "solte o PV do PVC, mas preserve o disco e seus dados; um humano decide o que fazer depois". Para qualquer armazenamento com dados de valor, Retain é a escolha prudente: ela transforma uma deleção acidental de PVC numa recuperação chata, e não numa perda definitiva.
Access modes: quem pode montar, e como#
Os modos de acesso descrevem quantos nós podem montar o volume simultaneamente, e é uma fonte enorme de confusão. ReadWriteOnce (RWO) permite montagem leitura-escrita por um único nó — não um único Pod, um único nó. É o modo dos discos de bloco de nuvem típicos e o mais comum para bancos. ReadOnlyMany (ROX) permite montagem somente-leitura por muitos nós. ReadWriteMany (RWX) permite leitura-escrita por muitos nós ao mesmo tempo, e exige um sistema de arquivos de rede que suporte isso — nem todo storage oferece. Pedir RWX quando o seu backend só faz RWO é uma receita de Pod que não sobe, travado tentando montar um volume que já está preso a outro nó.
volumeBindingMode: por que a topologia importa#
Há um parâmetro na StorageClass que salva incidentes sutis: o volumeBindingMode. O padrão histórico, Immediate, provisiona o volume assim que o PVC é criado — antes de o scheduler decidir em qual nó o Pod vai rodar. O problema: se o volume nasce na zona de disponibilidade A e o scheduler depois decide colocar o Pod num nó da zona B, o Pod não consegue montar o disco, porque discos de bloco não atravessam zonas. O resultado é um Pod eternamente Pending. A solução é WaitForFirstConsumer: o provisionamento espera o scheduler escolher o nó, e só então cria o volume na topologia certa. Em qualquer cluster multi-zona, esse modo deveria ser o padrão.
Expandir volumes sem downtime#
Cedo ou tarde 20Gi viram pouco. Se a StorageClass tem allowVolumeExpansion: true e o driver CSI suporta, você aumenta o tamanho editando o campo resources.requests.storage do PVC para um valor maior. Muitos drivers fazem a expansão online, sem desmontar. O que você não pode fazer é encolher — armazenamento só cresce. Por isso, provisione com uma margem realista desde o início, mas sem exagero que vira custo ocioso.
"Posso rodar Postgres no Kubernetes?"#
Essa é a pergunta que sempre aparece, e a resposta honesta é: pode, mas o StatefulSet só resolve a parte da infraestrutura — identidade, disco fixo, ordem de boot. Ele não sabe fazer failover, não elege um novo primário quando o atual cai, não reconfigura replicação, não roda backup consistente. Essas são responsabilidades de nível de aplicação, e é justamente por isso que bancos sérios no Kubernetes rodam sob um Operator dedicado, que codifica esse conhecimento operacional em cima do StatefulSet. O StatefulSet é o alicerce; o Operator é a inteligência. Rodar um banco de missão crítica apenas com um StatefulSet cru, sem essa camada, é possível — e é também como muita gente descobre, do jeito difícil, que "está no ar" não é o mesmo que "está seguro".
Armadilhas de armazenamento em produção#
O PVC órfão é o clássico. Você reduz réplicas ou deleta o StatefulSet, os Pods somem, mas os PVCs (e os PVs, e os discos reais que custam dinheiro) continuam lá. Meses depois, alguém descobre dezenas de volumes pagos sem nenhum Pod usando. A disciplina é ter um processo — manual ou automatizado com cuidado — de auditar e limpar PVCs desassociados, sabendo que apagá-los pode destruir dados conforme a reclaim policy.
O dado preso a uma zona é o segundo. Um volume de bloco criado na zona A não migra para a zona B. Se aquela zona tem um problema, o Pod que depende daquele disco não consegue subir em outro lugar — a "alta disponibilidade" do StatefulSet esbarra na física do armazenamento. O planejamento de resiliência de cargas stateful precisa considerar réplicas em zonas diferentes com replicação de dados entre elas, não só espalhar Pods.
O RWO tratado como se fosse RWX derruba muita gente. Dois Pods em nós diferentes tentando montar o mesmo volume RWO simplesmente não funciona; o segundo fica preso esperando a montagem que nunca vem. Cada réplica de um StatefulSet ter o seu próprio volume é o desenho correto justamente para não cair nisso.
O scale-down que você esperava que limpasse já foi mencionado, mas repita mentalmente: reduzir réplicas nunca apaga PVC. É comportamento intencional e você tem que contar com ele.
Por fim, o backup que ninguém testou. Ter volumes persistentes dá uma falsa sensação de segurança. Persistência protege contra o Pod morrer; não protege contra corrupção de dados, comando destrutivo ou falha do próprio storage. Snapshots de volume via CSI ajudam, mas snapshot que nunca foi restaurado é uma hipótese, não um backup. A regra vale para estado no Kubernetes tanto quanto em qualquer lugar: o backup que importa é o que você já provou que restaura.
Fechando#
StatefulSets e armazenamento persistente são a resposta do Kubernetes para uma verdade incômoda: nem tudo é descartável. Identidade estável, DNS previsível por Pod, disco que segue a réplica pelo ordinal, provisionamento dinâmico via StorageClass e CSI, e políticas de acesso e reciclagem que privilegiam a segurança dos dados — esse conjunto transforma a plataforma efêmera em algo capaz de hospedar estado com garantias. O que ele não faz é te dispensar de pensar. Estado exige respeito: entender access modes antes de escolher o storage, ligar WaitForFirstConsumer em cluster multi-zona, usar Retain para dados de valor, limpar PVCs órfãos com consciência, e nunca confundir "tem volume persistente" com "tem plano de recuperação". Acerte isso, e o Kubernetes deixa de ser um lugar assustador para dados e passa a ser só mais um lugar onde eles ficam — desde que você continue tratando estado com o cuidado que ele sempre mereceu.