--- title: "Logs de Kubernetes con Fluentd, Elasticsearch y Kibana" description: "Centraliza los logs de tu clúster Kubernetes con Fluentd como DaemonSet, Elasticsearch y Kibana gestionados por ECK, con retención automática de 7 días." category: "Kubernetes Avanzado" language: es source: https://cubepath.com/es/docs/kubernetes-avanzado/registro-de-kubernetes-con-fluentd-y-elasticsearch updated: 2026-09-25 --- # Logs de Kubernetes con Fluentd, Elasticsearch y Kibana 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 con `kubectl get storageclass`: una de ellas debe aparecer marcada como `(default)`. - `kubectl` y 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: ```bash helm repo add elastic https://helm.elastic.co helm repo update ``` Instala el operador en su propio namespace: ```bash helm install elastic-operator elastic/eck-operator -n elastic-system --create-namespace ``` Comprueba que el pod del operador está en ejecución: ```bash kubectl get pods -n elastic-system ``` ```output 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: ```bash kubectl create namespace logging ``` Crea el manifiesto del clúster de Elasticsearch: ```bash 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: ```yaml 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 ``` > Nota: Esta 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 sube `count` a 3 para tener alta disponibilidad. Aplica el manifiesto: ```bash 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`: ```bash kubectl get elasticsearch -n logging ``` ```output 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: ```bash 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: ```bash 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`: ```bash curl -k -u "elastic:$ES_PASSWORD" "https://localhost:9200/_cluster/health?pretty" ``` ```output { "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: ```bash nano kibana.yaml ``` ```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: ```bash kubectl apply -f kibana.yaml kubectl get kibana -n logging ``` ```output 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: ```bash nano fluentd.yaml ``` ```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 Secret `logs-es-http-certs-public` y activa la verificación. - La contraseña se lee del Secret que genera ECK, nunca se escribe en el manifiesto. > Importante: Para simplificar, Fluentd usa el superusuario `elastic`. En producción crea en Elasticsearch un usuario dedicado con permisos solo de escritura sobre `logstash-*` y usa sus credenciales. Aplica el manifiesto y espera a que haya un pod por nodo: ```bash kubectl apply -f fluentd.yaml kubectl rollout status daemonset/fluentd -n logging ``` ```output daemon set "fluentd" successfully rolled out ``` Revisa los logs de uno de los pods para confirmar que conecta con Elasticsearch sin errores: ```bash 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: ```bash curl -k -u "elastic:$ES_PASSWORD" "https://localhost:9200/_cat/indices/logstash-*?v" ``` ```output 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: ```bash 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: ```bash curl -k -u "elastic:$ES_PASSWORD" "https://localhost:9200/logstash-*/_count?q=kubernetes.pod_name:log-test" ``` ```output {"count":5,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0}} ``` Borra el pod de prueba: ```bash kubectl delete pod log-test ``` ## Paso 6: Consultar los logs en Kibana Abre un túnel al servicio de Kibana en otra terminal: ```bash 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: 1. Abre **Stack Management** y después **Data Views**. 2. Pulsa **Create data view**. 3. Escribe `logstash-*` en **Index pattern** y elige `@timestamp` como campo de tiempo. 4. Guarda la vista. En **Discover**, selecciona la vista `logstash-*` y filtra con KQL, por ejemplo: ```text 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. > Consejo: Para acceso permanente sin `port-forward`, publica el servicio `logs-kb-http` con 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: ```bash 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":{}}}}}}' ``` ```output {"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): ```bash 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: ```bash 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: ```bash curl -k -u "elastic:$ES_PASSWORD" "https://localhost:9200/logstash-*/_ilm/explain?filter_path=indices.*.policy,indices.*.managed" ``` ```output {"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. Revisa `kubectl describe pvc -n logging` y confirma que existe una StorageClass por defecto, o añade `storageClassName` al `volumeClaimTemplates`. - **Fluentd muestra `pattern not matched` o los mensajes llegan vacíos**: el parser no coincide con el runtime. Con containerd o CRI-O usa `cri`; solo los nodos antiguos con Docker necesitan `json`. - **Fluentd devuelve `401 Unauthorized`**: la variable `FLUENT_ELASTICSEARCH_PASSWORD` no apunta al Secret correcto. Comprueba con `kubectl get secret logs-es-elastic-user -n logging` que 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. Sube `memory` en 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.