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.