Crossplane compositions are just OCI images, and you’ve been doing it wrong this whole time.

Let’s see this in action. Imagine you’ve got a composition that provisions a Google Cloud SQL instance. Here’s the CompositeResourceDefinition (CRD) and the Composition itself:

# gcp-sql-instance-crd.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: gcp-sqlinstances.sql.gcp.example.com
spec:
  group: sql.gcp.example.com
  names:
    kind: GcpSqlInstance
    plural: gcp-sqlinstances
  claimNames:
    kind: SqlInstance
    plural: sqlinstances
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                parameters:
                  type: object
                  properties:
                    storageGB:
                      type: integer
                    databaseVersion:
                      type: string
              required:
                - parameters
            status:
              type: object
              properties:
                instance:
                  type: object
                  properties:
                    name:
                      type: string
                    connectionName:
                      type: string

---
# gcp-sql-instance-composition.yaml
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: gcp-sql-instance-composition
  labels:
    provider: gcp
    type: database
spec:
  compositeTypeRef:
    apiVersion: sql.gcp.example.com/v1alpha1
    kind: GcpSqlInstance
  resources:
    - name: sql-instance
      base:
        apiVersion: sqladmin.gcp.upbound.io/v1beta1
        kind: Instance
        spec:
          forProvider:
            region: us-central1
            settings:
              tier: db-f1-micro
              ipConfiguration:
                ipv4Enabled: true
          providerConfigRef:
            name: gcp-provider
      patches:
        - fromFieldPath: spec.parameters.storageGB
          toFieldPath: spec.forProvider.settings.tier
          transforms:
            - type: map
              match:
                value: "10"
              toValue: "db-g1-small"
            - type: map
              match:
                value: "20"
              toValue: "db-g2-small"
        - fromFieldPath: spec.parameters.databaseVersion
          toFieldPath: spec.forProvider.databaseVersion

---
# gcp-sql-instance-claim.yaml
apiVersion: sql.gcp.example.com/v1alpha1
kind: SqlInstance
metadata:
  name: my-postgres-instance
spec:
  parameters:
    storageGB: 10
    databaseVersion: POSTGRES_13

Now, to distribute this, you’d typically use kubectl apply -f or Helm. But Crossplane’s real power comes when you package these as OCI images.

First, you need to build the OCI image. You can use docker build or buildah. Let’s assume you have a Dockerfile that looks like this:

FROM scratch
COPY crds.yaml /crds/
COPY compositions.yaml /compositions/
# Add any other Kubernetes manifests you want to include

And your crds.yaml and compositions.yaml are simply concatenations of your YAML files:

# crds.yaml
cat gcp-sql-instance-crd.yaml > crds.yaml

# compositions.yaml
cat gcp-sql-instance-composition.yaml > compositions.yaml

Then you build the image:

docker build -t your-registry/crossplane-gcp-sql:v0.1.0 .
docker push your-registry/crossplane-gcp-sql:v0.1.0

Now, in your Crossplane installation, you configure a Provider (or Function) to pull and apply these OCI images. If you’re using Crossplane as a platform (which you should be), you’d define a Configuration resource:

apiVersion: pkg.crossplane.io/v1
kind: Configuration
metadata:
  name: crossplane-gcp-sql-config
spec:
  package: your-registry/crossplane-gcp-sql:v0.1.0
  packagePullPolicy: IfNotPresent

When Crossplane sees this Configuration, it pulls the OCI image, extracts the crds.yaml and compositions.yaml (or whatever you put in your Dockerfile), and applies them to the Kubernetes cluster. It’s not just about applying YAML; it’s about managing the lifecycle of your composed infrastructure definitions as versioned artifacts.

The surprising thing is that Crossplane’s Configuration resource is the mechanism for distributing these OCI images. You don’t need a separate chart or operator to manage the distribution of your compositions. Crossplane itself handles the pulling, unpacking, and applying of the OCI image contents. This allows you to treat your infrastructure definitions just like you treat your application code: versioned, distributed via registries, and deployed declaratively.

The real magic happens when you realize that the Configuration resource doesn’t just apply static YAML. It can also trigger the execution of Crossplane Functions. These Functions can dynamically generate or modify resources based on inputs, allowing for much more sophisticated composition logic than static YAML can provide. You can even package these Functions as OCI images themselves, creating a powerful, extensible system for infrastructure as code.

The next step is to explore how to use Crossplane Functions to build dynamic compositions that react to external events or input parameters.

Want structured learning?

Take the full Crossplane course →