Crossplane convierte un clúster de Kubernetes en un plano de control para infraestructura: defines APIs propias (por ejemplo, "una aplicación" o "una base de datos") y Crossplane crea y mantiene los recursos que hay detrás, ya sean objetos de Kubernetes o servicios de un proveedor cloud. En este tutorial instalarás Crossplane v2 con Helm, crearás una API App que genera un Deployment y un Service mediante una Composition, y opcionalmente gestionarás un bucket de S3 con el proveedor de AWS.
Requisitos previos
- Un clúster de Kubernetes con una versión soportada y al menos 2 GB de RAM libres. Sirve un clúster de un solo nodo con k3s en un VPS de CubePath con Ubuntu 24.04.
kubectlconfigurado contra ese clúster, con permisos de administrador.- Helm 3.2 o posterior instalado en la máquina desde la que trabajas.
- Solo para el paso 7: una cuenta de AWS y un par de claves de acceso con permiso para crear buckets de S3.
Comprueba que tienes acceso al clúster:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
cp-node Ready control-plane,master 3d v1.33.x+k3s1
Conceptos básicos
Antes de empezar, conviene tener claros los términos que usa Crossplane:
| Término | Qué es |
|---|---|
| XRD (CompositeResourceDefinition) | Define el esquema de una API nueva, como App |
| XR (composite resource) | Cada objeto creado con esa API, como my-app |
| Composition | Indica qué recursos crear para cada XR y cómo rellenarlos |
| Function | Módulo que ejecuta la lógica de la Composition (YAML, plantillas, Python...) |
| Provider | Paquete que añade recursos gestionados de un servicio externo, como AWS S3 |
| Managed resource (MR) | Un recurso externo representado en Kubernetes, como un Bucket |
Paso 1: Instalar Crossplane con Helm
Añade el repositorio estable de Crossplane y actualiza el índice:
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
Instala Crossplane en su propio namespace:
helm install crossplane \
--namespace crossplane-system \
--create-namespace \
crossplane-stable/crossplane
Comprueba que los pods están en marcha:
kubectl get pods -n crossplane-system
NAME READY STATUS RESTARTS AGE
crossplane-6d67f8cd9d-g2gjw 1/1 Running 0 45s
crossplane-rbac-manager-86d9b5cf9f-2vc4s 1/1 Running 0 45s
Crossplane también ha registrado sus tipos en la API de Kubernetes:
kubectl api-resources | grep -E 'compositeresourcedefinitions|compositions|functions|providers'
La salida debe incluir compositeresourcedefinitions, compositions, functions y providers.
Paso 2: Definir la API con un XRD
Vas a crear un tipo nuevo, App, con un único campo obligatorio: la imagen del contenedor. En Crossplane v2 los XR pueden ser de namespace (scope: Namespaced), igual que un Deployment. Crea el archivo:
nano xrd.yaml
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: apps.example.crossplane.io
spec:
scope: Namespaced
group: example.crossplane.io
names:
kind: App
plural: apps
versions:
- name: v1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
image:
description: Imagen OCI del contenedor de la aplicación.
type: string
required:
- image
status:
type: object
properties:
replicas:
description: Número de réplicas disponibles.
type: integer
address:
description: IP interna del servicio.
type: string
El nombre del XRD debe ser <plural>.<group>. Aplícalo y comprueba que Kubernetes ya sirve la nueva API:
kubectl apply -f xrd.yaml
kubectl get xrd
NAME ESTABLISHED OFFERED AGE
apps.example.crossplane.io True 12s
Paso 3: Instalar la función patch-and-transform
Crossplane no sabe todavía qué hacer al crear un App. Esa lógica la ejecutan las funciones de composición. La función patch-and-transform permite describir los recursos en YAML y copiar valores del XR a ellos. Crea el archivo:
nano function.yaml
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: crossplane-contrib-function-patch-and-transform
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.8.2
Aplícalo y espera a que el paquete esté instalado y sano:
kubectl apply -f function.yaml
kubectl get functions
NAME INSTALLED HEALTHY PACKAGE AGE
crossplane-contrib-function-patch-and-transform True True xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.8.2 30s
Notaconsulta las versiones publicadas de la función y usa la más reciente si lo prefieres.
Paso 4: Crear la Composition
La Composition indica que cada App se compone de un Deployment y un Service. Los parches copian el nombre del XR a las etiquetas y la imagen al contenedor, y devuelven al status del XR las réplicas disponibles y la IP del servicio. Crea el archivo:
nano composition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: app-yaml
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1
kind: App
mode: Pipeline
pipeline:
- step: create-deployment-and-service
functionRef:
name: crossplane-contrib-function-patch-and-transform
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
- name: deployment
base:
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 2
template:
spec:
containers:
- name: app
ports:
- containerPort: 80
patches:
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: metadata.labels[example.crossplane.io/app]
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.selector.matchLabels[example.crossplane.io/app]
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.template.metadata.labels[example.crossplane.io/app]
- type: FromCompositeFieldPath
fromFieldPath: spec.image
toFieldPath: spec.template.spec.containers[0].image
- type: ToCompositeFieldPath
fromFieldPath: status.availableReplicas
toFieldPath: status.replicas
readinessChecks:
- type: MatchCondition
matchCondition:
type: Available
status: "True"
- name: service
base:
apiVersion: v1
kind: Service
spec:
ports:
- protocol: TCP
port: 8080
targetPort: 80
patches:
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: metadata.labels[example.crossplane.io/app]
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.selector[example.crossplane.io/app]
- type: ToCompositeFieldPath
fromFieldPath: spec.clusterIP
toFieldPath: status.address
readinessChecks:
- type: NonEmpty
fieldPath: spec.clusterIP
Los readinessChecks le dicen a Crossplane cuándo considerar listo cada recurso: el Deployment cuando su condición Available es True, y el Service cuando tiene IP asignada. Aplica la Composition:
kubectl apply -f composition.yaml
kubectl get compositions
NAME XR-KIND XR-APIVERSION AGE
app-yaml App example.crossplane.io/v1 8s
Paso 5: Crear una App y comprobar el resultado
Ahora cualquier usuario del clúster puede pedir una aplicación con cuatro líneas de YAML, sin conocer los detalles del Deployment ni del Service:
nano app.yaml
apiVersion: example.crossplane.io/v1
kind: App
metadata:
namespace: default
name: my-app
spec:
image: nginx
Aplícalo y espera a que esté listo:
kubectl apply -f app.yaml
kubectl get app my-app
NAME SYNCED READY COMPOSITION AGE
my-app True True app-yaml 40s
SYNCED indica que Crossplane ha aplicado la Composition sin errores, y READY que todos los recursos compuestos han pasado sus comprobaciones. Mira los recursos que ha creado:
kubectl get deploy,service -l example.crossplane.io/app=my-app
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/my-app-2r2rk 2/2 2 2 45s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/my-app-xfkzg ClusterIP 10.43.148.56 <none> 8080/TCP 45s
Y comprueba que el status del App recoge los datos devueltos por los parches:
kubectl get app my-app -o jsonpath='{.status}{"\n"}'
{"address":"10.43.148.56","conditions":[...],"replicas":2}
Paso 6: Actualizar y borrar la App
Crossplane reconcilia de forma continua. Cambia la imagen en app.yaml a nginx:alpine y vuelve a aplicarlo:
kubectl apply -f app.yaml
kubectl get deploy -l example.crossplane.io/app=my-app -o jsonpath='{.items[0].spec.template.spec.containers[0].image}{"\n"}'
nginx:alpine
Si alguien edita o borra a mano el Deployment, Crossplane lo devuelve al estado que marca la Composition. Al borrar el App, Crossplane elimina también los recursos compuestos:
kubectl delete -f app.yaml
kubectl get deploy,service -l example.crossplane.io/app=my-app
No resources found in default namespace.
Paso 7: Gestionar un bucket de S3 con el proveedor de AWS (opcional)
Los proveedores añaden recursos gestionados que representan servicios externos. Instala el proveedor de AWS S3:
nano provider.yaml
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: crossplane-contrib-provider-aws-s3
spec:
package: xpkg.crossplane.io/crossplane-contrib/provider-aws-s3:v2.0.0
kubectl apply -f provider.yaml
kubectl get providers
NAME INSTALLED HEALTHY PACKAGE AGE
crossplane-contrib-provider-aws-s3 True True xpkg.crossplane.io/crossplane-contrib/provider-aws-s3:v2.0.0 60s
crossplane-contrib-provider-family-aws True True xpkg.crossplane.io/crossplane-contrib/provider-family-aws:v2.0.0 55s
El proveedor de S3 instala automáticamente provider-family-aws, que gestiona la autenticación de todos los proveedores de AWS. Guarda tus claves en un archivo temporal:
nano aws-credentials.ini
[default]
aws_access_key_id = your_access_key_id
aws_secret_access_key = your_secret_access_key
Crea un Secret con ellas y borra el archivo:
kubectl create secret generic aws-secret \
--namespace=crossplane-system \
--from-file=creds=./aws-credentials.ini
shred -u aws-credentials.ini
Indica al proveedor que use ese Secret con una ClusterProviderConfig, válida para recursos de todos los namespaces:
nano providerconfig.yaml
apiVersion: aws.m.upbound.io/v1beta1
kind: ClusterProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
namespace: crossplane-system
name: aws-secret
key: creds
kubectl apply -f providerconfig.yaml
Crea un bucket. Los nombres de S3 son únicos a nivel global, así que generateName añade un sufijo aleatorio:
nano bucket.yaml
apiVersion: s3.aws.m.upbound.io/v1beta1
kind: Bucket
metadata:
namespace: default
generateName: crossplane-bucket-
spec:
forProvider:
region: eu-west-1
Usa kubectl create en lugar de apply, porque generateName no funciona con apply:
kubectl create -f bucket.yaml
kubectl get buckets.s3.aws.m.upbound.io
NAME SYNCED READY EXTERNAL-NAME AGE
crossplane-bucket-7tfcj True True crossplane-bucket-7tfcj 2m
Cuando SYNCED y READY valen True, el bucket existe en AWS. Bórralo con kubectl delete buckets.s3.aws.m.upbound.io crossplane-bucket-7tfcj (usa el nombre que te haya salido) y Crossplane lo eliminará también de AWS.
Importanteborra los recursos gestionados antes de desinstalar el proveedor o Crossplane. Si el control plane desaparece, nadie limpia los recursos externos y tendrás que borrarlos a mano en la consola del proveedor.
Solución de problemas
El XR se queda con SYNCED en False. Revisa los eventos y las condiciones del objeto. Suelen indicar un error en un parche o que la función no está disponible:
kubectl describe app my-app
Una función o un proveedor no llega a HEALTHY. Revisa la descripción del paquete y los pods de crossplane-system:
kubectl describe function crossplane-contrib-function-patch-and-transform
kubectl get pods -n crossplane-system
El bucket se queda con READY en False. Casi siempre son las credenciales o los permisos de IAM. Los detalles aparecen en las condiciones del recurso: kubectl describe buckets.s3.aws.m.upbound.io <nombre>.
Error de permisos al componer otro tipo de recurso. Crossplane puede componer Deployment y Service sin configuración adicional, pero para otros tipos de Kubernetes puede necesitar permisos RBAC extra. Consulta la sección sobre acceso a recursos compuestos en la documentación de Compositions de Crossplane.
Conclusión
Has instalado Crossplane v2, definido una API App con un XRD, implementado su lógica con una Composition y la función patch-and-transform, y gestionado un recurso de AWS desde Kubernetes. Como siguientes pasos puedes añadir un recurso gestionado (por ejemplo el bucket) a la Composition de App, guardar XRD y Compositions en un repositorio Git sincronizado con Argo CD o Flux, o probar funciones con lógica más flexible como function-go-templating o function-python.
