Por defecto, los logs de los contenedores de Kubernetes viven en el disco de cada nodo y desaparecen cuando el pod se borra o el nodo rota los ficheros. El stack EFK (Elasticsearch, Fluentd y Kibana) los recoge de todos los nodos, los enriquece con los metadatos del pod y los guarda en un único sitio donde puedes buscarlos. En este tutorial desplegarás Elasticsearch y Kibana con el operador oficial ECK (Elastic Cloud on Kubernetes), ejecutarás Fluentd como DaemonSet en cada nodo y configurarás una política que borra los logs de más de 7 días.
Requisitos previos
Para seguir esta guía necesitas:
- Un clúster Kubernetes 1.30 o superior con runtime containerd (kubeadm, K3s, RKE2 o un Kubernetes gestionado), por ejemplo sobre VPS de CubePath.
- Al menos 4 GB de RAM libres en el clúster: Elasticsearch reserva 2 GiB, Kibana 1 GiB y Fluentd unos 200 MiB por nodo.
- Una StorageClass por defecto que pueda crear volúmenes
ReadWriteOnce. Compruébalo conkubectl get storageclass: una de ellas debe aparecer marcada como(default). kubectly Helm 3 configurados en tu equipo contra el clúster, con permisos de administrador.
Paso 1: Instalar el operador ECK
ECK es el operador de Elastic para Kubernetes. Crea los StatefulSets, los certificados TLS y el usuario administrador a partir de un recurso sencillo, lo que evita mantener a mano cientos de líneas de manifiestos. Añade el repositorio de Helm de Elastic:
helm repo add elastic https://helm.elastic.co
helm repo update
Instala el operador en su propio namespace:
helm install elastic-operator elastic/eck-operator -n elastic-system --create-namespace
Comprueba que el pod del operador está en ejecución:
kubectl get pods -n elastic-system
NAME READY STATUS RESTARTS AGE
elastic-operator-0 1/1 Running 0 45s
Paso 2: Desplegar Elasticsearch
Crea el namespace donde vivirá todo el stack de logs:
kubectl create namespace logging
Crea el manifiesto del clúster de Elasticsearch:
nano elasticsearch.yaml
Este ejemplo usa un único nodo de Elasticsearch con 30 GiB de disco, suficiente para un clúster pequeño. La opción node.store.allow_mmap: false evita tener que subir vm.max_map_count en los nodos de Kubernetes:
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: logs
namespace: logging
spec:
version: 8.19.0
nodeSets:
- name: default
count: 1
config:
node.store.allow_mmap: false
podTemplate:
spec:
containers:
- name: elasticsearch
resources:
requests:
memory: 2Gi
cpu: 500m
limits:
memory: 2Gi
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 30Gi
NotaEsta guía usa la rama 8.19 de Elasticsearch porque es la que soporta la imagen de Fluentd con el plugin
elasticsearch8. Usa el último parche disponible de la 8.19. En producción subecounta 3 para tener alta disponibilidad.
Aplica el manifiesto:
kubectl apply -f elasticsearch.yaml
El primer arranque tarda un par de minutos mientras se crea el volumen y se descarga la imagen. Consulta el estado del recurso hasta que la fase sea Ready:
kubectl get elasticsearch -n logging
NAME HEALTH NODES VERSION PHASE AGE
logs green 1 8.19.0 Ready 3m
ECK genera la contraseña del usuario elastic en un Secret llamado logs-es-elastic-user. Guárdala en una variable de tu sesión:
ES_PASSWORD=$(kubectl get secret logs-es-elastic-user -n logging -o go-template='{{.data.elastic | base64decode}}')
Para hablar con la API de Elasticsearch desde tu equipo, abre un túnel al servicio logs-es-http en una segunda terminal y déjalo abierto durante el resto de la guía:
kubectl port-forward -n logging service/logs-es-http 9200
En la primera terminal, consulta la salud del clúster. ECK activa TLS con un certificado autofirmado, por eso se usa -k:
curl -k -u "elastic:$ES_PASSWORD" "https://localhost:9200/_cluster/health?pretty"
{
"cluster_name" : "logs",
"status" : "green",
"number_of_nodes" : 1,
...
}
Paso 3: Desplegar Kibana
Kibana es la interfaz web para buscar y visualizar los logs. Con ECK basta con indicar a qué clúster de Elasticsearch debe conectarse y el operador configura credenciales y certificados:
nano kibana.yaml
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
name: logs
namespace: logging
spec:
version: 8.19.0
count: 1
elasticsearchRef:
name: logs
Aplica el manifiesto y espera a que Kibana esté sano:
kubectl apply -f kibana.yaml
kubectl get kibana -n logging
NAME HEALTH NODES VERSION AGE
logs green 1 8.19.0 2m
Paso 4: Desplegar Fluentd como DaemonSet
Fluentd debe ejecutarse en todos los nodos porque cada kubelet escribe los logs de sus contenedores en /var/log/pods del propio nodo (con enlaces en /var/log/containers). Un DaemonSet garantiza un pod de Fluentd por nodo, que lee esos ficheros, añade el namespace, el pod y las etiquetas consultando la API de Kubernetes, y envía el resultado a Elasticsearch.
La imagen oficial fluent/fluentd-kubernetes-daemonset ya trae una configuración completa que se ajusta con variables de entorno, así que no hace falta escribir fluent.conf a mano. Crea el manifiesto:
nano fluentd.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluentd
namespace: logging
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: fluentd
rules:
- apiGroups: [""]
resources: ["pods", "namespaces"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: fluentd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fluentd
subjects:
- kind: ServiceAccount
name: fluentd
namespace: logging
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: logging
labels:
app: fluentd
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
serviceAccountName: fluentd
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: fluentd
image: fluent/fluentd-kubernetes-daemonset:v1-debian-elasticsearch8
env:
- name: K8S_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: FLUENT_ELASTICSEARCH_HOST
value: logs-es-http.logging.svc
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
- name: FLUENT_ELASTICSEARCH_SCHEME
value: https
- name: FLUENT_ELASTICSEARCH_SSL_VERIFY
value: "false"
- name: FLUENT_ELASTICSEARCH_SSL_VERSION
value: TLSv1_2
- name: FLUENT_ELASTICSEARCH_USER
value: elastic
- name: FLUENT_ELASTICSEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: logs-es-elastic-user
key: elastic
- name: FLUENT_CONTAINER_TAIL_PARSER_TYPE
value: cri
- name: FLUENT_CONTAINER_TAIL_PARSER_TIME_FORMAT
value: "%Y-%m-%dT%H:%M:%S.%N%:z"
- name: FLUENTD_SYSTEMD_CONF
value: disable
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
memory: 512Mi
volumeMounts:
- name: varlog
mountPath: /var/log
volumes:
- name: varlog
hostPath:
path: /var/log
Los puntos importantes del manifiesto son:
FLUENT_CONTAINER_TAIL_PARSER_TYPE: cri: containerd escribe los logs en formato CRI (fecha stream etiqueta mensaje), no en el JSON que usaba Docker. Sin esta variable Fluentd no entiende las líneas.FLUENT_ELASTICSEARCH_SSL_VERIFY: "false": el tráfico va cifrado, pero no se valida el certificado autofirmado de ECK. En producción monta el CA del Secretlogs-es-http-certs-publicy activa la verificación.- La contraseña se lee del Secret que genera ECK, nunca se escribe en el manifiesto.
ImportantePara simplificar, Fluentd usa el superusuario
elastic. En producción crea en Elasticsearch un usuario dedicado con permisos solo de escritura sobrelogstash-*y usa sus credenciales.
Aplica el manifiesto y espera a que haya un pod por nodo:
kubectl apply -f fluentd.yaml
kubectl rollout status daemonset/fluentd -n logging
daemon set "fluentd" successfully rolled out
Revisa los logs de uno de los pods para confirmar que conecta con Elasticsearch sin errores:
kubectl logs -n logging daemonset/fluentd --tail=20
Si ves líneas de following tail of /var/log/containers/... y ningún Could not communicate to Elasticsearch, Fluentd está enviando datos.
Paso 5: Comprobar que los logs llegan a Elasticsearch
La configuración por defecto de la imagen crea un índice diario con el prefijo logstash-. Lista los índices:
curl -k -u "elastic:$ES_PASSWORD" "https://localhost:9200/_cat/indices/logstash-*?v"
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open logstash-2026.09.25 Xb3kQ0V1TWe2Rk9mLJ8Aqw 1 1 18342 0 9.1mb 9.1mb
El estado yellow es normal con un solo nodo, porque Elasticsearch no tiene dónde colocar la réplica. Lo corregirás en el paso 7.
Genera ahora unos logs reconocibles con un pod de prueba:
kubectl run log-test --image=busybox:1.37 --restart=Never -- sh -c 'for i in 1 2 3 4 5; do echo "prueba efk $i"; sleep 1; done'
Espera unos 10 segundos (Fluentd envía en lotes) y busca los documentos de ese pod. El campo kubernetes.pod_name lo añade el filtro de metadatos de Fluentd:
curl -k -u "elastic:$ES_PASSWORD" "https://localhost:9200/logstash-*/_count?q=kubernetes.pod_name:log-test"
{"count":5,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0}}
Borra el pod de prueba:
kubectl delete pod log-test
Paso 6: Consultar los logs en Kibana
Abre un túnel al servicio de Kibana en otra terminal:
kubectl port-forward -n logging service/logs-kb-http 5601
Entra en https://localhost:5601 (el navegador avisará del certificado autofirmado) e inicia sesión con el usuario elastic y la contraseña de $ES_PASSWORD. Para que Kibana sepa qué índices consultar:
- Abre Stack Management y después Data Views.
- Pulsa Create data view.
- Escribe
logstash-*en Index pattern y elige@timestampcomo campo de tiempo. - Guarda la vista.
En Discover, selecciona la vista logstash-* y filtra con KQL, por ejemplo:
kubernetes.namespace_name : "default" and kubernetes.pod_name : "log-test"
Verás las cinco líneas prueba efk con el nodo, el contenedor y las etiquetas del pod.
ConsejoPara acceso permanente sin
port-forward, publica el serviciologs-kb-httpcon un Ingress y TLS de tu dominio en lugar de exponerlo con un NodePort.
Paso 7: Configurar la retención de logs
Sin retención, los índices diarios crecen hasta llenar el volumen de 30 GiB. Una política de ILM (Index Lifecycle Management) borra cada índice cuando cumple 7 días. Crea la política:
curl -k -u "elastic:$ES_PASSWORD" -X PUT "https://localhost:9200/_ilm/policy/k8s-logs-retention" \
-H 'Content-Type: application/json' \
-d '{"policy":{"phases":{"hot":{"actions":{}},"delete":{"min_age":"7d","actions":{"delete":{}}}}}}'
{"acknowledged":true}
Crea una plantilla de índice para que todos los índices logstash-* nuevos usen esa política. Como el clúster tiene un solo nodo, la plantilla también fija las réplicas a 0 (quita esa línea si usas 3 nodos):
curl -k -u "elastic:$ES_PASSWORD" -X PUT "https://localhost:9200/_index_template/k8s-logs" \
-H 'Content-Type: application/json' \
-d '{"index_patterns":["logstash-*"],"priority":200,"template":{"settings":{"index.lifecycle.name":"k8s-logs-retention","index.number_of_replicas":0}}}'
La plantilla solo afecta a los índices que se creen a partir de ahora. Aplica los mismos ajustes al índice de hoy:
curl -k -u "elastic:$ES_PASSWORD" -X PUT "https://localhost:9200/logstash-*/_settings" \
-H 'Content-Type: application/json' \
-d '{"index.lifecycle.name":"k8s-logs-retention","index.number_of_replicas":0}'
Comprueba que el índice está gestionado por la política:
curl -k -u "elastic:$ES_PASSWORD" "https://localhost:9200/logstash-*/_ilm/explain?filter_path=indices.*.policy,indices.*.managed"
{"indices":{"logstash-2026.09.25":{"managed":true,"policy":"k8s-logs-retention"}}}
Si vuelves a listar los índices, el estado habrá pasado a green.
Solución de problemas
- El pod de Elasticsearch se queda en
Pending: el PVC no se ha podido crear. Revisakubectl describe pvc -n loggingy confirma que existe una StorageClass por defecto, o añadestorageClassNamealvolumeClaimTemplates. - Fluentd muestra
pattern not matchedo los mensajes llegan vacíos: el parser no coincide con el runtime. Con containerd o CRI-O usacri; solo los nodos antiguos con Docker necesitanjson. - Fluentd devuelve
401 Unauthorized: la variableFLUENT_ELASTICSEARCH_PASSWORDno apunta al Secret correcto. Comprueba conkubectl get secret logs-es-elastic-user -n loggingque existe en el mismo namespace que el DaemonSet. - Elasticsearch se reinicia con
OOMKilled: el límite de memoria es demasiado bajo para el volumen de logs. Subememoryen requests y limits a la vez (ECK ajusta el heap a la mitad automáticamente).
Conclusión
Ya tienes los logs de todos los nodos del clúster centralizados en Elasticsearch, con los metadatos de cada pod, consultables desde Kibana y con borrado automático a los 7 días. Como siguientes pasos, puedes subir Elasticsearch a 3 nodos para tener alta disponibilidad, crear un usuario de solo escritura para Fluentd y activar la verificación TLS, o publicar Kibana con un Ingress y un certificado de Let's Encrypt.
