RFC 0001 — Storage
| Campo | Valor |
|---|---|
| Status | Proposed |
| Data | 2026-03-17 |
| Afeta | charts/kelbi-app, todos os apps com volumes |
| Depende de | — |
| Requerido por | RFC 0002 — CDN |
Contexto do ambiente
O cluster roda k3s em uma única VPS. Todos os dados persistentes ficam em $HOME/kelbi-volumes/ no nó, montados via hostPath nos pods. O único app que hoje usa PVC é kelbi-pg (PostgreSQL), provisionado pelo local-path-provisioner padrão do k3s.
VPS (single node k3s)
└── $HOME/kelbi-volumes/
├── halk-patch-server/
│ ├── en/ ← patch data do halk EN
│ └── jp/ ← patch data do halk JP
└── kelbi/
└── static/ ← assets estáticos do kelbi
Mapa de volumes atual
Seis apps montam volumes via hostPath. Três paths são compartilhados entre múltiplos apps:
| Path no nó | Apps que montam | Modo | Quem escreve |
|---|---|---|---|
~/kelbi-volumes/halk-patch-server/en | halk-en-api, halk-en-cdn | RW / RO | halk-en-api (dentro do k8s) |
~/kelbi-volumes/halk-patch-server/jp | halk-jp-api, halk-jp-cdn | RW / RO | halk-jp-api (dentro do k8s) |
~/kelbi-volumes/kelbi/static | kelbi-api, kelbi-cdn, kelbi-jobs | RW / RO / RO | kelbi-api (dentro do k8s) |
Volumes efêmeros
| App | Volumes emptyDir |
|---|---|
kelbi-cdn | /var/cache/nginx, /tmp, /run |
Problema
1. Infraestrutura nos valores da aplicação
# dev precisa saber o path exato do nó
volumes:
- name: data
hostPath:
path: /root/volumes/halk-patch-server/en
type: Directory
Se o path mudar, todos os apps que o referenciam precisam ser atualizados manualmente. Não há inventário centralizado de quais apps dependem de qual diretório.
2. Sem visibilidade operacional
Com hostPath, o Kubernetes não sabe nada sobre o storage — sem métricas, sem status, sem alertas. Com o Prometheus + kube-state-metrics já rodando no cluster, estamos perdendo dados que já poderiam existir:
| Métrica | O que mostra | Disponível hoje |
|---|---|---|
kubelet_volume_stats_capacity_bytes | Capacidade total do volume | Não — apenas em PVC |
kubelet_volume_stats_used_bytes | Espaço usado pelo volume | Não — apenas em PVC |
kubelet_volume_stats_available_bytes | Espaço livre no volume | Não — apenas em PVC |
kube_persistentvolumeclaim_status_phase | Estado do PVC (Bound/Pending/Lost) | Não — apenas em PVC |
O kelbi-pg já beneficia dessas métricas por usar PVC. Os apps CDN não têm nenhuma visibilidade.
3. Acoplamento ao path do nó
O path /root/volumes/... está hardcoded em todos os values.yaml. Mover os dados para outro diretório ou migrar para outro nó exige alterar todos os apps.
Vale a pena migrar para PVC?
Depende do tipo de volume. Cada grupo tem características diferentes:
Análise por grupo
- emptyDir — Vale (trivial)
- kelbi/static — Vale (moderado)
- halk-patch-server — Vale (moderado)
Volumes: cache, tmp, run do kelbi-cdn
Hoje declarados como emptyDir: {} no pod spec. Migrar para o bloco storage é puramente cosmético — sem mudança de comportamento, apenas sintaxe mais limpa.
Recomendação: migrar junto com a adoção do bloco storage. Risco zero.
Volumes: ~/kelbi-volumes/kelbi/static
Apps: kelbi-api (escrita), kelbi-cdn (leitura), kelbi-jobs (leitura)
Todos os três apps rodam dentro do k8s. O local-path-provisioner do k3s suporta ReadWriteOnce, que permite múltiplos pods no mesmo nó montarem o mesmo PVC — comportamento compatível com a situação atual.
Ganhos com PVC:
- Métricas de uso de disco no Grafana
- Alerta quando espaço livre ficar abaixo de X%
- Path gerenciado pelo Kubernetes — não hardcoded nos values
- Lifecycle separado do pod (dados sobrevivem ao delete do app)
Riscos:
- O k3s armazena PVCs em
/var/lib/rancher/k3s/storage/pvc-<uid>/— path não-determinístico - Migração requer cópia dos dados existentes
Recomendação: vale migrar. Todos os escritores estão dentro do k8s.
Volumes: ~/kelbi-volumes/halk-patch-server/en e /jp
Apps: halk-en-api (escrita), halk-en-cdn (leitura) — e o equivalente para jp
Todos os apps rodam dentro do k8s. O halk-en-api escreve os dados de patch e o halk-en-cdn os lê. Com k3s em nó único, ReadWriteOnce permite múltiplos pods no mesmo nó montarem o mesmo PVC — compatível com a situação atual.
Ganhos com PVC:
- Métricas de uso por volume separadas por região (en/jp)
- Alerta quando espaço de patch ficar baixo
- Path gerenciado pelo Kubernetes
Riscos:
- Dois PVCs compartilhados entre dois pods cada — testar que ambos montam corretamente
- Migração requer cópia dos dados existentes
Recomendação: vale migrar, mesma abordagem do kelbi/static.
Trade-offs consolidados
| Aspecto | hostPath | PVC (local-path) |
|---|---|---|
| Métricas de uso no Grafana | Não | Sim — kubelet_volume_stats_* |
| Alerta de disco cheio | Manual (node exporter) | Por volume, por app |
| Path previsível e estável | Sim (~/kelbi-volumes/X) | Não (/var/lib/rancher/k3s/storage/pvc-uid/) |
| Escrita de serviços externos ao k8s | Direto no filesystem | Impossível sem adaptação |
| Lifecycle independente do pod | Não (dados ficam no nó) | Sim (retain policy configurável) |
Visibilidade via kubectl | Não | kubectl get pvc, kubectl describe pvc |
| Complexidade de migração | — | Cópia de dados + downtime breve |
| Funciona em multi-nó | Não (hostPath é local) | Não (local-path é RWO, single-node) |
| Risco de perda de dados | Igual | Igual (mesmo nó, mesmo disco) |
Em termos de durabilidade, hostPath e PVC com local-path são equivalentes — ambos armazenam no mesmo disco da VPS. A diferença é operacional: visibilidade, abstração e lifecycle.
Estratégia de migração
Fase 1 — Adoção do bloco storage (sem mudar backend)
Implementar o bloco storage no chart mantendo hostPath como backend. Nenhum dado se move. Os apps trocam a sintaxe verbose por storage[] sem risco operacional.
# fase 1 — mesmos dados, sintaxe nova
storage:
- name: patch-data
hostPath: /root/volumes/halk-patch-server/en
mountPath: /app/data
Fase 2 — Migrar todos os volumes para PVC
Todos os escritores estão dentro do k8s — sem bloqueadores. Os três grupos podem ser migrados de forma independente.
Downtime esperado por grupo: breve (tempo do rollout do deployment)
Como criar os PVCs via GitOps
PVCs têm um ciclo de vida diferente de pods e deployments — eles devem sobreviver ao delete de um app e nunca ser removidos automaticamente. Isso exige tratamento especial no ArgoCD.
O risco do prune automático
O kelbi-apps-appset já tem prune: false, o que protege parcialmente. Mas PVCs dentro de um Helm release são deletados quando helm delete é executado — a menos que tenham a annotation helm.sh/resource-policy: keep.
Um PVC gerenciado pelo Helm sem helm.sh/resource-policy: keep é deletado junto com o release. Em k3s com local-path, o diretório de dados é apagado imediatamente e irrecuperável.
Opções de onde gerenciar
- A — homelab/storage (recomendado)
- B — kelbi-applications
- C — dentro do kelbi-app chart
Criar um diretório storage/ no repositório homelab com os YAMLs de PVC. Um ArgoCD Application dedicado aplica esses recursos com prune: false permanente.
homelab/
└── storage/
├── pvcs/
│ ├── kelbi-static-assets.yaml
│ ├── halk-en-patch-data.yaml ← fase 2
│ └── halk-jp-patch-data.yaml
└── argocd-app.yaml ← Application que aponta para storage/pvcs/
ArgoCD Application para storage:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cluster-storage
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/Invasor-de-Fronteiras/homelab.git
targetRevision: HEAD
path: storage/pvcs
destination:
server: https://kubernetes.default.svc
namespace: apps
syncPolicy:
automated:
prune: false # nunca deletar PVCs automaticamente
selfHeal: true
PVC exemplo:
# storage/pvcs/kelbi-static-assets.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: kelbi-static-assets
namespace: apps
annotations:
platform.arcamh.com/owner: kelbi-cdn # documentação de quem usa
platform.arcamh.com/path: kelbi/static # path original no nó
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 5Gi
Prós:
- Plataforma controla o ciclo de vida do storage
- PVC nunca é deletado acidentalmente —
prune: falsepermanente - Inventário centralizado de todos os PVCs do cluster
- Separação clara:
homelab= infraestrutura,kelbi-applications= apps
Contras:
- Mudança em dois repos quando um app precisa de novo storage (kelbi-applications + homelab)
Adicionar um pvc.yaml ao lado do values.yaml de cada app. O kelbi-apps-appset precisaria ser expandido para também processar esses arquivos, ou um segundo appset leria pvc.yaml.
kelbi-applications/
└── apps/Invasor-de-Fronteiras/
└── kelbi-cdn/
├── values.yaml
└── pvc.yaml ← novo
Prós:
- Storage declarado junto com o app que o usa
- Um único repo para o time de app
Contras:
- O
kelbi-apps-appsetatual só lêvalues.yaml— precisaria de adaptação - PVC com o mesmo ciclo de vida do app: risco de deleção acidental
- Storage é infraestrutura, não config de app — mistura responsabilidades
O chart gera o PVC como recurso Helm, com annotation para evitar deleção.
# templates/pvc.yaml (dentro do chart)
{{- range .Values.storage }}
{{- if .claim }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .claim }}
annotations:
helm.sh/resource-policy: keep # não deletar no helm uninstall
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources:
requests:
storage: {{ .size | default "5Gi" }}
{{- end }}
{{- end }}
Prós:
- Zero configuração extra — PVC criado automaticamente no primeiro deploy
- Tamanho declarado junto com o app
Contras:
- Depende da annotation
helm.sh/resource-policy: keep— um erro humano a menos - O chart não sabe se o PVC já existe com dados — pode criar um novo PVC vazio
- PVC fica "órfão" no Helm (não rastreado após
keep) — sem visibilidade viahelm status - Tamanho do storage em
values.yamldo app — mistura infraestrutura com config
Recomendação — chart kelbi-storage + ApplicationSet
Criar um chart dedicado kelbi-storage e um ApplicationSet que lê homelab/storage/ — o mesmo padrão do kelbi-apps-appset.
Estrutura no repositório homelab:
homelab/
├── charts/
│ └── kelbi-storage/ ← novo chart
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
│ └── pvc.yaml
├── storage/ ← um diretório por PVC
│ ├── kelbi-static-assets/
│ │ └── values.yaml
│ ├── halk-en-patch-data/ ← fase 2
│ │ └── values.yaml
│ └── halk-jp-patch-data/
│ └── values.yaml
└── argocd/
└── storage-appset.yaml ← novo ApplicationSet
storage/kelbi-static-assets/values.yaml:
name: kelbi-static-assets
namespace: apps
size: 10Gi
storageClass: local-path
accessMode: ReadWriteOnce
description: "Assets estáticos do kelbi — usado por kelbi-api, kelbi-cdn, kelbi-jobs"
charts/kelbi-storage/templates/pvc.yaml:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Values.name }}
namespace: {{ .Values.namespace | default "apps" }}
labels:
app.kubernetes.io/managed-by: kelbi-storage
annotations:
helm.sh/resource-policy: keep
{{- with .Values.description }}
platform.arcamh.com/description: {{ . | quote }}
{{- end }}
spec:
accessModes:
- {{ .Values.accessMode | default "ReadWriteOnce" }}
storageClassName: {{ .Values.storageClass | default "local-path" }}
resources:
requests:
storage: {{ .Values.size }}
helm.sh/resource-policy: keepEssa annotation faz o Helm ignorar o PVC em qualquer operação de delete. Mesmo que alguém rode helm delete diretamente (fora do ArgoCD), o PVC e os dados são preservados. É a segunda linha de defesa, além do prune: false do ArgoCD.
argocd/storage-appset.yaml:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: cluster-storage
namespace: argocd
spec:
goTemplate: true
generators:
- git:
repoURL: https://github.com/Invasor-de-Fronteiras/homelab.git
revision: HEAD
files:
- path: 'storage/**/values.yaml'
values:
storage: '{{ index .path.segments 1 }}'
template:
metadata:
name: 'storage-{{ .values.storage }}'
spec:
project: default
sources:
- ref: repo
repoURL: https://github.com/Invasor-de-Fronteiras/homelab.git
targetRevision: HEAD
- repoURL: https://github.com/Invasor-de-Fronteiras/homelab.git
targetRevision: HEAD
path: charts/kelbi-storage
helm:
valueFiles:
- $repo/{{ .path.path }}/values.yaml
destination:
server: https://kubernetes.default.svc
namespace: apps
syncPolicy:
automated:
prune: false # nunca deletar PVCs automaticamente
selfHeal: true
Separação de responsabilidades
| Repositório | Arquivo | Responsável | Declara |
|---|---|---|---|
kelbi-applications | values.yaml | Desenvolvedor | storage[].claim: kelbi-static-assets |
homelab | storage/kelbi-static-assets/values.yaml | Plataforma | tamanho, storageClass, namespace |
homelab | charts/kelbi-storage/ | Plataforma | como o PVC é criado |
Fluxo para adicionar novo storage
Proposta do bloco storage
O bloco abstrai o backend. O desenvolvedor declara nome lógico e mountPath. A plataforma configura o backend.
storage:
- name: patch-data # nome lógico — referenciado por cdn[], staticFiles, etc.
claim: halk-en-patch-data # PVC (ou hostPath durante fase 1)
mountPath: /app/data
O chart gera volumes e volumeMounts automaticamente:
# gerado pelo chart
volumes:
- name: patch-data
persistentVolumeClaim:
claimName: halk-en-patch-data
volumeMounts:
- name: patch-data
mountPath: /app/data
Backends suportados
- PVC
- hostPath
- emptyDir
storage:
- name: static-assets
claim: kelbi-static-assets
mountPath: /data/assets
readOnly: true
storage:
- name: patch-data
hostPath: /root/volumes/halk-patch-server/en
mountPath: /app/data
storage:
- name: cache
emptyDir: true
mountPath: /var/cache/nginx
volumes e volumeMounts raw continuam suportados e podem coexistir com storage[] no mesmo values.yaml.
Mudanças no chart (kelbi-app 3.3.0)
| Arquivo | Mudança |
|---|---|
templates/deployment.yaml | Gera volumes e volumeMounts a partir de storage[] |
values.yaml | Adiciona storage: [] com comentários de exemplo |
Compatibilidade
storage é um novo bloco opcional. Apps sem esse campo continuam funcionando sem alteração. volumes e volumeMounts raw continuam válidos.
- Chart version:
3.3.0(minor bump, junto com RFC 0002) - Apps que migram na fase 1: todos os que têm volumes (sintaxe apenas)
- Apps que migram na fase 2: todos —
kelbi-api,kelbi-cdn,kelbi-jobs,halk-en-api,halk-en-cdn,halk-jp-api,halk-jp-cdn
Fora do escopo
- Provisioning automático de PVCs — responsabilidade de ops
storage.intent— plataforma provisiona PVC automaticamente (roadmap futuro)- Migração do
halk-patch-serverpara k8s — pré-condição para fase 3 - Multi-node storage (NFS, Longhorn) — fora do escopo para single-node VPS
Critério de aceitação
-
storage[].claimgera volume PVC + volumeMount no Deployment -
storage[].hostPathgera volume hostPath + volumeMount -
storage[].emptyDirgera volume emptyDir + volumeMount -
storage[].readOnlyé aplicado no volumeMount -
volumes/volumeMountsraw continuam funcionando -
volumes/volumeMountsraw estorage[]podem coexistir - Testes unitários cobrindo os três backends e coexistência com volumes raw