Crossplane’s Composite Resource Definitions (XRDs) let you define your own cloud primitives, effectively building your own internal PaaS on top of existing cloud providers.
Let’s see this in action. Imagine you want a "ManagedDatabase" that’s always a PostgreSQL instance with specific configurations, regardless of whether it’s running on AWS RDS or GCP Cloud SQL.
First, we define the Composite Resource Definition (XRD):
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: manageddatabases.mycompany.com
spec:
group: mycompany.com
names:
kind: ManagedDatabase
plural: manageddatabases
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
storageGB:
type: integer
version:
type: string
required:
- storageGB
- version
This tells Crossplane about a new kind of resource: ManagedDatabase. It has two spec fields: storageGB and version.
Next, we define a Composite Resource (XResource) of this new kind. This is where we specify the actual cloud resources that will be provisioned. We’ll use a CompositeToManaged composition.
apiVersion: mycompany.com/v1alpha1
kind: ManagedDatabase
metadata:
name: my-prod-db
spec:
parameters:
storageGB: 100
version: "13.4"
compositionSelector:
matchLabels:
composition: aws-rds-postgres
This ManagedDatabase resource, my-prod-db, requests 100GB storage and PostgreSQL version 13.4. It also uses a compositionSelector to find a specific composition.
Now, the composition itself. This is the "glue" that maps our abstract ManagedDatabase to concrete cloud resources.
apiVersion: core.crossplane.io/v1
kind: Composition
metadata:
name: aws-rds-postgres
labels:
composition: aws-rds-postgres
spec:
compositeTypeRef:
apiVersion: mycompany.com/v1alpha1
kind: ManagedDatabase
writeConnectionSecretToFieldPaths:
- connectionSecretRef.name
- connectionSecretRef.namespace
patchSets:
- name: common-patches
patches:
- fromFieldPath: spec.parameters.version
toFieldPath: spec.forProvider.engineVersion
- fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.allocatedStorage
resources:
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
region: us-east-1
skipFinalSnapshot: true
dbSubnetGroupName: default-rds-subnet-group
vpcSecurityGroupIds:
- sg-0123456789abcdef0
publiclyAccessible: false
patches:
- patchSetRef:
name: common-patches
- fromFieldPath: spec.parameters.version
toFieldPath: spec.forProvider.engine
transforms:
- type: map
map:
"13.4": postgres
"14.2": postgres
- fromFieldPath: metadata.name
toFieldPath: spec.writeConnectionSecretToRef.name
transforms:
- type: prepend
string: managed-db-
- fromFieldPath: metadata.namespace
toFieldPath: spec.writeConnectionSecretToRef.namespace
This Composition named aws-rds-postgres describes how to fulfill a ManagedDatabase. It uses patchSets to copy version and storageGB from our ManagedDatabase spec to the underlying AWS RDS Instance spec. It also maps the version to the correct engine type (e.g., 13.4 becomes postgres). Crucially, it defines the actual AWS RDS Instance resource that will be created.
When you apply these, Crossplane’s engine orchestrates the creation of an AWS RDS instance. The ManagedDatabase resource becomes a single pane of glass for your developers, hiding the underlying cloud specifics. They don’t need to know about RDS instance classes, storage types, or regions; they just ask for a ManagedDatabase with their desired specs.
The writeConnectionSecretToFieldPaths and the patching for spec.writeConnectionSecretToRef are key for making connection details (like endpoints and credentials) available to your applications. Crossplane automatically populates a Kubernetes secret with this information.
What most people miss is how you can use transforms within patches to perform complex data manipulation. For example, you might need to convert a simple version string into a specific database engine identifier, or conditionally set a field based on a parameter value. This allows your abstract types to remain clean while the composition handles the intricate mapping to provider-specific APIs.
The next step is to define a composition for GCP Cloud SQL, allowing my-prod-db to be provisioned there with a simple change in its compositionSelector.