En la lección anterior usamos kubectl apply como si fuera un comando obvio y vimos manifiestos YAML de pasada, sin desmenuzarlos. Toca saldar esa deuda, porque el manifiesto es la unidad de trabajo real de Kubernetes: todo lo que despliegues durante los once módulos que quedan será un fichero YAML que describe un objeto. Esta lección te enseña la anatomía exacta de cualquier objeto de la API —los cuatro campos que siempre están, hagas lo que hagas—, cómo descubrir qué apiVersion corresponde a cada tipo, el subconjunto de YAML que necesitas dominar, la diferencia real entre apply, create y replace y por qué importa, cómo validar un manifiesto antes de aplicarlo, y cómo organizar los ficheros del proyecto Rutas Norte en el directorio k8s/ para que el repositorio sea la fuente de verdad.

Contenido

  1. Anatomía de un objeto de Kubernetes
  2. Grupos, versiones y niveles de estabilidad de la API
  3. El YAML que hace falta saber
  4. Estado deseado y estado observado
  5. apply frente a create y replace
  6. Server-side apply y la propiedad de campos
  7. Validar antes de aplicar
  8. Organización de los manifiestos del proyecto

  1. Anatomía de un objeto de Kubernetes

Absolutamente todos los objetos de Kubernetes —un pod, un secreto, un permiso RBAC, un CRD de un operador— comparten la misma estructura de cuatro campos de primer nivel. Aprender esa estructura una vez sirve para siempre.

# k8s/base/api-reservas-pod.yaml
apiVersion: v1                          # 1. QUÉ versión de la API interpreta esto
kind: Pod                               # 2. QUÉ tipo de objeto es
metadata:                               # 3. QUIÉN es: identidad y metadatos
  name: api-reservas
  namespace: rutas-norte-dev
  labels:
    app: api-reservas
    app.kubernetes.io/part-of: rutas-norte
    entorno: dev
  annotations:
    rutasnorte.example/responsable: [email protected]
spec:                                   # 4. CÓMO debe ser: el estado deseado
  containers:
    - name: api
      image: registry.rutasnorte.example/api-reservas:2.4.0
      ports:
        - containerPort: 3000
      env:
        - name: DB_HOST
          value: postgres-reservas

1.1. apiVersion

Indica el grupo y la versión de la API que debe interpretar el objeto. Determina qué campos son válidos y cómo se comportan. Ejemplos: v1 (grupo core), apps/v1, networking.k8s.io/v1, batch/v1. Es el campo que más errores provoca cuando se copian manifiestos antiguos de internet.

1.2. kind

El tipo concreto: Pod, Deployment, Service, Ingress, ConfigMap. Se escribe siempre en CamelCase, mientras que el recurso en la URL de la API va en minúsculas y plural (pods, deployments). La pareja apiVersion + kind identifica unívocamente el esquema del objeto.

1.3. metadata

La identidad del objeto. Sus campos más relevantes:

Campo Quién lo escribe Para qué sirve
name Nombre único dentro del namespace y del tipo. Debe ser un nombre DNS válido: minúsculas, números y guiones
namespace Tú (o el contexto) Namespace donde vive. Si se omite, se usa el del contexto de kubectl
labels Metadatos consultables: son la base de selectores, Services y controladores
annotations Tú y las herramientas Metadatos no consultables: configuración de terceros, trazabilidad
uid El sistema Identificador único e irrepetible en todo el clúster
resourceVersion El sistema Versión interna del objeto; sirve para detectar escrituras concurrentes
creationTimestamp El sistema Fecha de creación
ownerReferences El sistema Quién "posee" el objeto. Un pod creado por un ReplicaSet lo apunta aquí, y por eso al borrar el ReplicaSet se borran sus pods (recolección en cascada)
finalizers El sistema o herramientas Tareas que deben completarse antes de que el objeto pueda borrarse

Un detalle que explica un comportamiento desconcertante: si un namespace o un PVC se queda "colgado" en estado Terminating, casi siempre es por un finalizer cuyo responsable no ha terminado su trabajo.

1.4. spec

El estado deseado. Es lo que tú describes y su contenido depende por completo del kind: el spec de un Pod tiene containers, el de un Service tiene selector y ports, el de un PVC tiene resources.requests.storage. Todo el curso consiste, en el fondo, en aprender a escribir spec de distintos tipos.

1.5. status

No aparece en el manifiesto de arriba porque no lo escribes tú: lo escribe el clúster. Es el estado observado. Si consultas el objeto ya creado lo verás:

kubectl get pod api-reservas -n rutas-norte-dev -o yaml
status:
  phase: Running
  podIP: 10.244.0.17
  hostIP: 192.168.49.2
  startTime: "2026-08-05T09:14:22Z"
  conditions:
    - type: Initialized
      status: "True"
    - type: Ready
      status: "True"
    - type: ContainersReady
      status: "True"
  containerStatuses:
    - name: api
      ready: true
      restartCount: 0
      image: registry.rutasnorte.example/api-reservas:2.4.0
      imageID: registry.rutasnorte.example/api-reservas@sha256:9f3a1c2...

Fíjate en dos cosas. Primera: conditions es el formato estándar con el que Kubernetes expresa "cómo va" un objeto, y es lo que consultan kubectl wait y las herramientas de despliegue. Segunda: imageID contiene el digest real de la imagen que se está ejecutando, mientras que spec.image contiene la etiqueta que tú pediste. Cuando alguien reutiliza una etiqueta, esos dos valores dejan de corresponderse; volveremos a ello al hablar de etiquetas inmutables en la próxima lección.

  1. Grupos, versiones y niveles de estabilidad de la API

La API de Kubernetes está dividida en grupos para poder evolucionar por partes y permitir extensiones.

Grupo apiVersion Tipos que contiene
core (o legacy) v1 Pod, Service, ConfigMap, Secret, Namespace, PersistentVolume, PersistentVolumeClaim, ServiceAccount, Node
apps apps/v1 Deployment, ReplicaSet, StatefulSet, DaemonSet
batch batch/v1 Job, CronJob
networking.k8s.io networking.k8s.io/v1 Ingress, NetworkPolicy, IngressClass
rbac.authorization.k8s.io rbac.authorization.k8s.io/v1 Role, RoleBinding, ClusterRole, ClusterRoleBinding
autoscaling autoscaling/v2 HorizontalPodAutoscaler
storage.k8s.io storage.k8s.io/v1 StorageClass, VolumeAttachment
policy policy/v1 PodDisruptionBudget

El grupo core es el histórico y por eso su apiVersion no lleva prefijo: es v1, no core/v1. Todos los demás siguen el formato grupo/versión.

Cómo descubrirlo en tu propio clúster

No hay que memorizar esta tabla: se consulta.

# Todos los grupos y versiones que soporta este clúster
kubectl api-versions

# Todos los recursos, con su apiVersion, abreviatura y ámbito
kubectl api-resources

# Filtrar por grupo
kubectl api-resources --api-group=networking.k8s.io
NAME              SHORTNAMES   APIVERSION                NAMESPACED   KIND
ingressclasses                 networking.k8s.io/v1      false        IngressClass
ingresses         ing          networking.k8s.io/v1      true         Ingress
networkpolicies   netpol       networking.k8s.io/v1      true         NetworkPolicy

Este es el reflejo que hay que adquirir: antes de copiar un apiVersion de un blog, compruébalo con kubectl api-resources. La versión correcta es siempre la que tu clúster dice.

Alpha, beta y estable

Nivel Ejemplo Qué significa Recomendación
Alpha v1alpha1 Puede desaparecer o cambiar sin aviso. Desactivada por defecto. Puede haber pérdida de datos Nunca en producción
Beta v2beta2 Bien probada, pero los campos aún pueden cambiar. Desde 1.24 las APIs beta nuevas vienen desactivadas por defecto Solo con plan de migración
Estable v1, v2 Compatibilidad garantizada durante muchas versiones La que debes usar

Kubernetes retira versiones antiguas con cada actualización, y ese es el motivo de que manifiestos copiados de tutoriales viejos fallen:

error: unable to recognize "deploy.yaml": no matches for kind "Deployment"
in version "extensions/v1beta1"

Grupo extensions/v1beta1 desapareció hace muchas versiones. Hoy un Deployment es apps/v1 y un Ingress es networking.k8s.io/v1, sin excepciones. Antes de actualizar un clúster, herramientas como kubent (kube-no-trouble) revisan tus manifiestos en busca de APIs a punto de retirarse.

  1. El YAML que hace falta saber

YAML es simple pero tiene trampas. Este es el subconjunto que necesitas.

3.1. Mapas, listas e indentación

# Un mapa (clave: valor)
metadata:
  name: api-reservas          # indentación con ESPACIOS, nunca tabuladores
  namespace: rutas-norte-dev

# Una lista de cadenas
args:
  - "--puerto=3000"
  - "--modo=produccion"

# Una lista de mapas: el guion marca el inicio de cada elemento
containers:
  - name: api                 # este 'name' pertenece al primer elemento
    image: nginx:1.27
    ports:
      - containerPort: 3000
  - name: sidecar-metricas    # segundo elemento
    image: prom/statsd-exporter:v0.26.0

Reglas innegociables:

  • Espacios, nunca tabuladores. Un tabulador es un error de sintaxis en YAML. Configura tu editor para expandir tabuladores a 2 espacios en ficheros .yaml.
  • La indentación define la jerarquía. Dos espacios por nivel es la convención en Kubernetes.
  • El guion - indica elemento de lista y su contenido se indenta al mismo nivel que el guion o más.

3.2. Tipos y comillas: la trampa clásica

data:
  replicas_texto: "3"      # cadena
  replicas_numero: 3       # entero
  activo: true             # booleano
  activo_texto: "true"     # cadena
  version: "1.30"          # cadena; sin comillas sería el número 1.30
  puerto_noruega: "NO"     # ¡IMPRESCINDIBLE! sin comillas, YAML 1.1 lo lee como false

El caso NO es célebre (se conoce como the Norway problem): YAML 1.1 interpreta yes, no, on, off, y, n como booleanos. Regla práctica: en un ConfigMap o Secret, entrecomilla siempre los valores, porque esos campos exigen cadenas y un valor mal tipado produce un error tan claro como este:

error: error validating data: ValidationError(ConfigMap.data.puerto):
invalid type for io.k8s.api.core.v1.ConfigMap.data: got "boolean", expected "string"

3.3. Cadenas multilínea

Fundamentales para incrustar ficheros de configuración en un ConfigMap:

data:
  # | conserva los saltos de línea (literal). Es el que querrás casi siempre
  nginx.conf: |
    server {
      listen 80;
      root /usr/share/nginx/html;
      location /api/ {
        proxy_pass http://api-reservas:80/;
      }
    }

  # > pliega los saltos en espacios (folded): útil para textos largos
  descripcion: >
    Configuracion de la tienda web de Rutas Norte
    para el entorno de desarrollo.
Indicador Efecto Uso típico
| Conserva saltos de línea; elimina el último Ficheros de configuración, scripts
|- Conserva saltos; elimina el salto final Cuando la línea final sobra
> Convierte saltos en espacios Textos descriptivos largos

3.4. Varios documentos en un fichero

El separador --- permite poner varios objetos en un mismo fichero, y kubectl apply los procesa en orden:

# k8s/base/redis-cache.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-cache-config
  namespace: rutas-norte-dev
data:
  maxmemory: "256mb"
---
apiVersion: v1
kind: Pod
metadata:
  name: redis-cache
  namespace: rutas-norte-dev
  labels:
    app: redis-cache
spec:
  containers:
    - name: redis
      image: redis:7.2-alpine

Criterio recomendado: agrupa en un fichero los objetos que forman una unidad desplegable (un componente con su ConfigMap y su Service) y separa en ficheros distintos los componentes diferentes. Un único fichero gigante con toda la plataforma es difícil de revisar y de aplicar parcialmente.

  1. Estado deseado y estado observado

Aquí es donde el modelo declarativo se vuelve concreto. Ya conoces el bucle de reconciliación de la lección Arquitectura de Kubernetes; veámoslo desde el punto de vista del fichero.

flowchart LR
    Y["Manifiesto YAML<br/>en k8s/"] -->|kubectl apply| API["kube-apiserver"]
    API -->|persiste spec| E[("etcd")]
    C["Controlador"] -->|lee spec| API
    C -->|observa| R["Mundo real<br/>pods, nodos"]
    C -->|escribe status| API
    R -->|diferencia| C

Cuatro consecuencias prácticas que conviene tener muy claras:

  1. Tú escribes spec; el sistema escribe status. Editar un status a mano no sirve de nada: el controlador lo sobrescribirá en su siguiente ciclo.
  2. Aplicar no es ejecutar. kubectl apply termina cuando el objeto está guardado, no cuando la aplicación está lista. Para esperar de verdad:
kubectl apply -f k8s/base/api-reservas.yaml
kubectl wait --for=condition=Ready pod/api-reservas -n rutas-norte-dev --timeout=90s
  1. Es idempotente. Aplicar diez veces el mismo fichero da el mismo resultado que aplicarlo una. Esto es lo que hace posible que un pipeline de CI/CD ejecute kubectl apply en cada despliegue sin comprobar antes qué existe.
kubectl apply -f k8s/base/api-reservas.yaml
pod/api-reservas created
kubectl apply -f k8s/base/api-reservas.yaml   # segunda vez, sin cambios
pod/api-reservas unchanged
  1. El fichero es la fuente de verdad, no el clúster. Si alguien cambia algo con kubectl edit, el siguiente apply desde el repositorio lo revierte. Esa es exactamente la propiedad sobre la que se construye GitOps (módulo 10).

  1. apply frente a create y replace

Tres comandos que parecen intercambiables y no lo son en absoluto.

Comando Si el objeto NO existe Si el objeto YA existe Conserva cambios de otros
kubectl create -f Lo crea Error: AlreadyExists
kubectl replace -f Error: NotFound Lo sustituye entero No: borra lo que no esté en el fichero
kubectl apply -f Lo crea Lo fusiona con lo existente Sí, si son de otro gestor de campos
kubectl create -f k8s/base/api-reservas.yaml
Error from server (AlreadyExists): pods "api-reservas" already exists
kubectl apply -f k8s/base/api-reservas.yaml
pod/api-reservas configured

La conclusión operativa es simple: usa apply siempre. create sirve para comandos imperativos rápidos (kubectl create namespace) y replace para casos muy concretos de sustitución total.

Por qué apply sabe qué borrar: la anotación last-applied

Imagina que aplicas un pod con dos etiquetas, luego editas el fichero y dejas solo una, y vuelves a aplicar. ¿Cómo sabe kubectl que debe eliminar la segunda etiqueta, en lugar de dejarla porque "no está en el fichero pero tampoco he pedido borrarla"?

La respuesta clásica es que apply guarda una copia de lo último que aplicaste en una anotación:

kubectl get pod api-reservas -n rutas-norte-dev \
  -o jsonpath='{.metadata.annotations.kubectl\.kubernetes\.io/last-applied-configuration}'
{"apiVersion":"v1","kind":"Pod","metadata":{"labels":{"app":"api-reservas","entorno":"dev"},...

Con esa información, kubectl hace una fusión de tres vías: compara (a) lo que aplicaste la última vez, (b) lo que aplicas ahora y (c) lo que hay en el clúster. Lo que estaba en (a) y desaparece en (b) se borra; lo que solo está en (c) —porque lo puso un controlador o un webhook— se respeta.

Esto explica un error frecuente: si creaste un objeto con kubectl create y después lo modificas con apply, no existe la anotación de referencia y kubectl avisa de que la fusión puede ser incorrecta. Por coherencia, usa apply desde el principio de la vida de cada objeto.

  1. Server-side apply y la propiedad de campos

Desde Kubernetes 1.22, la fusión puede hacerla el servidor en lugar del cliente, y es el mecanismo hacia el que se está migrando todo el ecosistema.

kubectl apply --server-side -f k8s/base/api-reservas.yaml

La diferencia es conceptual: en lugar de una anotación con el último estado, el servidor registra qué gestor es propietario de cada campo en metadata.managedFields.

kubectl get pod api-reservas -n rutas-norte-dev --show-managed-fields -o yaml | head -20
metadata:
  managedFields:
    - manager: kubectl
      operation: Apply
      apiVersion: v1
      fieldsV1:
        f:spec:
          f:containers:
            k:{"name":"api"}:
              f:image: {}
    - manager: kubelet
      operation: Update
      subresource: status

Ventajas frente a la fusión en cliente:

  • Detecta conflictos. Si intentas modificar un campo que pertenece a otro gestor (un HPA que controla replicas, por ejemplo), la petición falla con un aviso claro en lugar de provocar una pelea silenciosa entre herramientas:
error: Apply failed with 1 conflict: conflict with "hpa-controller":
.spec.replicas

Si de verdad quieres apropiarte del campo, se fuerza explícitamente:

kubectl apply --server-side --force-conflicts -f k8s/base/api-reservas.yaml
  • No depende de una anotación que en objetos grandes puede ser enorme.
  • Varios gestores pueden coexistir sobre el mismo objeto de forma ordenada: tu pipeline, un operador y un autoescalador, cada uno dueño de sus campos.

Para el curso seguiremos usando kubectl apply a secas, que es lo habitual, pero conviene que reconozcas managedFields cuando aparezca en un -o yaml y que sepas qué significa un error de conflicto.

  1. Validar antes de aplicar

Aplicar sin comprobar es la receta del incidente. Kubernetes ofrece cuatro niveles de verificación, de más barato a más completo.

7.1. --dry-run=client: validación local

kubectl apply -f k8s/base/api-reservas.yaml --dry-run=client
pod/api-reservas configured (dry run)

kubectl construye el objeto y valida el esquema sin enviar nada. Detecta errores de sintaxis YAML y campos inexistentes. No detecta problemas de permisos, cuotas ni webhooks. Es también la bandera que usamos en la lección anterior para generar manifiestos:

kubectl create configmap tienda-web-config \
  --from-literal=api_url=http://api-reservas \
  -n rutas-norte-dev --dry-run=client -o yaml > k8s/base/tienda-web-configmap.yaml

7.2. --dry-run=server: validación real sin persistir

kubectl apply -f k8s/base/api-reservas.yaml --dry-run=server

La petición llega al apiserver y recorre todo el camino de la lección 01-02 —autenticación, autorización RBAC, admisión, validación— pero no se escribe en etcd. Detecta lo que el cliente no puede ver: falta de permisos, cuotas superadas, políticas de seguridad de pods, webhooks de admisión propios. Es la validación que debe ejecutar tu pipeline de CI antes de desplegar.

7.3. kubectl diff: ver el cambio antes de hacerlo

kubectl diff -f k8s/base/api-reservas.yaml
diff -u -N /tmp/LIVE-1234/v1.Pod.rutas-norte-dev.api-reservas /tmp/MERGED-5678/...
--- LIVE
+++ MERGED
@@ -18,7 +18,7 @@
   containers:
   - name: api
-    image: registry.rutasnorte.example/api-reservas:2.3.1
+    image: registry.rutasnorte.example/api-reservas:2.4.0

Este es el comando que convierte un despliegue en algo revisable: te dice exactamente qué va a cambiar en el clúster antes de cambiarlo. Debería ser un paso obligatorio en cualquier despliegue manual a producción. Su código de salida es distinto de cero cuando hay diferencias, lo que permite usarlo en scripts.

7.4. kubectl explain: consultar el esquema

kubectl explain pod.spec.containers.livenessProbe
kubectl explain deployment.spec.strategy.rollingUpdate
kubectl explain ingress.spec.rules --recursive

Ya lo vimos en la lección anterior; aquí conviene subrayar que es la respuesta correcta a "¿existe este campo y qué tipo tiene?" cuando estás escribiendo un manifiesto.

Flujo de trabajo recomendado

# 1. ¿El YAML es válido y los campos existen?
kubectl apply -f k8s/base/ --dry-run=client

# 2. ¿Lo aceptaría el clúster con mis permisos y sus políticas?
kubectl apply -f k8s/base/ --dry-run=server

# 3. ¿Qué va a cambiar exactamente?
kubectl diff -f k8s/base/

# 4. Aplicar
kubectl apply -f k8s/base/

# 5. Esperar a que el estado real alcance al deseado
kubectl wait --for=condition=Ready pod -l app.kubernetes.io/part-of=rutas-norte \
  -n rutas-norte-dev --timeout=120s

  1. Organización de los manifiestos del proyecto

Los manifiestos de Rutas Norte viven en el directorio k8s/ del repositorio del proyecto, versionados en Git. Esta es la estructura que usaremos durante todo el curso:

rutas-norte/
├── src/
├── Dockerfile
└── k8s/
    ├── base/                          # definición común a todos los entornos
    │   ├── namespace.yaml
    │   ├── tienda-web.yaml
    │   ├── api-reservas.yaml
    │   ├── postgres-reservas.yaml
    │   ├── redis-cache.yaml
    │   ├── worker-notificaciones.yaml
    │   └── informes-ocupacion.yaml
    ├── entornos/
    │   ├── dev/                       # lo específico de rutas-norte-dev
    │   ├── pre/
    │   └── pro/
    ├── entorno-local/
    │   └── kind-rutas-norte.yaml      # de la leccion 01-04
    └── README.md

Convenciones que seguiremos:

Convención Regla
Un fichero por componente api-reservas.yaml contiene el Deployment, su Service y su ConfigMap, separados por ---
Nombre del fichero = nombre del componente Encontrar el manifiesto de algo es inmediato
Namespace explícito Cada objeto declara su metadata.namespace, para no depender del contexto activo
Etiquetas comunes en todos los objetos app, app.kubernetes.io/part-of: rutas-norte, entorno
Nunca secretos reales en Git Los Secrets van con valores de ejemplo o cifrados (módulo 3 y 8)
Orden de aplicación Namespaces y ConfigMaps antes que las cargas que los consumen

Sobre el orden: aplicar un directorio completo funciona porque el modelo es reconciliador —un pod que no encuentra su ConfigMap se queda esperando y arranca cuando aparece—, pero para evitar errores transitorios conviene aplicar primero el namespace:

kubectl apply -f k8s/base/namespace.yaml
kubectl apply -f k8s/base/

Un aviso importante para no invadir módulos posteriores: esta estructura de base/ y entornos/ es deliberadamente YAML plano, sin plantillas. Cuando la duplicación entre entornos se vuelva molesta, existen dos soluciones estándar —Helm (plantillas y empaquetado) y Kustomize (superposiciones sin plantillas)— que se estudian en las lecciones Helm y Kustomize. Aprender primero el YAML plano no es tiempo perdido: es lo que te permitirá entender qué generan esas herramientas.

Errores Comunes y Consejos

  • Usar tabuladores en YAML. Error de sintaxis puro. Configura el editor: expandtab, 2 espacios, y una extensión de YAML que valide al guardar.
  • Copiar apiVersion de tutoriales antiguos. extensions/v1beta1 y apps/v1beta2 no existen. Verifica siempre con kubectl api-resources.
  • Confundir el nombre del kind con el del recurso. kind: Deployment (CamelCase, singular) en el YAML; kubectl get deployments (minúsculas, plural) en la CLI.
  • Valores sin comillar en ConfigMaps y Secrets. "3", "true", "NO". Esos campos exigen cadenas y YAML convierte tipos alegremente.
  • Editar el status a mano. No tiene ningún efecto: lo escribe el controlador.
  • Mezclar create y apply sobre el mismo objeto. Sin la anotación last-applied, la fusión puede comportarse de forma inesperada. Empieza siempre con apply.
  • Aplicar sin diff en producción. kubectl diff cuesta dos segundos y evita despliegues sorpresa.
  • Guardar un -o yaml como manifiesto. La salida del clúster incluye status, uid, resourceVersion, managedFields y creationTimestamp, que no deben versionarse. Límpialo (a mano o con el plugin kubectl neat) antes de guardarlo en k8s/.
  • Consejo: trata el directorio k8s/ como código. Revisión por pull request, validación en CI con --dry-run=server, y la regla de oro: si un cambio no está en Git, no existe.

Ejercicios

Ejercicio 1: Anatomía y descubrimiento de la API

  1. Escribe el manifiesto de un ConfigMap llamado tienda-web-config en el namespace rutas-norte-dev, con las etiquetas del proyecto y dos claves: api_url con valor http://api-reservas y modo_mantenimiento con valor "NO". Explica por qué el segundo valor necesita comillas.
  2. Averigua con kubectl, sin buscar en internet, a qué apiVersion pertenecen CronJob, Ingress y HorizontalPodAutoscaler, y cuáles de ellos son de ámbito de namespace.
  3. Comprueba con kubectl explain si el campo spec.containers.imagePullPolicy existe en un Pod y qué valores admite.

Ejercicio 2: Validación e idempotencia

Partiendo del manifiesto del pod api-reservas de la sección 1:

  1. Valídalo en local sin tocar el clúster.
  2. Aplícalo, y aplícalo otra vez sin cambiar nada. ¿Qué dice kubectl la segunda vez y por qué?
  3. Cambia la imagen a la versión 2.5.0 en el fichero y, antes de aplicar, muestra exactamente qué cambiaría en el clúster.
  4. Muestra el status.phase y la IP del pod usando jsonpath.
  5. Explica qué diferencia habría si en el paso 2 hubieras usado kubectl create en lugar de apply.

Ejercicio 3: Fichero multi-documento y organización

Crea el fichero k8s/base/redis-cache.yaml que contenga, en un solo fichero y en el orden correcto, tres objetos:

  1. Un ConfigMap redis-cache-config con una clave redis.conf que contenga varias líneas de configuración (usa el indicador literal).
  2. Un Pod redis-cache con la imagen redis:7.2-alpine.
  3. Todo ello en rutas-norte-dev, con las etiquetas del proyecto.

Después, indica qué comando aplicaría el directorio completo y por qué conviene aplicar antes namespace.yaml.

Soluciones

Solución 1

# k8s/base/tienda-web-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: tienda-web-config
  namespace: rutas-norte-dev
  labels:
    app: tienda-web
    app.kubernetes.io/part-of: rutas-norte
    entorno: dev
data:
  api_url: "http://api-reservas"
  modo_mantenimiento: "NO"

"NO" necesita comillas porque YAML 1.1 interpreta NO (igual que no, yes, on, off) como el booleano false. El campo data de un ConfigMap solo admite cadenas, así que sin comillas el apiserver rechazaría el objeto con un error de tipo.

# 2
kubectl api-resources | grep -E 'cronjobs|ingresses|horizontalpodautoscalers'
cronjobs                    cj       batch/v1              true    CronJob
horizontalpodautoscalers    hpa      autoscaling/v2        true    HorizontalPodAutoscaler
ingresses                   ing      networking.k8s.io/v1  true    Ingress

Los tres son de ámbito de namespace (NAMESPACED = true).

# 3
kubectl explain pod.spec.containers.imagePullPolicy
FIELD: imagePullPolicy <string>
DESCRIPTION:
    Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always
    if :latest tag is specified, or IfNotPresent otherwise.

Solución 2

# 1
kubectl apply -f k8s/base/api-reservas-pod.yaml --dry-run=client

# 2
kubectl apply -f k8s/base/api-reservas-pod.yaml   # pod/api-reservas created
kubectl apply -f k8s/base/api-reservas-pod.yaml   # pod/api-reservas unchanged

# 3
kubectl diff -f k8s/base/api-reservas-pod.yaml

# 4
kubectl get pod api-reservas -n rutas-norte-dev \
  -o jsonpath='{.status.phase}{"\t"}{.status.podIP}{"\n"}'

En el paso 2, la segunda ejecución dice unchanged porque apply es idempotente: kubectl compara la anotación last-applied-configuration, el fichero actual y el objeto vivo, no encuentra diferencias y no envía ninguna modificación. Esa propiedad es la que permite ejecutar apply en cada despliegue de un pipeline sin efectos secundarios.

Con kubectl create en el paso 2, la primera vez habría funcionado y la segunda habría fallado con Error from server (AlreadyExists), porque create no fusiona: solo sabe crear objetos nuevos.

Solución 3

# k8s/base/redis-cache.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-cache-config
  namespace: rutas-norte-dev
  labels:
    app: redis-cache
    app.kubernetes.io/part-of: rutas-norte
    entorno: dev
data:
  redis.conf: |
    maxmemory 256mb
    maxmemory-policy allkeys-lru
    appendonly no
    save ""
---
apiVersion: v1
kind: Pod
metadata:
  name: redis-cache
  namespace: rutas-norte-dev
  labels:
    app: redis-cache
    app.kubernetes.io/part-of: rutas-norte
    entorno: dev
spec:
  containers:
    - name: redis
      image: redis:7.2-alpine
      ports:
        - containerPort: 6379
kubectl apply -f k8s/base/namespace.yaml
kubectl apply -f k8s/base/

El ConfigMap va primero porque el pod lo consumirá y así se evita un error transitorio. Conviene aplicar namespace.yaml por separado y antes porque todos los demás objetos declaran namespace: rutas-norte-dev y, si ese namespace todavía no existe, sus creaciones fallan con namespaces "rutas-norte-dev" not found. El namespace es la única dependencia dura del conjunto.

Conclusión

Ya dominas la unidad de trabajo de Kubernetes. Todo objeto tiene la misma anatomía —apiVersion, kind, metadata, spec que escribes tú y status que escribe el clúster—, pertenece a un grupo de API cuya versión debes verificar en tu propio clúster con kubectl api-resources, y se expresa en un YAML con reglas estrictas sobre indentación, tipos y cadenas multilínea. Has visto por qué apply es superior a create y replace, cómo funciona la fusión de tres vías y su anotación last-applied, hacia dónde evoluciona el modelo con server-side apply y la propiedad de campos, y qué secuencia de validación —--dry-run=client, --dry-run=server, kubectl diff, kubectl explain— convierte un despliegue en algo predecible. Y tienes la estructura del directorio k8s/ que sostendrá el proyecto durante el resto del curso.

Tienes clúster, tienes CLI y tienes el lenguaje de los manifiestos. Solo falta el paciente. En la siguiente lección, El Proyecto del Curso: la Plataforma Rutas Norte, conocerás en detalle la empresa, cada uno de los seis componentes de la plataforma, la arquitectura objetivo y las convenciones del proyecto; y terminarás desplegando de verdad tu primer trozo de Rutas Norte en el clúster, viéndolo funcionar en tu navegador.

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