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.
compositeTypeRefexplicitly defines the target composite resource.rdsInstanceandserviceare 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 thespec.forProviderof theRDSInstanceandspec.portsof theService. This is the "patching" mechanism. $ {self.spec.parameters.dbEngine}-{self.metadata.name}demonstrates KCL’s templating capabilities for generating dynamic resource names.- The
compositionvariable assembles these into the final CrossplaneCompositionobject.
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.