The success of running an infrastructure platform product with Crossplane depends on following a few principles and patterns as and when required. This chapter will explore some of these critical practices. We will also learn a few debugging skills while exploring the concepts. After learning the basics of the Crossplane in the last few chapters, this will be a place for learning advanced patterns that are key to building the state-of-the-art infrastructure platform for your organization. You will learn a few critical aspects of building robust XR APIs and debugging issues with ease.
The topics covered in this chapter are as follows:
Crossplane is primarily an API-based infrastructure automation platform. Changes to the APIs are inevitable as the business requirements and technology landscape evolve. We can classify these changes into three different buckets:
Let’s start with the API implementation change.
These changes are limited to the API implementation details without any changes to the contract. In other words, these are changes to Compositions YAML, a construct used by XR for API implementation. CompositionRevision is the Crossplane concept that will work with compositions to support such changes. If the --enable-composition-revisions flag is set while installing Crossplane, a CompositionRevision object is created with all the updates to composition. The name of the CompositionRevision object is autogenerated on every increment. The compositions are mutable objects that can change forever, but individual CompositionRevision is immutable. Composition and CompositionRevision are in one-to-many relationships. We will have only one CompositionRevision active at any given instance. The latest revision number will always be active, excluding the following scenario.
Tip
Each configuration state of the composition maps to a single CompositionRevision. Let’s say we are in revision 2 and changing the composition configuration the same as the first revision. A new revision is not created. Instead, revision 1 becomes active, making revision 2 inactive.
In a Crossplane environment where the composition revision flag is enabled, we will have two attributes automatically added to every XR/Claim object by Crossplane. The following are attribute names and how they are used:
The following diagram represents how Composition and CompositionRevision work together to evolve infrastructure API implementation continuously:
To manually migrate the composition, update the spec.compositionRevisionRef configuration in the XR/Claim with the latest revision name. This specific design enables the separation of concerns between the platform API creator and consumers. Infrastructure API creators will update the compositions, and API consumers can choose their revision upgrade strategy. If you want a specific revision of composition to be used while creating an XR/Claim, explicitly mention the revision name under spec.compositionRevisionRef.
Let’s look at some examples of such changes:
The composition revision flag is not enabled by default. Use the --enable-composition-revisions argument with a Crossplane pod to enable composition revision. The following Helm command will set up/update the Crossplane environment with composition revision:
#Enable Composition revision in an existing environment
helm upgrade crossplane –namespace crossplane-system crossplane-stable/crossplane –set args='{--enable-composition-revisions}'
#Enable Composition revision in a new Crossplane setup
helm install crossplane –namespace crossplane-system crossplane-stable/crossplane –set args='{--enable-composition-revisions}'
The following section will look at composition revision with an example.
Let’s go through a hands-on journey to experience composition revision. The objectives of the exercise will be as follows:
Let’s use a simple XRD and composition to explore composition revision. The following is the XRD with just one parameter that takes the MySQL disk size:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xmysqls.composition-revision.imarunrk.com
spec:
group: composition-revision.imarunrk.com
names:
kind: XMySql
plural: xmysqls
claimNames:
kind: MySql
plural: mysqls
versions:
- name: v1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
size:
type: integer
required:
- size
required:
- parameters
The composition for the preceding XRD is as follows, which patches the size attribute from XR into the GCP CloudSQLInstance MR:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: gcp-mysql
spec:
compositeTypeRef:
apiVersion: composition-revision.imarunrk.com/v1
kind: XMySql
resources:
- name: cloudsqlinstance
base:
apiVersion: database.gcp.crossplane.io/v1beta1
kind: CloudSQLInstance
spec:
providerConfigRef:
name: gcp-credentials-project-1
forProvider:
region: us-central1
databaseVersion: MYSQL_5_7
settings:
tier: db-g1-small
dataDiskSizeGb: 40
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.size
toFieldPath: spec.forProvider.settings.dataDiskSizeGb
Apply both the YAML to a target Crossplane cluster with composition revision enabled. You will see that CompositionRevision is created for the composition. Execute the following command to view all CompositionRevision for the given composition:
# List of revisions for Composition named gcp-mysql
kubectl get compositionrevision -l crossplane.io/composition-name=gcp-mysql
Refer to the following screenshot with one revision object created for the gcp-mysql composition. Note that the current attribute is true for revision 1. It will change if we update the composition:
Now, let’s provision two MySQL instances with the Claim API. An example of manual revision update policy configuration is as follows. The automated revision version of the YAML will be the same without the compositionUpdatePolicy parameter, which defaults to an automatic revision update:
apiVersion: composition-revision.imarunrk.com/v1
kind: MySql
metadata:
namespace: alpha
name: mysql-db-manual
spec:
compositionUpdatePolicy: Manual
compositionRef:
name: gcp-mysql
parameters:
size: 10
You can refer to the following screenshot with two MySQL instances onboarded:
Now, update the composition patch with a transform function to multiply the disk size by four before patching. The patches section of the updated composition will look like the following:
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.size
toFieldPath: spec.forProvider.settings.dataDiskSizeGb
transforms:
- type: math
math:
multiply: 4
After updating the composition, you will see two revisions. Only the latest revision will have the current flag of true. Also, we can notice that the MySQL provisioned with an automated revision update policy would have increased the storage. The following screenshot summarizes the output after applying the updated composition:
Finally, we can manually upgrade the second MySQL instance by adding the spec.compositionRevisionRef attribute to the XR/Claim configuration. The field will hold the autogenerated composition revision name. The composition revision hands-on journey example is available at https://github.com/PacktPublishing/End-to-End-Automation-with-Kubernetes-and-Crossplane/tree/main/Chapter05/Hand-on-examples/Composition-Revision. In the following section, we will explore the ways to change the XR API contract.
API implementation details are just one direction in which XR changes can evolve. The highly interoperable API contract between the XR creating and consuming teams also needs to change over time. Contract change can fall under two categories:
Let’s delve into non-breaking changes.
Adding one or more optional parameters to the XRD contract can be considered a non-breaking change. It is non-breaking because the old external resources provisioned can co-exist with the new schema as the new parameters are optional. Note that removing an existing optional parameter in the XRD is a breaking change as Crossplane upfront does not know how to reconcile existing provisioned resources. A simple way to think about this is that if Composition/CompositionRevision can handle the co-existence of old and newly provisioned resources, then the XRD contract change is non-breaking. A new optional parameter in the MySQL XR to choose the disk size is an example of a non-breaking change. The change will involve both a contract change and a composition revision. Let’s go through a hands-on journey to make the previous XR example. All the configuration YAML required for this journey is available at https://github.com/PacktPublishing/End-to-End-Automation-with-Kubernetes-and-Crossplane/tree/main/Chapter05/Hand-on-examples/XRD-Contract-Change-Non-Breaking. Refer to the following screenshot of our hands-on journey:
The following are the steps to be performed throughout the hands-on journey to experiment with the non-breaking contract change:
We did not upgrade the API version when updating the contract. We will discuss this more in the upcoming section.
In the previous section, we did not change the XRD version number from v1. Crossplane does not currently support XR version upgrades once a contract changes. API versioning without a contract change will be helpful in indicating API stability (alpha, beta, v1, and so on). We can just move from alpha to beta to a more stable version without changing the contract. The version upgrade is currently achieved by listing the old and new version definitions in the XRD. The versions array is the construct used for listing multiple versions. The two critical Boolean attributes under each version are served and referenceable. The referenceable flag will determine whether we can define a composition implementation for the given version. Only one version can have the referenceable flag set to true. This will be the version used by any new XR create/update event. A create/update event triggered by the old API version will still use the composition from the latest version, marked as referenceable. The served flag will indicate whether the given XR API version is in use. Some teams may still use the old version to consume the API. Switching off the served flag means that the given version is no longer available for clients. It will be the last step before removing the old version from the XR.
Look at a sample XRD with three versions, alpha, beta, and v1, at https://github.com/PacktPublishing/End-to-End-Automation-with-Kubernetes-and-Crossplane/blob/main/Chapter05/Samples/XRD-Versions/xrd-multiple-version.yaml. This XRD has three versions. Version alpha will no longer be served, and beta will be served but cannot be referred for resource creation or update. The latest version, v1, will be the preferred version for any resource creation or updates.
Kubernetes CRDs support multiple API versions both with and without an API contract change. When there is an API contract change, a conversion webhook is configured by the CRD author to support conversion between the versions. Conversions are required as CR objects will be stored in the etcd with both old and new contracts. XRD, the Crossplane equivalent to CRDs, does not take this approach. A conversion webhook involves programming. Taking that route will violate the no-code agenda of Crossplane when composing APIs. It’s important to note that the Crossplane community is actively working to build a configuration-based solution to support conversion and migration between versions.
An alternative approach supports breaking contracts by introducing a new XR API parallel. This approach uses an external naming technique and deletion policy to handle breaking changes. With this pattern, we will migrate the resources to a new XR API and remove the old API once the migration is finished in its entirety. The steps to achieve such a version upgrade are as follows:
Note that this type of migration to a new API version must be coordinated with all the XR-consuming teams. Once the migration is completed, the old API version will no longer be available. The following figure represents the migration process:
Tip
It is always good to have a standard way of generating external resource names. In addition to version migration, a reproducible naming pattern can afford several other advantages. Using pre-provisioned resources for a shared or cached infrastructure is an example of using the standard external resource naming pattern. Migrating resources to a new Crossplane environment can be another example.
It’s recommended to go through a hands-on journey of breaking API contract, with the sample configuration provided at https://github.com/PacktPublishing/End-to-End-Automation-with-Kubernetes-and-Crossplane/tree/main/Chapter05/Hand-on-examples/XRD-Contract-Change-Breaking. Perform the following steps to go through the hands-on journey to handle breaking contract changes:
Refer to the following screenshot where the preceding example is tested:
Following is the code snippet relating to external resource name patching from the preceding Composition example. This must be present in both composition versions, and the name generated should be the same for both versions:
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: metadata.annotations[crossplane.io/external-name]
transforms:
- type: string
string:
fmt: "%s-gcp-mysql-cloudsqlinstance"
Note that we have used a new transform type to format the string before we patch. With this, we conclude the different ways of evolving the XR APIs. We will dive into an interesting case in the following section to build one XR composing another XR.
Every software product depends on more than one infrastructure resource. It is essential to build single infrastructure recipes in order for the product teams to consume with a unified experience. The orchestration of infrastructure dependencies should remain abstracted. Such recipes require multiple resources to be composed into a single XR. In all the examples hitherto, we have always composed a single GCP resource inside an XR. Let’s look at an XR sample where multiple GCP resources are composed into a single XR API. The following figure represents the resources and XR APIs that we are going to work with in the example:
In addition to multiple resource provisioning in a single XR, we also have a nested XR pattern in Figure 5.8. We are composing three resources within two XRs. The first XR composes two resources, and the second XR composes the first XR and a database resource. Let’s look at the details of each XR:
All examples in this hands-on journey are available at https://github.com/PacktPublishing/End-to-End-Automation-with-Kubernetes-and-Crossplane/tree/main/Chapter05/Hand-on-examples/Nested-Multi-Resource-XR.
Let’s first create the XRD and Composition for both the XRs. Apply xrd k8s.yaml, Composition k8s.yaml, xrd Application.yaml, and Composition Application.yaml to the Crossplane cluster. You will see that the ESTABLISHED flag is True for both the XRDs. This indicates that the Crossplane has started a new controller to reconcile the established XR. The OFFERED flag will be True for the application XR and False for the Kubernetes XR. This indicates that the Crossplane has started a new controller to reconcile the established Claim only for the application XR. It is false for the Kubernetes XR because we don’t have the respective claim. Refer to the following screenshot regarding XRD creation:
Tip
Similar to creating an XR API with multiple resources from a single cloud provider, we can also mix and match resources from multiple clouds. We just have to add the resources concerned with respective ProviderConfig clouds.
It’s now time to create an application Claim resource. Apply Claim Application.yaml to the Crossplane cluster. You will see that a CloudSQLInstance instance, a cluster, and a bucket resource have been provisioned. Refer to the following screenshot where the resources are provisioned successfully:
If you would like to explore each resource in detail, use the Resource references. Execute kubectl describe application my-application -n alpha to see the details of the claim. It will refer to the XApplication XR object. If we look at the details of the XApplication object, it will hold the reference to the CloudSQLInstance MR and XGCPCluster XR. Similarly, we can go on till you reach the last MR. This is beneficial for debugging activities. Sometimes you may see that the resources are not getting ready. In those instances, explore each nested resource and refer to the events section to ascertain what is happening. An example of referring nested resources from the resource description is as follows:
The preceding screenshot represented the Application claim description referring to the XApplication XR resource. The following screenshot represents the XApplication XR description referring to the XGCPCluster XR instance and CloudSQLInstance MR:
The following is an example event that tells us that we have provided the wrong region as a parameter:
Important
We need to follow many more patterns when we compose multiple resources to give a unified experience for product teams. The preceding example is a simple example to start the topic. We will see more on this in the upcoming chapters.
If you look at the composition in the preceding example, you can see that we have used a new pattern called PatchSets. If you find yourself repeating the same patch operation again and again under each resource, then PatchSets is the way to go. Here, we define the patch operation as a static function and include it under the required resource sections. The following is an example of the patchSets function definition to patch a region:
patchSets:
- name: region
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.region
toFieldPath: spec.forProvider.region
We can define multiple patchSet functions. To include a specific patch set function within a given resource, use the following code snippet:
patches:
- type: PatchSet
patchSetName: region
We will see more nested and multi-resource XR examples in the upcoming chapters. In the following section, we will look at detailed configuration options for defining the XRD schema.
While looking at Composite Resource Definition (XRD) in the previous chapter, we touched on limited configuration options required to learn the basics of XR. It’s now time to look at more detailed configuration options to build clean and robust XR APIs. A significant part of the details we will look at are about openAPIV3Schema, which is used to define the input and output of the XR API. The following are the topics we will cover in this section:
Let’s start with the Naming the versions section.
The version name of our XRD cannot have any random string. It has a specific validation inherited from the CRDs and standard Kubernetes APIs. The string can contain only lowercase alphanumeric characters and -. Also, it must always start with an alphabetic character and end with an alphanumeric character, which means that - cannot be the start or ending character. Also, a number cannot be the starting character. Some valid versions are my-version, version-1, abc-version1, and v1. While we can have many permutations and combinations for naming a version, some standard practices are followed across CRDs. Following the same with XRDs will enable API consumers to understand the stability of the API. The version string starts with v followed by a number with these standards (v1, v2). This is then optionally followed by either alpha or beta, depending on the API’s stability. Generally, the alpha string represents the lowest stability (v5alpha), while beta is the next stability level (v3beta). If both texts are missing, the XR is ready for production use. An optional number can follow the optional alpha/beta text representing the incremental releases (v2alpha1, v2alpha2, and so on).
If you have an invalid version string provided with the XRD, you will see that the XRD will not get configured properly. The ESTABLISHED flag will not be set to True. You apply the – xrd invalid version test.yaml file from the samples folder to see what happens when you have an incorrect version number. Refer to the following screenshot:
Also, you will be able to see the following error logs in the Crossplane pod in the crossplane-system namespace:
2022-01-15T20:06:46.217Z ERROR crossplane.controller-runtime.manager.controller.defined/compositeresourcedefinition.apiextensions.crossplane.io Reconciler error {"reconciler group": "apiextensions.crossplane.io", "reconciler kind": "CompositeResourceDefinition", "name": "xbuckets.version-test.imarunrk.com", "namespace": "", "error": "cannot apply rendered composite resource CustomResourceDefinition: cannot create object: CustomResourceDefinition.apiextensions.k8s.io "xbuckets.version-test.imarunrk.com" is invalid: [spec.versions[0].name: Invalid value: "v1.0": a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?'), spec.version: Invalid value: "v1.0": a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')]"}
Tip
When we troubleshoot an issue with Crossplane, logs from the Crossplane pod can help. Enable debugging mode by adding an argument, --debug, to the Crossplane pod. Similarly, we can even look at the provider’s container logs.
The specification of the XR API is defined using openAPIV3Schema. Every configuration element in the XRD under this section represents the input and output of the XR API:
versions:
- name: v1alpha
schema:
openAPIV3Schema:
# Input and output definition for the XR
Generally, we configure the openAPIV3Schema section with two objects, spec and status. The spec object represents the API input, while the status object represents the response. We can skip defining the status section in XRD if we don’t have any custom requirements. Crossplane would inject the standard status fields into the XR/Claim. Refer to the following code snippet representing the openAPIV3Schema configuration template for the XR API input and output:
openAPIV3Schema:
type: object
properties:
# spec – the API input configuration
spec:
type: object
properties:
............. configuration continues
# status – the API output configuration
status:
type: object
properties:
............. configuration continues
The schema configuration is all about a mix of - attributes, their types, and properties. An attribute type of object will hold a list of properties. For example, the root attribute openAPIV3Schema: is of the object type followed by a list of properties (spec and status). A list of properties is nothing but a list of attributes. Suppose the attribute type is primitive, such as string or integer. Such an attribute will be the end node. The object-properties recursion can continue in as much depth as we require. Refer to the following code snippet:
# The root attribute openAPIV3Schema of type object
openAPIV3Schema:
type: object
# spec/status - attributes (properties) of openAPIV3Schema
properties:
# spec – the XR input
spec:
type: object
properties:
# parameters - again an object with attributes list
parameters:
type: object
properties:
# region – string primitive - node ends
region:
type: string
# status – API output configuration
# The exact structure of configuration as before
# Attributes, their types, and properties
status:
type: object
properties:
zone:
description: DB zone.
type: string
In the following section, we can look at a few additional valuable configuration options along with the basic openAPIV3Schema configuration.
The attribute node can configure a few other critical configurations that API developers will use daily. Following are some of the frequently used configurations:
In addition to these fields, there is a list of validation-related configurations including minimum, maximum, pattern, maxLength, and minLength. Refer to the following sample configuration:
spec:
type: object
description: API input specification
properties:
parameters:
type: object
description: Parameter's to configure the resource
properties:
size:
type: integer
description: Disk size of the database
default: 20
minimum: 10
maximum: 100
vm:
type: string
description: Size of the virtual machine.
enum:
- small
- medium
- large
required:
- size
required:
- parameters
To explore more detailed possibilities, visit https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#schemaObject.
Tip
We can use the description field to announce the parameter deprecation information. This technique can be helpful in delaying breaking changes to a contract by making a mandatory field optional with a deprecation message.
We can use the printer columns to add what kubectl will display when we get the resource list. We should provide a name, data type, and JSON path mapping to the attribute we wish to display for each column. Optionally, we may also provide a description. Refer to the following sample configuration:
additionalPrinterColumns:
- name: Zone
type: string
description:
jsonPath: .spec.zone
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
The printer column configuration remains parallel to the schema configuration.
This concludes our discussion of detailed XRD configuration. We have covered most of the configuration required for day-to-day work, but there are endless possibilities. It will add value by reading up on CRD at https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/.
We have always talked about managing external infrastructure resources using Crossplane from the beginning of this book. However, it does not always have to be just an infrastructure resource. We could even manage external software applications from Crossplane. For a software application to be able to work best with the Crossplane ecosystem, it must have the following qualities:
It’s time to look at an example. Think about deploying an application in Kubernetes using Helm. Helm can package any application and provide a well-defined CRUD API to deploy, read, update, and uninstall. Above all, we can create granular control over the application configuration with parameters. We have a helm Crossplane provider already available and used extensively by the community. The idea of managing external applications from a Crossplane control plane can enable a new world of unifying application and infrastructure automation. The following section will cover the unifying aspect in more detail.
Managing external software resources with Crossplane is the crossroad for unifying infrastructure and application DevOps. We could package software and infrastructure dependencies into a single XRs. Such a complete package of applications and infrastructure introduces numerous advantages, some of which are listed here:
The following figure represents a unified XR API:
Important
In a later chapter, we can go through a hands-on journey to experience building an XR API covering both applications and infrastructure dependencies.
I hope it’s been fun to read this chapter and go through the hands-on journey. It covered different patterns that are useful in our day-to-day work when adopting Crossplane. We covered different ways to evolve our XR APIs, detailed XR configurations, how to manage application resources, and nested and multi-resource XRs. There are more patterns to be covered.
The next chapter will discuss more advanced Crossplane methods and their respective hands-on journeys.