The Universal Control Plane – Crossplane 102 – XRDs

In the previous post, I introduced the basic components of the Crossplane machine. In this post, I’ll dig a little deeper into Compositions and Composite Resource Definitions.

We know Kubernetes allows us to extend the functionality of a K8s control plane with Custom Controllers and Custom Resources. The Custom Resource Definition (CRD) API provides us with the interface to define our custom resources and register them with the K8s API server.

Core Crossplane creates a number of CRDs to enable its machinery. When we install a Crossplane Provider, we create yet more CRDs that are specific to the Provider’s machinery. Composition and Composite Resource Definition (XRD) are two of the primary core crossplane CRDs that we will look at in detail here.

Note: Custom Resource Definition is a core K8s component and is abbreviated ‘CRD’. Composite Resource Definition is a core Crossplane component, abbreviated ‘XRD’ to avoid confusion with ‘CRD’. The ‘X’ can be interchanged with ‘Composite’ when thinking of this resource type.

I think of Composition as a vehicle for defining our own Custom Controller and Composite Resource (XR, which is created from a Composite Resource Definition) as the vehicle for defining API paths and CRDs that our controller will be associated with.

When first reading the Crossplane docs, the relationship between XRD and XR was a bit confusing to me. It is actually quite simple and straight forward. An XRD is the definition/schema we write, that is then instantiated as an XR. When we send the XRD to  the api server, it will actually create a K8s CRD that can be created as a resource (XR) for our compositions. That last part was the head scratcher for me at first glance. If we think of Compositions as our Custom Controller, we can think of XRD (Define) -> XR (Instantiate) as the Custom Resources for our Custom Controllers (Compositions).

To recap, an XRD will become a CRD, the CRD is instantiated as a Custom Resource (Referred to as a Composite Resource (XR) in Crossplane parlance), and a Composition is passed the values we define in the XR to perform the operations we have configured within it. In this way, Crossplane allows us to create our own Custom Resources that feed into our own Custom Controllers, all with zero coding required.

To illustrate the XRD -> CRD -> XR concept, I’ll define an XRD and show the resulting K8s CRD, then I’ll create an instance of the Custom Resource and show it with kubectl get <custom resource type>. The XRD is created via the apiextensions.crossplane.io API as type CompositeResourceDefinition. There are two primary components to the resource definition. First, we provide info on what we want the resulting CRD to be called and what API it should fall under. These are both arbitrary and can be whatever we would like.

In this snippet, we define a group name on line 7. This will be the resulting API path coupled with the version from line 12. The name of the CRD is established from the spec.names.plural value located on line 10.

---
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xnetworks.azure.platformref.crossplane.io
spec:
  group: azure.platformref.crossplane.io
  names:
    kind: XNetwork
    plural: xnetworks
  versions:
    - name: v1alpha1

The second part of our definition will be where the schema is defined. For example, if we wanted to pass a value for network reference name/ID, we would define that as a field in our schema. You can see the beginning of this at line 16. As does core K8s with CRDs, Crossplane uses the openAPIV3Schema syntax to define schema for XRDs

At line 20, we begin with our ‘spec’ block, under spec we have defined two fields (id and clusterRef). Within clusterRef, we have defined a single field of id. Then at the end of each respective block in the hierarchy we are defining fields that are required. This is a minimal XRD.

---
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xnetworks.azure.platformref.crossplane.io
spec:
  claimNames:
    kind: Network
    plural: networks
  group: azure.platformref.crossplane.io
  names:
    kind: XNetwork
    plural: xnetworks
  versions:
    - name: v1alpha1
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                id:
                  type: string
                  description: ID of this Network that other objects will use to refer to it.
                clusterRef:
                  type: object
                  description: "A reference to the Cluster object that this network will be used for."
                  properties:
                    id:
                      type: string
                      description: ID of the Cluster object this ref points to.
                  required:
                    - id
              required:
                - id
               - clusterRef

From here, we kubectl create -f our XRD manifest, and then we can kubectl get crd to see the new CRD defined in our cluster.

Now all we have to do is create an instance of that CRD (Which we know will be referred to a Composite Resource and/or XR in Crossplane parlance). This sample manifest would do that for us:

---
apiVersion: azure.platformref.crossplane.io/v1alpha1
kind: XNetwork
metadata:
  name: network
spec:
  id: platformref-network-azure
  clusterRef:
    id: platformref-cluster-azure

We can see on line 2 we specify the API path, line 3 we specify the Custom Resource kind, line 5 specifies the name we will give this instance, and line 7 begins the schema we defined. Let’s see the result of creating our instance. You’ll see there are a lot of lines injected by the Crossplane machine, but you see the spec.clusterRef.id field here. This XR has already been consumed by a composition so there are some references from that as well. For now, you only need to take this as the end result after defining an XRD and creating an XR from it.

So now we can define and create our own personalized CRDs via Crossplane XRDs and create instances of them. The next step is to have the values from these instances do something meaningful. And as is the case with core K8s that has controllers to watch for resource instances they’re responsible for, we can create our own Custom Controllers that will be responsible for taking our XR’s values and doing something meaningful with them. That is where Composition comes in and where I’ll pickup in my next post.

For more information on Composite Resources, check out the Crossplane docs here.