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.
  • kubectl configurado 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érminoQué es
XRD (CompositeResourceDefinition)Define el esquema de una API nueva, como App
XR (composite resource)Cada objeto creado con esa API, como my-app
CompositionIndica qué recursos crear para cada XR y cómo rellenarlos
FunctionMódulo que ejecuta la lógica de la Composition (YAML, plantillas, Python...)
ProviderPaquete 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

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.

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.