Compositions let you package Kubernetes resources into higher-level abstractions, but making those abstractions truly reusable means letting them adapt to different environments.

Here’s a Composition that provisions an AWS S3 bucket. Notice the forProvider block – these are the parameters that will vary per environment.

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: composition.mycompany.com/s3bucket
spec:
  compositeTypeRef:
    apiVersion: mycompany.com/v1
    kind: S3Bucket
  resources:
    - name: s3bucket
      base:
        apiVersion: s3.aws.upbound.io/v1beta1
        kind: Bucket
        spec:
          forProvider:
            region: us-east-1 # Default region
            acl: private      # Default ACL
          # Other S3 bucket configurations...

The S3Bucket Custom Resource Definition (CRD) is what users will interact with. We’ll define fields here that map to the environment-specific values we want to inject.

apiVersion: apiextensions.crossplane.io/v1
kind: XRD
metadata:
  name: s3buckets.mycompany.com
spec:
  claim:
    apiVersion: mycompany.com/v1
    kind: S3Bucket
  # ... other XRD fields ...
   composiciónSelector:
    matchLabels:
      providerConfigRef: us-east-1 # Example selector

When a user creates an S3Bucket resource, they can specify environment-specific details. This is where the magic happens.

apiVersion: mycompany.com/v1
kind: S3Bucket
metadata:
  name: my-app-data
spec:
  parameters:
    bucketName: my-app-data-bucket
    region: us-west-2 # Override the default region
    storageClass: STANDARD_IA # Specify a different storage class

Inside the Composition, we use Patches to pull values from the S3Bucket claim into the underlying managed resource. This is how environment-specific configurations are injected.

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: composition.mycompany.com/s3bucket
spec:
  compositeTypeRef:
    apiVersion: mycompany.com/v1
    kind: S3Bucket
  resources:
    - name: s3bucket
      base:
        apiVersion: s3.aws.upbound.io/v1beta1
        kind: Bucket
        spec:
          forProvider:
            region: us-east-1
            acl: private
      patches:
        - fromFieldPath: spec.parameters.bucketName
          toFieldPath: spec.forProvider.bucketName
        - fromFieldPath: spec.parameters.region
          toFieldPath: spec.forProvider.region
        - fromFieldPath: spec.parameters.storageClass
          toFieldPath: spec.forProvider.storageClass

The fromFieldPath points to the claim’s fields, and toFieldPath points to the managed resource’s fields. Crossplane then reconciles these values, ensuring the S3 bucket is provisioned with the specified configurations.

This allows for a single Composition to provision resources that are tailored to different environments, such as development, staging, and production, without needing separate Compositions for each.

Consider a scenario where you want to enforce specific bucket naming conventions based on the environment. You can achieve this by using a combination of patches and potentially a transform to dynamically construct the bucketName.

# ... inside the Composition's resources section ...
      patches:
        # ... other patches ...
        - fromFieldPath: spec.parameters.bucketName
          toFieldPath: spec.forProvider.bucketName
        - fromFieldPath: spec.parameters.environmentSuffix # e.g., "-dev", "-staging"
          toFieldPath: spec.forProvider.bucketName
          transforms:
            - type: string
              string:
                fmt: "%s%s" # Concatenate claim's bucketName with environmentSuffix
              inputs:
                - fromFieldPath: spec.parameters.bucketName
                - fromFieldPath: spec.parameters.environmentSuffix

The transforms field is powerful. It allows you to manipulate values before they are applied. Here, we’re using a string transform with a fmt to concatenate the base bucketName with an environmentSuffix provided in the claim.

This technique allows for a high degree of flexibility. You can inject not just simple values but also dynamically generated ones, like timestamps for versioning or specific tags for cost allocation, all driven by the user’s S3Bucket claim.

The actual mechanism relies on Crossplane’s reconciliation loop. When a change is detected in the S3Bucket claim, Crossplane re-evaluates the Composition, applies the defined patches, and updates the managed S3 bucket resource accordingly.

The most surprising thing is that the spec.parameters block in your claim CRD doesn’t need a predefined schema if you’re only using it for dynamic patching. Crossplane will happily try to patch any path it finds, and if it doesn’t exist, it simply won’t apply that specific patch, making it quite resilient to evolving claim definitions.

The next step is exploring how to conditionally include or exclude resources within a Composition based on these environment-specific values.

Want structured learning?

Take the full Crossplane course →