Cerrábamos la lección anterior señalando un hilo suelto que ha recorrido todo el módulo. El selector de un ReplicaSet que adopta pods, el selector de un Service que encuentra sus destinos, la etiqueta entorno con la que las políticas de red seleccionarán namespaces, la anotación kubernetes.io/change-cause que documenta cada revisión, el pod-template-hash que el Deployment añade por su cuenta, y todas las consultas con -l que llevamos usando desde la primera lección: todo eso son etiquetas y anotaciones. Las hemos ido usando por necesidad, sin sistematizar. Esta lección pone orden, y no es un asunto menor: en un clúster con seis componentes por tres entornos, el esquema de etiquetado es lo que separa una plataforma navegable de un caos de objetos anónimos. Verás la diferencia exacta entre etiqueta y anotación, la sintaxis y las restricciones de claves y valores, las etiquetas recomendadas por Kubernetes y cómo se concretan en el esquema oficial de Rutas Norte, los selectores de igualdad y de conjunto en todas sus formas, quién los consume, por qué el selector de un Deployment es inmutable, el uso real de las anotaciones y las consultas del día a día que te harán productivo.
Contenido
- Etiquetas frente a anotaciones
- Sintaxis y restricciones de claves y valores
- Las etiquetas recomendadas por Kubernetes
- El esquema de etiquetado de Rutas Norte
- Selectores de igualdad
- Selectores de conjunto
- Selectores en manifiestos:
matchLabelsymatchExpressions - Quién consume selectores
- La regla de oro: el selector de un Deployment es inmutable
- Anotaciones en la práctica
kubectl labelykubectl annotate- Consultas útiles del día a día
- Etiquetas frente a anotaciones
Ambas viven en metadata y ambas son pares clave-valor. Ahí termina el parecido.
metadata:
name: api-reservas
labels: # para IDENTIFICAR y SELECCIONAR
app.kubernetes.io/name: api-reservas
entorno: pro
annotations: # para ADJUNTAR INFORMACIÓN
kubernetes.io/change-cause: "v2.5.0 - cache de disponibilidad (RN-482)"
rutasnorte.example/responsable: [email protected]| Aspecto | Etiquetas (labels) |
Anotaciones (annotations) |
|---|---|---|
| Propósito | Identificar y seleccionar objetos | Adjuntar metadatos no identificativos |
| ¿Se pueden consultar? | Sí: kubectl get -l, selectores |
No: no existe --annotation-selector |
| ¿Están indexadas? | Sí, etcd las indexa | No |
| Tamaño del valor | Máximo 63 caracteres | Hasta 256 KiB en total |
| Caracteres del valor | Alfanuméricos, -, _, . |
Cualquiera: JSON, texto largo, URLs, saltos de línea |
| Quién las usa | Controladores, Services, el planificador, tú | Herramientas, kubectl, controladores de Ingress, humanos |
| Coste de tener muchas | Impacto en el rendimiento de las consultas | Prácticamente nulo |
| Ejemplo | app: api-reservas |
kubernetes.io/change-cause: "..." |
La regla de decisión, que resuelve el 100 % de las dudas:
¿Vas a querer buscar objetos por este dato, o va a servirle a un selector? Es una etiqueta. ¿Es información que se lee pero no se busca? Es una anotación.
Ejemplos aplicados a Rutas Norte:
| Dato | Tipo | Por qué |
|---|---|---|
Componente (api-reservas) |
Etiqueta | Los Services y ReplicaSets seleccionan por él |
Entorno (pro) |
Etiqueta | Se consulta constantemente y las NetworkPolicies lo usarán |
Versión (2.5.0) |
Etiqueta | Útil para consultar qué versión corre dónde |
| Correo del equipo responsable | Anotación | Se lee en un incidente, no se busca por él |
| Motivo del último despliegue | Anotación | Texto libre y largo |
| URL del panel de monitorización | Anotación | Contiene : y /, prohibidos en valores de etiqueta |
| Hash del commit desplegado | Anotación (o etiqueta corta) | Suele ser informativo |
| Contraseña de la base de datos | Ninguna de las dos | Nunca. Va en un Secret (03-02) |
- Sintaxis y restricciones de claves y valores
Kubernetes es estricto con el formato, y conocer las reglas evita rechazos incomprensibles.
Estructura de una clave
Una clave tiene un nombre y, opcionalmente, un prefijo separado por /:
Reglas del nombre (la parte obligatoria):
- Máximo 63 caracteres.
- Debe empezar y terminar por carácter alfanumérico.
- Puede contener
-,_,.y alfanuméricos en medio.
Reglas del prefijo (opcional):
- Debe ser un subdominio DNS válido:
rutasnorte.example,app.kubernetes.io. - Máximo 253 caracteres.
- Se separa del nombre con
/. - Reservados: los prefijos
kubernetes.io/yk8s.io/están reservados para el propio Kubernetes y sus componentes principales. No los uses para tus etiquetas.
Reglas del valor de una etiqueta
- Máximo 63 caracteres (o vacío, que es válido).
- Si no está vacío, debe empezar y terminar por alfanumérico.
- Solo alfanuméricos,
-,_y.en medio. - Prohibidos: espacios,
/,:,@, acentos,ñ, emojis.
Ejemplos válidos e inválidos:
| Etiqueta | ¿Válida? | Motivo |
|---|---|---|
app: api-reservas |
Sí | Formato canónico |
app.kubernetes.io/version: 2.5.0 |
Sí | Prefijo estándar, valor con puntos |
rutasnorte.example/linea: BIL-SAN |
Sí | Prefijo propio de la empresa |
entorno: pro |
Sí | Sin prefijo, perfectamente válido |
equipo: backend_reservas |
Sí | El guion bajo está permitido |
responsable: [email protected] |
No | La @ está prohibida en valores |
panel: https://grafana.example/d/abc |
No | : y / prohibidos. Usa una anotación |
descripcion: API de reservas |
No | Los espacios están prohibidos |
región: norte |
No | Las tildes están prohibidas |
kubernetes.io/mi-etiqueta: x |
No (desaconsejado) | Prefijo reservado |
Comprueba el rechazo tú mismo:
kubectl label pod --all [email protected] -n rutas-norte-deverror: '[email protected]' is not a valid label value:
a valid label must be an empty string or consist of alphanumeric characters,
'-', '_' or '.', and must start and end with an alphanumeric characterLa versión correcta usa una anotación:
kubectl annotate deployment api-reservas \
rutasnorte.example/[email protected] -n rutas-norte-devReglas de las anotaciones
Las claves siguen las mismas reglas que las de las etiquetas. Los valores, en cambio, son casi libres: cualquier cadena, incluidos JSON, saltos de línea y caracteres especiales, con un límite conjunto de 256 KiB para todas las anotaciones de un objeto.
Ese límite generoso es el que permite que kubectl apply guarde el manifiesto completo en kubectl.kubernetes.io/last-applied-configuration, como vimos en Objetos y Manifiestos.
- Las etiquetas recomendadas por Kubernetes
Kubernetes define un conjunto de etiquetas comunes bajo el prefijo app.kubernetes.io/. No son obligatorias ni las impone nada, pero son el vocabulario compartido que entienden Helm, Kustomize, Argo CD, Prometheus y prácticamente cualquier herramienta del ecosistema.
| Etiqueta | Significado | Ejemplo en Rutas Norte |
|---|---|---|
app.kubernetes.io/name |
Nombre de la aplicación | api-reservas |
app.kubernetes.io/instance |
Identifica una instalación concreta de esa aplicación | api-reservas-pro |
app.kubernetes.io/version |
Versión actual | 2.5.0 |
app.kubernetes.io/component |
Papel que juega dentro de la arquitectura | api, frontend, cache, base-datos, worker |
app.kubernetes.io/part-of |
Aplicación de nivel superior a la que pertenece | rutas-norte |
app.kubernetes.io/managed-by |
Herramienta que gestiona el objeto | kubectl, helm, argocd |
La diferencia entre name e instance es la que más cuesta:
namees qué es:postgres. La misma en todas las instalaciones.instancees cuál es:postgres-reservas-pro. Distingue dos instalaciones de lo mismo.
Si Rutas Norte tuviera dos bases de datos PostgreSQL —una de reservas y otra de facturación—, ambas llevarían name: postgres pero instance: postgres-reservas y instance: postgres-facturacion.
Un ejemplo completo con las seis:
metadata:
name: api-reservas
labels:
app.kubernetes.io/name: api-reservas
app.kubernetes.io/instance: api-reservas-pro
app.kubernetes.io/version: "2.5.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: rutas-norte
app.kubernetes.io/managed-by: kubectlNota práctica: version: "2.5.0" va entre comillas. Sin ellas, YAML podría interpretar valores como 1.0 como número y provocar un error de tipo, ya que los valores de etiqueta deben ser cadenas.
Un aviso importante que enlaza con el apartado 9: app.kubernetes.io/version no debe ir en el selector. Si estuviera, al cambiar de versión el selector dejaría de casar con sus propios pods, y como el selector es inmutable, tendrías que recrear el Deployment en cada despliegue. La versión es informativa; la identidad la dan name e instance.
- El esquema de etiquetado de Rutas Norte
Ha llegado el momento de fijar el esquema definitivo del proyecto. Hasta ahora hemos usado tres etiquetas mínimas (app, app.kubernetes.io/part-of y entorno); las consolidamos y las ampliamos.
Las etiquetas del proyecto
| Etiqueta | Obligatoria | Valores posibles | Para qué |
|---|---|---|---|
app |
Sí | tienda-web, api-reservas, postgres-reservas, redis-cache, worker-notificaciones, informes-ocupacion |
Identificador corto del componente. Va en los selectores |
entorno |
Sí | dev, pre, pro |
Entorno. Va en los selectores y en las NetworkPolicies |
app.kubernetes.io/name |
Sí | Igual que app |
Compatibilidad con el ecosistema |
app.kubernetes.io/part-of |
Sí | rutas-norte |
Agrupa toda la plataforma. Nunca en un selector |
app.kubernetes.io/component |
Sí | frontend, api, base-datos, cache, worker, informes |
Papel arquitectónico |
app.kubernetes.io/version |
Sí | 1.8.0, 2.5.0... |
Versión desplegada. Nunca en un selector |
app.kubernetes.io/managed-by |
Recomendada | kubectl, argocd |
Quién gestiona el objeto |
rutasnorte.example/criticidad |
Recomendada | critica, alta, media, baja |
Prioriza la respuesta ante incidentes |
La regla de oro del esquema, que se deriva de todo lo aprendido en el módulo:
Solo
appyentornoentran en los selectores. Son las dos únicas etiquetas que identifican al componente de forma inequívoca y que no cambian nunca.
Todo lo demás —versión, componente, criticidad, gestor— es informativo y puede cambiar sin recrear nada.
La tabla maestra: seis componentes
| Componente | app |
component |
version |
criticidad |
¿Lleva Service? |
|---|---|---|---|---|---|
tienda-web |
tienda-web |
frontend |
1.8.0 |
alta |
Sí |
api-reservas |
api-reservas |
api |
2.4.0 |
critica |
Sí |
postgres-reservas |
postgres-reservas |
base-datos |
16.2 |
critica |
Sí (headless en el módulo 6) |
redis-cache |
redis-cache |
cache |
7.2 |
media |
Sí |
worker-notificaciones |
worker-notificaciones |
worker |
1.2.0 |
alta |
No |
informes-ocupacion |
informes-ocupacion |
informes |
1.0.3 |
baja |
No |
El manifiesto resultante
Así queda api-reservas con el esquema completo aplicado:
# k8s/base/api-reservas-deployment.yaml (metadatos completos)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-reservas
namespace: rutas-norte-dev
labels:
app: api-reservas
entorno: dev
app.kubernetes.io/name: api-reservas
app.kubernetes.io/instance: api-reservas-dev
app.kubernetes.io/version: "2.4.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: rutas-norte
app.kubernetes.io/managed-by: kubectl
rutasnorte.example/criticidad: critica
annotations:
kubernetes.io/change-cause: "v2.4.0 - despliegue inicial"
rutasnorte.example/responsable: [email protected]
rutasnorte.example/panel: "https://grafana.rutasnorte.example/d/api-reservas"
rutasnorte.example/runbook: "https://wiki.rutasnorte.example/runbooks/api-reservas"
spec:
replicas: 2
selector:
matchLabels:
app: api-reservas # SOLO estas dos.
entorno: dev # Inmutables.
template:
metadata:
labels:
app: api-reservas
entorno: dev
app.kubernetes.io/name: api-reservas
app.kubernetes.io/instance: api-reservas-dev
app.kubernetes.io/version: "2.4.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: rutas-norte
app.kubernetes.io/managed-by: kubectl
rutasnorte.example/criticidad: critica
spec:
containers:
- name: api
image: node:20-alpine
# ...Repara en la asimetría, que es exactamente la que aprendimos con los ReplicaSets: el template lleva nueve etiquetas y el selector solo exige dos. Está permitido y es lo correcto: el selector debe ser un subconjunto mínimo y estable; las demás etiquetas viajan con los pods para poder consultarlos.
Aplícalo y observa la potencia inmediata:
kubectl apply -f k8s/base/api-reservas-deployment.yaml
kubectl get pods -l app.kubernetes.io/component=api --show-labelsNAME READY STATUS LABELS
api-reservas-9c4d7e831-j2wsk 1/1 Running app=api-reservas,app.kubernetes.io/component=api,app.kubernetes.io/instance=api-reservas-dev,app.kubernetes.io/managed-by=kubectl,app.kubernetes.io/name=api-reservas,app.kubernetes.io/part-of=rutas-norte,app.kubernetes.io/version=2.4.0,entorno=dev,pod-template-hash=9c4d7e831,rutasnorte.example/criticidad=critica
- Selectores de igualdad
Un selector es una consulta sobre etiquetas. El tipo más simple compara valores.
| Operador | Significado | Ejemplo |
|---|---|---|
= |
Igual | app=api-reservas |
== |
Igual (sinónimo exacto de =) |
app==api-reservas |
!= |
Distinto | entorno!=pro |
Se usan con -l (o --selector), y varias condiciones separadas por comas se combinan con Y lógico:
# Los pods de api-reservas
kubectl get pods -l app=api-reservas
# Los pods de api-reservas EN desarrollo (Y logico)
kubectl get pods -l app=api-reservas,entorno=dev
# Todos los pods de la plataforma que NO estan en produccion
kubectl get pods -A -l app.kubernetes.io/part-of=rutas-norte,entorno!=proNAMESPACE NAME READY STATUS AGE
rutas-norte-dev api-reservas-9c4d7e831-j2wsk 1/1 Running 5m
rutas-norte-dev redis-cache-6c8f9d745-w2mtb 1/1 Running 1h
rutas-norte-dev tienda-web-6f9c4b8d7-42kxr 1/1 Running 1h
rutas-norte-pre api-reservas-7f1d3e942-c9plk 1/1 Running 45m
rutas-norte-pre tienda-web-6f9c4b8d7-d3jbx 1/1 Running 45mUn detalle sobre != que conviene conocer: también selecciona los objetos que no tienen esa etiqueta en absoluto. entorno!=pro devuelve tanto los de dev y pre como los que carecen de la etiqueta entorno.
No existe el O lógico en los selectores de igualdad. Para eso están los de conjunto.
- Selectores de conjunto
Más expresivos. Operan sobre conjuntos de valores y sobre la existencia de la clave.
| Operador | Significado | Ejemplo |
|---|---|---|
in |
El valor está en la lista | entorno in (pre,pro) |
notin |
El valor no está en la lista | entorno notin (dev) |
<clave> |
La etiqueta existe, con cualquier valor | rutasnorte.example/criticidad |
!<clave> |
La etiqueta no existe | !rutasnorte.example/criticidad |
Ejemplos aplicados:
# Los componentes de los entornos "serios" (O logico)
kubectl get pods -A -l 'entorno in (pre,pro)'
# Todo lo que NO es de desarrollo
kubectl get deploy -A -l 'entorno notin (dev)'
# Objetos a los que se les ha olvidado la criticidad: auditoria del esquema
kubectl get deploy -A -l 'app.kubernetes.io/part-of=rutas-norte,!rutasnorte.example/criticidad'
# Componentes criticos o de alta criticidad, en cualquier entorno
kubectl get pods -A -l 'rutasnorte.example/criticidad in (critica,alta)'NAMESPACE NAME READY STATUS AGE
rutas-norte-dev api-reservas-9c4d7e831-j2wsk 1/1 Running 12m
rutas-norte-dev api-reservas-9c4d7e831-p7xtr 1/1 Running 12m
rutas-norte-dev tienda-web-6f9c4b8d7-42kxr 1/1 Running 1h
rutas-norte-dev worker-notificaciones-8c7b5d94f-k2xrt 1/1 Running 1hNota sobre las comillas: los selectores de conjunto llevan paréntesis y espacios, que la shell interpretaría. Enciérralos siempre entre comillas simples.
Y se pueden combinar con los de igualdad, separando por comas:
kubectl get pods -A -l 'app.kubernetes.io/part-of=rutas-norte,entorno in (pre,pro),app.kubernetes.io/component!=cache'
- Selectores en manifiestos:
matchLabels y matchExpressions
matchLabels y matchExpressionsDentro de un YAML, los selectores tienen dos formas equivalentes a las dos familias que acabas de ver.
matchLabels: igualdad
Es un mapa: todas las claves deben coincidir (Y lógico). Es lo que hemos usado en todo el módulo.
matchExpressions: conjuntos
selector:
matchExpressions:
- key: app
operator: In
values: [api-reservas]
- key: entorno
operator: In
values: [pre, pro]
- key: rutasnorte.example/obsoleto
operator: DoesNotExistoperator |
Equivale a | ¿Requiere values? |
|---|---|---|
In |
in (…) |
Sí |
NotIn |
notin (…) |
Sí |
Exists |
<clave> |
No |
DoesNotExist |
!<clave> |
No |
Combinarlos
Se pueden usar los dos a la vez; todas las condiciones se combinan con Y lógico:
selector:
matchLabels:
app: api-reservas
matchExpressions:
- key: entorno
operator: In
values: [pre, pro]Traducción: "pods con app=api-reservas y con entorno igual a pre o a pro".
Cuál usar
| Caso | Recomendación |
|---|---|
| Selector de un Deployment, ReplicaSet o StatefulSet | matchLabels: simple, legible, y el selector debe ser mínimo |
| Selector de un Service | Obligatoriamente mapa plano: no admite ninguna de las dos formas, solo selector: directo |
| NetworkPolicy que abarca varios entornos o excluye algo | matchExpressions |
| Afinidad de pods | matchExpressions |
Atención a la excepción del Service, que ya vimos en su lección y conviene recordar aquí porque es una asimetría real de la API:
# Deployment: CON matchLabels
kind: Deployment
spec:
selector:
matchLabels:
app: api-reservas
entorno: devEl motivo es histórico: los Services son del grupo core v1, anterior al selector enriquecido, y solo soportan igualdad.
- Quién consume selectores
Los selectores no son solo para consultas manuales. Son el mecanismo de acoplamiento de todo Kubernetes: la forma en que unos objetos encuentran a otros sin conocer sus nombres.
| Quién | Qué selecciona | Lección |
|---|---|---|
| ReplicaSet | Los pods que debe mantener y contar | 02-02 |
| Deployment | Los pods de sus ReplicaSets (con pod-template-hash añadido) |
02-03 |
| Service | Los pods a los que enruta el tráfico | 02-05 |
| StatefulSet y DaemonSet | Sus pods | 06-01, 06-02 |
| Job y CronJob | Los pods de sus ejecuciones | 06-03 |
| NetworkPolicy | Qué pods protege y de quién acepta tráfico | 04-06 |
| Afinidad y antiafinidad de pods | Junto a qué pods colocarse o no | 06-05 |
| PodDisruptionBudget | Qué pods protege de los desalojos voluntarios | 09-05 |
| HorizontalPodAutoscaler | Indirectamente, vía el objeto que escala | 09-01 |
| Prometheus (ServiceMonitor) | De qué pods recoger métricas | 07-03 |
kubectl -l |
Lo que tú quieras | 01-05 |
flowchart TB
POD["Pods con etiquetas<br/>app=api-reservas<br/>entorno=dev"]
RS["ReplicaSet<br/>los mantiene"]
SVC["Service<br/>les enruta trafico"]
NP["NetworkPolicy<br/>los protege"]
PDB["PodDisruptionBudget<br/>evita desalojarlos todos"]
MON["ServiceMonitor<br/>recoge sus metricas"]
RS -->|selector| POD
SVC -->|selector| POD
NP -->|podSelector| POD
PDB -->|selector| POD
MON -->|selector| POD
La lección de fondo: las etiquetas de un pod no son decoración, son su interfaz con el resto del sistema. Cambiar una etiqueta de un pod puede sacarlo de un Service, dejarlo fuera de una política de red o hacerlo invisible para su ReplicaSet. Lo veremos en el ejercicio 3.
- La regla de oro: el selector de un Deployment es inmutable
Compruébalo:
kubectl patch deployment api-reservas --type=merge \
-p '{"spec":{"selector":{"matchLabels":{"app":"api-reservas","entorno":"dev","nuevo":"si"}}}}'Lo mismo ocurre con ReplicaSets, StatefulSets y DaemonSets. Los Services son la excepción: su selector sí se puede modificar, y eso los convierte en la herramienta de las estrategias blue-green del módulo 11.
Por qué es inmutable
La razón es la seguridad de los datos... de los pods. Imagina que se permitiera cambiarlo:
- El Deployment tiene
selector: app=api-reservasy gobierna 4 pods con esa etiqueta. - Cambias el selector a
app=api-reservas-v2. - Instantáneamente, los 4 pods dejan de casar. Se convierten en huérfanos, exactamente como los del experimento
--cascade=orphande la lección de ReplicaSets. - El Deployment cuenta 0 pods propios y crea 4 nuevos.
- Resultado: 8 pods corriendo, 4 de ellos sin controlador, invisibles para el Deployment, consumiendo recursos y sin nadie que los actualice ni los recupere si caen.
Peor aún: si el Service seguía apuntando a app=api-reservas, esos 4 huérfanos seguirían recibiendo tráfico de producción sin que ningún controlador los vigile.
Kubernetes prefiere fallar al aplicar antes que dejarte llegar a ese estado.
Cómo cambiar un selector cuando de verdad hace falta
Hay dos caminos, y el primero es el bueno:
Camino A: sustituir el controlador sin cortar el servicio.
# 1. Borrar el Deployment dejando vivos los pods
kubectl delete deployment api-reservas --cascade=orphan
# 2. Aplicar el manifiesto nuevo, con el selector corregido
kubectl apply -f k8s/base/api-reservas-deployment.yaml
# 3. Comprobar. Si el selector nuevo casa con los pods viejos, los adopta;
# si no, crea los suyos y luego hay que limpiar los huerfanos
kubectl get pods -l app.kubernetes.io/part-of=rutas-norte -n rutas-norte-devCamino B: nombre nuevo y transición por el Service. Se crea un Deployment con otro nombre y el selector correcto, se espera a que esté listo y se cambia el selector del Service para que apunte a los pods nuevos. Es, en esencia, un despliegue blue-green (11-04).
La consecuencia práctica
Piensa bien el selector antes del primer
apply. Es la decisión menos reversible de un manifiesto.
Y de ahí la regla del proyecto que ya conoces: en el selector, solo app y entorno. Nunca la versión, nunca el gestor, nunca la criticidad, nunca part-of. Todo lo que pueda cambiar con el tiempo se queda fuera.
- Anotaciones en la práctica
Las anotaciones parecen menos importantes que las etiquetas hasta que descubres cuántas cosas funcionan gracias a ellas.
Anotaciones que ya has usado
{
"deployment.kubernetes.io/revision": "3",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",...}",
"kubernetes.io/change-cause": "v2.4.0 - despliegue inicial",
"rutasnorte.example/responsable": "[email protected]"
}| Anotación | Quién la pone | Para qué |
|---|---|---|
kubernetes.io/change-cause |
Tú o tu CI/CD | Aparece en kubectl rollout history (02-04) |
deployment.kubernetes.io/revision |
El controlador de Deployment | Número de revisión de cada ReplicaSet |
kubectl.kubernetes.io/last-applied-configuration |
kubectl apply |
Base de la fusión de tres vías (01-06) |
kubectl.kubernetes.io/restartedAt |
kubectl rollout restart |
Fuerza un cambio en el template para recrear los pods |
Anotaciones que configuran comportamiento
Este es el uso más potente: muchos controladores se configuran mediante anotaciones, no mediante campos del spec. El caso paradigmático es el controlador de Ingress:
# Adelanto del modulo 4: NO lo apliques todavia
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rutas-norte
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "8m"
nginx.ingress.kubernetes.io/rate-limit-rps: "50"
cert-manager.io/cluster-issuer: "letsencrypt-produccion"Ninguna de esas líneas está en el esquema del Ingress: son instrucciones que el controlador de NGINX y cert-manager leen y aplican. El motivo de este diseño es que cada implementación de Ingress tiene capacidades distintas, y la API estándar no puede cubrirlas todas. Se estudian en Controladores de Ingress y TLS y Certificados.
Anotaciones para humanos
Y el uso más simple y más rentable: información que salva una guardia nocturna.
metadata:
annotations:
rutasnorte.example/responsable: "[email protected]"
rutasnorte.example/guardia: "+34 600 000 000"
rutasnorte.example/panel: "https://grafana.rutasnorte.example/d/api-reservas"
rutasnorte.example/runbook: "https://wiki.rutasnorte.example/runbooks/api-reservas"
rutasnorte.example/repositorio: "https://git.rutasnorte.example/rutas-norte/api-reservas"
rutasnorte.example/commit: "a3f9c1b4e7d2"
rutasnorte.example/descripcion: |
API REST de reservas. Consulta disponibilidad en redis-cache y
persiste en postgres-reservas. Dependencias criticas: ambos.
Ante 5xx sostenidos, revisar primero la conexion a postgres-reservas.Fíjate en dos cosas imposibles con etiquetas: valores con @, : y /, y un valor multilínea. Esa es la razón de que existan las anotaciones.
A las tres de la madrugada, esto vale su peso en oro:
kubectl get deploy api-reservas -o jsonpath='{.metadata.annotations.rutasnorte\.example/runbook}{"\n"}'La regla innegociable: nunca secretos en anotaciones
Jamás guardes contraseñas, tokens, claves de API ni certificados privados en anotaciones.
Los motivos:
- Se leen en claro con
kubectl get -o yamlokubectl describe, que suelen ser permisos ampliamente concedidos. - Aparecen en los logs: cualquier volcado de un objeto las incluye.
- Se copian a
last-applied-configuration, quedando duplicadas. - Se van a los backups y a las herramientas de GitOps, propagándose por todas partes.
- No se cifran en reposo dentro de etcd salvo que el clúster lo tenga configurado, y aun así el mecanismo está pensado para Secrets.
Lo mismo vale para las etiquetas, con el agravante de que además son indexadas y consultables.
Para eso están los Secrets, en la lección 03-02, y su gestión segura en el módulo 8. Ese POSTGRES_PASSWORD en claro que dejamos en el Deployment provisional de postgres-reservas es exactamente la deuda que vamos a pagar en el módulo siguiente.
kubectl label y kubectl annotate
kubectl label y kubectl annotateAmbos comandos comparten sintaxis.
Añadir
kubectl label deployment api-reservas rutasnorte.example/criticidad=critica
kubectl annotate deployment api-reservas rutasnorte.example/guardia="+34 600 000 000"Sobrescribir: --overwrite
Si la clave ya existe, el comando falla salvo que lo indiques:
Es una protección deliberada contra pisar sin querer una etiqueta que algún selector esté usando.
Borrar: el sufijo -
kubectl label deployment api-reservas rutasnorte.example/criticidad-
kubectl annotate deployment api-reservas rutasnorte.example/guardia-Ese guion final es fácil de pasar por alto al leer un script. Recuérdalo: clave con guion al final significa borrar.
Operaciones masivas
# A varios objetos por nombre
kubectl label pods pod-a pod-b prueba=si
# A TODOS los objetos de un tipo en el namespace
kubectl label pods --all revisado=2026-08-05
# Filtrando por otro selector
kubectl label deploy -l app.kubernetes.io/part-of=rutas-norte \
app.kubernetes.io/managed-by=kubectl --overwrite
# En todos los namespaces
kubectl label ns -l app.kubernetes.io/part-of=rutas-norte auditado=siAdvertencia de seguridad: --all combinado con --overwrite sobre una etiqueta que aparece en algún selector puede sacar pods de su Service o de su ReplicaSet en el acto. Prueba siempre primero con --dry-run=server:
Un aviso sobre las etiquetas puestas a mano
Cambiar una etiqueta con kubectl label sobre un pod gestionado por un Deployment no sobrevive: en el próximo despliegue, los pods se recrean a partir del template y el cambio desaparece. Para que perdure, edita spec.template.metadata.labels en el manifiesto.
Y hay un efecto secundario mucho más grave, que exploraremos en el ejercicio 3: si cambias una etiqueta que está en el selector del ReplicaSet, el pod queda huérfano al instante y el ReplicaSet crea otro para sustituirlo.
- Consultas útiles del día a día
Aquí es donde el esquema de etiquetado deja de ser burocracia y se convierte en productividad. Las consultas más rentables combinan -l con los formatos de salida que vimos en La CLI de Kubernetes.
Inventario de la plataforma
kubectl get pods -A -l app.kubernetes.io/part-of=rutas-norte \
-o custom-columns='ENTORNO:.metadata.labels.entorno,COMPONENTE:.metadata.labels.app,VERSION:.metadata\.labels.app\.kubernetes\.io/version,POD:.metadata.name,ESTADO:.status.phase'ENTORNO COMPONENTE VERSION POD ESTADO
dev api-reservas 2.4.0 api-reservas-9c4d7e831-j2wsk Running
dev api-reservas 2.4.0 api-reservas-9c4d7e831-p7xtr Running
dev redis-cache 7.2 redis-cache-6c8f9d745-w2mtb Running
dev tienda-web 1.8.0 tienda-web-6f9c4b8d7-42kxr Running
pre api-reservas 2.4.0 api-reservas-7f1d3e942-c9plk Running
pro tienda-web 1.8.0 tienda-web-6f9c4b8d7-x4nbc RunningTruco de sintaxis importante: en custom-columns, los puntos que forman parte de la clave de la etiqueta hay que escaparlos con \.. Si no, kubectl los interpreta como separadores de ruta JSONPath y no encuentra nada.
Qué versión corre en cada entorno
La consulta que más veces te pedirán:
kubectl get deploy -A -l app=api-reservas \
-o custom-columns='NS:.metadata.namespace,DEPLOY:.metadata.name,IMAGEN:.spec.template.spec.containers[0].image,REPLICAS:.spec.replicas'NS DEPLOY IMAGEN REPLICAS
rutas-norte-dev api-reservas node:20-alpine 2
rutas-norte-pre api-reservas node:20-alpine 2Auditoría del esquema de etiquetado
Encontrar lo que incumple la convención del proyecto:
# Deployments sin la etiqueta part-of: se han escapado del esquema
kubectl get deploy -A -l '!app.kubernetes.io/part-of'
# Objetos de la plataforma sin criticidad asignada
kubectl get deploy,sts,ds -A -l 'app.kubernetes.io/part-of=rutas-norte,!rutasnorte.example/criticidad'
# Pods sin la etiqueta de entorno: no los alcanzaria ninguna NetworkPolicy
kubectl get pods -A -l 'app.kubernetes.io/part-of=rutas-norte,!entorno'Consultas de incidente
# Todo lo critico que NO esta corriendo
kubectl get pods -A -l 'rutasnorte.example/criticidad=critica' \
--field-selector status.phase!=Running
# Logs agregados de todas las replicas de un componente, con prefijo de pod
kubectl logs -l app=api-reservas -n rutas-norte-pro --tail=50 --prefix
# Ordenar por reinicios: quien esta sufriendo
kubectl get pods -A -l app.kubernetes.io/part-of=rutas-norte \
--sort-by=.status.containerStatuses[0].restartCount
# Los eventos recientes del namespace, lo primero que hay que mirar
kubectl get events -n rutas-norte-pro --sort-by=.lastTimestamp | tail -20Operaciones en bloque
# Reiniciar todos los componentes sin estado de un entorno
kubectl rollout restart deploy -l 'app.kubernetes.io/part-of=rutas-norte' -n rutas-norte-dev
# Borrar TODO un entorno de pruebas (con cuidado)
kubectl delete all -l 'app.kubernetes.io/part-of=rutas-norte' -n rutas-norte-dev --dry-run=clientEse --dry-run=client antes de un delete masivo es un hábito que conviene adquirir.
Combinaciones con -o wide y --show-labels
# Ver donde esta cada cosa
kubectl get pods -A -l entorno=pro -o wide
# Ver todas las etiquetas de un objeto
kubectl get deploy api-reservas --show-labels
# Agrupar visualmente por una etiqueta
kubectl get pods -A -L entorno,app.kubernetes.io/componentEl flag -L (ele mayúscula) es un pequeño tesoro: añade columnas con el valor de las etiquetas indicadas, sin filtrar nada.
NAMESPACE NAME READY STATUS AGE ENTORNO COMPONENT
rutas-norte-dev api-reservas-9c4d7e831-j2wsk 1/1 Running 30m dev api
rutas-norte-dev redis-cache-6c8f9d745-w2mtb 1/1 Running 2h dev cache
rutas-norte-dev tienda-web-6f9c4b8d7-42kxr 1/1 Running 2h dev frontend
rutas-norte-pre api-reservas-7f1d3e942-c9plk 1/1 Running 1h pre apiErrores Comunes y Consejos
- Meter la versión en el selector. Al desplegar una versión nueva el selector deja de casar, y como es inmutable, te obliga a recrear el Deployment. El selector solo lleva
appyentorno. - Selectores demasiado amplios. Un
app.kubernetes.io/part-of: rutas-nortecomo selector adopta y puede borrar pods de otros componentes, como vimos en ReplicaSets. - Usar caracteres prohibidos en valores de etiqueta. Nada de
@,:,/, espacios ni tildes. Si el dato los necesita, es una anotación. - Usar el prefijo
kubernetes.io/para etiquetas propias. Está reservado. Usa el dominio de tu organización:rutasnorte.example/. - Guardar secretos en anotaciones o etiquetas. Se leen en claro con
describe, viajan a los backups y acaban en Git. Van en Secrets. - Poner las etiquetas solo en
metadatay no enspec.template.metadata. Los pods heredan las del template, no las del Deployment. Es el error que rompe los Services. - Olvidar
--overwritey pensar que el comando ha funcionado.kubectl labelfalla si la clave existe. Lee siempre la salida. - Cambiar con
kubectl labeluna etiqueta que está en un selector. El pod queda huérfano y el controlador crea otro. Puede duplicar capacidad sin que te enteres. - Esperar que un cambio con
kubectl labelsobre un pod perdure. Se pierde en el próximo despliegue. Edita eltemplate. - Olvidar las comillas simples en selectores de conjunto. Los paréntesis y espacios los interpreta la shell.
- No escapar los puntos de las claves en
custom-columns.app.kubernetes.io/versiondebe escribirseapp\.kubernetes\.io/version. - Consejo: define el esquema de etiquetado antes de desplegar nada. Añadirlo después implica editar todos los manifiestos y, si toca selectores, recrear objetos.
- Consejo:
kubectl get pods -A -L entorno,appte da una vista tabulada de toda la plataforma sin filtrar. Es la consulta con la que empezar cualquier revisión. - Consejo: documenta el esquema en
k8s/README.mddel repositorio. Un esquema que solo conoce quien lo diseñó no es un esquema.
Ejercicios
Ejercicio 1: Aplicar el esquema completo a tienda-web
- Actualiza
k8s/base/tienda-web-deployment.yamlpara que el Deployment y sutemplatelleven las ocho etiquetas del esquema de Rutas Norte (app,entorno, las cinco deapp.kubernetes.io/ycriticidad), y cuatro anotaciones: responsable, panel, runbook ychange-cause. - Asegúrate de que el selector sigue teniendo solo
appyentorno, y explica en dos líneas por qué. - Aplícalo y demuestra con un comando que los pods llevan las ocho etiquetas.
- Recupera la URL del runbook con un solo comando.
Ejercicio 2: Consultas sobre la plataforma
Con los tres entornos desplegados, escribe el comando exacto para cada pregunta:
- Todos los pods de la plataforma en
preopro, en cualquier namespace. - Todos los Deployments de la plataforma que no son de desarrollo.
- Los pods de componentes con criticidad
criticaoaltaque no están en estadoRunning. - Una tabla con entorno, componente, versión y nodo de todos los pods de la plataforma.
- Los Deployments a los que les falta la etiqueta
rutasnorte.example/criticidad(auditoría del esquema). - Todos los pods, sin filtrar, mostrando
entornoyapp.kubernetes.io/componentcomo columnas adicionales.
Ejercicio 3: El experimento del pod huérfano
Este ejercicio demuestra por qué las etiquetas son la interfaz de un pod con el resto del sistema. Trabaja en rutas-norte-dev con el Deployment de tienda-web (3 réplicas) y su Service.
- Anota cuántos pods hay y cuántos endpoints tiene el Service.
- Elige un pod y cámbiale la etiqueta
appatienda-web-aisladoconkubectl label --overwrite. - Observa inmediatamente: ¿cuántos pods hay ahora? ¿Qué ha hecho el ReplicaSet? ¿Sigue vivo el pod modificado? ¿Quién es su dueño?
- Comprueba cuántos endpoints tiene ahora el Service y explica por qué.
- Explica para qué sirve esta técnica en el mundo real y qué peligro tiene.
- Deja el clúster como estaba.
Soluciones
Solución 1
# k8s/base/tienda-web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: tienda-web
namespace: rutas-norte-dev
labels:
app: tienda-web
entorno: dev
app.kubernetes.io/name: tienda-web
app.kubernetes.io/instance: tienda-web-dev
app.kubernetes.io/version: "1.8.0"
app.kubernetes.io/component: frontend
app.kubernetes.io/part-of: rutas-norte
app.kubernetes.io/managed-by: kubectl
rutasnorte.example/criticidad: alta
annotations:
kubernetes.io/change-cause: "v1.8.0 - esquema de etiquetado completo (RN-495)"
rutasnorte.example/responsable: "[email protected]"
rutasnorte.example/panel: "https://grafana.rutasnorte.example/d/tienda-web"
rutasnorte.example/runbook: "https://wiki.rutasnorte.example/runbooks/tienda-web"
spec:
replicas: 3
revisionHistoryLimit: 5
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: tienda-web
entorno: dev
template:
metadata:
labels:
app: tienda-web
entorno: dev
app.kubernetes.io/name: tienda-web
app.kubernetes.io/instance: tienda-web-dev
app.kubernetes.io/version: "1.8.0"
app.kubernetes.io/component: frontend
app.kubernetes.io/part-of: rutas-norte
app.kubernetes.io/managed-by: kubectl
rutasnorte.example/criticidad: alta
annotations:
rutasnorte.example/responsable: "[email protected]"
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"- El selector lleva solo
appyentornoporque es inmutable y debe identificar al componente de forma estable. Si incluyeraapp.kubernetes.io/version, al desplegar la 1.9.0 el selector dejaría de casar con los pods nuevos, y como no se puede modificar, habría que borrar y recrear el Deployment en cada despliegue. Además, un selector mínimo evita el riesgo de adopciones indebidas si otro objeto comparte etiquetas informativas.
kubectl apply -f k8s/base/tienda-web-deployment.yaml
kubectl get pods -l app=tienda-web -o jsonpath='{.items[0].metadata.labels}' | python3 -m json.tool{
"app": "tienda-web",
"app.kubernetes.io/component": "frontend",
"app.kubernetes.io/instance": "tienda-web-dev",
"app.kubernetes.io/managed-by": "kubectl",
"app.kubernetes.io/name": "tienda-web",
"app.kubernetes.io/part-of": "rutas-norte",
"app.kubernetes.io/version": "1.8.0",
"entorno": "dev",
"pod-template-hash": "5b7c9f4d8",
"rutasnorte.example/criticidad": "alta"
}Las ocho del esquema, más el pod-template-hash que añade el Deployment.
kubectl get deploy tienda-web -o jsonpath='{.metadata.annotations.rutasnorte\.example/runbook}{"\n"}'Solución 2
# 1. Pods de la plataforma en pre o pro
kubectl get pods -A -l 'app.kubernetes.io/part-of=rutas-norte,entorno in (pre,pro)'
# 2. Deployments que no son de desarrollo
kubectl get deploy -A -l 'app.kubernetes.io/part-of=rutas-norte,entorno notin (dev)'
# 3. Criticos o altos que no estan Running
kubectl get pods -A -l 'rutasnorte.example/criticidad in (critica,alta)' \
--field-selector status.phase!=Running
# 4. Tabla de inventario
kubectl get pods -A -l app.kubernetes.io/part-of=rutas-norte \
-o custom-columns='ENTORNO:.metadata.labels.entorno,COMPONENTE:.metadata.labels.app,VERSION:.metadata.labels.app\.kubernetes\.io/version,NODO:.spec.nodeName'
# 5. Auditoria: sin criticidad
kubectl get deploy -A -l 'app.kubernetes.io/part-of=rutas-norte,!rutasnorte.example/criticidad'
# 6. Columnas adicionales sin filtrar
kubectl get pods -A -L entorno,app.kubernetes.io/component# Salida de la 4
ENTORNO COMPONENTE VERSION NODO
dev api-reservas 2.4.0 rutas-norte
dev api-reservas 2.4.0 rutas-norte
dev redis-cache 7.2 rutas-norte
dev tienda-web 1.8.0 rutas-norte
pre api-reservas 2.4.0 rutas-norte
pro tienda-web 1.8.0 rutas-norteNota sobre la pregunta 3: -l filtra por etiquetas y --field-selector por campos del objeto. Son mecanismos distintos y complementarios; los campos disponibles para --field-selector son limitados (status.phase, spec.nodeName, metadata.name, metadata.namespace y poco más).
Solución 3
# 1. Situacion de partida
kubectl get pods -l app=tienda-web -n rutas-norte-dev
kubectl get endpoints tienda-web -n rutas-norte-devNAME READY STATUS RESTARTS AGE
tienda-web-5b7c9f4d8-6kdnp 1/1 Running 0 10m
tienda-web-5b7c9f4d8-q3xwt 1/1 Running 0 10m
tienda-web-5b7c9f4d8-z8mrv 1/1 Running 0 10m
NAME ENDPOINTS AGE
tienda-web 10.244.0.81:80,10.244.0.82:80,10.244.0.83:80 2h3 pods, 3 endpoints.
# 2. Cambiar la etiqueta del selector en un pod
kubectl label pod tienda-web-5b7c9f4d8-6kdnp app=tienda-web-aislado --overwrite -n rutas-norte-dev
# 3. Observar
kubectl get pods -n rutas-norte-dev -l app.kubernetes.io/part-of=rutas-norte,app.kubernetes.io/component=frontendpod/tienda-web-5b7c9f4d8-6kdnp labeled
NAME READY STATUS RESTARTS AGE
tienda-web-5b7c9f4d8-6kdnp 1/1 Running 0 11m
tienda-web-5b7c9f4d8-h7pnk 1/1 Running 0 4s
tienda-web-5b7c9f4d8-q3xwt 1/1 Running 0 11m
tienda-web-5b7c9f4d8-z8mrv 1/1 Running 0 11mAhora hay 4 pods. Lo ocurrido, paso a paso:
- Al cambiar
appdetienda-webatienda-web-aislado, el pod dejó de casar con el selector del ReplicaSet. - En la siguiente vuelta de su bucle de reconciliación, el ReplicaSet contó 2 pods propios donde debía haber 3 y creó uno nuevo (
h7pnk, con 4 segundos de edad). - El pod modificado sigue vivo, pero ha quedado huérfano: ningún controlador lo vigila.
kubectl get pod tienda-web-5b7c9f4d8-6kdnp -n rutas-norte-dev \
-o jsonpath='{.metadata.ownerReferences}{"\n"}'Salida vacía: no tiene dueño. Kubernetes le retiró el ownerReferences al dejar de casar con el selector.
Siguen siendo 3, pero uno ha cambiado. El pod aislado (10.244.0.81) ha salido del Service porque su selector también exige app=tienda-web; en su lugar ha entrado el pod nuevo (10.244.0.87). El pod huérfano ya no recibe tráfico aunque siga perfectamente vivo y sano.
-
Para qué sirve en el mundo real: es la técnica clásica de aislamiento para depuración en producción. Si uno de cinco pods presenta un comportamiento anómalo —una fuga de memoria, respuestas erráticas, un log sospechoso—, cambiarle una etiqueta del selector lo saca del Service y del ReplicaSet en un instante. Deja de recibir tráfico de clientes reales, el ReplicaSet crea un sustituto para no perder capacidad, y tú conservas el pod intacto, con su memoria y su estado, para inspeccionarlo con calma:
kubectl exec,kubectl logs, volcados, perfilado. No hay que reproducir el problema en un entorno de pruebas: tienes el paciente vivo sobre la mesa.El peligro es doble. Primero, ese pod ya no lo gestiona nadie: si el nodo cae o alguien lo desaloja, desaparece sin sustituto, y sobre todo es fácil olvidarse de él, consumiendo CPU y memoria indefinidamente. Segundo, si el cambio de etiqueta se hace por error o en el sentido contrario —dar a un pod suelto las etiquetas de un componente gobernado—, provocas la adopción y el posible borrado inmediato que vimos en la lección de ReplicaSets. La disciplina obligatoria es etiquetar el pod aislado con la fecha y el motivo, y borrarlo al terminar:
kubectl label pod tienda-web-5b7c9f4d8-6kdnp \
rutasnorte.example/aislado-el=2026-08-05 \
rutasnorte.example/incidente=RN-501 -n rutas-norte-dev# 6. Limpieza
kubectl delete pod tienda-web-5b7c9f4d8-6kdnp -n rutas-norte-dev
kubectl get pods -l app=tienda-web -n rutas-norte-dev
kubectl get endpoints tienda-web -n rutas-norte-devpod "tienda-web-5b7c9f4d8-6kdnp" deleted
NAME READY STATUS RESTARTS AGE
tienda-web-5b7c9f4d8-h7pnk 1/1 Running 0 6m
tienda-web-5b7c9f4d8-q3xwt 1/1 Running 0 17m
tienda-web-5b7c9f4d8-z8mrv 1/1 Running 0 17m
NAME ENDPOINTS AGE
tienda-web 10.244.0.82:80,10.244.0.83:80,10.244.0.87:80 2hTres pods, tres endpoints, todo en orden. Y date cuenta de que al borrar el huérfano no apareció ningún sustituto: el ReplicaSet ya tenía sus tres pods y ese no era suyo.
Conclusión
Cierras el módulo 2 con el hilo que lo recorría entero convertido en sistema. Sabes distinguir sin dudar una etiqueta de una anotación: las primeras identifican y se consultan, están indexadas, tienen 63 caracteres y un juego de caracteres restringido; las segundas adjuntan información que se lee pero no se busca, admiten hasta 256 KiB y cualquier contenido, incluidos JSON y texto multilínea. Conoces la sintaxis completa de claves y valores, sabes que kubernetes.io/ es un prefijo reservado y que el dominio de tu organización es el sitio correcto para lo propio, y dominas las seis etiquetas recomendadas por Kubernetes, incluida la distinción entre name (qué es) e instance (cuál es).
Has fijado el esquema de etiquetado definitivo de Rutas Norte para sus seis componentes y sus tres entornos, con su regla de oro: solo app y entorno entran en los selectores, porque el selector de un Deployment es inmutable y meter en él la versión te condenaría a recrear el objeto en cada despliegue. Manejas las dos familias de selectores —igualdad con =, == y !=; conjuntos con in, notin, exists y su negación— tanto en la línea de comandos con -l como en los manifiestos con matchLabels y matchExpressions, sin olvidar la asimetría de los Services, que solo admiten mapa plano. Sabes quién consume selectores, que son casi todos: ReplicaSets, Deployments, Services, NetworkPolicies, PodDisruptionBudgets, afinidad y las herramientas de monitorización. Usas las anotaciones para lo que sirven —documentar el cambio, configurar controladores de Ingress, dejar el runbook y el teléfono de guardia a mano— sin cometer jamás el error de guardar en ellas un secreto. Y tienes en el cinturón kubectl label y kubectl annotate con --overwrite y el borrado con guion final, más el repertorio de consultas que convierte un clúster grande en algo navegable.
Con esto cierras el módulo 2 y la plataforma Rutas Norte ya está viva. tienda-web pasó de ser aquel pod frágil del módulo 1 a un Deployment de tres réplicas que se recupera solo; api-reservas, redis-cache y worker-notificaciones tienen los suyos; los cuatro componentes que reciben conexiones tienen una dirección estable con balanceo entre réplicas; sabes desplegar sin corte de servicio y deshacer un despliegue roto en menos de un minuto; tienes tres entornos separados en namespaces con el mismo manifiesto desplegado en varios de ellos; y todo está etiquetado con un esquema coherente que las lecciones siguientes van a explotar.
Pero hay deudas pendientes muy visibles. La contraseña de PostgreSQL sigue escrita en claro dentro de un manifiesto que está en Git, y esa base de datos guarda el nombre, el DNI, el teléfono y el correo de cada cliente de Rutas Norte. La URL de la API va incrustada en la imagen en lugar de inyectarse según el entorno. Ningún namespace tiene cuota, así que un error en desarrollo puede dejar sin recursos a producción. Y todos nuestros pods declaran requests y limits a ojo, sin criterio. El módulo 3, Gestión de Configuración y Secretos, ataca exactamente eso: separaremos la configuración del código con ConfigMaps, sacaremos las credenciales de los manifiestos con Secrets, inyectaremos ambos como variables de entorno, pondremos cuotas y límites a cada entorno, entenderemos las clases de calidad de servicio que deciden qué pod muere primero cuando falta memoria, y daremos a cada componente su propia identidad ante la API con ServiceAccounts.
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
