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

  1. Etiquetas frente a anotaciones
  2. Sintaxis y restricciones de claves y valores
  3. Las etiquetas recomendadas por Kubernetes
  4. El esquema de etiquetado de Rutas Norte
  5. Selectores de igualdad
  6. Selectores de conjunto
  7. Selectores en manifiestos: matchLabels y matchExpressions
  8. Quién consume selectores
  9. La regla de oro: el selector de un Deployment es inmutable
  10. Anotaciones en la práctica
  11. kubectl label y kubectl annotate
  12. Consultas útiles del día a día

  1. 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? : kubectl get -l, selectores No: no existe --annotation-selector
¿Están indexadas? , 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)

  1. 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 /:

app.kubernetes.io/component
└──────┬─────────┘ └───┬───┘
    prefijo         nombre

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/ y k8s.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 Formato canónico
app.kubernetes.io/version: 2.5.0 Prefijo estándar, valor con puntos
rutasnorte.example/linea: BIL-SAN Prefijo propio de la empresa
entorno: pro Sin prefijo, perfectamente válido
equipo: backend_reservas 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-dev
error: '[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 character

La versión correcta usa una anotación:

kubectl annotate deployment api-reservas \
  rutasnorte.example/[email protected] -n rutas-norte-dev
deployment.apps/api-reservas annotated

Reglas 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.

  1. 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:

  • name es qué es: postgres. La misma en todas las instalaciones.
  • instance es 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: kubectl

Nota 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.

  1. 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 tienda-web, api-reservas, postgres-reservas, redis-cache, worker-notificaciones, informes-ocupacion Identificador corto del componente. Va en los selectores
entorno dev, pre, pro Entorno. Va en los selectores y en las NetworkPolicies
app.kubernetes.io/name Igual que app Compatibilidad con el ecosistema
app.kubernetes.io/part-of rutas-norte Agrupa toda la plataforma. Nunca en un selector
app.kubernetes.io/component frontend, api, base-datos, cache, worker, informes Papel arquitectónico
app.kubernetes.io/version 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 app y entorno entran 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
api-reservas api-reservas api 2.4.0 critica
postgres-reservas postgres-reservas base-datos 16.2 critica Sí (headless en el módulo 6)
redis-cache redis-cache cache 7.2 media
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-labels
NAME                           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

  1. 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!=pro
NAMESPACE         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   45m

Un 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.

  1. 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   1h

Nota 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'

  1. Selectores en manifiestos: matchLabels y matchExpressions

Dentro de un YAML, los selectores tienen dos formas equivalentes a las dos familias que acabas de ver.

matchLabels: igualdad

selector:
  matchLabels:
    app: api-reservas
    entorno: dev

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: DoesNotExist
operator Equivale a ¿Requiere values?
In in (…)
NotIn notin (…)
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:

# Service: mapa plano, SIN matchLabels
kind: Service
spec:
  selector:
    app: api-reservas
    entorno: dev
# Deployment: CON matchLabels
kind: Deployment
spec:
  selector:
    matchLabels:
      app: api-reservas
      entorno: dev

El motivo es histórico: los Services son del grupo core v1, anterior al selector enriquecido, y solo soportan igualdad.

  1. 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.

  1. 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"}}}}'
The Deployment "api-reservas" is invalid: spec.selector: Invalid value:
field is immutable

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:

  1. El Deployment tiene selector: app=api-reservas y gobierna 4 pods con esa etiqueta.
  2. Cambias el selector a app=api-reservas-v2.
  3. Instantáneamente, los 4 pods dejan de casar. Se convierten en huérfanos, exactamente como los del experimento --cascade=orphan de la lección de ReplicaSets.
  4. El Deployment cuenta 0 pods propios y crea 4 nuevos.
  5. 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-dev

Camino 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.

  1. 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

kubectl get deployment api-reservas -o jsonpath='{.metadata.annotations}' | python3 -m json.tool
{
    "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 yaml o kubectl 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.

  1. kubectl label y kubectl annotate

Ambos 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"
deployment.apps/api-reservas labeled
deployment.apps/api-reservas annotated

Sobrescribir: --overwrite

Si la clave ya existe, el comando falla salvo que lo indiques:

kubectl label deployment api-reservas rutasnorte.example/criticidad=alta
error: 'rutasnorte.example/criticidad' already has a value (critica),
and --overwrite is false
kubectl label deployment api-reservas rutasnorte.example/criticidad=alta --overwrite
deployment.apps/api-reservas labeled

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-
deployment.apps/api-reservas unlabeled
deployment.apps/api-reservas annotated

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=si

Advertencia 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:

kubectl label pods --all entorno=pre --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.

  1. 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             Running

Truco 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   2

Auditorí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 -20

Operaciones 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=client

Ese --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/component

El 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       api

Errores 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 app y entorno.
  • Selectores demasiado amplios. Un app.kubernetes.io/part-of: rutas-norte como 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 metadata y no en spec.template.metadata. Los pods heredan las del template, no las del Deployment. Es el error que rompe los Services.
  • Olvidar --overwrite y pensar que el comando ha funcionado. kubectl label falla si la clave existe. Lee siempre la salida.
  • Cambiar con kubectl label una 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 label sobre un pod perdure. Se pierde en el próximo despliegue. Edita el template.
  • 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/version debe escribirse app\.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,app te 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.md del repositorio. Un esquema que solo conoce quien lo diseñó no es un esquema.

Ejercicios

Ejercicio 1: Aplicar el esquema completo a tienda-web

  1. Actualiza k8s/base/tienda-web-deployment.yaml para que el Deployment y su template lleven las ocho etiquetas del esquema de Rutas Norte (app, entorno, las cinco de app.kubernetes.io/ y criticidad), y cuatro anotaciones: responsable, panel, runbook y change-cause.
  2. Asegúrate de que el selector sigue teniendo solo app y entorno, y explica en dos líneas por qué.
  3. Aplícalo y demuestra con un comando que los pods llevan las ocho etiquetas.
  4. 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:

  1. Todos los pods de la plataforma en pre o pro, en cualquier namespace.
  2. Todos los Deployments de la plataforma que no son de desarrollo.
  3. Los pods de componentes con criticidad critica o alta que no están en estado Running.
  4. Una tabla con entorno, componente, versión y nodo de todos los pods de la plataforma.
  5. Los Deployments a los que les falta la etiqueta rutasnorte.example/criticidad (auditoría del esquema).
  6. Todos los pods, sin filtrar, mostrando entorno y app.kubernetes.io/component como 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.

  1. Anota cuántos pods hay y cuántos endpoints tiene el Service.
  2. Elige un pod y cámbiale la etiqueta app a tienda-web-aislado con kubectl label --overwrite.
  3. Observa inmediatamente: ¿cuántos pods hay ahora? ¿Qué ha hecho el ReplicaSet? ¿Sigue vivo el pod modificado? ¿Quién es su dueño?
  4. Comprueba cuántos endpoints tiene ahora el Service y explica por qué.
  5. Explica para qué sirve esta técnica en el mundo real y qué peligro tiene.
  6. 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"
  1. El selector lleva solo app y entorno porque es inmutable y debe identificar al componente de forma estable. Si incluyera app.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"}'
https://wiki.rutasnorte.example/runbooks/tienda-web

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-norte

Nota 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-dev
NAME                         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   2h

3 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=frontend
pod/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          11m

Ahora hay 4 pods. Lo ocurrido, paso a paso:

  • Al cambiar app de tienda-web a tienda-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.

# 4. Endpoints del Service
kubectl get endpoints tienda-web -n rutas-norte-dev
NAME         ENDPOINTS                                      AGE
tienda-web   10.244.0.82:80,10.244.0.83:80,10.244.0.87:80   2h

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.

  1. 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-dev
pod "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   2h

Tres 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

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados