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:

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

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 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.

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:

  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:

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.

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. 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.