Despite DevOps being a relatively new phenomenon in its current form, it has taken the tech world by storm, in the never-ending race to automate everything and anything, and consolidate management of infrastructure in the most optimized, reusable way possible.
We have seen the rise of IaC, the mass migration to cloud services, the gradual disassembly of monolithes into micro-services, the emphasis on monitoring and observability, GitOps, and the list goes on and on like a comically long cartoon scroll.
For a while now, the leading IaC tool has been Terraform, but between the recent licensing controversy and the seemingly arbitrary nature of HCL, along with security concerns and a rigid state, its competitors in the space are keeping a watchful eye for an opportunity to overtake it. One such tool is Crossplane, which we’re going to talk about in this post.
Crossplane is, for lack of better words – a control plane for your control plane, and an extensible and robust one at that.
Say you use AWS CLI to provision, update, and query resources in your AWS account, Crossplane will do that for you based on things you set, over and over again, because it’s actually a Kubernetes controller.
You can provision cloud resources with kubernetes-native manifests, with very complex, and frankly quite impressive logic being acheived by the controller behind the scenes.
Why Crossplane?
Crossplane allows us to constantly monitor and correct the state of our resources, with ongoing drift detection to make sure the remote state always matches the desired outcome of our declerative configuration.
How does crossplane work?
Crossplane leverages the concept of Providers to map API endpoints and fields in the remote provider, to kubernetes-native objects. In this post, we will work with the AWS provider, leveraging Crossplane to provision resources.
The basics of Crossplane
I’m not going to cover the Installation of Crossplane, for that you can take a look at the documentation.
To start using Crossplane after installation, you need to be faimiliar with the following resources:
- Providers
- CompositeResourceDefinitions
- Compositions
Providers
We have already touched on providers earlier, but haven’t really defined what they are and how we can use them.
Providers are the glue that enables Crossplane to “talk” with our cloud of choice, a provider maps Kubernetes-native objects to API endpoints in the cloud.
There are quite a few providers available as of the time of writing this, but the 3 main ones are, of course, the providers from AWS, GCP, and Azure.
To be able to use a Provider, you need to install it as a Crossplane package; You might also need to deploy a DeploymentRuntimeConfig, which will define the in-cluster behavior of the provider (basically another controller running on our cluster), and a ProviderConfig, which will define the aws-side configuration of said provider (in our example, we will use it to authenticate against AWS using IRSA).
Here’s a simple example of an AWS provider with a DeploymentRuntimeConfig and ProviderConfig authenticating with IRSA:
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: aws-config
spec:
deploymentTemplate:
metadata:
labels:
managed-by: crossplane
serviceAccountTemplate:
metadata:
name: crossplane
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::223851089411:role/orelfichman-crossplane-role
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: aws-provider
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-aws:v0.48.0
runtimeConfigRef:
name: aws-config
---
apiVersion: aws.crossplane.io/v1beta1
kind: ProviderConfig
metadata:
name: aws-provider-config
spec:
credentials:
source: InjectedIdentity
Applying this manifest (after deploying Crossplane, installing its various CRDs) will install the provider on the cluster, making the various AWS resources available for Crossplane management.
CompositeResourceDefinitions
CompositeResourceDefinitions (often referred to as XRDs) are used to define schemas for our future custom objects (called Composite Resources, or XRs).
When we create our own Composite Resources, they have a schema just like any other resource in Kubernetes.
by creating an XRD, we are creating a skeleton for our XRs.
Compositions
Compositions are an amazing resource used to simplify creating resources in the cloud. While we can directly deploy each and every one of our needed resources seperately, it can get complicated quite quickly.
Compositions let us group as many cloud resources as we’d like under one object, allowing us to also cross-reference these resources while hiding the complex logic within its definition.
So… What now?
Both XRDs and Compositions are templates, the former is a template for the Kubernetes resource schema and the latter is more of a declaration of the cloud resources contained within the resource.
Now we can get to the actual provisioning of our resources. Let’s say we want to create a CloudFront distribution in AWS, along with a corresponding DNS record in a Route53 zone.
We COULD accomplish this by just directly deploying the resources individually, supposedly like so:
apiVersion: cloudfront.aws.crossplane.io/v1alpha1
kind: Distribution
spec:
providerConfigRef:
name: aws-provider-config
forProvider:
region: us-east-2
distributionConfig:
enabled: true
comment: "Managed by Crossplane"
... # Removed for brevity, a distribution has lots of fields
restrictions:
geoRestriction:
items:
- US
- IL
restrictionType: "whitelist"
---
apiVersion: route53.aws.crossplane.io/v1alpha1
kind: ResourceRecordSet
metadata:
annotations:
crossplane.io/external-name: blahblah.orelfichman.com
spec:
forProvider:
setIdentifier: "crossplane"
region: us-east-2
ttl: 300
type: CNAME
zoneId: Z031951122JLXXOJ8IOXE
resourceRecords: ??????? #(We wouldn't know until after the distribution is created)
providerConfigRef:
name: aws-provider-config
Now, this approach is problematic for a few reasons:
- This doesn’t scale, what if we would like to reuse this configuration?
- We can’t reuse any variables (you could use Helm, but why should you be forced to?)
- Most importantly, you can’t cross-reference values. We would have to first create the distribution, note the randomly generated DNS name, and then update our manifest.
Therefore, as a best practice, we should leverage XRDs and Compositions, which will make it much easier to accomplish our desired behavior; Let’s take a look at a possible example:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: cdns.orelfichman.com
spec:
group: orelfichman.com
names:
kind: CDN
plural: cdns
claimNames:
kind: CDNClaim
plural: cdnclaims
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
env:
type: string
status:
type: object
properties:
backendDomainName:
type: string
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: "cloudfront-with-recordset"
spec:
compositeTypeRef:
apiVersion: orelfichman.com/v1alpha1
kind: CDN
patchSets:
- name: cdnPatchSet
patches:
- fromFieldPath: spec.env
toFieldPath: spec.forProvider.distributionConfig.origins.items[0].originPath
transforms:
- type: string
string:
type: Format
fmt: "/%s"
- fromFieldPath: spec.env
toFieldPath: spec.forProvider.distributionConfig.aliases.items[0]
transforms:
- type: string
string:
type: Format
fmt: "%s.orelfichman.com"
resources:
- name: distribution
patches:
- type: PatchSet
patchSetName: cdnPatchSet
- type: ToCompositeFieldPath
fromFieldPath: status.atProvider.distribution.domainName
toFieldPath: status.backendDomainName
base:
apiVersion: cloudfront.aws.crossplane.io/v1alpha1
kind: Distribution
spec:
providerConfigRef:
name: aws-provider-config
forProvider:
region: us-east-2
distributionConfig:
enabled: true
comment: "Managed by Crossplane"
... # Removed for brevity, a distribution has lots of fields
restrictions:
geoRestriction:
items:
- US
- IL
restrictionType: "whitelist"
- name: "cdn-dns-record"
patches:
- type: FromCompositeFieldPath
fromFieldPath: status.backendDomainName
toFieldPath: spec.forProvider.resourceRecords[0].value
- type: FromCompositeFieldPath
fromFieldPath: spec.env
toFieldPath: metadata.annotations['crossplane.io/external-name']
transforms:
- type: string
string:
type: Format
fmt: "%s.orelfichman.com"
base:
apiVersion: route53.aws.crossplane.io/v1alpha1
kind: ResourceRecordSet
spec:
forProvider:
setIdentifier: "crossplane"
region: us-east-2
ttl: 300
type: CNAME
zoneId: Z00195152TJ2XROJ8BUXN # Just a random Zone ID, you use own
providerConfigRef:
name: aws-provider-config
Yeah, yeah, I know; it’s comically long. Get used to it, your compositions are more likely than not to span hundreds of lines. Let’s break down the XRD first, as it’s shorter and simpler.
By defining the above XRD, we tell Crossplane “I would like to be able to create a resource called CDN
, it’s part of the orelfichman.com
API, version v1alpha
(you can make up whatever API and version you want, that’s the beauty of creating a brand new resource definition), which needs to be provided with a field named env
of type string, and an object called status
containing a string field called backendDomainName
(we will use this to “communicate” the DNS name between our CloudFront distribution and DNS recordset. That’s it.
We haven’t yet configured what happens when a CDN
XR is provisioned, and that’s exactly what we’re going to do with the composition.
The above composition contains a compositeTypeRef
field, which tells Crossplane that when a CDN
is created, the underlying cloud resources to be created, are detailed in the composition.
Our composition’s resources also contains patches
, which are powerful declerative expressions enabling cross-resource value sharing and templating.
For example, we apply a patch on the distribution
to store the domain name of the resulting CloudFront distribution (on the AWS side) in the CDN
‘s backendDomainName
field under the status
field.
We then read this value within the ResourceRecordSet
, and set it as the external name of the resource (if we don’t Crossplane will try to come up with a random name which the AWS API would refuse.
Bringing everything together
So we have defined an XRD and a Composition, but none of these have actually made any change to our cloud infrastructure, only when we create the corresponding XR (in our case, the CDN
), does Crossplane go and create the relevant resources in the cloud. Let’s create our CDN:
apiVersion: orelfichman.com/v1alpha1
kind: CDN
metadata:
name: "demo-cdn"
spec:
env: demo
Now if we do kubectl get cdn
, we can see our CDN resource has been created
We can also run kubectl get distribution
or kubectl get resourcerecordset
, we’ll be able to see the underlying resources that make up our CDN
resource.
This is just one way you can leverage Crossplane to manage your cloud infrastructure, your imagination (and arguably your organization’s budget) are the limit for what you can accomplish; this is just the tip of the iceberg.
Some great resources you can use to learn more include:
No responses yet