
🌐 También en: English · Deutsch · Français
Deep Dive de la serie en torno al Ansible Tour. Nivel: administración Linux avanzada. El Tour explica los roles; los deep dives explican la tecnología que hay debajo – cómo funciona, por qué está aquí, qué hace exactamente esta instalación con ella, cómo se opera en el día a día y qué otras opciones habría habido.
K3s es Kubernetes certificado en un único binario de unos 70 MB. API server, scheduler, controller manager, kubelet, kube-proxy, containerd, Flannel, CoreDNS y un controlador de NetworkPolicy arrancan todos desde un proceso y una unidad de systemd. Eso lo convierte en la elección obvia para un clúster de exactamente un nodo – y esconde muchas cosas. Este artículo explica qué corre realmente dentro de ese binario, qué configura la parte 2 de la visita, y seis cosas que K3s no hizo por sí mismo y que solo se descubrieron en la operación.
🧭 Qué hay dentro del binario
Un supervisor, muchos componentes
k3s server es un supervisor. Arranca los componentes del plano de control de Kubernetes como goroutines dentro de su propio proceso – API server, scheduler, controller manager – y, a su lado, los componentes del nodo: el kubelet, kube-proxy, el CNI Flannel y un controlador de NetworkPolicy integrado (código de kube-router). containerd corre como proceso hijo aparte, con su propio socket en /run/k3s/containerd/. Todo comparte un flujo de logs en el journal (journalctl -u k3s) y un número de versión.
El almacén de datos: SQLite en lugar de etcd
Kubernetes upstream guarda su estado en etcd. Un K3s de un nodo usa SQLite a través de kine, una capa que traduce la API de etcd a SQL. Todo el estado del clúster – cada Deployment, Secret, ConfigMap, evento – vive en /var/lib/rancher/k3s/server/db/state.db: 24 MB en el blog, 23 MB en un servidor Nextcloud, más un log de escritura anticipada de 20 MB. Con más de un nodo servidor, K3s pasa a un etcd integrado.
containerd, el CRI y el contenedor pause
El kubelet habla con containerd mediante la Container Runtime Interface. Para cada pod, containerd arranca primero un diminuto contenedor pause que mantiene el espacio de nombres de red del pod; los contenedores reales se unen a él. k3s crictl es el cliente CRI para mirar directamente ese nivel – pods, contenedores, imágenes – sin pasar por el API server. En AlmaLinux 9, K3s usa cgroup v2, así que los límites de recursos de un pod son simples ficheros cgroup bajo /sys/fs/cgroup/kubepods.slice/.
Red: Flannel, kube-proxy, el controlador de políticas
- Flannel da a cada pod una dirección de
10.42.0.0/16(en un nodo: un/24), crea un puentecni0y un parvethpor pod. Backend aquí: VXLAN – irrelevante en un nodo, pero el valor por defecto para un segundo. - kube-proxy convierte los Services en reglas de iptables (a través de iptables-nft, en tablas de nftables): DNAT de ClusterIP, NodePorts, reparto de carga entre endpoints.
- El controlador de NetworkPolicy convierte las NetworkPolicies en cadenas (
KUBE-ROUTER-*,KUBE-NWPLCY-*, unaKUBE-POD-FW-*por pod). Forma parte del binario y está activo por defecto. Conviene recordarlo.
Certificados y tokens
K3s crea en el primer arranque su propia CA y todos los certificados (/var/lib/rancher/k3s/server/tls/), válidos durante un año, y los renueva automáticamente cuando el servicio arranca dentro de los 90 días anteriores a su caducidad. El kubeconfig de administración es /etc/rancher/k3s/k3s.yaml; el token del nodo en /var/lib/rancher/k3s/server/token es lo que un segundo nodo necesitaría para unirse.
🛡️ Por qué está aquí
- Semántica de Kubernetes en un servidor pequeño. Deployments con actualización progresiva o recreación, sondas que reinician un contenedor colgado, NetworkPolicies entre pods, límites de recursos, una política de admisión para imágenes – sin operar un clúster de plano de control.
- Un binario, una unidad, una versión fijada. Toda la plataforma es reproducible desde Ansible; una actualización es «sustituir el binario, reiniciar la unidad».
- Buen soporte de AlmaLinux y SELinux: una política SELinux mantenida (
k3s-selinux) y containerd con etiquetado SELinux.
🔧 Qué hace este repositorio
config.yaml: qué se desactiva, qué se activa
disable:
- traefik # en su lugar F5 nginx-ingress (instalado con Helm)
- servicelb # sin Services LoadBalancer; el ingress usa hostNetwork
- local-storage # ningún PVC - todo es hostPath (0 PVC en todos los hosts)
flannel-backend: vxlan
write-kubeconfig-mode: "0600"
selinux: true
kube-apiserver-arg:
- "audit-log-path=/var/lib/rancher/k3s/server/logs/audit.log"
- "audit-policy-file=/etc/rancher/k3s/audit-policy.yaml"
- "audit-log-maxage=30"
- "audit-log-maxbackup=10"
- "audit-log-maxsize=100"
- "enable-admission-plugins=NodeRestriction,EventRateLimit"
- "admission-control-config-file=/etc/rancher/k3s/admission-control-config.yaml"
kubelet-arg:
- "pod-max-pids=4096"La mayoría de los argumentos vienen del benchmark CIS de Kubernetes: un log de auditoría de la API con rotación, EventRateLimit para que un pod en bucle de fallos no inunde la API de eventos, un límite de PID por pod contra fork bombs. Una recomendación no se sigue a propósito: AlwaysPullImages descargaría cada imagen del registro en cada arranque de pod – incluido el pod cron de WordPress cada cinco minutos – y convertiría un corte breve del registro en pods fallando por todas partes, a cambio de una protección multiinquilino que un clúster con un solo operador no necesita.
Fijado, e instalado en dos pasos
El binario viene de la release de GitHub de k3s_version (versions.yml), el script de instalación de la misma etiqueta – no de get.k3s.io, que siempre sirve el script más reciente y respondió un rato con HTTP 500 el día en que se montó el segundo servidor Nextcloud. El script se ejecuta con INSTALL_K3S_SKIP_DOWNLOAD=true, solo para crear la unidad. Alrededor de K3s el rol instala Helm, cert-manager con un ClusterIssuer de Let’s Encrypt y el controlador F5 nginx-ingress, y parchea cada ServiceAccount default para que los pods ya no reciban un token de API que nunca usan (CIS 5.1.5). Los roles de las aplicaciones añaden por namespace una ValidatingAdmissionPolicy que rechaza los pods cuyas imágenes no vienen de una lista de registros permitidos – integrada en el API server desde Kubernetes 1.30, sin componente adicional.
El camino de una actualización
- Leer la versión en marcha (
k3s --version). - Si difiere de la fijada: descargar el binario con
get_url,force: true. restoreconsobre el binario.- Ejecutar los handlers en el acto – el reinicio ocurre aquí, no al final del play.
- Esperar a que
kubectl get nodesresponda y seguir con los roles que despliegan en el clúster. - Al final de la ejecución,
common_version_activecompara la versión del kubelet que informa la API con la fijada.
Los pasos 2, 3 y 6 existen cada uno por un error, ver abajo.
🐛 Seis cosas que K3s no hizo por sí mismo
1. La actualización que no hacía nada
Hasta el 19 de octubre de 2026, la tarea de descarga no tenía force. El get_url de Ansible se salta entonces la descarga si el fichero de destino ya existe – así que cada subida de versión desde la primera instalación se había quedado, en silencio, sin efecto en los hosts existentes. Salió a la luz porque la comprobación de deriva para v1.37.1 no mostraba ningún cambio en K3s. Desde entonces, la prueba de humo al final de cada ejecución (common_version_active) lee la versión que el kubelet informa de verdad, no la del inventario.
2. Pods que SELinux no confinaba
SELinux estaba en enforcing en todos los hosts y los directorios HostPath llevaban container_file_t. Pero K3s solo arranca containerd con soporte de SELinux cuando su configuración dice selinux: true – y no lo decía. Cada pod corría como spc_t o unconfined_service_t, que SELinux no restringe en absoluto. Desde el 19 de octubre de 2026 la opción está activa y la política k3s-selinux instalada; cada pod corre ahora como container_t con sus propias categorías MCS, y los pods que existían antes del cambio tuvieron que recrearse. El deep dive de SELinux cuenta toda la historia.
3. Un binario nuevo con la etiqueta equivocada
get_url escribe a través de un fichero temporal y lo renombra. El binario nuevo recibe así la etiqueta de su directorio (bin_t) en lugar de container_runtime_exec_t de la política, y K3s arrancaría en el dominio equivocado. Un restorecon tras cada descarga lo corrige.
4. El motor de políticas que nunca hizo falta
Durante dos meses, Calico corrió junto a K3s como supuesto motor de NetworkPolicy. No aplicaba nada – Felix, el componente de Calico, solo gestiona interfaces llamadas cali*, Flannel crea veth* – y toda la aplicación venía desde el principio del controlador propio de K3s. La prueba fue desagradable: con ese controlador desactivado para una prueba (disable-network-policy), un pod del namespace default llegó a MariaDB en el puerto 3306. Calico ya no está, y la configuración lleva ahora un comentario en mayúsculas: no desactivar nunca el controlador integrado. El deep dive de nftables describe el lado del cortafuegos.
5. Un flag de admisión que sustituía en lugar de añadir
En Kubernetes upstream, --enable-admission-plugins añade plugins a la lista por defecto. En K3s sustituye la lista propia de K3s. Puesto solo a EventRateLimit, eliminó en silencio NodeRestriction – el plugin que impide que un kubelet comprometido modifique objetos de otros nodos. Una nueva pasada de kube-bench lo detectó (CIS 1.2.14 pasó de PASS a FAIL). Ambos figuran ahora explícitamente. Una trampa parecida: el API server no crea el directorio de su log de auditoría y no arranca sin él; el rol lo crea antes del reinicio.
📊 Medido
| Blog | Servidor Nextcloud | |
|---|---|---|
| Versión de K3s | v1.37.1+k3s1 | v1.37.1+k3s1 |
| Pods | 17 | 14 |
Memoria de k3s server (RSS) | 789 MB | 826 MB |
| containerd (RSS) | 202 MB | 190 MB |
Tiempo de CPU de k3s server | 23 min en 7,5 h (≈ 5 % de un núcleo) | 2,5 h en 9,5 h (≈ 26 % de un núcleo) |
| Almacén de datos (state.db + WAL) | 24 + 20 MB | 23 + 20 MB |
| Log de auditoría de la API antes del 20/10/2026 | 1,1 GB (en el límite configurado) | 1,0 GB, ~230 MB/día |
| Log de auditoría tras el cambio de política | – | ~28 MB/día |
Alrededor de un gigabyte de RAM para todo el plano de control es el precio de «Kubernetes de verdad en un nodo». Dos cifras llamaban la atención. El log de auditoría estaba en su límite de diez copias de 100 MB – la rotación funcionaba, pero los «30 días» de la configuración eran en realidad cuatro y medio. Y en el servidor Nextcloud el plano de control usaba cinco veces la CPU del blog con menos pods.
Séptima, no un fallo: el mismo trabajo a cinco veces el precio
El log de auditoría mostraba la misma carga de escritura en los tres servidores, y un perfil de CPU de 30 segundos – tomado sin reiniciar a través del /debug/pprof del kubelet, que perfila todo el proceso de K3s – no señaló a ningún culpable concreto: todo costaba unas cuatro veces y media más, sobre todo el scheduler de Go despertando goroutines y las llamadas al sistema detrás. Limitar Go a cuatro hilos (GOMAXPROCS=4) no cambió nada. La respuesta estaba en /sys/devices/system/cpu/vulnerabilities/: los servidores Nextcloud corren sobre una generación más antigua de AMD EPYC, vulnerable a Retbleed, y el kernel protege cada retorno de una función del kernel con un «return thunk». Para un programa que vive de llamadas al sistema y cambios de contexto eso cuesta mucho; la CPU más nueva del blog es «not affected». Desactivar la protección en una máquina virtual compartida sería una imprudencia – alrededor del 2,6 % de un servidor de ocho núcleos es el precio.
Sexta: un log de auditoría ahogado en su propio ruido
El desglose de un fichero de 100 MB mostró de dónde salía el volumen: los latidos de los leases de las elecciones de líder y del kubelet (23 %), las peticiones de salud, descubrimiento y OpenAPI (14 %), el API server hablando consigo mismo (12 %) y las lecturas de los componentes de la plataforma, los ServiceAccounts y los nodos (alrededor del 40 %). Nada de eso tiene valor de seguridad; solo empujaba las entradas interesantes fuera de la ventana de retención. La política descarta ahora exactamente eso – con dos reglas colocadas a propósito antes de la regla de descarte: las lecturas de secretos siguen registrándose para cualquier lector, y todo lo que hace un administrador también. Una simulación sobre el fichero real predijo que quedaría un 10 %; tras el despliegue, el log creció unos 28 MB al día en lugar de 230.
rules:
- level: None
nonResourceURLs: ["/healthz*", "/livez*", "/readyz*", "/version", "/openapi*", "/api*", "/metrics"]
- level: None
users: ["system:apiserver"]
- level: None
resources: [{group: "coordination.k8s.io", resources: ["leases"]}]
- level: Metadata # antes de la regla siguiente: secretos para cualquier lector
resources: [{group: "", resources: ["secrets"]}]
- level: None
verbs: ["get", "list", "watch"]
userGroups: ["system:serviceaccounts", "system:nodes"]
... # exec/attach en Request, RBAC en RequestResponse, el resto en Metadata🧰 Administración en el día a día
# el clúster
k3s kubectl get nodes -o wide
k3s kubectl get pods -A
k3s kubectl top pods -A # metrics-server forma parte de K3s
# un nivel más abajo: containerd directamente, sin el API server
k3s crictl pods
k3s crictl ps
k3s crictl images
# logs de toda la plataforma (API server, kubelet, Flannel, controlador de políticas)
journalctl -u k3s --since -1h
# certificados: ¿cuándo caducan? (se renuevan al reiniciar dentro de los 90 días)
for c in /var/lib/rancher/k3s/server/tls/*.crt; do
printf '%-45s ' "$(basename "$c")"; openssl x509 -enddate -noout -in "$c" | cut -d= -f2
done
# SELinux: ¿en qué dominio corren los pods?
ps -eZ | grep container_t | headActualizaciones
Una versión menor cada vez, como exige Kubernetes (1.36 → 1.37, no 1.35 → 1.37). Subir k3s_version en versions.yml, ejecutar la comprobación de deriva (--check --diff) – ahora debe mostrar la descarga del binario –, ejecutar el playbook y leer el informe de versiones al final. Un reinicio de K3s no reinicia ni los pods ni los contenedores de containerd; la API desaparece unos 20 segundos, los sitios web no.
Copias de seguridad – y lo que de verdad hace falta
El almacén de datos no se respalda aquí, a propósito. Cada objeto que contiene lo crea Ansible a partir del repositorio y del vault: namespaces, Secrets, Deployments, políticas. Los datos que importan viven fuera del clúster, en directorios HostPath y en las bases de datos, y el servidor de copias los recoge cada noche. Restaurar un servidor significa, por tanto: instalar AlmaLinux, ejecutar el playbook, restaurar los directorios de datos. El clúster en sí es desechable – el argumento más fuerte para tenerlo todo en código.
🆘 Chuleta: síntoma → comando
La versión corta de este artículo para el momento en que algo va mal. Ejecutar en el host como root; <…> son marcadores.
| Síntoma | Comando |
|---|---|
| Un pod no arranca | k3s kubectl get pods -A |
| ¿Por qué se cayó un contenedor? | k3s kubectl -n <ns> logs <pod> -c <container> --previous |
| ¿Qué ha pasado últimamente? | k3s kubectl get events -A --sort-by=.lastTimestamp | tail -20 |
| kubectl se cuelga o el nodo está NotReady | systemctl status k3s |
| ¿Quién usa CPU y memoria? | k3s kubectl top pods -A |
| ConfigMap o Secret cambiado no activo | k3s kubectl -n <ns> rollout restart deployment/<name> |
| El disco se llena de imágenes antiguas | k3s crictl rmi --prune |
| ¿Pod bloqueado por SELinux? | ausearch -m AVC -ts recent | grep container_t |
| ¿Cuándo caducan los certificados? | openssl x509 -enddate -noout -in /var/lib/rancher/k3s/server/tls/serving-kube-apiserver.crt |
🔭 Lo que viene
- kube-proxy en modo nftables en cuanto K3s lo haga por defecto – entonces podrá desaparecer la capa de compatibilidad iptables-nft.
- Un segundo nodo cambiaría tres cosas: etcd en lugar de SQLite, los volúmenes HostPath dejarían de funcionar para pods que pueden moverse, y el VXLAN de Flannel transportaría tráfico de verdad.
⚖️ Alternativas – y por qué K3s
| Opción | Punto fuerte | Por qué no aquí |
|---|---|---|
| kubeadm | Kubernetes upstream, nada oculto | etcd, los componentes del plano de control y el CNI hay que operarlos y actualizarlos uno mismo, por separado – mucho para un nodo. |
| k0s | También un solo binario, valores por defecto limpios | Comparable; K3s tiene la política SELinux mantenida y más documentación para AlmaLinux. |
| MicroK8s | Complementos sencillos | Se distribuye como snap – no está en su casa en AlmaLinux. |
| Docker/Podman Compose | Mucho más sencillo | Sin sondas con reinicio, sin NetworkPolicies, sin política de admisión, sin actualizaciones progresivas. |
| K3s | API de Kubernetes completa, huella mínima, política SELinux | Elegido. |
K3s cumple su promesa: Kubernetes de verdad en un solo binario. Las últimas semanas han mostrado que «un binario» también es «un lugar donde se esconden los valores por defecto». Cuatro de los seis problemas de arriba no eran fallos de K3s, sino valores por defecto y suposiciones – SELinux apagado mientras no se pida, un flag de admisión que sustituye en lugar de añadir, un controlador de políticas que ya estaba ahí y nuestra propia regla de auditoría comodín que apuntaba cada latido. Cada uno figura ahora como comentario en el rol, justo al lado de la línea que lo traería de vuelta.


