Crossplane’s Composite Resource Definitions (XRDs) let you define your own higher-level abstractions on top of existing Kubernetes resources, essentially building your own API.

Let’s say you want to offer a "ManagedDatabase" to your developers. They don’t need to know about the underlying AWS RDS instances, VPCs, or security groups. They just want a database, and they want to provision it via an API.

Here’s a simplified look at what a developer might experience:

apiVersion: db.example.com/v1alpha1
kind: ManagedDatabase
metadata:
  name: my-app-db
spec:
  parameters:
    storageGB: 50
    engine: postgres
    version: "13.4"

This ManagedDatabase is a custom resource (CR) defined by an XRD. When a developer creates this ManagedDatabase object, Crossplane, using a Composition, translates this high-level request into concrete, lower-level Kubernetes resources.

The Problem: Bridging the Cloud and Kubernetes Gap

Cloud resources (like databases, message queues, load balancers) are often provisioned and managed outside of Kubernetes. Developers need a way to request these resources using familiar Kubernetes patterns (declarative YAML, kubectl) without needing to understand the intricacies of each cloud provider’s API or the specific Kubernetes operators (like the AWS Provider for Crossplane) that manage them. This is where Crossplane’s XRDs shine. They allow you to create a unified API for both Kubernetes-native resources and external cloud services.

How it Works: XRDs, Compositions, and Providers

  1. Composite Resource Definition (XRD): This is the schema for your new, higher-level API. It defines the kind, apiVersion, and the spec.parameters that users will provide. It’s the blueprint for your custom resource.

    apiVersion: apiextensions.crossplane.io/v1
    kind: CompositeResourceDefinition
    metadata:
      name: xmanageddatabases.db.example.com
    spec:
      group: db.example.com
      names:
        kind: ManagedDatabase
        plural: manageddatabases
      versions:
        - name: v1alpha1
          served: true
          referenceable: true
          schema:
            openAPIV3Schema:
              type: object
              properties:
                spec:
                  type: object
                  properties:
                    parameters:
                      type: object
                      properties:
                        storageGB:
                          type: integer
                          description: "Size of the database in GiB."
                        engine:
                          type: string
                          description: "Database engine type (e.g., postgres, mysql)."
                        version:
                          type: string
                          description: "Database engine version."
                      required:
                        - storageGB
                        - engine
                        - version
                  required:
                    - parameters
    

    This XRD defines ManagedDatabase as a new Kubernetes API resource. It specifies that users must provide storageGB, engine, and version in the spec.parameters.

  2. Composition: This is the implementation of your XRD. It tells Crossplane how to provision the actual cloud resources based on the ManagedDatabase definition. A Composition references a CompositeResourceDefinition and lists the Managed Resources (e.g., RDSInstance, DBSubnetGroup, DBSecurityGroup) that should be created.

    apiVersion: apiextensions.crossplane.io/v1
    kind: Composition
    metadata:
      name: manageddatabases.db.example.com
      labels:
        provider: aws
    spec:
      compositeTypeRef:
        apiVersion: db.example.com/v1alpha1
        kind: ManagedDatabase
      resources:
        # Define the RDS Instance
        - name: rdsinstance
          base:
            apiVersion: rds.aws.upbound.io/v1beta1 # This comes from the AWS Provider
            kind: Instance
            spec:
              storageEncrypted: true
              skipFinalSnapshot: true
              dbSubnetGroupName: ${local.dbSubnetGroupName}
              vpcSecurityGroupIDRefs:
                - name: rds-sg
          patches:
            - fromFieldPath: spec.parameters.storageGB
              toFieldPath: spec.forProvider.allocatedStorage
            - fromFieldPath: spec.parameters.engine
              toFieldPath: spec.forProvider.engine
            - fromFieldPath: spec.parameters.version
              toFieldPath: spec.forProvider.engineVersion
            - fromFieldPath: metadata.name
              toFieldPath: spec.forProvider.identifier
              transforms:
                - type: string
                  string:
                    # Truncate and add suffix to avoid identifier too long
                    fmt: "%s-db"
    
        # Define the DB Subnet Group
        - name: dbsubnetgroup
          base:
            apiVersion: rds.aws.upbound.io/v1beta1
            kind: DBSubnetGroup
            spec:
              # This would typically be populated from a Network resource
              # For simplicity, let's assume a common subnet set for now
              subnetIdRefs:
                - name: subnet-1a # Placeholder, would be dynamic
                - name: subnet-1b # Placeholder, would be dynamic
          patches:
            - fromFieldPath: metadata.name
              toFieldPath: spec.forProvider.name
              transforms:
                - type: string
                  string:
                    fmt: "%s-subnet-group"
    
        # Define the Security Group
        - name: rds-sg
          base:
            apiVersion: ec2.aws.upbound.io/v1beta1
            kind: SecurityGroup
            spec:
              description: "Allow PostgreSQL traffic"
              ingress:
                - fromPort: 5432
                  toPort: 5432
                  protocol: tcp
                  cidrBlocks:
                    - 0.0.0.0/0 # WARNING: Insecure! Should be restricted.
          patches:
            - fromFieldPath: metadata.name
              toFieldPath: spec.forProvider.tags:0:value
              transforms:
                - type: string
                  string:
                    fmt: "%s-rds-sg"
    
    

    This Composition links the ManagedDatabase XRD to actual AWS RDS resources (rds.aws.upbound.io/v1beta1.Instance, rds.aws.upbound.io/v1beta1.DBSubnetGroup, ec2.aws.upbound.io/v1beta1.SecurityGroup). It uses patches to map the user-provided spec.parameters to the spec.forProvider fields of the managed resources. It also defines dependencies between resources (e.g., rdsinstance depends on dbsubnetgroup and rds-sg).

  3. Provider: This is the piece that actually talks to the cloud API. In this example, we’re using the AWS Provider for Crossplane (aws.upbound.io). This provider has controllers that watch for specific managed resources (like rds.aws.upbound.io/v1beta1.Instance) and translate their desired state into AWS API calls.

When a ManagedDatabase CR is created, Crossplane’s core controllers:

  • Find a Composition that matches the ManagedDatabase’s apiVersion and kind.
  • Use the Composition to render the desired Managed Resources (e.g., rds.aws.upbound.io/v1beta1.Instance, ec2.aws.upbound.io/v1beta1.SecurityGroup).
  • The relevant Crossplane Provider (e.g., AWS Provider) controllers then act on these Managed Resources, creating or updating the actual cloud infrastructure.

The Hidden Lever: Resource Overlays and Transformations

When defining Compositions, you’re not just mapping fields directly. You can use transforms to manipulate data. For instance, you might take a string input and format it into a specific identifier pattern for AWS, or combine multiple fields into one. You can also use patches to set default values, enforce specific configurations (like storageEncrypted: true), or reference other resources created by the Composition. This allows for complex logic and automation within your self-service API without requiring the end-user to specify every detail.

The next step in building a robust self-service platform is to manage the lifecycle of these composed resources, such as handling upgrades and deletions gracefully.

Want structured learning?

Take the full Crossplane course →