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
-
Composite Resource Definition (XRD): This is the schema for your new, higher-level API. It defines the
kind,apiVersion, and thespec.parametersthat 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: - parametersThis XRD defines
ManagedDatabaseas a new Kubernetes API resource. It specifies that users must providestorageGB,engine, andversionin thespec.parameters. -
Composition: This is the implementation of your XRD. It tells Crossplane how to provision the actual cloud resources based on the
ManagedDatabasedefinition. A Composition references aCompositeResourceDefinitionand 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
ManagedDatabaseXRD to actual AWS RDS resources (rds.aws.upbound.io/v1beta1.Instance,rds.aws.upbound.io/v1beta1.DBSubnetGroup,ec2.aws.upbound.io/v1beta1.SecurityGroup). It usespatchesto map the user-providedspec.parametersto thespec.forProviderfields of the managed resources. It also defines dependencies between resources (e.g.,rdsinstancedepends ondbsubnetgroupandrds-sg). -
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 (likerds.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’sapiVersionandkind. - 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.