Un tipo de recurso propio (CRD)
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.
💡 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 enkubectl get promociones.singular(promocion): lo que aparece en mensajes de error y en la ayuda.shortNames(promo): el alias corto, para escribirkubectl 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 sinbannerse rechaza en elapply.minimum: 1/maximum: 10:replicas: 50se rechaza. Nadie escribe código para esto.default: 1: si el usuario omitereplicas, 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
openAPIV3Schemano es burocracia: es tu contrato de API, y lo hace cumplir el API server, no tu código. additionalPrinterColumnsconvierte 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.
- Previous lesson
- Los comandos que no salen en los manuales
- Next lesson
- El operador