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.

Want structured learning?

Take the full Crossplane course →