En la lección anterior resolvimos el problema de las cargas con estado: el StatefulSet da a cada réplica su nombre, su disco y su turno. Pero hay una familia de procesos para los que la pregunta "¿cuántas réplicas quiero?" no tiene sentido. Un recolector de logs no necesita tres copias ni diez: necesita exactamente una en cada nodo, porque los ficheros de log que debe leer están en el disco de cada nodo. Si mañana el clúster crece a doce nodos, hacen falta doce copias, sin que nadie tenga que acordarse de editar un replicas.
Ese es el trabajo del DaemonSet: garantizar que hay un pod —uno, exactamente uno— por cada nodo que cumpla unos criterios, y mantener esa invariante cuando se añaden o se quitan nodos.
Es un objeto que, curiosamente, llevas usando desde la primera lección sin haberlo mirado: el kube-proxy que estudiamos en 04-01 y el plugin de red del CNI son DaemonSets. En esta lección los entenderemos por dentro y desplegaremos el primer agente de infraestructura propio de Rutas Norte: un recolector de logs que lee los ficheros de los contenedores de todos los nodos.
Contenido
- Una carga de trabajo por nodo: qué es y qué la distingue
- Los cuatro casos de uso canónicos
- Anatomía del manifiesto: por qué no hay
replicas - Dónde corre:
nodeSelector,affinityytolerations - Acceso privilegiado:
hostPath,hostNetwork,hostPIDy su coste - Estrategias de actualización
- Práctica: recolector de logs para Rutas Norte
- Comprobación: qué pasa al añadir un nodo
- Recursos: por qué un DaemonSet mal dimensionado es caro
- Una carga de trabajo por nodo: qué es y qué la distingue
Un DaemonSet parece un Deployment en el que alguien hubiera puesto replicas igual al número de nodos. La diferencia es sustancial, y está en quién decide dónde va cada pod.
Con un Deployment, el kube-scheduler reparte las réplicas según recursos disponibles, afinidad y equilibrio. Nada garantiza que haya una por nodo: podrían caer tres en el mismo nodo si tiene sitio. Con un DaemonSet, el controlador crea un pod con el nodo ya asignado, uno por cada nodo elegible, y vigila permanentemente esa correspondencia.
| Aspecto | Deployment | DaemonSet |
|---|---|---|
| Número de pods | El que indica replicas |
Uno por nodo elegible; no hay replicas |
| Colocación | La decide el scheduler | Uno por nodo, por construcción |
| Al añadir un nodo | Nada cambia | Aparece un pod nuevo automáticamente |
| Al quitar un nodo | El scheduler recoloca las réplicas | El pod desaparece con el nodo, sin recolocar |
| Escalado horizontal | kubectl scale o HPA |
No aplica: escala con el clúster |
| Caso de uso | Aplicaciones que atienden peticiones | Agentes de infraestructura del nodo |
La consecuencia práctica más importante es la elasticidad automática frente al clúster. Si activas el autoescalado de nodos (que veremos en 09-03) y el clúster pasa de 3 a 20 nodos en un pico de un puente festivo, los 17 nodos nuevos reciben su recolector de logs y su agente de métricas sin intervención humana. Esa propiedad es la razón de ser del objeto.
graph TB
subgraph Nodo1[nodo-1]
D1[pod recolector] --- L1[/var/log/containers/]
A1[api-reservas]
T1[tienda-web]
end
subgraph Nodo2[nodo-2]
D2[pod recolector] --- L2[/var/log/containers/]
A2[api-reservas]
end
subgraph Nodo3[nodo-3 nuevo]
D3[pod recolector<br/>creado automáticamente] --- L3[/var/log/containers/]
end
DS[DaemonSet<br/>recolector-logs] --> D1
DS --> D2
DS --> D3
- Los cuatro casos de uso canónicos
Prácticamente todo lo que se despliega como DaemonSet cae en una de estas cuatro categorías. La regla que las une: el pod necesita algo que solo existe en el nodo donde corre.
Recolectores de logs
Fluent Bit, Fluentd, Vector, Promtail. Leen los ficheros de /var/log/containers/ del nodo —donde el runtime de contenedores escribe la salida estándar de todos los pods de ese nodo—, los enriquecen con metadatos de Kubernetes y los envían a un backend central.
No podrían ser un Deployment: un pod solo puede leer los ficheros del nodo en el que está. Para leer los logs de todos los nodos hace falta un pod en cada nodo.
Agentes de métricas
Node Exporter de Prometheus, agentes de Datadog o New Relic. Leen /proc y /sys del nodo para exponer CPU, memoria, disco, red y temperatura del hardware. También aquí, la información es estrictamente local al nodo.
El metrics-server que activamos como addon de minikube en 01-04 es un caso distinto: no es un DaemonSet, sino un Deployment que consulta la API de cada kubelet. Lo veremos en 07-02.
Plugins de red del CNI
Calico, Cilium, Flannel, Weave. En 04-01 dijimos que "el CNI programa las rutas del nodo". Quien lo hace es un pod de un DaemonSet que escribe configuración en /etc/cni/net.d, instala binarios en /opt/cni/bin y manipula las tablas de rutas y las reglas de iptables/eBPF del nodo.
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
kube-proxy 3 3 3 3 3 kubernetes.io/os=linux 14dAhí está: kube-proxy, el componente que estudiamos en 04-01 y que traduce los Services a reglas de red, es un DaemonSet. Tiene todo el sentido: cada nodo necesita su propio conjunto de reglas.
Controladores de almacenamiento
Los drivers CSI que vimos en 05-05 se despliegan en dos piezas: un controlador (Deployment o StatefulSet, uno por clúster, que habla con la API del proveedor para crear volúmenes) y un node plugin (DaemonSet, que monta y desmonta los volúmenes en el sistema de ficheros del nodo). El montaje solo puede hacerlo alguien que esté en ese nodo.
| Categoría | Qué necesita del nodo | Ejemplos |
|---|---|---|
| Logs | Ficheros en /var/log |
Fluent Bit, Vector, Promtail |
| Métricas | /proc, /sys, red del host |
Node Exporter, agentes APM |
| Red (CNI) | Rutas, iptables, /etc/cni/net.d |
Calico, Cilium, Flannel |
| Almacenamiento (CSI node) | Montar en el sistema de ficheros | Drivers CSI de EBS, Ceph, etc. |
| Seguridad | Llamadas al sistema, auditoría | Falco, agentes de detección |
- Anatomía del manifiesto: por qué no hay
replicas
replicasUn DaemonSet mínimo:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: recolector-logs
namespace: rutas-norte-pro
spec:
selector:
matchLabels:
app: recolector-logs
entorno: pro
template:
metadata:
labels:
app: recolector-logs
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
containers:
- name: recolector
image: busybox:1.36Comparado con un Deployment falta una sola cosa: spec.replicas. Y su ausencia no es un olvido de la API, es la definición del objeto. El número de pods no es un valor que tú declares, es una consecuencia del estado del clúster: tantos como nodos elegibles haya. Si intentas añadirlo, la API lo rechaza:
error: error validating "ds.yaml": ValidationError(DaemonSet.spec):
unknown field "replicas" in io.k8s.api.apps.v1.DaemonSetSpecPor lo demás, selector y template funcionan igual que en Deployments y StatefulSets, y spec.selector es inmutable igual que allí. La template sigue las mismas convenciones de Rutas Norte: solo app y entorno en el selector, app.kubernetes.io/part-of como etiqueta informativa.
El estado se lee de una forma característica:
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
recolector-logs 3 3 3 3 3 <none> 2m| Columna | Significado |
|---|---|
DESIRED |
Nodos elegibles según selectores, afinidad y tolerations |
CURRENT |
Pods creados |
READY |
Pods que superan sus sondas de disponibilidad |
UP-TO-DATE |
Pods con la plantilla actual (no la anterior) |
AVAILABLE |
Pods listos durante al menos minReadySeconds |
Si DESIRED es menor de lo que esperas, el problema está en el apartado siguiente: hay nodos que el DaemonSet no considera elegibles.
- Dónde corre:
nodeSelector, affinity y tolerations
nodeSelector, affinity y tolerationsPor defecto, "un pod por nodo" significa todos los nodos. Hay tres mecanismos para restringir o ampliar ese conjunto.
nodeSelector: el filtro simple
Solo se crean pods en nodos que tengan esa etiqueta. Es lo que hace kube-proxy en la salida de antes: en un clúster mixto con nodos Windows, el binario de Linux no serviría.
Un caso propio de Rutas Norte: si en producción hay un grupo de nodos con SSD etiquetado disco: ssd, un agente que solo interese ahí se restringe así:
affinity: el filtro expresivo
Cuando el filtro no es una igualdad simple, se usa afinidad de nodo. Aquí solo lo justo para que funcione; el mecanismo completo, con sus operadores y sus variantes preferidas, es la lección 06-05.
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/analitica
operator: DoesNotExistEste ejemplo excluye del recolector los nodos dedicados a analítica.
tolerations: el permiso para entrar donde se rechaza
Este es el mecanismo clave de los DaemonSets y merece detenerse.
Algunos nodos llevan un taint, una marca que dice "no coloques pods aquí salvo que estén expresamente autorizados". El caso más habitual es el plano de control, que en un clúster estándar viene con:
Ese taint impide que las aplicaciones normales acaben compitiendo por CPU con el apiserver y etcd. Pero un recolector de logs sí debe correr ahí: los logs del apiserver son justo los que más falta hacen en una incidencia.
La forma de decir "yo sí puedo" es una toleration:
Léase: "tolero el taint cuya clave existe con ese efecto". No es una preferencia por ir al plano de control; es un permiso para no ser rechazado si el DaemonSet me lleva ahí.
Los agentes de infraestructura que deben correr absolutamente en todos los nodos, pasase lo que pasase usan una toleration universal:
Es lo que hace el CNI: si el plugin de red no corre en un nodo, ese nodo no tiene red, así que ninguna condición debe impedírselo. Úsala con criterio: para tu recolector de logs es probablemente excesiva, porque también lo colocaría en nodos marcados como no listos o bajo presión de memoria.
Hay además una cortesía que Kubernetes concede automáticamente a los DaemonSets: el controlador añade por su cuenta tolerations a ciertos taints del sistema (node.kubernetes.io/not-ready, unreachable, disk-pressure, memory-pressure, pid-pressure, unschedulable) para que los agentes no sean desalojados justo cuando el nodo tiene problemas y más falta hace la telemetría. Puedes verlo si inspeccionas un pod de DaemonSet:
kubectl get pod -n kube-system -l k8s-app=kube-proxy -o jsonpath='{.items[0].spec.tolerations}' | tr ',' '\n'La mecánica completa de taints, efectos y tolerationSeconds es el contenido de 06-05.
- Acceso privilegiado:
hostPath, hostNetwork, hostPID y su coste
hostPath, hostNetwork, hostPID y su costeLos agentes de nodo necesitan ver cosas que un pod normal no ve. Kubernetes lo permite, y cada permiso amplía la superficie de ataque de forma que conviene entender antes de escribirla en un manifiesto.
hostPath
Monta un directorio del sistema de ficheros del nodo dentro del contenedor. Lo vimos en 05-01 como volumen efímero peligroso; aquí es imprescindible.
volumes:
- name: logs-contenedores
hostPath:
path: /var/log/containers
type: Directory
- name: logs-pods
hostPath:
path: /var/log/pods
type: DirectoryDetalle técnico que sorprende a mucha gente: los ficheros de /var/log/containers son enlaces simbólicos a /var/log/pods, que a su vez suelen apuntar al directorio de datos del runtime (/var/lib/docker/containers o el equivalente de containerd). Si solo montas el primero, el contenedor ve enlaces rotos. Por eso los recolectores montan los dos o tres directorios.
Riesgo: un hostPath con permiso de escritura sobre un directorio sensible del nodo (/etc, /var/lib/kubelet, el socket del runtime) equivale a control del nodo. Monta siempre en solo lectura (readOnly: true) lo que solo necesitas leer.
hostNetwork
El pod comparte la pila de red del nodo: sus puertos son puertos del nodo y ve las interfaces reales. Es obligatorio para los plugins CNI y para agentes que capturan tráfico. Nótese el dnsPolicy: sin él, un pod con hostNetwork usaría el DNS del nodo y perdería la resolución de nombres de Kubernetes que estudiamos en 04-03.
Riesgo: se salta el aislamiento de red, y con él las NetworkPolicies de 04-06, que operan sobre las IP de pod. Un pod con hostNetwork en rutas-norte-pro no está sujeto al deny-all.
hostPID
El contenedor ve todos los procesos del nodo. Necesario para agentes de perfilado o de seguridad que inspeccionan procesos.
Riesgo: alto. Ver el árbol de procesos del host permite leer líneas de comando (que a veces contienen credenciales) y, combinado con privilegios, entrar en el espacio de nombres de otros contenedores.
| Permiso | Para qué se usa | Qué expone | Mitigación |
|---|---|---|---|
hostPath (lectura) |
Leer logs, /proc, /sys |
Contenido del disco del nodo | readOnly: true, ruta lo más específica posible |
hostPath (escritura) |
Instalar binarios CNI, montar volúmenes | Control efectivo del nodo | Evitar salvo en drivers de infraestructura |
hostNetwork |
CNI, captura de tráfico | Toda la red del nodo; salta NetworkPolicies | Solo cuando no hay alternativa |
hostPID |
Perfilado, seguridad | Procesos y sus argumentos | Solo agentes de seguridad auditados |
privileged: true |
Manipular el kernel | Todo | Preferir capacidades concretas |
La regla de oro: cada uno de estos campos convierte a tu DaemonSet en parte del plano de confianza del clúster. Un atacante que comprometa la imagen de tu recolector de logs con hostPath de escritura tiene los mismos poderes que si comprometiera el kubelet. Y como corre en todos los nodos, el compromiso es total, no parcial. En 08-02 endureceremos estos contenedores con securityContext, capacidades y sistemas de ficheros de solo lectura.
- Estrategias de actualización
Igual que los otros controladores, el DaemonSet define cómo propaga un cambio de plantilla.
RollingUpdate (por defecto)
Actualiza nodo a nodo. maxUnavailable limita cuántos pods pueden estar simultáneamente no disponibles; con 1 en un clúster de 3 nodos, la actualización tarda tres pasos pero nunca deja más de un nodo sin agente.
maxSurge (estable desde 1.25) permite lo contrario: crear el pod nuevo antes de quitar el viejo, para no dejar ningún hueco de cobertura. Como en un nodo caben los dos temporalmente, exige que ambos puedan coexistir: si el agente usa hostNetwork con un puerto fijo, maxSurge: 1 provoca conflicto de puertos. Reglas:
| Combinación | Comportamiento | Cuándo |
|---|---|---|
maxUnavailable: 1, maxSurge: 0 |
Un nodo sin agente durante la actualización | Por defecto; agentes que pueden tener huecos |
maxUnavailable: 0, maxSurge: 1 |
Nunca hay hueco; solape temporal | Agentes críticos sin puertos de host |
maxUnavailable: 25%, maxSurge: 0 |
Más rápido en clústeres grandes | Clústeres de decenas de nodos |
No pueden ser ambos cero, y no pueden ser ambos distintos de cero.
OnDelete
Los pods se actualizan solo cuando los borras a mano o cuando el nodo se recrea. Es lo habitual para plugins CNI: actualizar la red de un nodo en producción es una operación que se quiere hacer con supervisión, nodo a nodo y con un plan de vuelta atrás.
El historial y la vuelta atrás funcionan como en los Deployments de 02-04:
kubectl rollout status daemonset/recolector-logs -n rutas-norte-pro
kubectl rollout history daemonset/recolector-logs -n rutas-norte-pro
kubectl rollout undo daemonset/recolector-logs -n rutas-norte-pro --to-revision=2
- Práctica: recolector de logs para Rutas Norte
Vamos a desplegar un agente que lea los logs de los contenedores de todos los nodos y —de momento— los muestre por su propia salida estándar. La pila completa de registro centralizado (Elasticsearch, Fluentd, Kibana) se monta en 07-05; aquí el objetivo es el DaemonSet, no el destino de los datos.
Usamos Fluent Bit, ligero y estándar en la industria.
Configuración del agente
# k8s/base/recolector-logs-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: recolector-logs-config
namespace: rutas-norte-pro
labels:
app: recolector-logs
app.kubernetes.io/part-of: rutas-norte
entorno: pro
data:
fluent-bit.conf: |
[SERVICE]
Flush 5
Daemon Off
Log_Level info
Parsers_File parsers.conf
[INPUT]
Name tail
Tag rutasnorte.*
Path /var/log/containers/*rutas-norte-pro*.log
Parser cri
DB /var/log/flb-rutasnorte.db
Mem_Buf_Limit 5MB
Skip_Long_Lines On
Refresh_Interval 10
[FILTER]
Name kubernetes
Match rutasnorte.*
Kube_Tag_Prefix rutasnorte.var.log.containers.
Merge_Log On
Keep_Log Off
Labels On
Annotations Off
[OUTPUT]
Name stdout
Match rutasnorte.*
Format json_lines
parsers.conf: |
[PARSER]
Name cri
Format regex
Regex ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<message>.*)$
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%zQué hace cada bloque:
[INPUT] tailsigue los ficheros de/var/log/containers/cuyo nombre contengarutas-norte-pro. El nombre de esos ficheros tiene la forma<pod>_<namespace>_<contenedor>-<id>.log, así que ese patrón filtra por namespace.DBguarda la posición leída de cada fichero para no reenviar todo tras un reinicio del agente: es un fichero de estado, y por eso lo colocaremos en unhostPath.[FILTER] kubernetesconsulta la API para añadir a cada línea el pod, el namespace, las etiquetas y el nodo. Esto es lo que permitirá filtrar porapp: api-reservasen 07-05, y requiere permisos de lectura sobre pods.[OUTPUT] stdoutvuelca a la salida estándar del propio agente. En 07-05 se sustituirá por un output hacia Elasticsearch.
Permisos
El filtro de Kubernetes necesita leer pods y namespaces del clúster entero. Siguiendo la práctica de 03-06, una ServiceAccount dedicada:
# k8s/base/recolector-logs-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: recolector-logs
namespace: rutas-norte-pro
labels:
app: recolector-logs
app.kubernetes.io/part-of: rutas-norte
entorno: pro
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: recolector-logs
rules:
- apiGroups: [""]
resources: ["pods", "namespaces"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: recolector-logs
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: recolector-logs
subjects:
- kind: ServiceAccount
name: recolector-logs
namespace: rutas-norte-proSolo verbos de lectura y solo sobre dos recursos: el mínimo necesario. El detalle de RBAC —qué es un ClusterRole, cómo se acota, cómo se audita— es la lección 08-01; aquí lo damos hecho para que el ejemplo funcione.
Fíjate en que este pod sí necesita montar su token de ServiceAccount, al contrario que casi todos los componentes de Rutas Norte.
El DaemonSet
# k8s/base/recolector-logs-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: recolector-logs
namespace: rutas-norte-pro
labels:
app: recolector-logs
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
selector:
matchLabels:
app: recolector-logs
entorno: pro
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 0
minReadySeconds: 10
template:
metadata:
labels:
app: recolector-logs
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
serviceAccountName: recolector-logs
terminationGracePeriodSeconds: 30
priorityClassName: system-node-critical
tolerations:
# Correr también en los nodos del plano de control
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: fluent-bit
image: fluent/fluent-bit:3.1.9
resources:
requests:
cpu: 50m
memory: 96Mi
limits:
cpu: 200m
memory: 192Mi
securityContext:
runAsNonRoot: false # necesita leer ficheros del nodo propiedad de root
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
env:
- name: NODO
valueFrom:
fieldRef:
fieldPath: spec.nodeName
volumeMounts:
- name: config
mountPath: /fluent-bit/etc/
readOnly: true
- name: logs-contenedores
mountPath: /var/log/containers
readOnly: true
- name: logs-pods
mountPath: /var/log/pods
readOnly: true
- name: estado
mountPath: /var/log/flb-estado
volumes:
- name: config
configMap:
name: recolector-logs-config
- name: logs-contenedores
hostPath:
path: /var/log/containers
type: Directory
- name: logs-pods
hostPath:
path: /var/log/pods
type: Directory
- name: estado
hostPath:
path: /var/lib/rutasnorte/fluent-bit
type: DirectoryOrCreateDecisiones que merece la pena explicar:
priorityClassName: system-node-critical: si el nodo se queda sin memoria, este pod no debe ser el primero en caer. La mecánica de prioridades y desalojo es contenido de 06-05.- Las tolerations: solo la del plano de control. No usamos
operator: Existssin clave porque no queremos que el recolector se coloque en los nodos de analítica que taintaremos en 06-05. readOnly: trueen los doshostPathde logs: el agente lee, no escribe. El único con escritura es el directorio de estado, y apunta a una ruta propia bajo/var/lib/rutasnorte, no a un directorio del sistema.readOnlyRootFilesystem: truecon las capacidades a cero: endurecimiento básico que ampliaremos en 08-02.- La variable
NODOviene de la Downward API de 03-03 y sirve para que el propio agente sepa dónde está sin consultar nada.
Desplegar y verificar
kubectl apply -f k8s/base/recolector-logs-rbac.yaml
kubectl apply -f k8s/base/recolector-logs-configmap.yaml
kubectl apply -f k8s/base/recolector-logs-daemonset.yaml
kubectl rollout status daemonset/recolector-logs -n rutas-norte-pro
kubectl get pods -n rutas-norte-pro -l app=recolector-logs -o wideNAME READY STATUS RESTARTS AGE IP NODE
recolector-logs-4wq7n 1/1 Running 0 52s 10.244.0.31 rutas-norte
recolector-logs-hk2zp 1/1 Running 0 52s 10.244.1.44 rutas-norte-m02
recolector-logs-t8xrb 1/1 Running 0 52s 10.244.2.22 rutas-norte-m03Un pod por nodo, y la columna NODE lo confirma: no hay dos en el mismo sitio.
{"date":1754380522.4,"log":"GET /api/reservas/4471 200 12ms","kubernetes":{"pod_name":"api-reservas-6c8f9d4b7-jm2xq","namespace_name":"rutas-norte-pro","container_name":"api","labels":{"app":"api-reservas","entorno":"pro"},"host":"rutas-norte-m02"}}Ahí está el resultado: una línea de log de api-reservas enriquecida con el pod, el namespace, las etiquetas y el nodo. Ese enriquecimiento es lo que hará posible en 07-05 buscar "todos los errores de api-reservas en producción de las últimas dos horas".
- Comprobación: qué pasa al añadir un nodo
La propiedad central del DaemonSet se demuestra en treinta segundos con minikube:
NAME STATUS ROLES AGE VERSION
rutas-norte Ready control-plane 9d v1.30.3
rutas-norte-m02 Ready <none> 9d v1.30.3
rutas-norte-m03 Ready <none> 9d v1.30.3
rutas-norte-m04 Ready <none> 38s v1.30.3
NAME READY STATUS RESTARTS AGE NODE
recolector-logs-4wq7n 1/1 Running 0 11m rutas-norte
recolector-logs-hk2zp 1/1 Running 0 11m rutas-norte-m02
recolector-logs-t8xrb 1/1 Running 0 11m rutas-norte-m03
recolector-logs-x9k2d 1/1 Running 0 22s rutas-norte-m04Nadie ha tocado el manifiesto. El controlador de DaemonSet vigila los nodos, ha visto uno nuevo elegible y ha creado su pod. Es exactamente el mismo bucle de reconciliación que describimos en 01-02, aplicado a la relación "un pod por nodo".
Al revés funciona igual:
DESIRED ha vuelto a 3 por sí solo. Nótese la diferencia con un Deployment: cuando desaparece un nodo, el Deployment recoloca sus réplicas en los demás nodos para mantener el número; el DaemonSet no recoloca nada, porque el pod tenía sentido únicamente en ese nodo.
- Recursos: por qué un DaemonSet mal dimensionado es caro
Aquí está la consecuencia económica que se olvida con más frecuencia.
Cuando pides 200 MiB a un Deployment de tres réplicas, pides 600 MiB. Cuando pides 200 MiB a un DaemonSet, pides 200 MiB × número de nodos, y ese número crece con el clúster.
Un ejemplo con números realistas de un clúster de 50 nodos y cuatro DaemonSets habituales:
| Agente | requests.cpu |
requests.memory |
× 50 nodos (CPU) | × 50 nodos (memoria) |
|---|---|---|---|---|
| Recolector de logs | 50m | 96Mi | 2,5 CPU | 4,7 GiB |
| Exportador de métricas | 30m | 64Mi | 1,5 CPU | 3,1 GiB |
| Plugin CNI | 100m | 128Mi | 5 CPU | 6,3 GiB |
| Node plugin CSI | 20m | 64Mi | 1 CPU | 3,1 GiB |
| Total | 10 CPU | 17,2 GiB |
Diez CPU y diecisiete gigas reservados antes de desplegar una sola línea de código de Rutas Norte. Y son requests: capacidad apartada por el scheduler, esté usándose o no, que reduce lo que queda para api-reservas y tienda-web.
Peor aún si te equivocas por exceso. Poner requests.memory: 512Mi en un recolector que consume 60 MiB "por si acaso" desperdicia 22 GiB en ese clúster: más de un nodo entero pagado para nada.
Consejos concretos:
- Mide antes de fijar. Despliega con requests bajos en
rutas-norte-dev, observa conkubectl top pod(07-02) durante unos días de tráfico real y ajusta. - Pon límites de memoria siempre. Un recolector con una fuga o con un pico de logs puede consumir toda la memoria del nodo y provocar desalojos de las aplicaciones.
Mem_Buf_Limiten la configuración de Fluent Bit es la primera línea de defensa; ellimits.memorydel contenedor es la segunda. - Cuidado con los límites de CPU. Un recolector estrangulado por CPU acumula retraso y pierde líneas cuando el nodo está más cargado, que es justo cuando más falta hacen los logs. Un límite generoso o ninguno es defendible aquí.
- Revisa las cuotas. Los
ResourceQuotapor namespace de 03-04 se aplican también a los pods del DaemonSet. Si el quota derutas-norte-prose agota, los pods del DaemonSet en nodos nuevos no se crearán, y lo descubrirás cuando falten logs. - Pregúntate si de verdad hace falta uno por nodo. Es el consejo más rentable. Un agente que consulta la API de Kubernetes no necesita estar en cada nodo: le basta un Deployment de una réplica.
Errores Comunes y Consejos
Intentar poner replicas en el manifiesto. La API lo rechaza. Si vienes de Deployments es el primer tropiezo, y es sano: te obliga a interiorizar que el número de pods lo dicta el clúster.
No poner tolerations y perder los nodos del plano de control. El síntoma es sutil: el DaemonSet aparece 3/3 y todo parece bien, pero faltan los logs del apiserver. Compara siempre DESIRED con kubectl get nodes | wc -l.
Montar solo /var/log/containers. Como son enlaces simbólicos a /var/log/pods, el agente ve nombres pero no contenido, y falla con errores de fichero no encontrado que despistan mucho. Monta ambos.
hostPath con type incorrecto. Con type: Directory, si la ruta no existe en el nodo el pod se queda en ContainerCreating con un evento MountVolume.SetUp failed. Para directorios de estado propios usa DirectoryOrCreate.
Olvidar dnsPolicy: ClusterFirstWithHostNet con hostNetwork: true. El pod hereda el /etc/resolv.conf del nodo, deja de resolver nombres de Services y falla al hablar con cualquier componente interno. Es un error que cuesta horas de diagnóstico.
maxSurge: 1 con hostNetwork y puerto fijo. El pod nuevo no puede arrancar porque el viejo tiene el puerto ocupado, y la actualización se queda bloqueada. Con hostNetwork usa siempre maxSurge: 0.
Creer que un DaemonSet garantiza cobertura durante la actualización. Con maxUnavailable: 1 y maxSurge: 0 hay siempre un nodo sin agente durante unos segundos. Para logs de auditoría eso puede ser inaceptable: usa maxUnavailable: 0 y maxSurge: 1 si el agente lo permite.
Consejo: kubectl get pods -o wide es tu verificación. La columna NODE es la única forma directa de confirmar la invariante "uno por nodo". Añade --sort-by=.spec.nodeName para leerlo cómodo.
Consejo: un DaemonSet es un vector de ataque de alcance total. Corre en todos los nodos, suele tener hostPath y a veces hostNetwork. Fija la imagen por etiqueta inmutable —mejor aún, por digest—, revisa qué RBAC pide y aplica el endurecimiento de 08-02 y el escaneo de imágenes de 08-05.
Ejercicios
Ejercicio 1: DaemonSet mínimo y verificación de cobertura
En rutas-norte-dev de tu minikube (perfil rutas-norte, al menos dos nodos), despliega un DaemonSet llamado sonda-nodos con busybox:1.36 que cada 30 segundos escriba en su salida estándar el nombre del nodo (obtenido por Downward API) y el espacio libre de / del nodo, leyendo /host-raiz montado en solo lectura desde hostPath: /.
Verifica que hay un pod por nodo y consulta la salida.
Ejercicio 2: tolerations y nodos del plano de control
Comprueba si el pod de sonda-nodos se ha creado en el nodo del plano de control. Si no, averigua por qué e incorpora la toleration necesaria. Verifica que DESIRED aumenta.
Ejercicio 3: actualización sin huecos de cobertura
Cambia la imagen de sonda-nodos a busybox:1.37 con una estrategia que no deje ningún nodo sin agente en ningún momento. Comprueba el comportamiento observando los pods durante la actualización y explica qué diferencia habrías visto con la estrategia por defecto.
Soluciones
Solución 1
# /tmp/sonda-nodos.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: sonda-nodos
namespace: rutas-norte-dev
labels:
app: sonda-nodos
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
selector:
matchLabels:
app: sonda-nodos
entorno: dev
template:
metadata:
labels:
app: sonda-nodos
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
automountServiceAccountToken: false
containers:
- name: sonda
image: busybox:1.36
command:
- sh
- -c
- 'while true; do echo "$(date +%H:%M:%S) nodo=$NODO libre=$(df -h /host-raiz | tail -1 | awk "{print \$4}")"; sleep 30; done'
env:
- name: NODO
valueFrom:
fieldRef:
fieldPath: spec.nodeName
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 50m
memory: 32Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: raiz-host
mountPath: /host-raiz
readOnly: true
volumes:
- name: raiz-host
hostPath:
path: /
type: Directorykubectl apply -f /tmp/sonda-nodos.yaml
kubectl rollout status daemonset/sonda-nodos -n rutas-norte-dev
kubectl get pods -n rutas-norte-dev -l app=sonda-nodos -o wide
kubectl logs -n rutas-norte-dev daemonset/sonda-nodos --tail=2NAME READY STATUS RESTARTS AGE NODE
sonda-nodos-c4m8p 1/1 Running 0 25s rutas-norte-m02
sonda-nodos-q7t2v 1/1 Running 0 25s rutas-norte-m03
18:22:41 nodo=rutas-norte-m02 libre=12.4GLos recursos son mínimos a propósito: es un DaemonSet y todo lo que pida se multiplica por el número de nodos.
Solución 2
NAME STATUS ROLES AGE VERSION
rutas-norte Ready control-plane 9d v1.30.3
rutas-norte-m02 Ready <none> 9d v1.30.3
rutas-norte-m03 Ready <none> 9d v1.30.3
NAME DESIRED CURRENT READY AGE
sonda-nodos 2 2 2 3mTres nodos pero DESIRED es 2: falta el plano de control. La causa:
(En un minikube de un solo nodo este taint no está puesto, precisamente para que se puedan desplegar aplicaciones; en un clúster multinodo o real sí aparece.)
La solución es tolerar ese taint:
kubectl apply -f /tmp/sonda-nodos.yaml
kubectl get daemonset sonda-nodos -n rutas-norte-dev
kubectl get pods -n rutas-norte-dev -l app=sonda-nodos -o wideNAME DESIRED CURRENT READY AGE
sonda-nodos 3 3 3 5m
NAME READY STATUS RESTARTS AGE NODE
sonda-nodos-c4m8p 1/1 Running 0 5m rutas-norte-m02
sonda-nodos-q7t2v 1/1 Running 0 5m rutas-norte-m03
sonda-nodos-zr9kd 1/1 Running 0 14s rutas-norteObsérvese que la toleration no atrae al pod hacia el plano de control: simplemente deja de excluirlo. Quien lo coloca ahí es la lógica de "uno por nodo" del DaemonSet.
Solución 3
Para no dejar huecos hay que crear antes de destruir, es decir maxUnavailable: 0 y maxSurge: 1. Es viable porque sonda-nodos no usa hostNetwork ni puertos de host, así que dos pods pueden coexistir un instante en el mismo nodo.
kubectl patch daemonset sonda-nodos -n rutas-norte-dev -p '{
"spec": {
"updateStrategy": {
"type": "RollingUpdate",
"rollingUpdate": {"maxUnavailable": 0, "maxSurge": 1}
}
}
}'
# Observar en otra terminal
kubectl get pods -n rutas-norte-dev -l app=sonda-nodos -o wide --watchkubectl set image daemonset/sonda-nodos -n rutas-norte-dev sonda=busybox:1.37
kubectl rollout status daemonset/sonda-nodos -n rutas-norte-devDurante la actualización se observa este patrón en cada nodo:
sonda-nodos-c4m8p 1/1 Running 0 8m rutas-norte-m02 (viejo)
sonda-nodos-n5j3w 0/1 Pending 0 0s rutas-norte-m02 (nuevo)
sonda-nodos-n5j3w 1/1 Running 0 4s rutas-norte-m02 (nuevo, listo)
sonda-nodos-c4m8p 1/1 Terminating 0 8m rutas-norte-m02 (viejo, se retira)El pod nuevo llega a Running antes de que el viejo empiece a terminar: en ningún momento el nodo m02 se queda sin agente.
Con la estrategia por defecto (maxUnavailable: 1, maxSurge: 0) el orden sería el inverso: primero Terminating el viejo, después la descarga de la imagen nueva y el arranque. Entre ambos momentos —que con una imagen no cacheada pueden ser decenas de segundos— ese nodo no tiene recolector y sus logs se pierden.
Conclusión
El DaemonSet es el controlador de las cargas de trabajo que pertenecen al nodo, no a la aplicación. Se distingue de un Deployment en que no tiene replicas: el número de pods es una consecuencia de cuántos nodos elegibles hay, y se ajusta solo cuando el clúster crece o mengua. Los cuatro usos canónicos —logs, métricas, plugins CNI y node plugins CSI— comparten un rasgo: necesitan algo que solo existe en el nodo donde corren, y por eso kube-proxy y el propio CNI de tu clúster son DaemonSets.
Hemos visto cómo se acota su alcance con nodeSelector y affinity, y sobre todo cómo se amplía con tolerations para llegar a los nodos del plano de control. Hemos revisado el precio de los permisos privilegiados que estos agentes suelen pedir (hostPath, hostNetwork, hostPID), que convierten al DaemonSet en parte del plano de confianza del clúster. Hemos desplegado el recolector de logs de Rutas Norte —cuyo destino final montaremos en 07-05—, comprobado que un nodo nuevo recibe su pod sin intervención, y calculado por qué unos requests mal ajustados se multiplican por el número de nodos hasta costar servidores enteros.
Nos queda una familia entera de cargas de trabajo por cubrir. Deployments, StatefulSets y DaemonSets tienen algo en común: sus pods están pensados para correr indefinidamente, y si un contenedor termina, el sistema lo considera un fallo y lo reinicia. Pero hay trabajo que consiste precisamente en terminar: generar un informe, migrar un esquema, procesar un lote. Para eso Kubernetes tiene otros dos objetos, y con ellos desplegaremos por fin el componente que falta en Rutas Norte, informes-ocupacion. Es el tema de la siguiente lección: Trabajos y CronJobs.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
