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:

  1. This doesn’t scale, what if we would like to reuse this configuration?
  2. We can’t reuse any variables (you could use Helm, but why should you be forced to?)
  3. 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:

About the Author

Orel Fichman

Tech Blogger, DevOps Engineer, and Microsoft Certified Trainer

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *

Newsletter

Categories