Compositions are how Crossplane lets you define custom, reusable cloud resources from existing managed resources.
Let’s see a simple Composition in action. Imagine you want a ProductionDatabase that is always a PostgreSQL instance with a specific tier, a specific backup policy, and a specific network configuration. You don’t want developers to have to remember all that every time.
Here’s a CompositeResourceDefinition (XRD) that defines our new ProductionDatabase type:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: productiondatabases.database.example.org
spec:
group: database.example.org
names:
kind: ProductionDatabase
plural: productiondatabases
claimNames:
kind: ProductionDatabase # This is what users will create
plural: productiondatabases
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 called ProductionDatabase in the database.example.org group. It has a spec that requires storageGB and version.
Now, here’s the Composition that defines what a ProductionDatabase is under the hood. It will create an RDS instance and a security group.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: rds-postgres-composition
labels:
provider: aws
db-type: postgres
spec:
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: ProductionDatabase
resources:
# This is the AWS RDS Instance
- name: rds-instance
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
region: us-east-1
instanceClass: db.r5.large
skipFinalSnapshot: true
# These will be patched from the Composite Resource Claim
parameters:
allocatedStorage: 100 # Default, but will be patched
engine: postgres
engineVersion: "13.7" # Default, but will be patched
publiclyAccessible: false
patches:
# Patch storageGB from the claim's spec to the RDS instance's allocatedStorage
- type: FromCompositeFieldPath
fromFieldPath: spec.storageGB
toFieldPath: spec.parameters.allocatedStorage
# Patch version from the claim's spec to the RDS instance's engineVersion
- type: FromCompositeFieldPath
fromFieldPath: spec.version
toFieldPath: spec.parameters.engineVersion
# Patch the RDS instance's ARN to the composite resource's status
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.arn
toFieldPath: status.atProvider.rdsArn
# This is the AWS Security Group
- name: rds-security-group
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroup
spec:
region: us-east-1
description: "Allow access to RDS"
# These will be patched from the Composite Resource Claim
parameters:
ingress:
- fromPort: 5432
toPort: 5432
protocol: tcp
cidrBlocks: ["0.0.0.0/0"] # Insecure default, will be patched
patches:
# Patch the VPC ID from the RDS instance's status to the Security Group's vpcId
- type: FromCompositeFieldPath
fromFieldPath: status.atProvider.vpcId # This is the RDS instance's VPC ID
toFieldPath: spec.parameters.vpcId
# Patch the security group ID to the composite resource's status
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.id
toFieldPath: status.atProvider.securityGroupId
# This is the Security Group Rule to allow access to the RDS instance
- name: rds-security-group-rule
base:
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroupRule
spec:
region: us-east-1
type: ingress
fromPort: 5432
toPort: 5432
protocol: tcp
sourceSecurityGroupId: "" # Will be patched from the security group
securityGroupId: "" # Will be patched from the security group
patches:
# Patch the security group ID from the created security group
- type: FromCompositeFieldPath
fromFieldPath: status.id
toFieldPath: spec.parameters.securityGroupId
# Patch the source security group ID from the created security group
- type: FromCompositeFieldPath
fromFieldPath: status.id
toFieldPath: spec.parameters.sourceSecurityGroupId
This Composition tells Crossplane: "When someone creates a ProductionDatabase, create an RDS Instance and an RDS SecurityGroup and a SecurityGroupRule."
The base block defines the desired state of the managed resource (e.g., an AWS RDS instance). The patches block is where the magic happens. It maps fields from the ProductionDatabase claim (the user’s request) to the managed resources, and also maps status back up.
For instance, - type: FromCompositeFieldPath fromFieldPath: spec.storageGB toFieldPath: spec.parameters.allocatedStorage means "take the value from spec.storageGB on the ProductionDatabase claim and put it into spec.parameters.allocatedStorage on the RDS Instance."
Now, a developer can simply create a ProductionDatabase claim:
apiVersion: database.example.org/v1alpha1
kind: ProductionDatabase
metadata:
name: my-app-db
spec:
storageGB: 500
version: "14.3"
Crossplane sees this ProductionDatabase claim, finds the rds-postgres-composition (because its compositeTypeRef matches), and then creates the rds.aws.upbound.io/v1beta1.Instance and ec2.aws.upbound.io/v1beta1.SecurityGroup and ec2.aws.upbound.io/v1beta1.SecurityGroupRule resources based on the Composition’s definition and the claim’s spec.
The most surprising thing about Compositions is that they are themselves managed resources. You can version them, apply policies to them, and even use other Compositions to build more complex ones. It’s a recursive, declarative way to model your infrastructure.
The real power here is abstraction. Developers don’t need to know about RDS instance classes, security group rules, or AWS regions. They just ask for a ProductionDatabase with a certain storage size and version, and Crossplane, via the Composition, fulfills that request using the underlying cloud provider resources.
When you define a CompositeResourceDefinition, you’re essentially creating a new API type for your organization. The Composition is the implementation of that API type. You can have multiple Compositions for a single XRD, allowing for different implementations (e.g., a ProductionDatabase could be backed by AWS RDS or GCP Cloud SQL, and you’d have separate Compositions for each).
You can define arbitrary fields in your XRD’s schema. These fields can be simple values like strings and integers, or complex nested objects. These fields become the inputs to your Compositions, allowing you to parameterize your custom resources.
The ToCompositeFieldPath patch type is crucial for exposing information about the created managed resources back to the Composite Resource. This allows you to build more sophisticated Compositions where one managed resource’s status informs another’s configuration, or where you can surface important identifiers (like ARNs or IDs) to the user of the Composite Resource.
The base block in a Composition can include multiple managed resources. Crossplane will create all of them. You can also use patches to establish relationships between these managed resources. For example, you can patch the id of a created SecurityGroup into the securityGroupId field of a SecurityGroupRule managed resource.
The next step is exploring how to use patchSets to avoid repetition when patching multiple managed resources with the same logic.