Today I’m going to give you a sneak peek into an upcoming OCI provider for Crossplane.  Normally I’m not into spoiling surprises, however this is so exciting I’m breaking my normal form to give a pre-release view of what’s coming!

WARNING: What you’re about to see is pre-release.  It’s subject to change.  By the time the OCI Crossplane Provider is released, it’s possible that it’ll look vastly different than what you see here.  The concepts will hopefully still be relevant, but your mileage may vary.

What is Crossplane?

Crossplane is a new (relatively speaking) entrant in the infrastructure-as-code (IaC) space.  Like most IaC tools, it uses a declarative format for defining resources.  It is a CNCF project, which means it has a healthy and active community behind it.  Since the IaC tool space is fairly crowded (and has some very mature members), let’s look at a few of the capabilities that make Crossplane attractive.

Why Crossplane?

Crossplane is designed from the ground-up to deliver a “K8s-native” experience.  It uses built-in K8s constructs to provide a management solution for different kinds of resources.  Crossplane (like many other IaC tools) supports many different cloud providers, allowing it to integrate nicely into a multi-cloud strategy.

You can use traditional K8s role-based access control (RBAC) concepts and functionality to secure and control the interface used by Crossplane to manage resources.  K8s data storage is used in the storage of resource definitions (both the Custom Resource Definition (CRD) and managed resource instantiations).

The “K8s-native” experience Crossplane delivers another inherent benefit: broad visibility into the end-to-end environment of an application.  Crossplane delivers a single pane of glass, allowing you to view your entire environment from the OCI resources themselves up to the pods running in a Deployment. 

This gives full visibility into the app environment, all available via the familiar K8s API (and tools like kubectl, k9s, etc.).  OCI resources are defined in K8s manifests (and/or resources created via the API/kubectl), yielding a consistent management experience (and controls) as you get from deploying and managing applications in K8s (Deployments, Services, etc.).

Another Crossplane benefit is the “always on” functionality, meaning in a typical installation the Crossplane control-plane will be running constantly in the background, looking for changes that it might need to make, based on the resources that have been defined.  Changes might be creating new resources, deleting previously resources or updating existing resources (often called “drift detection and correction”).  The “always on” experience is not typical for open-source IaC tools, such as Terraform, Ansible and others.  Because few open-source tools are always (or regularly) executing, drift detection/correction is also not common.  I would be remiss if I didn’t point out that there are exceptions!  OCI Resource Manager (ORM) provides drift detection and correction, but again, this isn’t typical in the standalone open-source tool space.

Some Important Crossplane Concepts

Before diving into some examples, you need to understand some basic Crossplane concepts.  Each Crossplane provider includes Custom Resource Definitions (CRDs) (which is a native K8s construct) to extend the K8s API into recognizing resources that are unique to the specific Crossplane provider.  This is how Crossplane is able to extend K8s to recognize new cloud resources (such as OCI resources).  Here’s a snippet from looking at the output of kubectl api-resources on a cluster that has the OCI Crossplane provider:

$ kubectl api-resources
NAME                                                  SHORTNAMES   APIVERSION                                              NAMESPACED   KIND
<snip>
persistentvolumeclaims                                pvc          v1                                                      true         PersistentVolumeClaim
persistentvolumes                                     pv           v1                                                      false        PersistentVolume
pods                                                  po           v1                                                      true         Pod
<snip>
routetables                                                        core.oci.jet.crossplane.io/v1alpha1                     false        RouteTable
securitylists                                                      core.oci.jet.crossplane.io/v1alpha1                     false        SecurityList
subnets                                                            core.oci.jet.crossplane.io/v1alpha1                     false        Subnet
vcns                                                               core.oci.jet.crossplane.io/v1alpha1                     false        Vcn
vlans                                                              core.oci.jet.crossplane.io/v1alpha1                     false        Vlan
vnicattachments                                                    core.oci.jet.crossplane.io/v1alpha1                     false        VnicAttachment
volumeattachments                                                  core.oci.jet.crossplane.io/v1alpha1                     false        VolumeAttachment
volumebackuppolicies                                               core.oci.jet.crossplane.io/v1alpha1                     false        VolumeBackupPolicy
volumebackuppolicyassignments                                      core.oci.jet.crossplane.io/v1alpha1                     false        VolumeBackupPolicyAssignment
volumebackups                                                      core.oci.jet.crossplane.io/v1alpha1                     false        VolumeBackup
volumegroupbackups                                                 core.oci.jet.crossplane.io/v1alpha1                     false        VolumeGroupBackup
volumegroups                                                       core.oci.jet.crossplane.io/v1alpha1                     false        VolumeGroup
volumes                                                            core.oci.jet.crossplane.io/v1alpha1                     false        Volume
<snip>

The above snippet shows many different API resources that are part of the OCI Crossplane Provider, including (this is not an exhaustive list): Subnet, Vcn, RouteTable, etc.  These aren’t typically in a plain vanilla OCI Container Engine for Kubernetes (OKE) or any other K8s implementation, but are installed by the OCI Crossplane Provider.  The net result is that we’re able to manage these new types of OCI resources using familiar K8s manifests (typically in YAML format).

The user interface (UI) – the inputs and outputs – of a configuration is effectively a K8s CRD which Crossplane calls a Composite Resource Definition (abbreviated as XRD).  The part of a configuration that defines the resources is called a Composition.  A Composition is paired with an XRD.  The name used in the XRD and Composition are identical, effectively creating a new kind of resource, which can be used by a Claim.  To deploy the resource(s), an instantiation of a Composition is created, which is called a Claim.  There can be many Claims (many instantiations of a given XRD/Composition).  Here’s a visual representation of what this looks like:

Visual representation of some basic Crossplane concepts

For those familiar with Terraform, the inputs (variables) and outputs are defined in the XRD.  The different resources themselves are defined in the Composition.  Unlike Terraform where you can define resources in an adhoc manner or optionally create modules, Crossplane effectively takes a modules-only approach.  It’s up to you whether a Composition contains one or many resources.

What about the kind?  It is a bit confusing… an XRD is a kind of CompositeResourceDefinition, a Composition is a kind of Composition and a Claim is a kind of a given XRD/Composition.  A claim’s kind is set to the name given to the desired XRD/Composition.

Each Crossplane provider defines resources which can be managed – it’s up to you to assemble these into a configuration that fits your needs.  This is no different than how other IaC tools work.  The Crossplane configuration format is a bit different than many other IaC tools.

Process for using Crossplane to manage OCI resources

Let’s assume that the OCI provider is already installed in Crossplane.  The following high-level steps are what you might use to create an XRD, Composition and Claim:

  1. Create XRD (text file)
  2. Apply XRD
    1. kubectl apply -f <xrd_file_name.yaml>
    2. kubectl describe -f <xrd_file_name.yaml>
  3. Create Composition (text file)
  4. Apply Composition
    1. kubectl apply -f <composition_file_name.yaml>
    2. kubectl describe -f <composition_file_name.yaml>
  5. Create Claim (text file)
  6. Apply Claim
    1. kubectl apply -f <claim_file_name.yaml>
    2. kubectl describe -f <claim_file_name.yaml>
  7. Look at the resources defined to see info on the deployment status, etc. (kubectl get managed is a useful command)

That’s it!  Because the Crossplane control plane is “always on”, it’ll detect when there are XRDs, Compositions and Claims that are created, deleted and/or updated.

Example of managing OCI resources

Now that we’ve covered some of the basic concepts and understand the practicalities of how to work with Crossplane, let’s look at a few examples of what this might look like.  Remember, this is pre-release, so it might look different at release.  Let’s go with a really basic example where we’ll setup a single VCN.  Because we can make the XRD as simple or complex as we’d like, I’ll show a couple of potential examples.  Here’s a very fixed (rigid) XRD, where the only thing customizable is the Compartment OCID and the VCN name:

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: simplevcns1.oci.platformref.crossplane.io
spec:
  group: oci.platformref.crossplane.io
  names:
    kind: SimpleVCN1
    plural: simplevcns1
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              compartmentId:
                type: string
                description: The Compartment OCID to deploy resources into.
              vcn:
                type: object
                properties:
                  name:
                    type: string
                    description: The name to use for the VCN.
                required:
                - name
            required:
            - compartmentId
            - vcn
          status:
            type: object
            properties:
              vcnId:
                description: VCN OCID
                type: string

The above XRD has a name (which is the kind we’ll be using in Claims) of SimpleVCN1, which can be instantiated with a Claim (we’ll see a Claim example shortly).  What we’ve done above is defined two required input attributes: compartmentId and vcn.name.  The status.vcnId attribute is used to store the OCID of the VCN that’s created (it’s used as an output, effectively).

Here’s a Composition that goes with it:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: simplevcn1
spec:
  writeConnectionSecretsToNamespace: crossplane-system
  compositeTypeRef:
    apiVersion: oci.platformref.crossplane.io/v1alpha1
    kind: SimpleVCN1
  resources:
  # VCN
  - name: vcn
    base:
      apiVersion: core.oci.jet.crossplane.io/v1alpha1
      kind: Vcn
      spec:
        forProvider:
          cidrBlocks:
          - "10.10.0.0/16"
          dnsLabel: "test"
    patches:
    - type: ToCompositeFieldPath
      fromFieldPath: status.atProvider.id
      toFieldPath: status.vcnId
    - type: FromCompositeFieldPath
      fromFieldPath: spec.compartmentId
      toFieldPath: spec.forProvider.compartmentId
    - type: FromCompositeFieldPath
      fromFieldPath: spec.vcn.name
      toFieldPath: spec.forProvider.displayName

There’s a lot going on here… and we don’t have time to delve into all the specifics, but this example creates a VCN (it points to apiVersion core.oci.jet.crossplane.io/v1alpha1) with a kind of Vcn.  The kind effectively tells Crossplane what kind of resource that is to be managed.  The VCN has a hardcoded IPv4 CIDR block and DNS label.  The patches section is how to get data into and out of the resource as attributes (from and to).  We’re storing the VCN OCID in the status.vcnId attribute (of type ToCompositeFieldPath).  Likewise we’re pulling in the compartmentId and vcn.name fields from the provided spec and writing them to the resource attributes (patch type FromCompositeFieldPath).

Here’s a sample Claim that instantiates a SimpelVCN1:

apiVersion: oci.platformref.crossplane.io/v1alpha1
kind: SimpleVCN1
metadata:
  name: crossplane-demo
spec:
  compartmentId: ocid1.compartment.<redacted>
  vcn:
    name: crossplane_demo

This claim will create a VCN that has a name of crossplane_demo, placed in the Compartment given (redacted in the above sample).  Note how the kind field is set to SimpleVCN1 (the same kind as used in the XRD and Composition above).

Now let’s look at a new XRD/Composition that’s more extensible.  Here’s the XRD (the UI):

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: vcnexamples1.oci.platformref.crossplane.io
spec:
  group: oci.platformref.crossplane.io
  names:
    kind: VcnExample1
    plural: vcnexamples1
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              compartmentId:
                type: string
                description: The Compartment OCID to deploy resources into.
              vcn:
                type: object
                properties:
                  name:
                    type: string
                    description: The name to use for the environment.
                  cidrBlocks:
                    type: array
                    description: Array of CIDR blocks to use for the VCN (one or more).
                    items:
                      type: string
                  dnsLabel:
                    type: string
                    description: DNS label to use.
                required:
                - name
                - cidrBlocks
                - dnsLabel
            required:
            - compartmentId
            - vcn
          status:
            type: object
            properties:
              vcnId:
                description: VCN OCID
                type: string

The above example defines a UI where we require the VCN name, CIDR blocks and DNS label, along with the Compartment OCID (Compartment ID) where it is to be created.  Here’s the Composition that goes with it:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: vcnexample1
spec:
  writeConnectionSecretsToNamespace: crossplane-system
  compositeTypeRef:
    apiVersion: oci.platformref.crossplane.io/v1alpha1
    kind: VcnExample1
  resources:
  - name: vcn
    base:
      apiVersion: core.oci.jet.crossplane.io/v1alpha1
      kind: Vcn
    patches:
    - type: ToCompositeFieldPath
      fromFieldPath: status.atProvider.id
      toFieldPath: status.vcnId
    - type: FromCompositeFieldPath
      fromFieldPath: spec.compartmentId
      toFieldPath: spec.forProvider.compartmentId
    - type: FromCompositeFieldPath
      fromFieldPath: spec.vcn.dnsLabel
      toFieldPath: spec.forProvider.dnsLabel
    - type: FromCompositeFieldPath
      fromFieldPath: spec.vcn.name
      toFieldPath: spec.forProvider.displayName
    - type: FromCompositeFieldPath
      fromFieldPath: spec.vcn.cidrBlocks
      toFieldPath: spec.forProvider.cidrBlocks

There’s a few more patches here, which is how we’re able to pull in more of the user-provided data (such as the CIDR blocks, DNS label, etc.).  Now let’s look at a sample Claim, which is how we’re instantiating this:

apiVersion: oci.platformref.crossplane.io/v1alpha1
kind: VcnExample1
metadata:
  name: vcnexample1
spec:
  compartmentId: ocid1.compartment.<redacted>
  vcn:
    name: vcnexample1
    cidrBlocks:
    - 192.168.0.0/22
    dnsLabel: vcn1

Wrapping It Up

This has been a long article, but hopefully you’ve been able to see a brief glimpse into what Crossplane is and why you should consider it in your IaC toolbox.  Crossplane is not for everyone… it has some rough edges and limitations (as of the time of this writing, it is not as mature as many other its IaC counterparts).  For those who are wanting to be on the bleeding edge, really like K8s and would benefit from a tool that gives a broad, end-to-end view of your application environment, take a look at Crossplane.