DevOps, CI/CD y contenedores
Fundamentos de Kubernetes: arquitectura, Pods, Deployments y Services
Intermedio10 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Kubernetes sustituye un montón de scripts de despliegue por un bucle de control: declaras el estado que quieres y el clúster trabaja de forma continua para alcanzarlo. Esta guía explica los componentes que hacen funcionar ese bucle, los objetos básicos que usarás a diario —Pods, ReplicaSets, Deployments, Services, ConfigMaps y Secrets— y termina con un manifiesto completo para una aplicación real, junto con los comandos de kubectl para operarla.
Lo que aprenderás
- Describir los componentes del control plane y de los nodos, y de qué se encarga cada uno
- Explicar el bucle de reconciliación y por qué la configuración de Kubernetes es declarativa
- Crear Deployments y entender las capas de Pod y ReplicaSet que hay debajo
- Exponer workloads con Services y resolverlos mediante el DNS del clúster
- Proporcionar configuración y secretos a los contenedores sin reconstruir las imágenes
- Usar labels, selectors y namespaces para organizar y seleccionar objetos
Una vez que una aplicación está empaquetada como imagen de contenedor, surge una nueva pregunta: ¿qué la ejecuta? En una sola máquina basta con una política de reinicio y un script. En toda una flota necesitas algo que decida en qué máquina corre cada contenedor, lo reinicie cuando muera, lo mueva cuando falle un nodo, sustituya la versión antigua por la nueva sin perder tráfico y ofrezca a los demás servicios una forma estable de encontrarlo. Ese trabajo se llama orquestación, y Kubernetes se ha convertido en la respuesta estándar.
Lo que hace especial a Kubernetes no es su lista de funcionalidades, sino su modelo. No le dices que arranque un contenedor; registras el estado que quieres en la base de datos del clúster y un conjunto de controladores trabaja sin descanso para que la realidad coincida con él. Entender esa única idea hace que el resto del sistema —incluidos sus mensajes de error— sea mucho más fácil de leer. Esta guía avanza desde la arquitectura hasta un manifiesto completo y funcional, y da por hecho que ya sabes qué es una imagen gracias a la guía de fundamentos de Docker.
Cómo se compone un clúster
Un clúster es un control plane más un conjunto de nodos worker.
El control plane ejecuta cuatro componentes. El API server es la puerta de entrada: todo —kubectl, los controladores, los kubelets de cada nodo— habla con él a través de una API REST autenticada, y nada habla con nada más. etcd es el almacén clave-valor que guarda todo el estado del clúster; si vas a hacer copia de seguridad de una sola cosa, que sea etcd. El scheduler vigila los Pods que no tienen nodo asignado y les asigna uno según las solicitudes de recursos, las labels de los nodos, los taints y las reglas de afinidad. El controller manager ejecuta los bucles de reconciliación: el controlador de Deployments, el de nodos, el de jobs y decenas más, cada uno vigilando los objetos de un tipo y actuando sobre las diferencias. En un clúster gestionado nunca ves estos procesos; en uno autogestionado suelen ejecutarse como Pods estáticos en máquinas dedicadas, en número impar para el quórum de etcd.
Cada nodo worker ejecuta tres cosas. El kubelet es el agente que habla con el API server, recibe los Pods asignados a su nodo, le pide al container runtime que los arranque e informa de su estado. El container runtime —containerd o CRI-O en los clústeres actuales, ya que el shim de Docker se eliminó en la 1.24— es el que realmente descarga las imágenes y ejecuta los contenedores. kube-proxy programa las reglas de red del nodo para que el tráfico dirigido a la IP virtual de un Service llegue a uno de los Pods que tiene detrás.
Añade un plugin CNI (Calico, Cilium, Flannel y otros) que dé a cada Pod una IP enrutable, y CoreDNS ejecutándose como workload para resolver los nombres de los Services, y ya tienes un clúster.
El bucle de reconciliación
Todos los controladores ejecutan el mismo bucle: observar el estado actual a través del API server, compararlo con el estado deseado registrado en la spec del objeto y dar un paso para cerrar la diferencia. Nada es un comando puntual.
Por eso los objetos de Kubernetes tienen una spec (lo que quieres) y un status (lo que es cierto ahora mismo), y por eso la respuesta a «¿por qué no arranca mi Pod?» casi siempre se encuentra leyendo el estado y los eventos asociados. Borra un Pod que pertenece a un Deployment y aparecerá un sustituto en pocos segundos; no porque el borrado haya provocado una creación, sino porque un controlador ha detectado que hay un Pod menos de los que pide la spec.
Por eso también la configuración es declarativa. Escribes YAML que describe el estado final y lo aplicas:
kubectl apply -f k8s/ # create or update everything in the directory
kubectl diff -f k8s/ # what would change, before it changes
kubectl delete -f k8s/
kubectl apply registra la configuración que le has pasado, así que aplicar un fichero modificado elimina los campos que hayas quitado y deja en paz los que gestionan otros actores. Los comandos imperativos (kubectl create deployment, kubectl scale, kubectl edit) existen y son útiles para explorar, pero la configuración de producción debe vivir en ficheros bajo control de versiones: el mismo argumento que se aplica a la infraestructura como código en general.
Pods
Un Pod es la unidad más pequeña que planifica Kubernetes. Contiene uno o varios contenedores que comparten un namespace de red —una dirección IP, un espacio de puertos, de modo que se alcanzan entre sí en localhost— y pueden compartir volúmenes. Los contenedores de un Pod siempre se colocan en el mismo nodo y viven y mueren juntos.
La mayoría de los Pods tienen un único contenedor de aplicación. El segundo contenedor, cuando lo hay, es un auxiliar: un reenviador de logs que lee un volumen compartido, un proxy de service mesh que intercepta el tráfico, un agente que renueva un fichero de credenciales.
apiVersion: v1
kind: Pod
metadata:
name: shop-api
labels:
app: shop-api
spec:
containers:
- name: api
image: registry.example.com/acme/shop-api:1.4.2
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
Casi nunca deberías crear un Pod así. Un Pod suelto no se recrea si su nodo muere: nadie es su dueño. Los Pods son ganado, los crean y destruyen los controladores, y sus nombres, direcciones IP y tiempos de vida son efímeros. Lo que deba persistir va en un volumen; lo que deba ser accesible va detrás de un Service.
Deployments y ReplicaSets
Un ReplicaSet mantiene con vida un número determinado de Pods que coinciden con una plantilla. Un Deployment es dueño de ReplicaSets y gestiona las transiciones entre ellos, que es lo que te da rollouts versionados y rollback.
apiVersion: apps/v1
kind: Deployment
metadata:
name: shop-api
labels: { app: shop-api }
spec:
replicas: 3
selector:
matchLabels:
app: shop-api
template:
metadata:
labels:
app: shop-api
version: "1.4.2"
spec:
containers:
- name: api
image: registry.example.com/acme/shop-api:1.4.2
ports:
- containerPort: 8080
El selector le dice al Deployment qué Pods son suyos; el template es el Pod que fabrica en serie. Tienen que concordar —las labels de la plantilla deben satisfacer el selector— y el selector es inmutable tras la creación, así que elige una label estable como app en lugar de algo que cambie en cada release.
Cambia el tag de la imagen, vuelve a aplicar y el Deployment crea un segundo ReplicaSet, lo escala a medida que los Pods nuevos están listos y reduce el antiguo. kubectl rollout status deployment/shop-api sigue el progreso; kubectl rollout undo vuelve al ReplicaSet anterior. Los detalles de cuán gradual es el proceso —surge, indisponibilidad, espera por readiness— se tratan en la guía de patrones de Kubernetes.
Existen otros dos controladores de workloads para casos distintos: DaemonSet ejecuta exactamente un Pod por nodo (agentes de logs, node exporters) y StatefulSet da a los Pods identidades estables y almacenamiento propio (bases de datos, brokers).
Labels, selectors y namespaces
Kubernetes no tiene una jerarquía de objetos. En su lugar, todo se etiqueta con labels —pares clave/valor arbitrarios— y los demás objetos encuentran lo que necesitan mediante selectors. Un Service encuentra sus Pods por selector, un Deployment es dueño de sus Pods por selector, y tú también consultas por selector:
kubectl get pods -l app=shop-api
kubectl get pods -l 'environment in (staging,prod),tier!=batch'
kubectl label pod shop-api-7c9f-2k4x debug=true # add a label on the fly
Vale la pena recordar ese último truco: cambia las labels de un Pod para que el selector del Service deje de coincidir y el Pod saldrá del balanceo de carga, pero seguirá en ejecución. Así puedes conectar un depurador a una instancia que se comporta mal sin que le lleguen usuarios, y el Deployment arrancará un sustituto para mantener el número de réplicas.
Usa annotations para los metadatos que no identifican: una URL de build, un checksum, la configuración de un ingress controller. Los selectors las ignoran.
Los namespaces dividen un clúster: los nombres deben ser únicos dentro de un namespace, y las cuotas, las reglas de RBAC y las network policies se aplican por namespace. Lo habitual es dividir por equipo, o por entorno cuando varios entornos comparten clúster. Los objetos de ámbito de clúster —nodos, PersistentVolumes, StorageClasses— viven fuera de los namespaces.
kubectl create namespace staging
kubectl config set-context --current --namespace=staging # stop typing -n
kubectl get all -n staging
Services y cómo el tráfico encuentra un Pod
Las IP de los Pods cambian constantemente. Un Service ofrece una IP virtual y un nombre DNS estables delante de un conjunto de Pods seleccionados por label, y reparte la carga entre los que están listos.
apiVersion: v1
kind: Service
metadata:
name: shop-api
spec:
type: ClusterIP
selector:
app: shop-api
ports:
- name: http
port: 80 # the Service port
targetPort: 8080 # the container port
Hay cuatro tipos que importan:
| Tipo | Qué te da | Uso típico |
|---|---|---|
ClusterIP | Una IP virtual accesible solo desde dentro del clúster | Llamadas entre servicios. El tipo por defecto. |
NodePort | Lo mismo, más un puerto alto abierto en cada nodo | Clústeres bare metal, demos rápidas |
LoadBalancer | Un NodePort más un balanceador de carga cloud con IP externa | Puntos de entrada públicos en clústeres gestionados |
Headless (clusterIP: None) | Sin IP virtual; el DNS devuelve directamente las IP de los Pods | StatefulSets, balanceo de carga en el cliente |
Dentro del clúster, CoreDNS resuelve shop-api en el mismo namespace y shop-api.staging.svc.cluster.local desde cualquier parte. Así que la cadena de conexión en la configuración de otro servicio es simplemente http://shop-api/, y sigue funcionando a través de cada reinicio, replanificación y rollout.
Exponer muchos servicios HTTP a través de una sola dirección es tarea de un Ingress o de la más reciente Gateway API, no de un LoadBalancer por servicio; se trata en la guía de patrones.
Configuración y secretos
Las imágenes deberían ser idénticas en todos los entornos; solo cambia la configuración. Los ConfigMaps guardan configuración no sensible, los Secrets guardan credenciales, y ambos pueden exponerse como variables de entorno o como ficheros.
apiVersion: v1
kind: ConfigMap
metadata:
name: shop-api-config
data:
SPRING_PROFILES_ACTIVE: "prod"
application.yaml: |
shop:
currency: EUR
catalogue:
page-size: 50
---
apiVersion: v1
kind: Secret
metadata:
name: shop-api-db
type: Opaque
stringData:
username: "shop"
password: "correct-horse-battery-staple"
stringData te permite escribir valores en claro que el API server codifica en base64 al recibirlos, algo mucho más fácil de revisar que blobs ya codificados. Consúmelos en la spec del Pod:
envFrom:
- configMapRef:
name: shop-api-config
env:
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: shop-api-db
key: password
volumeMounts:
- name: config
mountPath: /config
readOnly: true
Montarlo como fichero tiene una propiedad útil: las actualizaciones del ConfigMap se propagan al contenedor en ejecución en uno o dos minutos, de modo que una aplicación que vigila su fichero de configuración puede recoger los cambios sin reiniciarse. Las variables de entorno, en cambio, quedan fijadas al arrancar el contenedor —cambiar un ConfigMap no reinicia nada—, y por eso los equipos añaden una annotation con un checksum a la plantilla del Pod para que una actualización provoque un rollout del Deployment.
No hagas commit en Git de manifiestos de Secrets con valores reales. Usa Sealed Secrets, el External Secrets Operator o el gestor de secretos de tu proveedor cloud, y activa el cifrado en reposo de etcd.
Un manifiesto completo
Todo lo anterior, para un solo servicio:
apiVersion: apps/v1
kind: Deployment
metadata:
name: shop-api
namespace: staging
labels: { app: shop-api }
spec:
replicas: 3
selector:
matchLabels: { app: shop-api }
template:
metadata:
labels: { app: shop-api }
annotations:
config-checksum: "8f4c1a9e"
spec:
serviceAccountName: shop-api
securityContext:
runAsNonRoot: true
runAsUser: 10001
containers:
- name: api
image: registry.example.com/acme/shop-api:1.4.2
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
envFrom:
- configMapRef: { name: shop-api-config }
env:
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef: { name: shop-api-db, key: password }
- name: POD_NAME
valueFrom:
fieldRef: { fieldPath: metadata.name }
volumeMounts:
- { name: config, mountPath: /config, readOnly: true }
- { name: tmp, mountPath: /tmp }
volumes:
- name: config
configMap: { name: shop-api-config }
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: shop-api
namespace: staging
spec:
selector: { app: shop-api }
ports:
- { name: http, port: 80, targetPort: http }
Fíjate en targetPort: http, que hace referencia al puerto con nombre del contenedor: si más adelante renombras el puerto, el Service lo sigue. fieldRef inyecta metadatos del clúster en el contenedor, y así es como las aplicaciones conocen el nombre de su propio Pod para los logs. Y el emptyDir para /tmp es lo que permite más adelante montar el sistema de ficheros del contenedor en modo solo lectura sin romper las librerías que necesitan espacio temporal.
Trabajar con kubectl
Una docena de comandos cubren la operación normal.
kubectl get pods -o wide # with node and IP
kubectl get deploy,svc,cm -n staging
kubectl describe pod shop-api-7c9f-2k4x # spec, status and events
kubectl logs -f deploy/shop-api # follow one Pod of the Deployment
kubectl logs shop-api-7c9f-2k4x --previous # the crashed container's logs
kubectl exec -it shop-api-7c9f-2k4x -- sh
kubectl port-forward svc/shop-api 8080:80 # reach a ClusterIP from your laptop
kubectl rollout status deploy/shop-api
kubectl rollout undo deploy/shop-api
kubectl scale deploy/shop-api --replicas=5
kubectl get events --sort-by=.lastTimestamp # what the cluster has been doing
kubectl top pods # live CPU and memory, needs metrics-server
Cuando algo va mal, el orden que lo resuelve más rápido es: get pods para ver la fase, describe pod para leer los eventos al final de la salida y logs --previous si está en crash loop. La fase suele nombrar el problema directamente: ImagePullBackOff (tag incorrecto o falta la credencial del registro), CrashLoopBackOff (el proceso termina; lee los logs), Pending (nada puede planificarlo; los eventos dicen si se trata de recursos, de un taint o de un volumen sin vincular), CreateContainerConfigError (no existe una clave de ConfigMap o de Secret referenciada).
Ideas clave
- El control plane —API server, etcd, scheduler, controller manager— registra el estado deseado y ejecuta los bucles que reconcilian la realidad con él; el kubelet, el container runtime y kube-proxy hacen el trabajo en cada nodo.
- Todo es declarativo: escribe la spec, aplícala y lee el
statusy los eventos cuando el resultado no sea el que esperabas. - Un Pod es la unidad de planificación y es desechable; los Deployments son dueños de ReplicaSets, que a su vez son dueños de Pods, y eso te da rollouts, rollback y autorreparación.
- Las labels y los selectors son la forma en que los objetos se encuentran entre sí, y los namespaces dividen nombres, cuotas y accesos.
- Los Services ofrecen una IP virtual y un nombre DNS estables delante de los Pods listos:
ClusterIPpara el tráfico interno,LoadBalancerpara los puntos de entrada en la nube, headless para un direccionamiento estable por Pod. - Los ConfigMaps y los Secrets mantienen la configuración fuera de las imágenes; los ficheros montados pueden actualizarse en caliente, las variables de entorno no, y los Secrets necesitan cifrado en reposo y un RBAC estricto.
get,describe,logs --previousyeventsdiagnostican casi todo, y la fase del Pod suele nombrar la avería sin rodeos.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Necesito Kubernetes para mi aplicación?
- No necesariamente. Kubernetes compensa cuando ejecutas muchos servicios, necesitas autorreparación ante fallos de máquinas, despliegas con frecuencia sin tiempo de inactividad o quieres una plataforma uniforme que compartan varios equipos. Para uno o dos servicios en un único servidor añade muchísima maquinaria y una curva de aprendizaje realmente empinada. Un clúster gestionado reduce el coste operativo, pero no el conceptual.
- ¿Por qué un Pod y no simplemente un contenedor?
- Un Pod es un grupo de contenedores que comparten un namespace de red, una dirección IP y, opcionalmente, volúmenes, y que siempre se planifican en el mismo nodo. Esa agrupación permite que un contenedor auxiliar —un recolector de logs, un proxy, un agente que renueva credenciales— se ejecute junto a la aplicación y la alcance en localhost. La mayoría de los Pods contienen exactamente un contenedor de aplicación y, a veces, un auxiliar.
- ¿Qué diferencia hay entre un Deployment y un ReplicaSet?
- Un ReplicaSet mantiene en ejecución un número fijo de Pods idénticos. Un Deployment gestiona ReplicaSets a lo largo del tiempo: cuando cambias la plantilla del Pod, crea un ReplicaSet nuevo, lo escala hacia arriba mientras reduce el antiguo y conserva los anteriores para hacer rollback. Casi siempre creas Deployments y dejas que sean dueños de los ReplicaSets, y por eso rara vez escribes un ReplicaSet a mano.
- ¿Los Secrets de Kubernetes son realmente secretos?
- Por defecto están codificados en base64, no cifrados, y cualquiera que pueda leer los Secrets de un namespace puede leer sus valores. Son mejores que una variable de entorno en un manifiesto porque el acceso lo controla RBAC y no quedan incrustados en las imágenes, pero una configuración seria añade cifrado en reposo en etcd, un RBAC estricto y, a menudo, un gestor de secretos externo sincronizado con el clúster.