Skip to main content

RFC 0001 — Storage

CampoValor
StatusProposed
Data2026-03-17
Afetacharts/kelbi-app, todos os apps com volumes
Depende de
Requerido porRFC 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 montamModoQuem escreve
~/kelbi-volumes/halk-patch-server/enhalk-en-api, halk-en-cdnRW / ROhalk-en-api (dentro do k8s)
~/kelbi-volumes/halk-patch-server/jphalk-jp-api, halk-jp-cdnRW / ROhalk-jp-api (dentro do k8s)
~/kelbi-volumes/kelbi/statickelbi-api, kelbi-cdn, kelbi-jobsRW / RO / ROkelbi-api (dentro do k8s)

Volumes efêmeros

AppVolumes 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étricaO que mostraDisponível hoje
kubelet_volume_stats_capacity_bytesCapacidade total do volumeNão — apenas em PVC
kubelet_volume_stats_used_bytesEspaço usado pelo volumeNão — apenas em PVC
kubelet_volume_stats_available_bytesEspaço livre no volumeNão — apenas em PVC
kube_persistentvolumeclaim_status_phaseEstado 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

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.


Trade-offs consolidados

AspectohostPathPVC (local-path)
Métricas de uso no GrafanaNãoSim — kubelet_volume_stats_*
Alerta de disco cheioManual (node exporter)Por volume, por app
Path previsível e estávelSim (~/kelbi-volumes/X)Não (/var/lib/rancher/k3s/storage/pvc-uid/)
Escrita de serviços externos ao k8sDireto no filesystemImpossível sem adaptação
Lifecycle independente do podNão (dados ficam no nó)Sim (retain policy configurável)
Visibilidade via kubectlNãokubectl get pvc, kubectl describe pvc
Complexidade de migraçãoCópia de dados + downtime breve
Funciona em multi-nóNão (hostPath é local)Não (local-path é RWO, single-node)
Risco de perda de dadosIgualIgual (mesmo nó, mesmo disco)
note

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.

danger

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

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: false permanente
  • 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)

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: keep

Essa 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órioArquivoResponsávelDeclara
kelbi-applicationsvalues.yamlDesenvolvedorstorage[].claim: kelbi-static-assets
homelabstorage/kelbi-static-assets/values.yamlPlataformatamanho, storageClass, namespace
homelabcharts/kelbi-storage/Plataformacomo 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

storage:
- name: static-assets
claim: kelbi-static-assets
mountPath: /data/assets
readOnly: true
Escape hatch

volumes e volumeMounts raw continuam suportados e podem coexistir com storage[] no mesmo values.yaml.


Mudanças no chart (kelbi-app 3.3.0)

ArquivoMudança
templates/deployment.yamlGera volumes e volumeMounts a partir de storage[]
values.yamlAdiciona storage: [] com comentários de exemplo

Compatibilidade

Sem breaking changes

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-server para 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[].claim gera volume PVC + volumeMount no Deployment
  • storage[].hostPath gera volume hostPath + volumeMount
  • storage[].emptyDir gera volume emptyDir + volumeMount
  • storage[].readOnly é aplicado no volumeMount
  • volumes/volumeMounts raw continuam funcionando
  • volumes/volumeMounts raw e storage[] podem coexistir
  • Testes unitários cobrindo os três backends e coexistência com volumes raw