KCL’s ability to define Crossplane Compositions isn’t about writing new Crossplane code; it’s about defining the desired state of your composed resources with a declarative language that’s more robust than YAML.

Let’s say you want to provision a Kubernetes Service and an AWS RDSInstance together as a single managed resource. Normally, you’d use a Crossplane Composition resource, which is a YAML file. This YAML defines a CompositeResourceDefinition (CRD) and a Composition that maps fields from the composite resource to the underlying managed resources.

Here’s a simplified example of a Crossplane Composition in YAML:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: manageddatabases.example.com
spec:
  compositeTypeRef:
    apiVersion: example.com/v1alpha1
    kind: ManagedDatabase
  resources:
    - name: rdsinstance
      base:
        apiVersion: database.aws.crossplane.io/v1beta1
        kind: RDSInstance
        spec:
          region: us-east-1
          providerConfigRef:
            name: aws-provider
          forProvider:
            dbInstanceClass: db.t3.small
            masterUsername: admin
      patches:
        - fromFieldPath: spec.parameters.storageGB
          toFieldPath: spec.forProvider.allocatedStorage
        - fromFieldPath: spec.parameters.dbEngine
          toFieldPath: spec.forProvider.engine
        - fromFieldPath: spec.parameters.dbEngineVersion
          toFieldPath: spec.forProvider.engineVersion
    - name: service
      base:
        apiVersion: v1
        kind: Service
        metadata:
          labels:
            app: managed-database
      spec:
        type: ClusterIP
      patches:
        - fromFieldPath: metadata.name
          toFieldPath: spec.selector.app
        - fromFieldPath: spec.parameters.port
          toFieldPath: spec.ports[0].port
        - fromFieldPath: spec.parameters.port
          toFieldPath: spec.ports[0].targetPort

Now, let’s translate this into KCL. KCL allows you to define this same logic in a structured, type-safe way. You’re not writing imperative code; you’re defining the shape and relationships of your Kubernetes resources.

First, you’d define your CompositeResourceDefinition (CRD) in KCL. This specifies the schema for your custom composite resource, ManagedDatabase in this case.

# manageddatabase_crd.kcl

schema ManagedDatabaseSpecParameters:
    storageGB: int
    dbEngine: str
    dbEngineVersion: str
    port: int

schema ManagedDatabaseSpec:
    parameters: ManagedDatabaseSpecParameters

schema ManagedDatabase:
    apiVersion: "example.com/v1alpha1"
    kind: "ManagedDatabase"
    spec: ManagedDatabaseSpec

Next, you define the Composition itself. This is where KCL shines by providing a more organized way to declare the underlying managed resources and how they map to your composite resource.

# manageddatabase_composition.kcl

import "github.com/crossplane/crossplane/apis/apiextensions/v1"
import "github.com/crossplane/crossplane/apis/apiextensions/v1beta1" # For Composition
import "github.com/crossplane/provider-aws/apis/database/v1beta1"      # For RDSInstance
import "k8s.io/api/core/v1"                                          # For Service

# Define the CRD reference
compositeTypeRef = v1.TypeReference {
    apiVersion: "example.com/v1alpha1"
    kind: "ManagedDatabase"
}

# Define the RDSInstance resource
rdsInstance = database.aws.crossplane.io.v1beta1.RDSInstance {
    metadata: {
        name: "$ {self.spec.parameters.dbEngine}-{self.metadata.name}" # Example of dynamic naming
    }
    spec: {
        region: "us-east-1"
        providerConfigRef: {
            name: "aws-provider"
        }
        forProvider: {
            dbInstanceClass: "db.t3.small"
            masterUsername: "admin"
            allocatedStorage: self.spec.parameters.storageGB
            engine: self.spec.parameters.dbEngine
            engineVersion: self.spec.parameters.dbEngineVersion
        }
    }
}

# Define the Service resource
service = core.v1.Service {
    metadata: {
        labels: {
            app: self.metadata.name # Example: label matches composite resource name
        }
    }
    spec: {
        type: "ClusterIP"
        ports: [
            core.v1.ServicePort {
                port: self.spec.parameters.port
                targetPort: self.spec.parameters.port
            }
        ]
        selector: {
            app: self.metadata.name # Selector matches the service's own label
        }
    }
}

# Assemble the Composition
composition = v1beta1.Composition {
    metadata: {
        name: "manageddatabases.example.com"
    }
    spec: {
        compositeTypeRef: compositeTypeRef
        resources: [
            v1beta1.ComposedTemplate {
                name: "rdsinstance"
                base: rdsInstance
            },
            v1beta1.ComposedTemplate {
                name: "service"
                base: service
            }
        ]
    }
}

# Export the composition
export composition

In this KCL Composition file:

  • We import necessary Crossplane and Kubernetes API types.
  • compositeTypeRef explicitly defines the target composite resource.
  • rdsInstance and service are KCL schemas that represent the desired state of the AWS RDS instance and Kubernetes Service.
  • Crucially, self.spec.parameters.storageGB, self.spec.parameters.dbEngine, etc., directly reference fields from the input composite resource (ManagedDatabase) and map them to the spec.forProvider of the RDSInstance and spec.ports of the Service. This is the "patching" mechanism.
  • $ {self.spec.parameters.dbEngine}-{self.metadata.name} demonstrates KCL’s templating capabilities for generating dynamic resource names.
  • The composition variable assembles these into the final Crossplane Composition object.

When you compile this KCL code, KCL generates the YAML for the Crossplane Composition resource. This allows you to use KCL’s features like schema validation, type checking, and modularity to manage your Crossplane configurations.

The most surprising thing about using KCL for Crossplane Compositions is that you’re not writing code to imperatively create resources. You’re writing a declarative schema that describes the desired state of your composite and its underlying managed resources, and KCL ensures that description is consistent and valid.

Consider a scenario where you want to provision a more complex setup, perhaps involving multiple instances of a resource or conditional creation based on input parameters. With KCL, you can use its control flow features (though sparingly, as the goal is declarative) or simply define more sophisticated schemas that represent these variations. For example, you could define a ManagedDatabase with an optional replicaCount parameter and then conditionally create read replicas based on that.

Here’s how you might dynamically generate a list of ports for a Service in KCL, which is harder to express cleanly in raw YAML:

# ... (previous definitions) ...

# Example of dynamic port generation (conceptual)
# If you had a list of ports in your parameters:
# schema ManagedDatabaseSpecParameters:
#     ...
#     ports: List[int]

# service = core.v1.Service {
#     ...
#     spec: {
#         ...
#         ports: [
#             core.v1.ServicePort {
#                 port: p
#                 targetPort: p
#             } for p in self.spec.parameters.ports # KCL list comprehension
#         ]
#         ...
#     }
# }

# ... (rest of composition) ...

This list comprehension [ ... for p in self.spec.parameters.ports ] is a powerful KCL construct that allows you to generate lists of objects programmatically within your declarative definitions, making complex configurations more manageable.

The next step after defining your Compositions in KCL is typically to integrate them into a CI/CD pipeline where KCL is compiled to YAML and then applied to your Kubernetes cluster, or to use Crossplane’s own tools that can consume KCL directly.

Want structured learning?

Take the full Crossplane course →