Aplicaciones stateful en Kubernetes: StatefulSets, Persistent Volumes y PostgreSQL en el clúster
Introducción: stateful vs stateless — cuál es la diferencia y por qué importa
Kubernetes fue diseñado originalmente como plataforma para servicios stateless: si un contenedor falla, se levanta uno nuevo y todo sigue funcionando. Pero el mundo real es diferente. Las bases de datos, las colas de mensajes y los cachés con persistencia son aplicaciones stateful que almacenan datos y requieren una identidad estable entre reinicios.
Diferencias clave entre stateful y stateless en el contexto de Kubernetes:
- Identidad del pod. Los pods stateless son intercambiables: pod-abc y pod-xyz hacen lo mismo. Los pods stateful tienen nombres estables (por ejemplo,
postgres-0,postgres-1) y registros DNS que se conservan tras el reinicio. - Almacenamiento persistente. Un contenedor stateless no necesita guardar datos en disco. PostgreSQL sin un volumen persistente perderá todos los datos al reiniciarse.
- Orden de arranque y parada. En un clúster PostgreSQL es importante que el primary arranque antes que las réplicas. StatefulSet garantiza la creación y eliminación ordenada de pods.
Precisamente para resolver estos problemas apareció el objeto StatefulSet en Kubernetes. Vamos a analizarlo en detalle.
StatefulSets: funcionamiento y diferencias con Deployment
StatefulSet es un controlador de Kubernetes diseñado específicamente para gestionar aplicaciones stateful. A diferencia de Deployment, ofrece tres garantías:
- Identificadores de red estables. Cada pod recibe un nombre DNS predecible con el formato
<pod-name>.<service-name>.<namespace>.svc.cluster.local. Para PostgreSQL, esto significa quepostgres-0.postgres-headless.default.svc.cluster.localsiempre apunta al nodo primary. - Almacenamiento persistente estable. Cada pod obtiene su propio
PersistentVolumeClaim, que no se elimina al reiniciar el pod: los datos se conservan. - Despliegue y escalado ordenados. Los pods se crean en orden (0, 1, 2...) y se eliminan en orden inverso.
StatefulSet vs Deployment: comparación práctica
Deployment es adecuado para servidores API, frontends y procesos worker sin estado. StatefulSet es necesario para PostgreSQL, Redis Cluster, Elasticsearch, Kafka — en cualquier caso donde cada instancia es única y almacena datos. Al actualizar un Deployment, todos los pods pueden reemplazarse simultáneamente (con RollingUpdate sin demoras). StatefulSet actualiza los pods de forma secuencial, lo cual es crítico para un clúster de base de datos.
Ejemplo de StatefulSet mínimo para PostgreSQL
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: default
spec:
serviceName: postgres-headless
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: mydb
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2"
readinessProbe:
exec:
command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"]
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-U", "$(POSTGRES_USER)"]
initialDelaySeconds: 30
periodSeconds: 10
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
Observe la sección volumeClaimTemplates — esta es la característica clave de StatefulSet. Para cada pod se crea automáticamente un PVC independiente con nombres como postgres-data-postgres-0, postgres-data-postgres-1, etc.
Headless Service para StatefulSet
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
namespace: default
spec:
clusterIP: None
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
El Headless Service (con clusterIP: None) no crea una IP virtual única, sino que registra entradas DNS para cada pod por separado. Es la base para el direccionamiento estable de los nodos del clúster PostgreSQL.
Persistent Volumes y Persistent Volume Claims: configuración del almacenamiento
El almacenamiento en Kubernetes se organiza a través de tres niveles de abstracción:
- PersistentVolume (PV) — recurso de almacenamiento real: un disco en la nube, un recurso compartido NFS, un SSD local. Lo crea el administrador del clúster o dinámicamente mediante un StorageClass.
- PersistentVolumeClaim (PVC) — solicitud de almacenamiento de un pod. Describe el tamaño requerido, el modo de acceso y el StorageClass.
- StorageClass — plantilla para la creación dinámica de PVs. Define el tipo de disco (SSD, HDD), la política de reciclaje y el provisioner.
StorageClass para PostgreSQL en producción
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
encrypted: "true"
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
Parámetros clave para producción:
reclaimPolicy: Retain— al eliminar el PVC, los datos del disco se conservan. Nunca useDeletepara bases de datos en producción.volumeBindingMode: WaitForFirstConsumer— el disco se crea en la misma zona de disponibilidad que el pod. Crítico para clústeres multi-AZ.allowVolumeExpansion: true— permite aumentar el tamaño del PVC sin recrearlo.encrypted: "true"— cifrado de datos a nivel de disco.
Ampliación del tamaño de un PVC
Para aumentar la capacidad de almacenamiento, basta con modificar el campo storage en el PVC existente:
kubectl patch pvc postgres-data-postgres-0 \
-p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'
Kubernetes ampliará automáticamente el volumen si el provisioner lo admite y el StorageClass tiene allowVolumeExpansion: true.
Despliegue de PostgreSQL en Kubernetes: paso a paso
Veamos el proceso completo de despliegue de PostgreSQL en Kubernetes desde cero. Crearemos un Secret, un ConfigMap, un Headless Service, un Service para acceso externo y un StatefulSet.
Paso 1: Secret con las credenciales
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
namespace: default
type: Opaque
stringData:
username: pgadmin
password: "S3cur3P@ssw0rd!"
replication-password: "R3pl1c@P@ss!"
En producción se recomienda utilizar gestores de secretos externos: HashiCorp Vault con el operador vault-secrets-operator, AWS Secrets Manager o Sealed Secrets.
Paso 2: ConfigMap con postgresql.conf
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
namespace: default
data:
postgresql.conf: |
max_connections = 200
shared_buffers = 512MB
effective_cache_size = 1536MB
maintenance_work_mem = 128MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 2621kB
min_wal_size = 1GB
max_wal_size = 4GB
max_worker_processes = 4
max_parallel_workers_per_gather = 2
max_parallel_workers = 4
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /archive/%f && cp %p /archive/%f'
hot_standby = on
log_timezone = 'UTC'
datestyle = 'iso, mdy'
timezone = 'UTC'
log_statement = 'ddl'
log_min_duration_statement = 1000
Paso 3: Service para acceso externo
apiVersion: v1
kind: Service
metadata:
name: postgres-primary
namespace: default
annotations:
service.beta.kubernetes.io/aws-load-balancer-internal: "true"
spec:
selector:
app: postgres
role: primary
ports:
- port: 5432
targetPort: 5432
type: ClusterIP
Paso 4: Aplicar los manifiestos y verificar
# Aplicamos todos los manifiestos
kubectl apply -f postgres-secret.yaml
kubectl apply -f postgres-config.yaml
kubectl apply -f postgres-headless-svc.yaml
kubectl apply -f postgres-statefulset.yaml
# Verificamos el estado
kubectl get statefulsets
kubectl get pods -l app=postgres
kubectl get pvc
# Nos conectamos a PostgreSQL
kubectl exec -it postgres-0 -- psql -U pgadmin -d mydb
# Verificamos el estado de la replicación
kubectl exec -it postgres-0 -- psql -U pgadmin -c "SELECT * FROM pg_stat_replication;"
Operador PostgreSQL (CloudNativePG) en 2026: automatización de la gestión del clúster
La gestión manual de StatefulSet es útil para aprender, pero en producción se recomienda encarecidamente usar un operador. CloudNativePG es el estándar de facto para ejecutar PostgreSQL en Kubernetes en 2026. Es un proyecto CNCF desarrollado activamente por el equipo de EDB (EnterpriseDB).
Qué ofrece CloudNativePG
- Gestión automática de la topología primary/replica con failover.
- Soporte nativo de replicación en streaming y slots de replicación.
- Integración nativa con pg_basebackup, Barman y WAL-G para copias de seguridad.
- Gestión de usuarios y bases de datos mediante CRDs.
- Aplicación automática de cambios de configuración en PostgreSQL sin tiempo de inactividad.
- Soporte de scheduled backups y point-in-time recovery (PITR).
- Integración con Prometheus y Grafana de serie.
Instalación de CloudNativePG
# Instalación mediante kubectl
kubectl apply -f \
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.25/releases/cnpg-1.25.0.yaml
# O mediante Helm
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
helm upgrade --install cnpg \
--namespace cnpg-system \
--create-namespace \
cnpg/cloudnative-pg
# Verificamos la instalación
kubectl get pods -n cnpg-system
Creación de un clúster PostgreSQL con CloudNativePG
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres-cluster
namespace: production
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.3
postgresql:
parameters:
max_connections: "200"
shared_buffers: "512MB"
effective_cache_size: "1536MB"
log_statement: "ddl"
log_min_duration_statement: "1000"
pg_hba:
- host all all 10.0.0.0/8 scram-sha-256
bootstrap:
initdb:
database: myapp
owner: myapp_user
secret:
name: myapp-db-credentials
storage:
size: 100Gi
storageClass: fast-ssd
walStorage:
size: 20Gi
storageClass: fast-ssd
backup:
barmanObjectStore:
destinationPath: s3://my-postgres-backups/cnpg
s3Credentials:
accessKeyId:
name: s3-credentials
key: ACCESS_KEY_ID
secretAccessKey:
name: s3-credentials
key: ACCESS_SECRET_KEY
wal:
compression: gzip
maxParallel: 8
retentionPolicy: "30d"
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "4"
affinity:
enablePodAntiAffinity: true
topologyKey: kubernetes.io/hostname
monitoring:
enablePodMonitor: true
Este manifiesto crea un clúster PostgreSQL 16 de tres nodos con replicación automática, copias de seguridad en S3 y monitorización integrada. CloudNativePG asigna automáticamente los roles primary y replica, gestiona el failover y la actualización de los nodos.
Verificación del estado del clúster mediante el plugin kubectl
# Instalación del plugin cnpg
kubectl krew install cnpg
# Estado del clúster
kubectl cnpg status postgres-cluster -n production
# Cambio manual del primary (switchover)
kubectl cnpg promote postgres-cluster postgres-cluster-2 -n production
# Ver los logs
kubectl cnpg logs cluster postgres-cluster -n production
Copias de seguridad y restauración de PostgreSQL en Kubernetes
Las copias de seguridad son una parte crítica de la operación en producción. CloudNativePG admite dos modos: copias físicas mediante pg_basebackup/Barman y archivado WAL para PITR.
Scheduled Backup
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: postgres-daily-backup
namespace: production
spec:
schedule: "0 2 * * *" # Cada día a las 02:00 UTC
backupOwnerReference: self
cluster:
name: postgres-cluster
method: barmanObjectStore
immediate: true
Point-in-Time Recovery (PITR)
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgres-restored
namespace: production
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.3
bootstrap:
recovery:
source: postgres-cluster
recoveryTarget:
targetTime: "2026-03-15 14:30:00"
externalClusters:
- name: postgres-cluster
barmanObjectStore:
destinationPath: s3://my-postgres-backups/cnpg
s3Credentials:
accessKeyId:
name: s3-credentials
key: ACCESS_KEY_ID
secretAccessKey:
name: s3-credentials
key: ACCESS_SECRET_KEY
storage:
size: 100Gi
storageClass: fast-ssd
PITR permite restaurar la base de datos a cualquier momento del tiempo para el que se hayan conservado segmentos WAL. Es imprescindible ante un DROP TABLE accidental o un error lógico de la aplicación.
Copia de seguridad manual con Velero
Para una capa adicional de protección se puede usar Velero para hacer copias de seguridad de los PVCs a nivel de Kubernetes. Sin embargo, para PostgreSQL son preferibles las copias application-consistent mediante Barman o pg_dump, ya que Velero realiza snapshots a nivel de sistema de archivos, lo que puede generar un estado inconsistente al capturar la imagen de una base de datos en ejecución.
Políticas de red y seguridad para servicios stateful
La seguridad de los servicios stateful en Kubernetes requiere atención adicional: una fuga de datos de la base de datos es catastrófica. Aplique el principio de mínimo privilegio.
NetworkPolicy para PostgreSQL
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-network-policy
namespace: production
spec:
podSelector:
matchLabels:
cnpg.io/cluster: postgres-cluster
policyTypes:
- Ingress
- Egress
ingress:
# Permitimos tráfico solo desde la aplicación
- from:
- namespaceSelector:
matchLabels:
name: production
podSelector:
matchLabels:
app: myapp
ports:
- protocol: TCP
port: 5432
# Permitimos la replicación interna del clúster
- from:
- podSelector:
matchLabels:
cnpg.io/cluster: postgres-cluster
ports:
- protocol: TCP
port: 5432
egress:
# Permitimos la replicación entre nodos
- to:
- podSelector:
matchLabels:
cnpg.io/cluster: postgres-cluster
ports:
- protocol: TCP
port: 5432
# Permitimos DNS
- ports:
- protocol: UDP
port: 53
# Permitimos copias de seguridad en S3
- ports:
- protocol: TCP
port: 443
Pod Security y RBAC
Medidas de seguridad adicionales para producción:
- Use
PodSecurityAdmissioncon la políticarestrictedpara el namespace de PostgreSQL. - Configure
SecurityContextconrunAsNonRoot: true,readOnlyRootFilesystem: trueyallowPrivilegeEscalation: false. - Restrinja RBAC: la ServiceAccount de PostgreSQL no debe tener acceso a los secretos de otros namespaces.
- Use TLS para las conexiones entre réplicas (CloudNativePG lo activa por defecto).
- Habilite el cifrado de etcd para proteger los Secrets a nivel del clúster Kubernetes.
- Rote las contraseñas mediante un vault externo y use credenciales de corta duración.
PodDisruptionBudget para alta disponibilidad
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: postgres-pdb
namespace: production
spec:
maxUnavailable: 1
selector:
matchLabels:
cnpg.io/cluster: postgres-cluster
El PDB garantiza que Kubernetes no elimine más de un pod simultáneamente durante el drenaje de un nodo o la actualización del clúster, asegurando el quórum para la replicación.
Monitorización de PostgreSQL en Kubernetes: pg_exporter y Grafana
Sin una monitorización completa es imposible gestionar PostgreSQL en producción. CloudNativePG incluye un endpoint compatible con Prometheus, pero para una monitorización detallada se recomienda usar postgres_exporter.
Despliegue de postgres_exporter
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres-exporter
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: postgres-exporter
template:
metadata:
labels:
app: postgres-exporter
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9187"
spec:
containers:
- name: postgres-exporter
image: prometheuscommunity/postgres-exporter:v0.15.0
ports:
- containerPort: 9187
env:
- name: DATA_SOURCE_NAME
valueFrom:
secretKeyRef:
name: postgres-exporter-secret
key: datasource
args:
- --collector.stat_statements
- --collector.replication
- --collector.replication_slot
- --collector.long_running_transactions
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "200m"
PodMonitor para CloudNativePG
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: postgres-cluster-monitor
namespace: production
spec:
selector:
matchLabels:
cnpg.io/cluster: postgres-cluster
podMetricsEndpoints:
- port: metrics
interval: 30s
scrapeTimeout: 25s
Métricas clave para el dashboard de Grafana
Configure alertas y dashboards para las siguientes métricas:
pg_stat_database_tup_fetched— número de operaciones fetch por base de datos.pg_stat_replication_pg_wal_lsn_diff— replication lag en bytes. Alerta si supera los 100 MB.pg_locks_count— número de bloqueos. Un aumento indica problemas con las consultas.pg_stat_bgwriter_buffers_alloc— presión sobre la caché de buffers.cnpg_collector_pg_postmaster_start_time— hora del último reinicio de PostgreSQL.pg_database_size_bytes— tamaño de las bases de datos. Útil para planificar la ampliación del almacenamiento.pg_stat_statements_mean_exec_time_seconds— tiempo medio de ejecución de las consultas.
Dashboard de Grafana
Use el dashboard oficial de CloudNativePG para Grafana (ID: 20417) — incluye todas las métricas críticas del clúster y paneles individuales para cada instancia. Para postgres_exporter use el dashboard ID 9628.
Conclusiones y recomendaciones
Ejecutar PostgreSQL en Kubernetes ha dejado de ser algo exótico — en 2026 es una práctica madura con un rico ecosistema de herramientas. Resumamos las recomendaciones clave para producción:
- Use CloudNativePG en lugar de gestionar StatefulSet manualmente. El operador se encarga del failover, la gestión de la replicación, las copias de seguridad y las actualizaciones.
- Configure siempre el StorageClass con
reclaimPolicy: Retain. La pérdida de datos por la eliminación accidental de un PVC es inaceptable. - Distribuya las réplicas en distintas zonas de disponibilidad mediante reglas de affinity y
topologySpreadConstraints. - Configure el archivado WAL desde el principio. PITR salva ante errores lógicos que no cubren las copias físicas.
- Use NetworkPolicy para restringir el acceso a PostgreSQL únicamente a los servicios autorizados.
- No ejecute PostgreSQL en nodos compartidos con aplicaciones de alto consumo de recursos. Use un node pool dedicado con taint/toleration para el aislamiento.
- Pruebe la restauración regularmente. Una copia de seguridad que no se ha probado no es una copia de seguridad.
- Monitorice el replication lag y el tamaño del WAL. Estas métricas son las primeras en señalar problemas de rendimiento.
El ecosistema de Kubernetes sigue evolucionando y las aplicaciones stateful en Kubernetes son cada vez más fiables. Un clúster PostgreSQL correctamente configurado en Kubernetes con CloudNativePG, monitorización sólida y copias de seguridad automatizadas puede satisfacer los requisitos de los sistemas de producción más exigentes.
Tecnologías
Etiquetas
Ruslan Ismailov
Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →