Skip to main content

How a Crossplane Control Plane Works: Driving Infrastructure Toward Your Desired State

Wondering what Crossplane is all about? Read my first blog here.

All About Control Planes

In my opinion, a good control plane should do three things:

  • Drive the data plane toward the desired state
  • Protect the data plane
  • Be reliably available

In this blog I’ll explore how a control plane built with Crossplane achieves the first goal. If you know how Kubernetes’s control plane works, you’ll notice a lot of familiar concepts in this blog. This is because Crossplane builds on the Kubernetes control plane and enjoys many of its benefits.

If you’d like to learn more about how Crossplane protects the data plane and is readily available, I’ve detailed everything you need to know in my report, “What Is Crossplane?: A Low-Code Framework for Building Cloud Native Control Planes” here.

Driving Toward the Desired State

A control plane’s core function is to reconcile the actual state of its data plane with a desired state. A thermostat is a good example: you might desire your room to be 72 degrees F, but its actual temperature is 65 degrees. The thermostat can reconcile the actual state of your room (its temperature) with your desired state by running the heat until the room reaches 72 degrees F.

You tell Crossplane what the desired state of your data plane is by making API calls. Crossplane exposes a JavaScript object notation (JSON) representational state transfer (REST) API over hypertext transfer protocol (HTTP). JSON REST APIs are broadly supported by tools and programming languages. This makes it easier to integrate Crossplane with other systems.

Crossplane APIs look a lot like Kubernetes APIs. They are compatible with popular tools in the Kubernetes ecosystem like Helm, ArgoCD, Kyverno, and of course Kubectl. All of Crossplane’s APIs are actually Kubernetes Custom Resources. This means that, like all Custom Resources, they’re declarative. You just tell Crossplane the desired state of the data plane — not how to achieve it.

APIs exist on a spectrum from purely declarative to purely imperative. Imperative APIs require you to specify how to achieve your desired state. Declarative APIs are simpler: would you rather imperatively tell your thermostat to run the heater for 20 minutes each time you notice it’s cold, or declaratively tell it to keep the room at 72 degrees?

A huge benefit of Crossplane’s declarative APIs is that if you save the body of your API request to disk, you have a configuration file. This lets you keep your desired state using a revision-control system like Git. Crossplane API objects use JSON, but tools like Kubectl typically convert them to YAML ain’t markup language (YAML) when you save them to a file. A desired-state API object saved as a YAML file is often called a manifest. Example 2-1 shows a Crossplane API resource in YAML form. This will look familiar to you if you’ve ever seen a Kubernetes Deployment or Pod manifest.

Each resource in Crossplane’s API represents a data plane entity. You can create, read, update, or delete that entity using HTTP verbs (such as POST, GET, PUT, and DELETE). API resources represent things, not actions. Think back to the SimplePostgres example from the first blog

: each SimplePostgres instance in your data plane would be represented by its own SimplePostgres API resource. Each resource has its own uniform resource locator (URL) in the API, for example https://api-server.example.org/apis/platform.example.org/v1/simplepostgres/product-team-db. Note that segments of this URL appear in the YAML manifest shown in Example 2-1.


All Crossplane API resources include at least three top-level fields:

apiVersion and kind

Specifies the type of the resource. This makes it easy to identify the type of the resource without knowing its URL, for example when reading a YAML manifest.

metadata

Metadata that is supported by all resources, including their name, creation time, and arbitrary labels and annotations.


Most Crossplane API resources contain only two other top-level fields: spec and status. The spec field specifies the desired state of the resource, while the status field reflects its observed actual state. All API resources have OpenAPI v3 schemas.


Example 2-1: A SimplePostgres YAML manifest

Crossplane offers a consistent, uniform API to express desired state and read actual state. This makes working with Crossplane predictable - all of its APIs look and feel similar. It reduces users’ cognitive burden and helps tools integrate with Crossplane. In many places, Crossplane APIs are more uniform than Kubernetes APIs. For example, the same API field in all Crossplane resources reflects whether they are ready to use.

You might have noticed that a thermostat must measure the temperature of the room to determine whether it needs to run the heat. The thermostat uses a feedback loop to make the right corrections over time. This is called closed-loop control, and it’s a desirable property of a control plane. Crossplane does this using controllers. A controller subscribes to the API that it is responsible for and watches for changes to its desired state. Each controller is responsible for a single kind of API resource. A SimplePostgres API is powered by a SimplePostgres controller.

When the desired state changes, the controller:

  1. Reads the desired state from its API
  2. Observes the actual state of the part of the data plane its API represents
  3. Determines whether the actual state is different from the desired state
  4. Corrects the actual state by creating, updating, or deleting something

Imagine you’ve configured Crossplane to satisfy calls to the SimplePostgres API by creating RDS instances. Each SimplePostgres instance corresponds to an RDS instance.

When the SimplePostgres API is called to create a new PostgreSQL instance, its controller will:

  1. Read the desired state from the SimplePostgres API
  2. Attempt to observe the actual state of the corresponding RDS instance
  3. Find that the RDS instance does not exist
  4. Create the RDS instance, deriving its configuration from the desired state read from the SimplePostgres API

Crossplane controllers use closed-loop control to ensure that your data plane never drifts from your desired state. Once you tell Crossplane the desired state of your data plane, it will constantly observe the data plane’s actual state and quickly self-heal when that drifts.

Want the Full Picture?

If you’d like to understand the full picture of how a Crossplane control plane works, I detail everything in this O’Reilly report: “What Is Crossplane?: A Low-Code Framework for Building Cloud Native Control Planes”. It goes into depth about Crossplane, how a Crossplane Control Plane works & how to build one, and more. Feel free to download it here to get the full rundown of Crossplane & Control planes to level up your infrastructure.

This excerpt was originally published in the O’Reilly Report: “What is Crossplane?”.

About the Author

Nic is a Senior Principal Engineer at Upbound. He's a long-time Crossplane maintainer and steering committee member.

Profile Photo of Nic Cope