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.