The Universal Control Plane – Crossplane 103 – Composition

In the previous posts of this series, I introduced core Crossplane and more detail on Composite Resource (XR). In this post, I’ll dig deeper into Composition. This is where we get to see how Crossplane does something with the data we feed it.

In review, Managed Resources (MR) are high-fidelity Crossplane representations of various external API resources (i.e., they have values that are essentially a 1:1 match for the external resources they represent). They are the most discreet entity in the Crossplane machine.  Similar to a K8s Pod, we could create MRs directly. But like a K8s pod, we’re generally better served with an abstraction that does a bit more than just creating something and forgetting about it. So let’s spend a few seconds looking at why the XR -> Composition -> MR pattern is more desirable.

At a high level, we can find a benefit of Composition akin to that of the K8s Deployment -> Pod pattern. We can create Pods directly, but we receive additional value when creating them via Deployment. One thing that both Composition and Deployment share in this analogy is versioning. As with Deployment, we can rollback via Composition versions.

Like K8s, Crossplane has the capability to associate resources through labels.  An example of this could be when we create a Virtual Network and need to ‘place’ it within an AWS VPC or Azure Resource Group. We could create the VPC or Resource Group, assign it a K8s label and then lookup its ID via a selector that matches on that label. Or, we create the VPC/Resource Group and Virtual Network within the same Composition, and use a piece of the Crossplane machinery called matchControllerRef to accomplish this without setting labels. When creating the VPC/Resource Group, the ID will be stored in the resulting Composition, and we simply refer to it from any other MR we are creating within the same Composition.

We can define multiple Compositions and consume them from a single Composite Resource Definition. A simple example would be to have one Composite Resource Definition that we instantiate and point to a Composition specific for AWS and another pointed to one that is specific to GCP (Or any other cloud provider API). There is a bit of a divergence here in the analog of K8s Custom Controller to Crossplane Composition. A K8s Controller will watch for Custom Resources it is responsible for. With Crossplane, a Composite Resource actually calls upon a Composition. In that way, we can use multiple types of Compositions for a single XR type.

Another benefit of Composition is secrets management for generated resources. Allowing our application components to be securely connected as they are provisioned.

There are many things Compositions provide that make our provisioning better/stronger/faster (Maybe not faster, just a ’70s show throwback). For now and long story short, use Composite Resources with Compositions to create Managed Resources. (For additional detail, jump over to the Crossplane docs.)

Note: One thing I haven’t covered yet (and I think I’ll avoid it for now) is a Composite Resource Claim (XRC). It is an important piece of the puzzle once we begin to look at using Crossplane in production, but not necessary to understand basic Crossplane workings. So I will cover it in a separate post.

Let’s look at the XRD from the previous post. I’m going to pare it back to an absolute minimal XRD. Here we have an XRD that can be instantiated as kind XNetwork with the API endpoint azuretest.azure.crossplane.io/v1alpha1 that requires us to provide a spec.id string value. Remember that all of the definition here is arbitrary, so you could define it with an API path of XNetwork.whatever.you.like/v1 or anything else that suits your needs.

Note: As a reminder, like K8s with CRDs, Crossplane uses the openAPIV3Schema syntax to define schema for XRDs (XRDs become CRDs). If you haven’t been coding K8s Custom Controllers and CRDs, you may not have encountered this yet. Don’t sweat it, it’s just another syntax for defining a data based schema.

---
apiVersion: apiextensions.crossplane.io/v1  # We're using the Crossplane machinery
kind: CompositeResourceDefinition           # We're creating an XRD
metadata:
  name: xnetworks.azuretest.azure.crossplane.io # We're combining the plural name with group
spec:
  group: azuretest.azure.crossplane.io  # We're making up any api path we like
  names:
    kind: XNetwork      # We're making up any name we choose, and prefacing with 'X' to denote Composite Resource
    plural: xnetworks   # We're providing the plural version of the kind, naming practices dictate it is lowercase
  claimNames:
    kind: Network
    plural: networks
  versions:            #
  - name: v1alpha1    # We're defining am API version, will be used with Composition versioning in a similar way to Deployment Replicaset versions
    served: true
    referenceable: true
    schema:          # Begin schema definition
      openAPIV3Schema: # Lines 13 - 19 are essentially boilerplate for this section
      type: object
      properties:
        spec:          # Begin spec block definition
          type: object
          properties:
            id:        # Begin spec.id field - This is the field we decided we want creators of this thing to be able to specify. This could be called anything and we can add as many fields as we like
              type: string
              description: ID of this Network that other objects will use to refer to it.

We have a defined XRD that will be turned into a K8s CRD (With all of the K8s things that have to happen for a CRD to work for us). We can create an instance of this CRD (XR) with a value defined for spec.id. What should we provide for that value? Whatever string we want. What will happen when we do that? Nothing. Nothing unless we point that XR to a Composition that is.

So the next piece of the puzzle is… how do we provide this XR to a Composition and what does a Composition look like. Let’s go deeper and get to the point of this post with a Composition that services this XR.

There are multiple ways we can point an XR to a Composition. From specifying nothing and relying on a Default Composition, to explicitly specifying one. We can also use a selector to match on fields. The selector method is a convenient way to categorize Compositions based on target API endpoint (e.g. AWS, GCP, AZURE, etc.) For now, we’ll use spec.CompositionRef.name. You can read more about this topic in the Crossplane docs.

Let’s define a composition. For this we will continue with the Azure network example.

---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition  # We're specifying that we are defining a core Crossplane component
metadata:
  name: xnetworks.azuretest.azure.crossplane.io #
spec:
  compositeTypeRef:
    apiVersion: azuretest.azure.crossplane.io/v1alpha1
    kind: XNetwork # Referencing our XR kind
  resources:  # The resources block is where we begin defining what to create
    - base: # For each thing (MR) we create, we will include a 'base' block
        apiVersion: azure.crossplane.io/v1alpha3 # Here we are saying we want to create something in Azure
        kind: ResourceGroup  # Here we are saying we want to create a resource group
        spec:
          location: West US 2          # Here we are saying will be placed in West US 2 region
       patches:                       # Patches is how we take values from our XR and pass to Composition
         - fromFieldPath: spec.id     # We're taking the spec.id value from the XR
           toFieldPath: metadata.name # And injecting it into the MR metadata.name field
             transforms:      # Transforms allow us to manipulate the value passed in before setting to MR
             - type: string # In this case, we're adding '-rg' to the end of the XR spec.id value
               string:
                 fmt: "%s-rg"
         - fromFieldPath: spec.id
           toFieldPath: metadata.labels[crossplane.io/app]  # Here we are setting a label so that we can refer to it with a selector from another Composition if needed
    - base:
        apiVersion: network.azure.crossplane.io/v1alpha3
        kind: VirtualNetwork
        spec:
          resourceGroupNameSelector:  # This is the use of 'matchControllerRef' mentioned earlier
            matchControllerRef: true  # We are pointing this VirtualNework' to the Resource Group created above
        location: West US 2 # This is defined in the network.azure.crossplane.io/v1alpha3 API, it sets the Azure Region
        properties:
          addressSpace:
            addressPrefixes: ['192.168.0.0/16'] # This is another part of the network.azure.crossplane.io/v1alpha3 API, setting the CIDR for the VirtualNetwork
      patches:
        - fromFieldPath: spec.id
          toFieldPath: metadata.labels[crossplane.io/app]
        - fromFieldPath: spec.id
          toFieldPath: metadata.name
          transforms:
          - type: string
            string:
              fmt: "%s-vnet"
    - base:
        apiVersion: network.azure.crossplane.io/v1alpha3
        kind: Subnet
        spec:
          resourceGroupNameSelector:
            matchControllerRef: true
          virtualNetworkNameSelector:
            matchControllerRef: true
          properties:
           addressPrefix: '192.168.1.0/24'
           serviceEndpoints:
             - service: Microsoft.Sql
      patches:
        - fromFieldPath: spec.id
          toFieldPath: metadata.labels[crossplane.io/app]
        - fromFieldPath: spec.id
          toFieldPath: metadata.name
          transforms:
          - type: string
            string:
              fmt: "%s-sn"

I’ve included comments in the above manifest to describe the what/why we are defining. You can see we are taking the spec.id value from the XR and using transforms to name the three resources we are creating in Azure. We can look to the various Provider API references to determine the syntax and required fields for each thing we want to provision.

Note the XR does not expose the ‘location’ (Azure Region in this case), so the Composition handles that. In this way, we are not allowing consumers of this Composition to create resources in any other Region (aka Governance, another benefit).

Of particular importance to our understanding is the spec.compositeTypeRef on line 7. Earlier, we talked about the compositionRef field of an XR and how it enables us to use a single XR to call multiple Compositions, thus the relationship of XR -> Composition is 1 to many. Here we see the relationship of Composition to XR is 1 to 1. A composition will only serve a single XR type.

To summarize, Composition is a bridge between an XR and potentially multiple MRs (Depending on how many things you create from the Composition). When we create an XR, we point it to a Composition we have already defined and instantiated as a resource in our cluster, the Composition creates MRs, the Providers responsible for those MRs create resources in their respective API. When we perform CRUD operations on an XR, the composition bridges those operations to the MRs.

I’ll wrap this post up at that level. In the next post, I’ll go through actually putting these concepts and examples to use.