Lesson  in  Kubernetes 101

Un tipo de recurso propio (CRD)

Define un tipo de recurso que Kubernetes no conoce, con su esquema y su validación. Crea instancias de ese tipo, compruébalas con kubectl get, y descubre la lección más importante del capítulo: no pasa absolutamente nada.

Enséñale un tipo nuevo a la API

Hasta ahora has trabajado con los tipos que Kubernetes trae de fábrica: Pod, Deployment, Service, ConfigMap. Todos comparten la misma mecánica: un apiVersion, un kind, un metadata, un spec, y un kubectl get que los lista.

La pregunta de esta lección es incómoda y muy productiva: ¿qué tienen esos tipos de especial?

El libro presenta los CRD en el capítulo de objetos de Kubernetes y deja caer una afirmación que cuesta creer hasta que se ve: crear un Custom Resource no provoca absolutamente nada en el clúster. Aquí lo vas a comprobar.

La respuesta es: nada. Y vas a demostrarlo creando el tuyo.

Un CustomResourceDefinition (CRD) es un objeto que le dice al API server: "a partir de ahora existe un tipo llamado Promocion, se guarda en /apis/tienda.example.com/v1/promociones, y estos son los campos que acepta". A partir de ese momento, tu tipo es un ciudadano de primera clase de la API: tiene endpoint, tiene validación, se guarda en etcd, funciona con kubectl get, con RBAC, con kubectl label, con kubectl apply. Con todo.

Note

💡 Esta lección construye la mitad del rompecabezas. La otra mitad (por qué tu tipo nuevo, de momento, no hace nada) es la lección siguiente. Guárdate la sensación de anticlímax que vas a tener al final de esta: es intencionada.

Paso 1: Define el tipo

Crea el archivo promocion-crd.yaml:

cat << 'EOF' > promocion-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: promociones.tienda.example.com
spec:
  group: tienda.example.com
  scope: Namespaced
  names:
    plural: promociones
    singular: promocion
    kind: Promocion
    shortNames:
    - promo
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            required:
            - banner
            properties:
              banner:
                type: string
              replicas:
                type: integer
                minimum: 1
                maximum: 10
                default: 1
    additionalPrinterColumns:
    - name: BANNER
      type: string
      jsonPath: .spec.banner
    - name: Replicas
      type: integer
      jsonPath: .spec.replicas
    - name: Edad
      type: date
      jsonPath: .metadata.creationTimestamp
EOF

El YAML, explicado con preguntas y respuestas

¿Por qué apiVersion: apiextensions.k8s.io/v1 y no v1 a secas?

Porque el CRD en sí mismo es un objeto nativo de Kubernetes, no un recurso personalizado. Vive en el grupo apiextensions.k8s.io, que es literalmente el grupo de la API encargado de extender la API. Es el mismo apiVersion para cualquier CRD que crees en tu vida, defina el tipo que defina.

¿Por qué metadata.name tiene que ser promociones.tienda.example.com y no promociones?

Porque Kubernetes lo exige: el nombre de un CRD es siempre <plural>.<group>. Y lo exige por una razón práctica. Si dos charts de Helm distintos definieran un tipo llamado promociones, uno en el grupo tienda.example.com y otro en otraempresa.example.com, no colisionarían. El grupo es tu espacio de nombres en la API.

¿Qué diferencia hay entre group y un Namespace de Kubernetes?

No tienen nada que ver, aunque el nombre invite a confundirlos. El grupo es un agrupador de la API: junto con la versión forma el apiVersion que escribirán tus usuarios (tienda.example.com/v1). El Namespace es un agrupador de objetos dentro del clúster. Un mismo grupo de API puede tener objetos repartidos por cien namespaces.

¿Qué significa scope: Namespaced?

Que las instancias de tu tipo vivirán dentro de un namespace, como los Pods o los Deployments. La alternativa es scope: Cluster, para objetos globales como los Nodes o los PersistentVolumes. La inmensa mayoría de los recursos personalizados son Namespaced, y hay un motivo de peso: es lo que te permite dar permisos RBAC por namespace y aislar equipos. Un recurso Cluster es de todos y de nadie.

¿Para qué sirven plural, singular, kind y shortNames?

Son las cuatro formas de nombrar la misma cosa:

  • kind (Promocion): lo que escribes en el YAML del recurso.
  • plural (promociones): el endpoint real de la API, y lo que usas en kubectl get promociones.
  • singular (promocion): lo que aparece en mensajes de error y en la ayuda.
  • shortNames (promo): el alias corto, para escribir kubectl get promo.

¿Qué son served y storage?

served: true significa que esta versión se puede consultar a través de la API. storage: true significa que es la versión en la que el objeto se guarda físicamente en etcd. Solo una versión puede tener storage: true a la vez. La distinción parece burocrática hasta el día en que tienes v1beta1 y v1 conviviendo y necesitas migrar de una a otra sin parar el clúster: entonces esos dos campos son todo el mecanismo.

¿Por qué el openAPIV3Schema es obligatorio?

Porque sin él, el API server aceptaría cualquier cosa dentro de tu spec. Un spec: {pepinillos: true} se guardaría tan tranquilo en etcd. El esquema es lo que convierte tu tipo en una API y no en un cajón de sastre: define qué campos existen, de qué tipo son y cuáles son obligatorios. En apiextensions.k8s.io/v1 no es opcional: un CRD sin esquema se rechaza.

¿Qué añaden required, minimum, maximum y default?

Validación y ergonomía, gratis y en el servidor:

  • required: [banner]: un Promocion sin banner se rechaza en el apply.
  • minimum: 1 / maximum: 10: replicas: 50 se rechaza. Nadie escribe código para esto.
  • default: 1: si el usuario omite replicas, el API server lo rellena solo.

Esto es lo que separa un CRD serio de un CRD de juguete. La validación no la hace tu controlador: la hace el API server, antes de guardar nada.

¿Y additionalPrinterColumns?

Es lo que hace que kubectl get promociones sea legible en lugar de mostrar solo NAME y AGE. Cada columna extrae un campo con un jsonPath. Cuesta seis líneas y es la diferencia entre un recurso que se puede operar y uno que hay que abrir con -o yaml cada vez.

Paso 2: Aplícalo

kubectl apply -f promocion-crd.yaml

Comprueba que el API server lo ha registrado:

kubectl get crd promociones.tienda.example.com
kubectl get crd promociones.tienda.example.com -o jsonpath='{.status.conditions}' | jq .

Busca la condición Established: True. Significa que el endpoint ya existe y acepta peticiones.

Paso 3: Tu tipo ya es de primera clase

Esto es lo que merece la pena entender de verdad. Pregúntale a la API qué recursos conoce en tu grupo:

kubectl api-resources --api-group=tienda.example.com

Y mira el endpoint directamente, sin intermediarios:

kubectl get --raw /apis/tienda.example.com/v1 | jq .

Ahí está: un endpoint REST, con sus verbos (get, list, watch, create, update, patch, delete), exactamente igual que el de los Pods. No hay dos categorías de recursos en Kubernetes. Los nativos y el tuyo se sirven por el mismo API server, se guardan en el mismo etcd, se protegen con el mismo RBAC y se consultan con el mismo kubectl.

Continúa con la siguiente unidad para crear objetos de tu tipo nuevo... y descubrir qué pasa cuando lo haces.

El silencio

Tienes un tipo nuevo en la API. Vamos a usarlo.

Paso 4: Crea un Custom Resource

Crea promocion.yaml:

cat << 'EOF' > promocion.yaml
apiVersion: tienda.example.com/v1
kind: Promocion
metadata:
  name: rebajas-verano
spec:
  banner: "https://tienda.example.com"
  replicas: 3
EOF

El YAML, explicado con preguntas y respuestas

¿De dónde sale apiVersion: tienda.example.com/v1?

De spec.group + spec.versions[].name del CRD. Es exactamente así como el API server sabe a qué CRD pertenece este objeto: busca un CRD registrado cuyo grupo y versión coincidan. Si no lo encuentra, kubectl apply falla con el error más común del mundo de los operadores: no matches for kind "Promocion" in version "tienda.example.com/v1". Cuando lo veas en un clúster real, casi siempre significa lo mismo: el CRD no está instalado.

¿Y kind: Promocion?

De spec.names.kind. Ese es el contrato entre los dos archivos: el CRD declara que el kind válido se llama Promocion, y el recurso lo usa tal cual.

¿Por qué solo banner y replicas?

Porque son los únicos campos que el esquema declaró. No es una convención: es una frontera. Lo vas a comprobar en el paso siguiente.

Y la pregunta que importa: si aplico esto, ¿se crean tres Pods?

No. No se crea absolutamente nada.

Guarda esa respuesta. La vas a verificar tú mismo en dos minutos.

Aplícalo

kubectl apply -f promocion.yaml
kubectl get promociones

Fíjate en la salida: ahí están tus additionalPrinterColumns, con el banner y las réplicas. kubectl get promo también funciona.

Paso 5: El esquema es una frontera real

Intenta engañar al API server. Aplica esto sin guardarlo, directamente desde la terminal:

cat << 'YAML' | kubectl apply -f -
apiVersion: tienda.example.com/v1
kind: Promocion
metadata:
  name: promocion-invalida
spec:
  banner: "https://ejemplo.com"
  replicas: "tres"
YAML

El API server lo rechaza: replicas está declarado como integer y "tres" es una cadena. Prueba también con replicas: 50 (supera el maximum), y con un campo que no existe:

cat << 'YAML' | kubectl apply -f -
apiVersion: tienda.example.com/v1
kind: Promocion
metadata:
  name: promocion-invalida
spec:
  banner: "https://ejemplo.com"
  color: azul
YAML

Ese último te devolverá un aviso de campo desconocido, y el color no se guardará.

Esto no lo está haciendo ningún código tuyo. Lo hace el API server, con el esquema que le diste, antes de escribir nada en etcd. La validación de tu API la escribiste en seis líneas de YAML.

Paso 6: El silencio

Ahora, el momento de la lección. Tu Promocion declara replicas: 3 y un banner. Mira qué ha provocado en el clúster:

kubectl get pods
kubectl get deployments
kubectl get services
kubectl get events --sort-by=.lastTimestamp

No hay nada. Ni un Pod. Ni un Deployment. Ni un evento. Ni una queja.

Esta es una tarea rara: la superas no haciendo nada. Y es la más importante del módulo.

Un CRD le enseña a Kubernetes un sustantivo, no un verbo. Le has dicho al API server cómo se escribe, se valida y se guarda un Promocion. No le has dicho qué significa. spec.replicas: 3 es, para el clúster, exactamente tan significativo como spec.color: azul: una cadena de bytes con un esquema. Nadie lo está leyendo.

Y aquí es donde mucha gente se lleva su primera decepción con los operadores: instala el CRD de un producto, aplica el recurso personalizado, y espera a que ocurra algo que nunca ocurre. El objeto está ahí. kubectl get lo muestra. Y no pasa nada, porque el controlador no está desplegado.

Resumen

  • Un CRD añade un tipo a la API de Kubernetes: endpoint REST, validación por esquema, almacenamiento en etcd, RBAC, kubectl. Todo lo que tienen los recursos nativos.
  • El openAPIV3Schema no es burocracia: es tu contrato de API, y lo hace cumplir el API server, no tu código.
  • additionalPrinterColumns convierte tu recurso en algo operable desde la terminal.
  • Un CRD no tiene comportamiento. Define un sustantivo. Crear el objeto no ejecuta nada.
  • La pieza que convierte un objeto declarativo en cambios reales en el clúster es un controlador. Sin él, tu recurso es un post-it muy bien validado.
Next lesson
El operador