Guía Completa de Kubernetes para Aplicaciones Stateful: Gestión Avanzada de Datos Persistentes
Las aplicaciones stateful representan uno de los desafíos más complejos en la orquestación de contenedores con Kubernetes. A diferencia de las aplicaciones stateless, que no mantienen información de estado entre sesiones y pueden ser fácilmente escaladas horizontalmente, las aplicaciones stateful requieren persistencia de datos, orden en el despliegue, identidades estables y estrategias sofisticadas de gestión del estado.
En el ecosistema cloud-native moderno, donde la mayoría de aplicaciones fueron inicialmente diseñadas para ser stateless, la necesidad de ejecutar bases de datos, message queues, sistemas de almacenamiento distribuido y otras aplicaciones que requieren persistencia de estado ha impulsado el desarrollo de herramientas y patrones especializados en Kubernetes.
Esta guía exhaustiva explora todos los aspectos de la gestión de aplicaciones stateful en Kubernetes, desde los conceptos fundamentales hasta implementaciones avanzadas de sistemas distribuidos complejos, proporcionando las herramientas y conocimientos necesarios para operar exitosamente cargas de trabajo con estado en entornos de producción.
Fundamentos de las Aplicaciones Stateful
Este punto requiere consideración cuidadosa en la implementación.
Diferencias Fundamentales: Stateful vs Stateless
La distinción entre aplicaciones stateful y stateless es fundamental para comprender las complejidades de la orquestación en Kubernetes. Las aplicaciones stateless son inherentemente simples de gestionar: cada instancia es idéntica e intercambiable, pueden ser iniciadas o terminadas sin consecuencias para el estado del sistema, y el escalado horizontal es trivial.
Las aplicaciones stateful, por el contrario, mantienen información persistente que debe sobrevivir a reinicios del contenedor, actualizaciones del sistema y fallos de hardware. Esta persistencia introduce dependencias temporales, requiere ordenamiento en operaciones de inicio y parada, y necesita identidades estables que permitan a las instancias reconocerse entre sí y mantener relaciones consistentes.
Casos de Uso Críticos para Aplicaciones Stateful
Los casos de uso más comunes para aplicaciones stateful incluyen sistemas de bases de datos relacionales como PostgreSQL, MySQL y Oracle, que requieren almacenamiento persistente para tablas, índices y logs de transacciones. Las bases de datos NoSQL como MongoDB, Cassandra y Elasticsearch necesitan persistencia para sus estructuras de datos distribuidas y mecanismos de replicación.
Los message queues como Apache Kafka, RabbitMQ y Apache Pulsar requieren almacenamiento persistente para buffers de mensajes y metadatos de particiones. Los sistemas de cache distribuido como Redis Cluster necesitan persistencia para snapshots y AOF logs. Las aplicaciones de análisis de datos como Apache Spark y sistemas de machine learning requieren almacenamiento persistente para datasets, modelos entrenados y checkpoints.
Desafíos Únicos en Kubernetes
Kubernetes fue diseñado inicialmente con un modelo cloud-native que favorecía aplicaciones stateless. La introducción de aplicaciones stateful requirió el desarrollo de nuevos primitivos y patrones que pudieran manejar las complejidades del estado persistente mientras mantenían los beneficios de la orquestación automatizada.
Los desafíos principales incluyen la gestión de identidades estables para pods, donde cada instancia debe tener un identificador consistente que persista a través de reinicios y reschedules. El ordering de operaciones es crítico, ya que muchas aplicaciones stateful requieren que las instancias se inicien en un orden específico y que las operaciones de shutdown sigan secuencias determinísticas.
La gestión de volúmenes persistentes añade complejidad significativa, requiriendo provisioning dinámico, binding apropiado de volúmenes a pods específicos, y estrategias de backup y recovery. La red debe proporcionar endpoints estables que permitan a las instancias comunicarse de manera consistente.
StatefulSets: El Primitivo Fundamental
Este punto requiere consideración cuidadosa en la implementación.
Arquitectura y Características de StatefulSets
StatefulSets es el recurso de Kubernetes diseñado específicamente para gestionar aplicaciones stateful. A diferencia de los Deployments, que crean pods idénticos e intercambiables, StatefulSets proporciona garantías de identidad estable, almacenamiento persistente y ordenamiento de operaciones.
Cada pod en un StatefulSet recibe un identificador ordinal que forma parte de su nombre (pod-0, pod-1, pod-2, etc.), proporcionando una identidad estable y predecible. Esta identidad persiste a través de reprogramaciones, asegurando que las aplicaciones puedan mantener relaciones consistentes entre instancias.
Los StatefulSets garantizan ordenamiento en las operaciones de scaling y updating. Durante el scale-up, los pods se crean secuencialmente en orden numérico, esperando que cada pod esté Ready antes de crear el siguiente. Durante el scale-down, los pods se eliminan en orden inverso. Las actualizaciones rolling siguen patrones similares, asegurando estabilidad durante los cambios.
# Ejemplo básico de StatefulSet para PostgreSQL
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-cluster
namespace: database
spec:
serviceName: postgres-headless
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
initContainers:
- name: postgres-init
image: postgres:15-alpine
command:
- bash
- -c
- |
set -ex
# Generate postgres server id from pod ordinal
[[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo $ordinal > /mnt/conf.d/server-id.conf
# Copy base configuration
cp /mnt/config-map/* /mnt/conf.d/ || true
# Configure replication based on pod ordinal
if [[ $ordinal -eq 0 ]]; then
echo "Configuring as master"
cat > /mnt/conf.d/master.conf <'EOF'
wal_level = replica
max_wal_senders = 3
max_replication_slots = 3
synchronous_commit = on
synchronous_standby_names = 'postgres-cluster-1,postgres-cluster-2'
EOF
else
echo "Configuring as replica"
cat > /mnt/conf.d/replica.conf <'EOF'
hot_standby = on
max_connections = 1000
EOF
fi
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
- name: config-map
mountPath: /mnt/config-map
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_DB
value: myapp
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: PGUSER
value: postgres
- name: POSTGRES_INITDB_ARGS
value: "-A md5"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 1000m
memory: 2Gi
livenessProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U postgres -h 127.0.0.1 -p 5432
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 6
readinessProbe:
exec:
command:
- /bin/sh
- -c
- |
pg_isready -U postgres -h 127.0.0.1 -p 5432 &&
[ -f /var/lib/postgresql/data/PG_VERSION ]
initialDelaySeconds: 5
periodSeconds: 2
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: conf
mountPath: /etc/postgresql/conf.d
readOnly: true
volumes:
- name: conf
emptyDir: {}
- name: config-map
configMap:
name: postgres-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-retain
resources:
requests:
storage: 100Gi
Gestión de Identidades y
La gestión de identidades estables es uno de los aspectos más críticos de StatefulSets. Cada pod obtiene un hostname predecible basado en el nombre del StatefulSet y su índice ordinal. Este hostname persiste a través de reprogramaciones, permitiendo que las aplicaciones mantengan configuraciones basadas en identidad.
Los Headless Services proporcionan la infraestructura de red necesaria para StatefulSets, creando registros DNS individuales para cada pod. Esto permite que las aplicaciones se conecten directamente a instancias específicas usando nombres DNS predecibles como postgres-cluster-0.postgres-headless.database.svc.cluster.local.
# Headless Service para StatefulSet
apiVersion: v1
kind: Service
metadata:
name: postgres-headless
namespace: database
labels:
app: postgres
spec:
clusterIP: None # Headless service
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
protocol: TCP
name: postgres
---
# Service regular para acceso de aplicaciones
apiVersion: v1
kind: Service
metadata:
name: postgres-read
namespace: database
labels:
app: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
protocol: TCP
name: postgres
sessionAffinity: None
---
# Service para master writes
apiVersion: v1
kind: Service
metadata:
name: postgres-write
namespace: database
labels:
app: postgres
spec:
selector:
app: postgres
role: master
ports:
- port: 5432
targetPort: 5432
protocol: TCP
name: postgres
Persistent Volumes y Storage Classes
Este punto requiere consideración cuidadosa en la implementación.
Arquitectura de Almacenamiento en Kubernetes
El subsistema de almacenamiento de Kubernetes proporciona abstracción entre las aplicaciones y la infraestructura de storage subyacente. Esta abstracción es crucial para aplicaciones stateful, ya que permite portabilidad entre diferentes proveedores de cloud y tecnologías de almacenamiento mientras mantiene consistencia en el comportamiento.
Persistent Volumes (PV) representan recursos de almacenamiento físico en el cluster, abstraídos como objetos de API. Persistent Volume Claims (PVC) representan solicitudes de almacenamiento por parte de usuarios o aplicaciones. El binding dinámico entre PVCs y PVs permite provisioning automático de storage basado en requisitos específicos.
Storage Classes proporcionan plantillas para provisioning dinámico de volúmenes, definiendo parámetros como tipo de almacenamiento, políticas de replicación, niveles de performance y opciones de backup. Esta abstracción permite que las aplicaciones soliciten storage con características específicas sin conocer detalles de implementación.
# Storage Classes para diferentes casos de uso
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ssd-retain
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
iops: "3000"
throughput: "125"
encrypted: "true"
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
mountOptions:
- debug
- noatime
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nvme-high-performance
provisioner: kubernetes.io/aws-ebs
parameters:
type: io2
iops: "10000"
encrypted: "true"
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: network-storage
provisioner: kubernetes.io/aws-efs
parameters:
provisioningMode: efs-utils
fileSystemId: fs-12345678
directoryPerms: "0755"
reclaimPolicy: Retain
allowVolumeExpansion: false
volumeBindingMode: Immediate
Estrategias de Volume Claims Templates
Los Volume Claim Templates en StatefulSets permiten que cada pod solicite su propio conjunto de volúmenes persistentes. Esta funcionalidad es esencial para aplicaciones que requieren almacenamiento dedicado por instancia, como bases de datos distribuidas.
Los templates definen especificaciones que se aplican a cada pod del StatefulSet, creando PVCs únicos con nombres predictibles. Esto permite configuraciones donde cada instancia de base de datos tiene su propio volumen de datos, logs de transacciones separados, y volúmenes de configuración específicos.
# StatefulSet con múltiples Volume Claim Templates
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch-cluster
namespace: logging
spec:
serviceName: elasticsearch-headless
replicas: 6
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
initContainers:
- name: configure-sysctl
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
command: ['sh', '-c', 'sysctl -w vm.max_map_count=262144']
securityContext:
privileged: true
- name: create-certs
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
command:
- bash
- -c
- |
if [[ ! -f /usr/share/elasticsearch/config/certs/elastic-certificates.p12 ]]; then
bin/elasticsearch-certutil ca --silent --pem -out /tmp/ca.zip
unzip /tmp/ca.zip -d /tmp
bin/elasticsearch-certutil cert --silent --pem --ca-cert /tmp/ca/ca.crt --ca-key /tmp/ca/ca.key --dns $ES_NODE_NAME --dns elasticsearch-headless --dns elasticsearch-headless.logging.svc.cluster.local --ip 127.0.0.1 --out /tmp/node.zip
unzip /tmp/node.zip -d /tmp
cp /tmp/ca/ca.crt /usr/share/elasticsearch/config/certs/
cp /tmp/instance/instance.crt /usr/share/elasticsearch/config/certs/
cp /tmp/instance/instance.key /usr/share/elasticsearch/config/certs/
bin/elasticsearch-certutil cert --silent --pem --ca-cert /tmp/ca/ca.crt --ca-key /tmp/ca/ca.key -out /tmp/elastic-certificates.p12
cp /tmp/elastic-certificates.p12 /usr/share/elasticsearch/config/certs/
fi
env:
- name: ES_NODE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: certs
mountPath: /usr/share/elasticsearch/config/certs
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
ports:
- containerPort: 9200
name: rest
- containerPort: 9300
name: inter-node
env:
- name: node.name
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: cluster.name
value: "elasticsearch-cluster"
- name: discovery.seed_hosts
value: "elasticsearch-headless"
- name: cluster.initial_master_nodes
value: "elasticsearch-cluster-0,elasticsearch-cluster-1,elasticsearch-cluster-2"
- name: ES_JAVA_OPTS
value: "-Xms2g -Xmx2g"
- name: xpack.security.enabled
value: "true"
- name: xpack.security.transport.ssl.enabled
value: "true"
- name: xpack.security.transport.ssl.verification_mode
value: "certificate"
- name: xpack.security.transport.ssl.certificate_authorities
value: "/usr/share/elasticsearch/config/certs/ca.crt"
- name: xpack.security.transport.ssl.certificate
value: "/usr/share/elasticsearch/config/certs/instance.crt"
- name: xpack.security.transport.ssl.key
value: "/usr/share/elasticsearch/config/certs/instance.key"
resources:
requests:
memory: 4Gi
cpu: 1000m
limits:
memory: 4Gi
cpu: 2000m
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
- name: logs
mountPath: /usr/share/elasticsearch/logs
- name: certs
mountPath: /usr/share/elasticsearch/config/certs
readOnly: true
readinessProbe:
httpGet:
scheme: HTTP
path: /_cluster/health?local=true
port: 9200
initialDelaySeconds: 5
livenessProbe:
httpGet:
scheme: HTTP
path: /_cluster/health?local=true
port: 9200
initialDelaySeconds: 90
periodSeconds: 10
volumes:
- name: certs
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-retain
resources:
requests:
storage: 100Gi
- metadata:
name: logs
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-retain
resources:
requests:
storage: 20Gi
Políticas de Reclamación y Expansión
Las políticas de reclamación (Reclaim Policies) determinan qué sucede con los Persistent Volumes cuando sus Persistent Volume Claims correspondientes son eliminados. La política “Retain” preserva los datos para recovery manual, mientras que “Delete” elimina automáticamente el volumen y sus datos.
Para aplicaciones stateful críticas, la política “Retain” es generalmente preferible, ya que previene pérdida accidental de datos durante operaciones de mantenimiento o errores operacionales. Esto permite recovery manual de datos en caso de eliminación inadvertida de resources.
La expansión de volúmenes permite aumentar el tamaño de volúmenes persistentes sin downtime de aplicaciones. Esta capacidad es crucial para aplicaciones stateful que experimentan crecimiento de datos orgánico y necesitan storage adicional sin interrupciones de servicio.
# Script para expansión segura de volúmenes
apiVersion: v1
kind: ConfigMap
metadata:
name: volume-expansion-script
data:
expand-volume.sh: |
#!/bin/bash
set -e
NAMESPACE=${1:-default}
STATEFULSET=${2}
NEW_SIZE=${3}
if [[ -z "$STATEFULSET" || -z "$NEW_SIZE" ]]; then
echo "Usage: $0 [namespace] <statefulset-name> <new-size>"
echo "Example: $0 database postgres-cluster 200Gi"
exit 1
fi
echo "Starting volume expansion for StatefulSet $STATEFULSET in namespace $NAMESPACE"
echo "New size: $NEW_SIZE"
# Get all PVCs for the StatefulSet
PVCS=$(kubectl get pvc -n $NAMESPACE -l app=$STATEFULSET -o name)
if [[ -z "$PVCS" ]]; then
echo "No PVCs found for StatefulSet $STATEFULSET"
exit 1
fi
# Expand each PVC
for pvc in $PVCS; do
echo "Expanding $pvc to $NEW_SIZE..."
# Check if StorageClass supports volume expansion
STORAGE_CLASS=$(kubectl get $pvc -n $NAMESPACE -o jsonpath='{.spec.storageClassName}')
EXPANSION_SUPPORTED=$(kubectl get storageclass $STORAGE_CLASS -o jsonpath='{.allowVolumeExpansion}')
if [[ "$EXPANSION_SUPPORTED" != "true" ]]; then
echo "Warning: StorageClass $STORAGE_CLASS does not support volume expansion"
continue
fi
# Patch the PVC
kubectl patch $pvc -n $NAMESPACE -p '{"spec":{"resources":{"requests":{"storage":"'$NEW_SIZE'"}}}}'
# Wait for expansion to complete
echo "Waiting for expansion to complete..."
kubectl wait --for=condition=FileSystemResizePending=false $pvc -n $NAMESPACE --timeout=300s
echo "Successfully expanded $pvc"
done
# Restart StatefulSet pods to recognize new size
echo "Restarting StatefulSet pods to recognize expanded storage..."
kubectl rollout restart statefulset/$STATEFULSET -n $NAMESPACE
kubectl rollout status statefulset/$STATEFULSET -n $NAMESPACE
echo "Volume expansion completed successfully!"
Implementación de Bases de Datos Distribuidas
Esta implementación requiere atención a los detalles y seguimiento de las mejores prácticas.
PostgreSQL con Alta Disponibilidad
PostgreSQL en Kubernetes requiere configuración cuidadosa de replicación, failover automático y backup strategies. La implementación de un cluster PostgreSQL con alta disponibilidad involucra configuración de streaming replication, automated failover con herramientas como Patroni, y integration con sistemas de backup externos.
# ConfigMap para configuración de PostgreSQL con replicación
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
namespace: database
data:
postgresql.conf: |
# Basic settings
max_connections = 200
shared_buffers = 256MB
effective_cache_size = 1GB
maintenance_work_mem = 64MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 4MB
min_wal_size = 1GB
max_wal_size = 4GB
# Replication settings
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
synchronous_commit = on
# Logging
log_destination = 'stderr'
logging_collector = on
log_directory = 'log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_truncate_on_rotation = on
log_rotation_age = 1d
log_rotation_size = 100MB
log_min_duration_statement = 1000
log_line_prefix = '%t [%p-%l] %q%u@%d '
# Monitoring
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
pg_stat_statements.save = on
pg_hba.conf: |
# TYPE DATABASE USER ADDRESS METHOD
local all all trust
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
host all all 10.0.0.0/8 md5
host replication all 10.0.0.0/8 md5
setup-replication.sh: |
#!/bin/bash
set -e
# Get pod ordinal
[[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
if [[ $ordinal -eq 0 ]]; then
echo "Setting up as primary"
# Initialize database if not exists
if [[ ! -f "$PGDATA/PG_VERSION" ]]; then
initdb -D "$PGDATA" -U postgres -A md5 --pwfile=<(echo "$POSTGRES_PASSWORD")
fi
# Configure as primary
cat >> "$PGDATA/postgresql.conf" <'EOF'
synchronous_standby_names = '*'
hot_standby = on
EOF
else
echo "Setting up as replica"
# Wait for primary to be ready
until pg_isready -h postgres-cluster-0.postgres-headless.database.svc.cluster.local -U postgres; do
echo "Waiting for primary..."
sleep 5
done
# Create base backup from primary
if [[ ! -f "$PGDATA/PG_VERSION" ]]; then
PGPASSWORD=$POSTGRES_PASSWORD pg_basebackup -h postgres-cluster-0.postgres-headless.database.svc.cluster.local -D "$PGDATA" -U postgres -v -P -W
# Configure as replica
cat > "$PGDATA/recovery.conf" <EOF
standby_mode = on
primary_conninfo = 'host=postgres-cluster-0.postgres-headless.database.svc.cluster.local port=5432 user=postgres password=$POSTGRES_PASSWORD'
primary_slot_name = 'replica_slot_$ordinal'
EOF
# Create replication slot on primary
PGPASSWORD=$POSTGRES_PASSWORD psql -h postgres-cluster-0.postgres-headless.database.svc.cluster.local -U postgres -c "SELECT pg_create_physical_replication_slot('replica_slot_$ordinal');"
fi
fi
---
# StatefulSet con configuración de replicación automática
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-cluster
namespace: database
spec:
serviceName: postgres-headless
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
initContainers:
- name: postgres-init
image: postgres:15-alpine
command: ['bash', '/scripts/setup-replication.sh']
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: config
mountPath: /scripts
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_DB
value: myapp
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
resources:
requests:
cpu: 1000m
memory: 2Gi
limits:
cpu: 2000m
memory: 4Gi
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: postgres-config
mountPath: /etc/postgresql
readOnly: true
livenessProbe:
exec:
command:
- /bin/sh
- -c
- exec pg_isready -U postgres -h 127.0.0.1 -p 5432
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- /bin/sh
- -c
- |
pg_isready -U postgres -h 127.0.0.1 -p 5432 &&
psql -U postgres -h 127.0.0.1 -lqt | cut -d \| -f 1 | grep -qw myapp
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: postgres-config
configMap:
name: postgres-config
- name: config
configMap:
name: postgres-config
defaultMode: 0755
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-retain
resources:
requests:
storage: 200Gi
MongoDB Replica
MongoDB Replica Sets proporcionan alta disponibilidad y redundancia de datos mediante replicación automática entre múltiples instancias. La configuración en Kubernetes requiere inicialización cuidadosa del replica set, gestión de roles primary/secondary, y configuración de authentication y authorization.
# MongoDB Replica Set con autenticación
apiVersion: v1
kind: Secret
metadata:
name: mongodb-secret
namespace: database
type: Opaque
data:
mongodb-root-username: YWRtaW4= # admin
mongodb-root-password: <base64-encoded-password>
mongodb-replica-set-key: <base64-encoded-keyfile>
---
apiVersion: v1
kind: ConfigMap
metadata:
name: mongodb-config
namespace: database
data:
mongod.conf: |
storage:
dbPath: /data/db
journal:
enabled: true
wiredTiger:
engineConfig:
cacheSizeGB: 1
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
verbosity: 1
net:
port: 27017
bindIp: 0.0.0.0
processManagement:
timeZoneInfo: /usr/share/zoneinfo
replication:
replSetName: rs0
security:
authorization: enabled
keyFile: /etc/secrets/mongodb-replica-set-key
init-replica-set.js: |
try {
rs.status();
print("Replica set already initialized");
} catch (e) {
print("Initializing replica set...");
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongodb-0.mongodb-headless.database.svc.cluster.local:27017", priority: 2 },
{ _id: 1, host: "mongodb-1.mongodb-headless.database.svc.cluster.local:27017", priority: 1 },
{ _id: 2, host: "mongodb-2.mongodb-headless.database.svc.cluster.local:27017", priority: 1 }
]
});
print("Waiting for replica set to be ready...");
while (!rs.isMaster().ismaster) {
sleep(1000);
}
print("Creating admin user...");
db = db.getSiblingDB('admin');
db.createUser({
user: "admin",
pwd: "$MONGO_INITDB_ROOT_PASSWORD",
roles: [
{ role: "root", db: "admin" },
{ role: "clusterAdmin", db: "admin" },
{ role: "dbAdminAnyDatabase", db: "admin" },
{ role: "userAdminAnyDatabase", db: "admin" },
{ role: "readWriteAnyDatabase", db: "admin" }
]
});
print("Replica set initialized successfully");
}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongodb
namespace: database
spec:
serviceName: mongodb-headless
replicas: 3
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
initContainers:
- name: install-tools
image: busybox:1.35
command:
- sh
- -c
- |
# Create directories
mkdir -p /var/log/mongodb
chown -R 999:999 /var/log/mongodb
# Set up keyfile permissions
cp /tmp/keyfile/mongodb-replica-set-key /etc/secrets/
chmod 400 /etc/secrets/mongodb-replica-set-key
chown 999:999 /etc/secrets/mongodb-replica-set-key
volumeMounts:
- name: secrets
mountPath: /etc/secrets
- name: keyfile-temp
mountPath: /tmp/keyfile
- name: logs
mountPath: /var/log/mongodb
containers:
- name: mongodb
image: mongo:7.0
command:
- mongod
- --config=/etc/mongod.conf
ports:
- containerPort: 27017
name: mongodb
env:
- name: MONGO_INITDB_ROOT_USERNAME
valueFrom:
secretKeyRef:
name: mongodb-secret
key: mongodb-root-username
- name: MONGO_INITDB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mongodb-secret
key: mongodb-root-password
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: 1000m
memory: 4Gi
volumeMounts:
- name: data
mountPath: /data/db
- name: config
mountPath: /etc/mongod.conf
subPath: mongod.conf
- name: secrets
mountPath: /etc/secrets
readOnly: true
- name: logs
mountPath: /var/log/mongodb
livenessProbe:
exec:
command:
- mongo
- --eval
- "db.adminCommand('ping')"
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
exec:
command:
- mongo
- --eval
- "db.adminCommand('ping')"
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
- name: replica-set-init
image: mongo:7.0
command:
- bash
- -c
- |
# Wait for MongoDB to be ready
until mongo --host localhost:27017 --eval "db.adminCommand('ping')"; do
echo "Waiting for MongoDB to start..."
sleep 5
done
# Initialize replica set only on first pod
if [[ $(hostname) == "mongodb-0" ]]; then
echo "Initializing replica set..."
envsubst /scripts/init-replica-set.js > /tmp/init.js
mongo /tmp/init.js
fi
# Keep container running
tail -f /dev/null
env:
- name: MONGO_INITDB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mongodb-secret
key: mongodb-root-password
volumeMounts:
- name: config
mountPath: /scripts
volumes:
- name: config
configMap:
name: mongodb-config
- name: secrets
emptyDir: {}
- name: keyfile-temp
secret:
secretName: mongodb-secret
items:
- key: mongodb-replica-set-key
path: mongodb-replica-set-key
- name: logs
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-retain
resources:
requests:
storage: 100Gi
Message Queues y Event Streaming
Este punto requiere consideración cuidadosa en la implementación.
Apache Kafka Cluster
Apache Kafka es una plataforma de streaming distribuida que requiere configuración cuidadosa de brokers, topics, replication factors y consumer groups. La implementación en Kubernetes involucra gestión de identidades de brokers, configuración de networking entre nodos, y integration con ZooKeeper para metadata management.
# ZooKeeper ensemble para Kafka
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
namespace: messaging
spec:
serviceName: zookeeper-headless
replicas: 3
selector:
matchLabels:
app: zookeeper
template:
metadata:
labels:
app: zookeeper
spec:
containers:
- name: zookeeper
image: confluentinc/cp-zookeeper:7.4.0
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
env:
- name: ZOOKEEPER_SERVER_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: ZOOKEEPER_CLIENT_PORT
value: "2181"
- name: ZOOKEEPER_TICK_TIME
value: "2000"
- name: ZOOKEEPER_INIT_LIMIT
value: "5"
- name: ZOOKEEPER_SYNC_LIMIT
value: "2"
- name: ZOOKEEPER_SERVERS
value: "zookeeper-0.zookeeper-headless.messaging.svc.cluster.local:2888:3888;zookeeper-1.zookeeper-headless.messaging.svc.cluster.local:2888:3888;zookeeper-2.zookeeper-headless.messaging.svc.cluster.local:2888:3888"
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 500m
memory: 1Gi
volumeMounts:
- name: data
mountPath: /var/lib/zookeeper/data
- name: logs
mountPath: /var/lib/zookeeper/log
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-retain
resources:
requests:
storage: 10Gi
- metadata:
name: logs
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-retain
resources:
requests:
storage: 10Gi
---
# Kafka Broker Cluster
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kafka
namespace: messaging
spec:
serviceName: kafka-headless
replicas: 3
selector:
matchLabels:
app: kafka
template:
metadata:
labels:
app: kafka
spec:
initContainers:
- name: init-config
image: confluentinc/cp-kafka:7.4.0
command:
- bash
- -c
- |
# Extract broker ID from hostname
[[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
export KAFKA_BROKER_ID=${BASH_REMATCH[1]}
# Generate server configuration
cat > /tmp/server.properties <EOF
broker.id=$KAFKA_BROKER_ID
listeners=PLAINTEXT://0.0.0.0:9092,INTERNAL://0.0.0.0:9093
advertised.listeners=PLAINTEXT://$(hostname).kafka-headless.messaging.svc.cluster.local:9092,INTERNAL://$(hostname).kafka-headless.messaging.svc.cluster.local:9093
listener.security.protocol.map=PLAINTEXT:PLAINTEXT,INTERNAL:PLAINTEXT
inter.broker.listener.name=INTERNAL
num.network.threads=8
num.io.threads=8
socket.send.buffer.bytes=102400
socket.receive.buffer.bytes=102400
socket.request.max.bytes=104857600
log.dirs=/var/lib/kafka/data
num.partitions=3
num.recovery.threads.per.data.dir=1
offsets.topic.replication.factor=3
transaction.state.log.replication.factor=3
transaction.state.log.min.isr=2
default.replication.factor=3
min.insync.replicas=2
log.retention.hours=168
log.retention.bytes=1073741824
log.segment.bytes=1073741824
log.retention.check.interval.ms=300000
zookeeper.connect=zookeeper-0.zookeeper-headless.messaging.svc.cluster.local:2181,zookeeper-1.zookeeper-headless.messaging.svc.cluster.local:2181,zookeeper-2.zookeeper-headless.messaging.svc.cluster.local:2181
zookeeper.connection.timeout.ms=18000
group.initial.rebalance.delay.ms=3000
auto.create.topics.enable=false
delete.topic.enable=true
EOF
cp /tmp/server.properties /config/
volumeMounts:
- name: config
mountPath: /config
containers:
- name: kafka
image: confluentinc/cp-kafka:7.4.0
ports:
- containerPort: 9092
name: kafka
- containerPort: 9093
name: internal
env:
- name: KAFKA_OPTS
value: "-Dlogging.level=INFO"
- name: KAFKA_HEAP_OPTS
value: "-Xmx2G -Xms2G"
command:
- /bin/bash
- -c
- |
exec kafka-server-start /config/server.properties
resources:
requests:
cpu: 1000m
memory: 3Gi
limits:
cpu: 2000m
memory: 4Gi
volumeMounts:
- name: data
mountPath: /var/lib/kafka/data
- name: config
mountPath: /config
readinessProbe:
exec:
command:
- bash
- -c
- |
kafka-broker-api-versions --bootstrap-server localhost:9092
initialDelaySeconds: 60
periodSeconds: 10
livenessProbe:
exec:
command:
- bash
- -c
- |
kafka-broker-api-versions --bootstrap-server localhost:9092
initialDelaySeconds: 120
periodSeconds: 30
volumes:
- name: config
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-retain
resources:
requests:
storage: 100Gi
Redis Cluster para Caching Distribuido
Redis Cluster proporciona sharding automático y alta disponibilidad para cargas de trabajo de caching intensivo. La configuración requiere inicialización del cluster, gestión de slots de hash, y configuración de replication entre master y slave nodes.
# Redis Cluster configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-cluster-config
namespace: cache
data:
redis.conf: |
port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
appendfsync everysec
# Memory management
maxmemory 2gb
maxmemory-policy allkeys-lru
# Network optimization
tcp-keepalive 60
timeout 300
# Logging
loglevel notice
logfile /var/log/redis/redis.log
# Security
protected-mode no
requirepass ${REDIS_PASSWORD}
masterauth ${REDIS_PASSWORD}
# Persistence tuning
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
# AOF tuning
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
cluster-init.sh: |
#!/bin/bash
set -e
# Wait for all pods to be ready
echo "Waiting for all Redis pods to be ready..."
for i in {0..5}; do
until redis-cli -h redis-cluster-$i.redis-headless.cache.svc.cluster.local -a $REDIS_PASSWORD ping; do
echo "Waiting for redis-cluster-$i..."
sleep 5
done
done
# Check if cluster is already initialized
if redis-cli -h redis-cluster-0.redis-headless.cache.svc.cluster.local -a $REDIS_PASSWORD cluster nodes | grep -q master; then
echo "Cluster already initialized"
exit 0
fi
# Create cluster
echo "Initializing Redis cluster..."
redis-cli --cluster create \
redis-cluster-0.redis-headless.cache.svc.cluster.local:6379 \
redis-cluster-1.redis-headless.cache.svc.cluster.local:6379 \
redis-cluster-2.redis-headless.cache.svc.cluster.local:6379 \
redis-cluster-3.redis-headless.cache.svc.cluster.local:6379 \
redis-cluster-4.redis-headless.cache.svc.cluster.local:6379 \
redis-cluster-5.redis-headless.cache.svc.cluster.local:6379 \
--cluster-replicas 1 \
--cluster-yes \
-a $REDIS_PASSWORD
echo "Redis cluster initialized successfully"
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
namespace: cache
spec:
serviceName: redis-headless
replicas: 6
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
initContainers:
- name: init-redis
image: redis:7-alpine
command:
- sh
- -c
- |
# Create config with password substitution
envsubst /tmp/redis.conf > /etc/redis/redis.conf
chown redis:redis /etc/redis/redis.conf
# Create log directory
mkdir -p /var/log/redis
chown redis:redis /var/log/redis
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-secret
key: password
volumeMounts:
- name: config
mountPath: /etc/redis
- name: config-template
mountPath: /tmp
- name: logs
mountPath: /var/log/redis
containers:
- name: redis
image: redis:7-alpine
command:
- redis-server
- /etc/redis/redis.conf
- --cluster-announce-ip
- $(POD_IP)
ports:
- containerPort: 6379
name: client
- containerPort: 16379
name: gossip
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-secret
key: password
resources:
requests:
cpu: 250m
memory: 3Gi
limits:
cpu: 500m
memory: 4Gi
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /etc/redis
- name: logs
mountPath: /var/log/redis
livenessProbe:
exec:
command:
- redis-cli
- -a
- $(REDIS_PASSWORD)
- ping
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- redis-cli
- -a
- $(REDIS_PASSWORD)
- ping
initialDelaySeconds: 10
periodSeconds: 5
volumes:
- name: config
emptyDir: {}
- name: config-template
configMap:
name: redis-cluster-config
- name: logs
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ssd-retain
resources:
requests:
storage: 50Gi
---
# Job para inicializar el cluster
apiVersion: batch/v1
kind: Job # Tipo de recurso Kubernetes
metadata:
name: redis-cluster-init
namespace: cache
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: cluster-init
image: redis:7-alpine
command:
- bash
- /scripts/cluster-init.sh
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-secret
key: password
volumeMounts:
- name: scripts
mountPath: /scripts
volumes:
- name: scripts
configMap:
name: redis-cluster-config
defaultMode: 0755
Estrategias de Backup y Recovery
Este punto requiere consideración cuidadosa en la implementación.
Automated Backup Strategies
Las estrategias de backup para aplicaciones stateful deben considerar consistencia de datos, point-in-time recovery, y restoration procedures. Esto incluye scheduled snapshots, continuous archiving, y cross-region replication.
# CronJob para backups automatizados de PostgreSQL
apiVersion: batch/v1
kind: CronJob
metadata:
name: postgres-backup
namespace: database
spec:
schedule: "0 2 * * *" # Daily at 2 AM
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: postgres-backup
image: postgres:15-alpine
command:
- bash
- -c
- |
set -e
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="postgres_backup_${TIMESTAMP}"
# Create database backup
echo "Starting backup: $BACKUP_NAME"
PGPASSWORD=$POSTGRES_PASSWORD pg_dump \
-h postgres-cluster-0.postgres-headless.database.svc.cluster.local \
-U postgres \
-d myapp \
--verbose \
--no-password \
--format=custom \
--compress=9 \
> /backups/${BACKUP_NAME}.dump
# Upload to S3
aws s3 cp /backups/${BACKUP_NAME}.dump s3://$S3_BUCKET/postgres-backups/
# Create WAL archive backup
echo "Creating WAL archive backup..."
mkdir -p /backups/wal_archive_${TIMESTAMP}
# Get WAL files from primary
PGPASSWORD=$POSTGRES_PASSWORD psql \
-h postgres-cluster-0.postgres-headless.database.svc.cluster.local \
-U postgres \
-d myapp \
-c "SELECT pg_start_backup('backup_${TIMESTAMP}', false, false);"
# Copy WAL files
rsync -av postgres-cluster-0.postgres-headless.database.svc.cluster.local:/var/lib/postgresql/data/pg_wal/ /backups/wal_archive_${TIMESTAMP}/
PGPASSWORD=$POSTGRES_PASSWORD psql \
-h postgres-cluster-0.postgres-headless.database.svc.cluster.local \
-U postgres \
-d myapp \
-c "SELECT pg_stop_backup(false, true);"
# Upload WAL archive
tar czf /backups/wal_archive_${TIMESTAMP}.tar.gz -C /backups wal_archive_${TIMESTAMP}
aws s3 cp /backups/wal_archive_${TIMESTAMP}.tar.gz s3://$S3_BUCKET/postgres-wal/
# Cleanup old local files
rm -rf /backups/wal_archive_${TIMESTAMP}
rm -f /backups/${BACKUP_NAME}.dump /backups/wal_archive_${TIMESTAMP}.tar.gz
# Cleanup old S3 backups (keep 30 days)
aws s3api list-objects-v2 --bucket $S3_BUCKET --prefix postgres-backups/ --query 'Contents[?LastModified<`'$(date -d '30 days ago' --iso-8601)'`].Key' --output text | xargs -r -n1 -I {} aws s3 rm s3://$S3_BUCKET/{}
echo "Backup completed: $BACKUP_NAME"
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: S3_BUCKET
value: "my-company-db-backups"
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: aws-credentials
key: access-key-id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: aws-credentials
key: secret-access-key
- name: AWS_DEFAULT_REGION
value: "us-east-1"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 1000m
memory: 2Gi
volumeMounts:
- name: backup-storage
mountPath: /backups
volumes:
- name: backup-storage
emptyDir:
sizeLimit: 10Gi
Mejores Prácticas y Patrones Avanzados
Este punto requiere consideración cuidadosa en la implementación.
Security y Compliance
La seguridad de aplicaciones stateful requiere consideraciones especiales para encryption at rest y in transit, access controls granulares, audit logging, y compliance con regulaciones de datos.
# Pod Security Policy para aplicaciones stateful
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: stateful-app-psp
spec:
privileged: false
allowPrivilegeEscalation: false
requiredDropCapabilities:
- ALL
volumes:
- 'persistentVolumeClaim'
- 'secret'
- 'configMap'
- 'emptyDir'
runAsUser:
rule: 'MustRunAsNonRoot'
seLinux:
rule: 'RunAsAny'
fsGroup:
rule: 'RunAsAny'
readOnlyRootFilesystem: false
---
# Network Policy para microsegmentación
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: database-network-policy
namespace: database
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: application
- podSelector:
matchLabels:
app: backend-api
ports:
- protocol: TCP
port: 5432
egress:
- to: []
ports:
- protocol: UDP
port: 53
- to:
- namespaceSelector:
matchLabels:
name: monitoring
ports:
- protocol: TCP
port: 9090
Monitoring y Observabilidad
El monitoreo de aplicaciones stateful requiere métricas específicas para performance de storage, replication lag, connection pools, y health de clusters distribuidos.
# ServiceMonitor para PostgreSQL con métricas customizadas
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: postgres-metrics
namespace: database
spec:
selector:
matchLabels:
app: postgres
endpoints:
- port: postgres
interval: 30s
path: /metrics
---
# Grafana Dashboard ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-dashboard
namespace: monitoring
data:
postgres-dashboard.json: |
{
"dashboard": {
"title": "PostgreSQL StatefulSet Dashboard",
"panels": [
{
"title": "Database Connections",
"type": "graph",
"targets": [
{
"expr": "sum(pg_stat_database_numbackends) by (instance)",
"legendFormat": "{{ instance }}"
}
]
},
{
"title": "Replication Lag",
"type": "graph",
"targets": [
{
"expr": "pg_replication_lag",
"legendFormat": "Lag (seconds)"
}
]
}
]
}
}
Conclusión
La gestión de aplicaciones stateful en Kubernetes representa uno de los desafíos más complejos en la orquestación de contenedores moderna. Sin embargo, con las herramientas, patrones y mejores prácticas adecuadas, es posible operar bases de datos, message queues y otras aplicaciones con estado de manera confiable y escalable.
Los StatefulSets proporcionan la base fundamental para aplicaciones stateful, ofreciendo identidades estables, almacenamiento persistente y garantías de ordenamiento. La combinación de Persistent Volumes, Storage Classes y Volume Claim Templates permite gestión flexible y escalable del almacenamiento.
La implementación exitosa requiere consideración cuidadosa de patrones de replicación, estrategias de backup y recovery, security policies, y monitoring comprehensivo. Las organizaciones que dominan estas técnicas pueden aprovechar los beneficios de Kubernetes mientras mantienen la persistencia y confiabilidad requeridas por aplicaciones críticas de negocio.
El futuro de las aplicaciones stateful en Kubernetes continúa evolucionando con nuevos operadores, improved storage solutions, y better integration con cloud services. La inversión en comprender y dominar estos patrones proporcionará dividendos significativos en términos de operaciones más eficientes, mayor confiabilidad y mejor utilización de recursos.