A la lliçó anterior vam resoldre el problema de les càrregues amb estat: el StatefulSet dona a cada rèplica el seu nom, el seu disc i el seu torn. Però hi ha una família de processos per als quals la pregunta "quantes rèpliques vull?" no té sentit. Un recol·lector de logs no necessita tres còpies ni deu: necessita exactament una a cada node, perquè els fitxers de log que ha de llegir són al disc de cada node. Si demà el clúster creix fins a dotze nodes, calen dotze còpies, sense que ningú s'hagi de recordar d'editar un replicas.

Aquesta és la feina del DaemonSet: garantir que hi ha un pod —un, exactament un— per cada node que compleixi uns criteris, i mantenir aquesta invariant quan s'afegeixen o es treuen nodes.

És un objecte que, curiosament, fas servir des de la primera lliçó sense haver-lo mirat: el kube-proxy que vam estudiar a 04-01 i el connector de xarxa del CNI són DaemonSets. En aquesta lliçó els entendrem per dins i desplegarem el primer agent d'infraestructura propi de Rutas Norte: un recol·lector de logs que llegeix els fitxers dels contenidors de tots els nodes.

Contingut

  1. Una càrrega de treball per node: què és i què la distingeix
  2. Els quatre casos d'ús canònics
  3. Anatomia del manifest: per què no hi ha replicas
  4. On corre: nodeSelector, affinity i tolerations
  5. Accés privilegiat: hostPath, hostNetwork, hostPID i el seu cost
  6. Estratègies d'actualització
  7. Pràctica: recol·lector de logs per a Rutas Norte
  8. Comprovació: què passa en afegir un node
  9. Recursos: per què un DaemonSet mal dimensionat és car

  1. Una càrrega de treball per node: què és i què la distingeix

Un DaemonSet sembla un Deployment en què algú hagués posat replicas igual al nombre de nodes. La diferència és substancial, i està en qui decideix on va cada pod.

Amb un Deployment, el kube-scheduler reparteix les rèpliques segons recursos disponibles, afinitat i equilibri. Res no garanteix que n'hi hagi una per node: en podrien caure tres al mateix node si hi ha lloc. Amb un DaemonSet, el controlador crea un pod amb el node ja assignat, un per cada node elegible, i vigila permanentment aquesta correspondència.

Aspecte Deployment DaemonSet
Nombre de pods El que indica replicas Un per node elegible; no hi ha replicas
Col·locació La decideix el scheduler Un per node, per construcció
En afegir un node No canvia res Apareix un pod nou automàticament
En treure un node El scheduler recol·loca les rèpliques El pod desapareix amb el node, sense recol·locar
Escalat horitzontal kubectl scale o HPA No aplica: escala amb el clúster
Cas d'ús Aplicacions que atenen peticions Agents d'infraestructura del node

La conseqüència pràctica més important és l'elasticitat automàtica davant del clúster. Si actives l'autoescalat de nodes (que veurem a 09-03) i el clúster passa de 3 a 20 nodes en un pic d'un pont festiu, els 17 nodes nous reben el seu recol·lector de logs i el seu agent de mètriques sense intervenció humana. Aquesta propietat és la raó de ser de l'objecte.

graph TB
  subgraph Node1[node-1]
    D1[pod recol·lector] --- L1[/var/log/containers/]
    A1[api-reserves]
    T1[botiga-web]
  end
  subgraph Node2[node-2]
    D2[pod recol·lector] --- L2[/var/log/containers/]
    A2[api-reserves]
  end
  subgraph Node3[node-3 nou]
    D3[pod recol·lector<br/>creat automàticament] --- L3[/var/log/containers/]
  end
  DS[DaemonSet<br/>recollector-logs] --> D1
  DS --> D2
  DS --> D3

  1. Els quatre casos d'ús canònics

Pràcticament tot el que es desplega com a DaemonSet cau en una d'aquestes quatre categories. La regla que les uneix: el pod necessita alguna cosa que només existeix al node on corre.

Recol·lectors de logs

Fluent Bit, Fluentd, Vector, Promtail. Llegeixen els fitxers de /var/log/containers/ del node —on el runtime de contenidors escriu la sortida estàndard de tots els pods d'aquell node—, els enriqueixen amb metadades de Kubernetes i els envien a un backend central.

No podrien ser un Deployment: un pod només pot llegir els fitxers del node on està. Per llegir els logs de tots els nodes cal un pod a cada node.

Agents de mètriques

Node Exporter de Prometheus, agents de Datadog o New Relic. Llegeixen /proc i /sys del node per exposar CPU, memòria, disc, xarxa i temperatura del maquinari. També aquí, la informació és estrictament local al node.

El metrics-server que vam activar com a addon de minikube a 01-04 és un cas diferent: no és un DaemonSet, sinó un Deployment que consulta l'API de cada kubelet. El veurem a 07-02.

Connectors de xarxa del CNI

Calico, Cilium, Flannel, Weave. A 04-01 vam dir que "el CNI programa les rutes del node". Qui ho fa és un pod d'un DaemonSet que escriu configuració a /etc/cni/net.d, instal·la binaris a /opt/cni/bin i manipula les taules de rutes i les regles d'iptables/eBPF del node.

kubectl get daemonsets -n kube-system
NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
kube-proxy   3         3         3       3            3           kubernetes.io/os=linux   14d

Aquí el tens: kube-proxy, el component que vam estudiar a 04-01 i que tradueix els Services a regles de xarxa, és un DaemonSet. Té tot el sentit: cada node necessita el seu propi conjunt de regles.

Controladors d'emmagatzematge

Els drivers CSI que vam veure a 05-05 es despleguen en dues peces: un controlador (Deployment o StatefulSet, un per clúster, que parla amb l'API del proveïdor per crear volums) i un node plugin (DaemonSet, que munta i desmunta els volums al sistema de fitxers del node). El muntatge només el pot fer algú que estigui en aquell node.

Categoria Què necessita del node Exemples
Logs Fitxers a /var/log Fluent Bit, Vector, Promtail
Mètriques /proc, /sys, xarxa del host Node Exporter, agents APM
Xarxa (CNI) Rutes, iptables, /etc/cni/net.d Calico, Cilium, Flannel
Emmagatzematge (CSI node) Muntar al sistema de fitxers Drivers CSI d'EBS, Ceph, etc.
Seguretat Crides al sistema, auditoria Falco, agents de detecció

  1. Anatomia del manifest: per què no hi ha replicas

Un DaemonSet mínim:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: recollector-logs
  namespace: rutas-norte-pro
spec:
  selector:
    matchLabels:
      app: recollector-logs
      entorn: pro
  template:
    metadata:
      labels:
        app: recollector-logs
        app.kubernetes.io/part-of: rutas-norte
        entorn: pro
    spec:
      containers:
        - name: recollector
          image: busybox:1.36

Comparat amb un Deployment hi falta una sola cosa: spec.replicas. I la seva absència no és un oblit de l'API, és la definició de l'objecte. El nombre de pods no és un valor que tu declaris, és una conseqüència de l'estat del clúster: tants com nodes elegibles hi hagi. Si intentes afegir-lo, l'API el rebutja:

error: error validating "ds.yaml": ValidationError(DaemonSet.spec):
unknown field "replicas" in io.k8s.api.apps.v1.DaemonSetSpec

Per la resta, selector i template funcionen igual que en Deployments i StatefulSets, i spec.selector és immutable igual que allà. La template segueix les mateixes convencions de Rutas Norte: només app i entorn al selector, app.kubernetes.io/part-of com a etiqueta informativa.

L'estat es llegeix d'una manera característica:

kubectl get daemonset recollector-logs -n rutas-norte-pro
NAME               DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
recollector-logs   3         3         3       3            3           <none>          2m
Columna Significat
DESIRED Nodes elegibles segons selectors, afinitat i tolerations
CURRENT Pods creats
READY Pods que superen les seves sondes de disponibilitat
UP-TO-DATE Pods amb la plantilla actual (no l'anterior)
AVAILABLE Pods llestos durant almenys minReadySeconds

Si DESIRED és menor del que esperes, el problema està a l'apartat següent: hi ha nodes que el DaemonSet no considera elegibles.

  1. On corre: nodeSelector, affinity i tolerations

Per defecte, "un pod per node" significa tots els nodes. Hi ha tres mecanismes per restringir o ampliar aquest conjunt.

nodeSelector: el filtre simple

    spec:
      nodeSelector:
        kubernetes.io/os: linux

Només es creen pods en nodes que tinguin aquesta etiqueta. És el que fa kube-proxy a la sortida d'abans: en un clúster mixt amb nodes Windows, el binari de Linux no serviria.

Un cas propi de Rutas Norte: si a producció hi ha un grup de nodes amb SSD etiquetat disc: ssd, un agent que només interessi allà es restringeix així:

      nodeSelector:
        disc: ssd

affinity: el filtre expressiu

Quan el filtre no és una igualtat simple, es fa servir afinitat de node. Aquí només el just perquè funcioni; el mecanisme complet, amb els seus operadors i les seves variants preferides, és la lliçó 06-05.

    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: node-role.kubernetes.io/analitica
                    operator: DoesNotExist

Aquest exemple exclou del recol·lector els nodes dedicats a analítica.

tolerations: el permís per entrar on es rebutja

Aquest és el mecanisme clau dels DaemonSets i val la pena aturar-s'hi.

Alguns nodes porten un taint, una marca que diu "no col·loquis pods aquí llevat que estiguin expressament autoritzats". El cas més habitual és el pla de control, que en un clúster estàndard ve amb:

node-role.kubernetes.io/control-plane:NoSchedule

Aquest taint impedeix que les aplicacions normals acabin competint per CPU amb l'apiserver i etcd. Però un recol·lector de logs sí que hi ha de córrer: els logs de l'apiserver són justament els que més falta fan en una incidència.

La manera de dir "jo sí que puc" és una toleration:

    spec:
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule

Llegeix-ho així: "tolero el taint la clau del qual existeix amb aquest efecte". No és una preferència per anar al pla de control; és un permís per no ser rebutjat si el DaemonSet m'hi porta.

Els agents d'infraestructura que han de córrer absolutament a tots els nodes, passi el que passi fan servir una toleration universal:

      tolerations:
        - operator: Exists      # tolera qualsevol taint, amb qualsevol efecte

És el que fa el CNI: si el connector de xarxa no corre en un node, aquest node no té xarxa, així que cap condició no ho ha d'impedir. Fes-la servir amb criteri: per al teu recol·lector de logs és probablement excessiva, perquè també el col·locaria en nodes marcats com a no llestos o sota pressió de memòria.

Hi ha a més una cortesia que Kubernetes concedeix automàticament als DaemonSets: el controlador afegeix pel seu compte tolerations a certs taints del sistema (node.kubernetes.io/not-ready, unreachable, disk-pressure, memory-pressure, pid-pressure, unschedulable) perquè els agents no siguin desallotjats justament quan el node té problemes i més falta fa la telemetria. Ho pots veure si inspecciones 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, efectes i tolerationSeconds és el contingut de 06-05.

  1. Accés privilegiat: hostPath, hostNetwork, hostPID i el seu cost

Els agents de node necessiten veure coses que un pod normal no veu. Kubernetes ho permet, i cada permís amplia la superfície d'atac d'una manera que convé entendre abans d'escriure-la en un manifest.

hostPath

Munta un directori del sistema de fitxers del node dins del contenidor. El vam veure a 05-01 com a volum efímer perillós; aquí és imprescindible.

      volumes:
        - name: logs-contenidors
          hostPath:
            path: /var/log/containers
            type: Directory
        - name: logs-pods
          hostPath:
            path: /var/log/pods
            type: Directory

Detall tècnic que sorprèn molta gent: els fitxers de /var/log/containers són enllaços simbòlics a /var/log/pods, que al seu torn solen apuntar al directori de dades del runtime (/var/lib/docker/containers o l'equivalent de containerd). Si només muntes el primer, el contenidor veu enllaços trencats. Per això els recol·lectors munten els dos o tres directoris.

Risc: un hostPath amb permís d'escriptura sobre un directori sensible del node (/etc, /var/lib/kubelet, el socket del runtime) equival a control del node. Munta sempre en només lectura (readOnly: true) allò que només necessites llegir.

hostNetwork

    spec:
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet

El pod comparteix la pila de xarxa del node: els seus ports són ports del node i veu les interfícies reals. És obligatori per als connectors CNI i per a agents que capturen trànsit. Fixa't en el dnsPolicy: sense ell, un pod amb hostNetwork faria servir el DNS del node i perdria la resolució de noms de Kubernetes que vam estudiar a 04-03.

Risc: se salta l'aïllament de xarxa, i amb ell les NetworkPolicies de 04-06, que operen sobre les IP de pod. Un pod amb hostNetwork a rutas-norte-pro no està subjecte al deny-all.

hostPID

    spec:
      hostPID: true

El contenidor veu tots els processos del node. Necessari per a agents de perfilat o de seguretat que inspeccionen processos.

Risc: alt. Veure l'arbre de processos del host permet llegir línies d'ordres (que de vegades contenen credencials) i, combinat amb privilegis, entrar a l'espai de noms d'altres contenidors.

Permís Per a què es fa servir Què exposa Mitigació
hostPath (lectura) Llegir logs, /proc, /sys Contingut del disc del node readOnly: true, ruta com més específica millor
hostPath (escriptura) Instal·lar binaris CNI, muntar volums Control efectiu del node Evitar llevat de drivers d'infraestructura
hostNetwork CNI, captura de trànsit Tota la xarxa del node; salta NetworkPolicies Només quan no hi ha alternativa
hostPID Perfilat, seguretat Processos i els seus arguments Només agents de seguretat auditats
privileged: true Manipular el kernel Tot Preferir capacitats concretes

La regla d'or: cadascun d'aquests camps converteix el teu DaemonSet en part del pla de confiança del clúster. Un atacant que comprometi la imatge del teu recol·lector de logs amb hostPath d'escriptura té els mateixos poders que si comprometés el kubelet. I com que corre a tots els nodes, el compromís és total, no parcial. A 08-02 endurirem aquests contenidors amb securityContext, capacitats i sistemes de fitxers de només lectura.

  1. Estratègies d'actualització

Igual que els altres controladors, el DaemonSet defineix com propaga un canvi de plantilla.

RollingUpdate (per defecte)

spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 0

Actualitza node a node. maxUnavailable limita quants pods poden estar simultàniament no disponibles; amb 1 en un clúster de 3 nodes, l'actualització triga tres passos però mai no deixa més d'un node sense agent.

maxSurge (estable des de 1.25) permet el contrari: crear el pod nou abans de treure el vell, per no deixar cap forat de cobertura. Com que en un node hi caben els dos temporalment, exigeix que tots dos puguin coexistir: si l'agent fa servir hostNetwork amb un port fix, maxSurge: 1 provoca conflicte de ports. Regles:

Combinació Comportament Quan
maxUnavailable: 1, maxSurge: 0 Un node sense agent durant l'actualització Per defecte; agents que poden tenir forats
maxUnavailable: 0, maxSurge: 1 Mai no hi ha forat; solapament temporal Agents crítics sense ports de host
maxUnavailable: 25%, maxSurge: 0 Més ràpid en clústers grans Clústers de desenes de nodes

No poden ser tots dos zero, i no poden ser tots dos diferents de zero.

OnDelete

spec:
  updateStrategy:
    type: OnDelete

Els pods s'actualitzen només quan els esborres a mà o quan el node es recrea. És el que és habitual per als connectors CNI: actualitzar la xarxa d'un node en producció és una operació que es vol fer amb supervisió, node a node i amb un pla de marxa enrere.

L'historial i la marxa enrere funcionen com als Deployments de 02-04:

kubectl rollout status daemonset/recollector-logs -n rutas-norte-pro
kubectl rollout history daemonset/recollector-logs -n rutas-norte-pro
kubectl rollout undo daemonset/recollector-logs -n rutas-norte-pro --to-revision=2

  1. Pràctica: recol·lector de logs per a Rutas Norte

Desplegarem un agent que llegeixi els logs dels contenidors de tots els nodes i —de moment— els mostri per la seva pròpia sortida estàndard. La pila completa de registre centralitzat (Elasticsearch, Fluentd, Kibana) es munta a 07-05; aquí l'objectiu és el DaemonSet, no el destí de les dades.

Fem servir Fluent Bit, lleuger i estàndard a la indústria.

Configuració de l'agent

# k8s/base/recollector-logs-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: recollector-logs-config
  namespace: rutas-norte-pro
  labels:
    app: recollector-logs
    app.kubernetes.io/part-of: rutas-norte
    entorn: 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%z

Què fa cada bloc:

  • [INPUT] tail segueix els fitxers de /var/log/containers/ el nom dels quals contingui rutas-norte-pro. El nom d'aquests fitxers té la forma <pod>_<namespace>_<contenidor>-<id>.log, així que aquest patró filtra per namespace. DB desa la posició llegida de cada fitxer per no reenviar-ho tot després d'un reinici de l'agent: és un fitxer d'estat, i per això el col·locarem en un hostPath.
  • [FILTER] kubernetes consulta l'API per afegir a cada línia el pod, el namespace, les etiquetes i el node. Això és el que permetrà filtrar per app: api-reserves a 07-05, i requereix permisos de lectura sobre pods.
  • [OUTPUT] stdout bolca a la sortida estàndard del mateix agent. A 07-05 se substituirà per un output cap a Elasticsearch.

Permisos

El filtre de Kubernetes necessita llegir pods i namespaces del clúster sencer. Seguint la pràctica de 03-06, una ServiceAccount dedicada:

# k8s/base/recollector-logs-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: recollector-logs
  namespace: rutas-norte-pro
  labels:
    app: recollector-logs
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: recollector-logs
rules:
  - apiGroups: [""]
    resources: ["pods", "namespaces"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: recollector-logs
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: recollector-logs
subjects:
  - kind: ServiceAccount
    name: recollector-logs
    namespace: rutas-norte-pro

Només verbs de lectura i només sobre dos recursos: el mínim necessari. El detall d'RBAC —què és un ClusterRole, com s'acota, com s'audita— és la lliçó 08-01; aquí el donem fet perquè l'exemple funcioni.

Fixa't que aquest pod sí que necessita muntar el seu token de ServiceAccount, al contrari que gairebé tots els components de Rutas Norte.

El DaemonSet

# k8s/base/recollector-logs-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: recollector-logs
  namespace: rutas-norte-pro
  labels:
    app: recollector-logs
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
spec:
  selector:
    matchLabels:
      app: recollector-logs
      entorn: pro
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 0
  minReadySeconds: 10
  template:
    metadata:
      labels:
        app: recollector-logs
        app.kubernetes.io/part-of: rutas-norte
        entorn: pro
    spec:
      serviceAccountName: recollector-logs
      terminationGracePeriodSeconds: 30
      priorityClassName: system-node-critical
      tolerations:
        # Córrer també als nodes del pla 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      # necessita llegir fitxers del node propietat de root
            readOnlyRootFilesystem: true
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
          env:
            - name: NODE
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          volumeMounts:
            - name: config
              mountPath: /fluent-bit/etc/
              readOnly: true
            - name: logs-contenidors
              mountPath: /var/log/containers
              readOnly: true
            - name: logs-pods
              mountPath: /var/log/pods
              readOnly: true
            - name: estat
              mountPath: /var/log/flb-estat
      volumes:
        - name: config
          configMap:
            name: recollector-logs-config
        - name: logs-contenidors
          hostPath:
            path: /var/log/containers
            type: Directory
        - name: logs-pods
          hostPath:
            path: /var/log/pods
            type: Directory
        - name: estat
          hostPath:
            path: /var/lib/rutasnorte/fluent-bit
            type: DirectoryOrCreate

Decisions que val la pena explicar:

  • priorityClassName: system-node-critical: si el node es queda sense memòria, aquest pod no ha de ser el primer a caure. La mecànica de prioritats i desallotjament és contingut de 06-05.
  • Les tolerations: només la del pla de control. No fem servir operator: Exists sense clau perquè no volem que el recol·lector es col·loqui als nodes d'analítica que taintarem a 06-05.
  • readOnly: true als dos hostPath de logs: l'agent llegeix, no escriu. L'únic amb escriptura és el directori d'estat, i apunta a una ruta pròpia sota /var/lib/rutasnorte, no a un directori del sistema.
  • readOnlyRootFilesystem: true amb les capacitats a zero: enduriment bàsic que ampliarem a 08-02.
  • La variable NODE ve de la Downward API de 03-03 i serveix perquè el mateix agent sàpiga on és sense consultar res.

Desplegar i verificar

kubectl apply -f k8s/base/recollector-logs-rbac.yaml
kubectl apply -f k8s/base/recollector-logs-configmap.yaml
kubectl apply -f k8s/base/recollector-logs-daemonset.yaml

kubectl rollout status daemonset/recollector-logs -n rutas-norte-pro
kubectl get pods -n rutas-norte-pro -l app=recollector-logs -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP            NODE
recollector-logs-4wq7n   1/1     Running   0          52s   10.244.0.31   rutas-norte
recollector-logs-hk2zp   1/1     Running   0          52s   10.244.1.44   rutas-norte-m02
recollector-logs-t8xrb   1/1     Running   0          52s   10.244.2.22   rutas-norte-m03

Un pod per node, i la columna NODE ho confirma: no n'hi ha dos al mateix lloc.

kubectl logs -n rutas-norte-pro daemonset/recollector-logs --tail=3
{"date":1754380522.4,"log":"GET /api/reserves/4471 200 12ms","kubernetes":{"pod_name":"api-reserves-6c8f9d4b7-jm2xq","namespace_name":"rutas-norte-pro","container_name":"api","labels":{"app":"api-reserves","entorn":"pro"},"host":"rutas-norte-m02"}}

Aquí tens el resultat: una línia de log d'api-reserves enriquida amb el pod, el namespace, les etiquetes i el node. Aquest enriquiment és el que farà possible a 07-05 buscar "tots els errors d'api-reserves en producció de les últimes dues hores".

  1. Comprovació: què passa en afegir un node

La propietat central del DaemonSet es demostra en trenta segons amb minikube:

minikube node add -p rutas-norte
😄  Adding node m04 to cluster rutas-norte
...
🏄  Successfully added m04 to rutas-norte!
kubectl get nodes
kubectl get pods -n rutas-norte-pro -l app=recollector-logs -o wide
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
recollector-logs-4wq7n   1/1     Running   0          11m   rutas-norte
recollector-logs-hk2zp   1/1     Running   0          11m   rutas-norte-m02
recollector-logs-t8xrb   1/1     Running   0          11m   rutas-norte-m03
recollector-logs-x9k2d   1/1     Running   0          22s   rutas-norte-m04

Ningú no ha tocat el manifest. El controlador de DaemonSet vigila els nodes, n'ha vist un de nou elegible i ha creat el seu pod. És exactament el mateix bucle de reconciliació que vam descriure a 01-02, aplicat a la relació "un pod per node".

A l'inrevés funciona igual:

minikube node delete m04 -p rutas-norte
kubectl get daemonset recollector-logs -n rutas-norte-pro
NAME               DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   AGE
recollector-logs   3         3         3       3            3           13m

DESIRED ha tornat a 3 tot sol. Fixa't en la diferència amb un Deployment: quan desapareix un node, el Deployment recol·loca les seves rèpliques als altres nodes per mantenir el nombre; el DaemonSet no recol·loca res, perquè el pod tenia sentit únicament en aquell node.

  1. Recursos: per què un DaemonSet mal dimensionat és car

Aquí hi ha la conseqüència econòmica que s'oblida amb més freqüència.

Quan demanes 200 MiB a un Deployment de tres rèpliques, demanes 600 MiB. Quan demanes 200 MiB a un DaemonSet, demanes 200 MiB × nombre de nodes, i aquest nombre creix amb el clúster.

Un exemple amb números realistes d'un clúster de 50 nodes i quatre DaemonSets habituals:

Agent requests.cpu requests.memory × 50 nodes (CPU) × 50 nodes (memòria)
Recol·lector de logs 50m 96Mi 2,5 CPU 4,7 GiB
Exportador de mètriques 30m 64Mi 1,5 CPU 3,1 GiB
Connector CNI 100m 128Mi 5 CPU 6,3 GiB
Node plugin CSI 20m 64Mi 1 CPU 3,1 GiB
Total 10 CPU 17,2 GiB

Deu CPU i disset gigues reservats abans de desplegar una sola línia de codi de Rutas Norte. I són requests: capacitat apartada pel scheduler, s'estigui fent servir o no, que redueix el que queda per a api-reserves i botiga-web.

Pitjor encara si t'equivoques per excés. Posar requests.memory: 512Mi en un recol·lector que consumeix 60 MiB "per si de cas" malbarata 22 GiB en aquell clúster: més d'un node sencer pagat per no res.

Consells concrets:

  • Mesura abans de fixar. Desplega amb requests baixos a rutas-norte-dev, observa amb kubectl top pod (07-02) durant uns dies de trànsit real i ajusta.
  • Posa límits de memòria sempre. Un recol·lector amb una fuita o amb un pic de logs pot consumir tota la memòria del node i provocar desallotjaments de les aplicacions. Mem_Buf_Limit a la configuració de Fluent Bit és la primera línia de defensa; el limits.memory del contenidor és la segona.
  • Compte amb els límits de CPU. Un recol·lector estrangulat per CPU acumula retard i perd línies quan el node està més carregat, que és justament quan més falta fan els logs. Un límit generós o cap és defensable aquí.
  • Revisa les quotes. Els ResourceQuota per namespace de 03-04 s'apliquen també als pods del DaemonSet. Si la quota de rutas-norte-pro s'esgota, els pods del DaemonSet en nodes nous no es crearan, i ho descobriràs quan faltin logs.
  • Pregunta't si de debò en cal un per node. És el consell més rendible. Un agent que consulta l'API de Kubernetes no necessita ser a cada node: en té prou amb un Deployment d'una rèplica.

Errors Comuns i Consells

Intentar posar replicas al manifest. L'API ho rebutja. Si véns de Deployments és la primera ensopegada, i és sa: t'obliga a interioritzar que el nombre de pods el dicta el clúster.

No posar tolerations i perdre els nodes del pla de control. El símptoma és subtil: el DaemonSet apareix 3/3 i tot sembla bé, però falten els logs de l'apiserver. Compara sempre DESIRED amb kubectl get nodes | wc -l.

Muntar només /var/log/containers. Com que són enllaços simbòlics a /var/log/pods, l'agent veu noms però no contingut, i falla amb errors de fitxer no trobat que despisten molt. Munta'ls tots dos.

hostPath amb type incorrecte. Amb type: Directory, si la ruta no existeix al node el pod es queda en ContainerCreating amb un esdeveniment MountVolume.SetUp failed. Per a directoris d'estat propis fes servir DirectoryOrCreate.

Oblidar dnsPolicy: ClusterFirstWithHostNet amb hostNetwork: true. El pod hereta el /etc/resolv.conf del node, deixa de resoldre noms de Services i falla en parlar amb qualsevol component intern. És un error que costa hores de diagnòstic.

maxSurge: 1 amb hostNetwork i port fix. El pod nou no pot arrencar perquè el vell té el port ocupat, i l'actualització es queda bloquejada. Amb hostNetwork fes servir sempre maxSurge: 0.

Creure que un DaemonSet garanteix cobertura durant l'actualització. Amb maxUnavailable: 1 i maxSurge: 0 hi ha sempre un node sense agent durant uns segons. Per a logs d'auditoria això pot ser inacceptable: fes servir maxUnavailable: 0 i maxSurge: 1 si l'agent ho permet.

Consell: kubectl get pods -o wide és la teva verificació. La columna NODE és l'única manera directa de confirmar la invariant "un per node". Afegeix-hi --sort-by=.spec.nodeName per llegir-ho còmode.

Consell: un DaemonSet és un vector d'atac d'abast total. Corre a tots els nodes, sol tenir hostPath i de vegades hostNetwork. Fixa la imatge per etiqueta immutable —millor encara, per digest—, revisa quin RBAC demana i aplica l'enduriment de 08-02 i l'escaneig d'imatges de 08-05.

Exercicis

Exercici 1: DaemonSet mínim i verificació de cobertura

A rutas-norte-dev del teu minikube (perfil rutas-norte, almenys dos nodes), desplega un DaemonSet anomenat sonda-nodes amb busybox:1.36 que cada 30 segons escrigui a la seva sortida estàndard el nom del node (obtingut per Downward API) i l'espai lliure de / del node, llegint /host-arrel muntat en només lectura des de hostPath: /.

Verifica que hi ha un pod per node i consulta'n la sortida.

Exercici 2: tolerations i nodes del pla de control

Comprova si el pod de sonda-nodes s'ha creat al node del pla de control. Si no, esbrina per què i incorpora-hi la toleration necessària. Verifica que DESIRED augmenta.

Exercici 3: actualització sense forats de cobertura

Canvia la imatge de sonda-nodes a busybox:1.37 amb una estratègia que no deixi cap node sense agent en cap moment. Comprova el comportament observant els pods durant l'actualització i explica quina diferència hauries vist amb l'estratègia per defecte.

Solucions

Solució 1

# /tmp/sonda-nodes.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: sonda-nodes
  namespace: rutas-norte-dev
  labels:
    app: sonda-nodes
    app.kubernetes.io/part-of: rutas-norte
    entorn: dev
spec:
  selector:
    matchLabels:
      app: sonda-nodes
      entorn: dev
  template:
    metadata:
      labels:
        app: sonda-nodes
        app.kubernetes.io/part-of: rutas-norte
        entorn: dev
    spec:
      automountServiceAccountToken: false
      containers:
        - name: sonda
          image: busybox:1.36
          command:
            - sh
            - -c
            - 'while true; do echo "$(date +%H:%M:%S) node=$NODE lliure=$(df -h /host-arrel | tail -1 | awk "{print \$4}")"; sleep 30; done'
          env:
            - name: NODE
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
            limits:
              cpu: 50m
              memory: 32Mi
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: arrel-host
              mountPath: /host-arrel
              readOnly: true
      volumes:
        - name: arrel-host
          hostPath:
            path: /
            type: Directory
kubectl apply -f /tmp/sonda-nodes.yaml
kubectl rollout status daemonset/sonda-nodes -n rutas-norte-dev
kubectl get pods -n rutas-norte-dev -l app=sonda-nodes -o wide
kubectl logs -n rutas-norte-dev daemonset/sonda-nodes --tail=2
NAME                READY   STATUS    RESTARTS   AGE   NODE
sonda-nodes-c4m8p   1/1     Running   0          25s   rutas-norte-m02
sonda-nodes-q7t2v   1/1     Running   0          25s   rutas-norte-m03

18:22:41 node=rutas-norte-m02 lliure=12.4G

Els recursos són mínims a propòsit: és un DaemonSet i tot el que demani es multiplica pel nombre de nodes.

Solució 2

kubectl get nodes
kubectl get daemonset sonda-nodes -n rutas-norte-dev
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-nodes   2         2         2       3m

Tres nodes però DESIRED és 2: falta el pla de control. La causa:

kubectl describe node rutas-norte | grep -A2 Taints
Taints:  node-role.kubernetes.io/control-plane:NoSchedule

(En un minikube d'un sol node aquest taint no hi és posat, precisament perquè s'hi puguin desplegar aplicacions; en un clúster multinode o real sí que apareix.)

La solució és tolerar aquest taint:

    spec:
      tolerations:
        - key: node-role.kubernetes.io/control-plane
          operator: Exists
          effect: NoSchedule
kubectl apply -f /tmp/sonda-nodes.yaml
kubectl get daemonset sonda-nodes -n rutas-norte-dev
kubectl get pods -n rutas-norte-dev -l app=sonda-nodes -o wide
NAME          DESIRED   CURRENT   READY   AGE
sonda-nodes   3         3         3       5m

NAME                READY   STATUS    RESTARTS   AGE   NODE
sonda-nodes-c4m8p   1/1     Running   0          5m    rutas-norte-m02
sonda-nodes-q7t2v   1/1     Running   0          5m    rutas-norte-m03
sonda-nodes-zr9kd   1/1     Running   0          14s   rutas-norte

Observa que la toleration no atreu el pod cap al pla de control: simplement deixa d'excloure'l. Qui l'hi col·loca és la lògica d'"un per node" del DaemonSet.

Solució 3

Per no deixar forats cal crear abans de destruir, és a dir maxUnavailable: 0 i maxSurge: 1. És viable perquè sonda-nodes no fa servir hostNetwork ni ports de host, així que dos pods poden coexistir un instant al mateix node.

kubectl patch daemonset sonda-nodes -n rutas-norte-dev -p '{
  "spec": {
    "updateStrategy": {
      "type": "RollingUpdate",
      "rollingUpdate": {"maxUnavailable": 0, "maxSurge": 1}
    }
  }
}'

# Observar en un altre terminal
kubectl get pods -n rutas-norte-dev -l app=sonda-nodes -o wide --watch
kubectl set image daemonset/sonda-nodes -n rutas-norte-dev sonda=busybox:1.37
kubectl rollout status daemonset/sonda-nodes -n rutas-norte-dev

Durant l'actualització s'observa aquest patró a cada node:

sonda-nodes-c4m8p   1/1   Running       0     8m    rutas-norte-m02   (vell)
sonda-nodes-n5j3w   0/1   Pending       0     0s    rutas-norte-m02   (nou)
sonda-nodes-n5j3w   1/1   Running       0     4s    rutas-norte-m02   (nou, llest)
sonda-nodes-c4m8p   1/1   Terminating   0     8m    rutas-norte-m02   (vell, es retira)

El pod nou arriba a Running abans que el vell comenci a terminar: en cap moment el node m02 no es queda sense agent.

Amb l'estratègia per defecte (maxUnavailable: 1, maxSurge: 0) l'ordre seria l'invers: primer Terminating el vell, després la descàrrega de la imatge nova i l'arrencada. Entre tots dos moments —que amb una imatge no cauada poden ser desenes de segons— aquell node no té recol·lector i els seus logs es perden.

# Neteja
kubectl delete -f /tmp/sonda-nodes.yaml

Conclusió

El DaemonSet és el controlador de les càrregues de treball que pertanyen al node, no a l'aplicació. Es distingeix d'un Deployment en què no té replicas: el nombre de pods és una conseqüència de quants nodes elegibles hi ha, i s'ajusta sol quan el clúster creix o minva. Els quatre usos canònics —logs, mètriques, connectors CNI i node plugins CSI— comparteixen un tret: necessiten alguna cosa que només existeix al node on corren, i per això kube-proxy i el mateix CNI del teu clúster són DaemonSets.

Hem vist com s'acota el seu abast amb nodeSelector i affinity, i sobretot com s'amplia amb tolerations per arribar als nodes del pla de control. Hem revisat el preu dels permisos privilegiats que aquests agents solen demanar (hostPath, hostNetwork, hostPID), que converteixen el DaemonSet en part del pla de confiança del clúster. Hem desplegat el recol·lector de logs de Rutas Norte —el destí final del qual muntarem a 07-05—, comprovat que un node nou rep el seu pod sense intervenció, i calculat per què uns requests mal ajustats es multipliquen pel nombre de nodes fins a costar servidors sencers.

Ens queda tota una família de càrregues de treball per cobrir. Deployments, StatefulSets i DaemonSets tenen una cosa en comú: els seus pods estan pensats per córrer indefinidament, i si un contenidor acaba, el sistema ho considera una fallada i el reinicia. Però hi ha feina que consisteix precisament a acabar: generar un informe, migrar un esquema, processar un lot. Per a això Kubernetes té dos altres objectes, i amb ells desplegarem per fi el component que falta a Rutas Norte, informes-ocupacio. És el tema de la lliçó següent: Treballs i CronJobs.

Curs de Kubernetes

Mòdul 1: Introducció a Kubernetes

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats