DevOps

Aplicaciones stateful en Kubernetes: StatefulSets, Persistent Volumes y PostgreSQL en el clúster

Ruslan Ismailov Publicado 18 min de lectura
A

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:

  1. 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 que postgres-0.postgres-headless.default.svc.cluster.local siempre apunta al nodo primary.
  2. Almacenamiento persistente estable. Cada pod obtiene su propio PersistentVolumeClaim, que no se elimina al reiniciar el pod: los datos se conservan.
  3. 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 use Delete para 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 PodSecurityAdmission con la política restricted para el namespace de PostgreSQL.
  • Configure SecurityContext con runAsNonRoot: true, readOnlyRootFilesystem: true y allowPrivilegeEscalation: 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í →