Crossplane Claims can’t validate themselves against arbitrary rules; they need an external policy engine to enforce complex constraints.
Let’s see Crossplane and CEL in action. Imagine we have a CompositeResourceDefinition (XRD) for a managed PostgreSQL instance. It looks something like this:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: pginstances.database.example.com
spec:
group: database.example.com
names:
kind: PGInstance
plural: pginstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
description: "Storage in GB"
instanceClass:
type: string
description: "Instance class, e.g., small, medium, large"
region:
type: string
description: "AWS region for the instance"
required:
- storageGB
- instanceClass
- region
Now, a user wants to provision a PGInstance by creating a Composite resource, which Crossplane calls a Claim.
apiVersion: database.example.com/v1alpha1
kind: PGInstance
metadata:
name: my-prod-db
spec:
parameters:
storageGB: 100
instanceClass: medium
region: us-east-1
Without any policy, this PGInstance claim could specify any integer for storageGB, any string for instanceClass, and any string for region. This is where CEL (Common Expression Language) comes in. We’ll use a policy engine, like OPA (Open Policy Agent) Gatekeeper, which supports CEL for its constraint validation.
First, we define a ConstraintTemplate that uses CEL. This template will be a blueprint for our validation rules.
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8spspgruntime
annotations:
description: "Restricts the allowed values for storageGB and instanceClass on PGInstance claims."
spec:
crd:
spec:
names:
kind: PGInstancePolicy
validation:
openAPIV3Schema:
type: object
properties:
allowedStorageGB:
type: array
items:
type: integer
allowedInstanceClasses:
type: array
items:
type: string
allowedRegions:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8spspgruntime
violation[{"msg": msg}] {
# Check storageGB
input.review.object.spec.parameters.storageGB < 10
msg := "Storage must be at least 10GB."
}
violation[{"msg": msg}] {
# Check instanceClass
instance_class := input.review.object.spec.parameters.instanceClass
not re_match("^(small|medium|large)$", instance_class)
msg := sprintf("Instance class '%v' is not allowed. Must be one of: small, medium, large.", [instance_class])
}
violation[{"msg": msg}] {
# Check region
region := input.review.object.spec.parameters.region
not some region_allowed in input.parameters.allowedRegions
region_allowed == region
msg := sprintf("Region '%v' is not allowed. Must be one of: %v.", [region, input.parameters.allowedRegions])
}
Notice the rego field. While this example uses Rego, Gatekeeper allows direct embedding of CEL expressions within its spec.targets.rego field (or more commonly, via spec.targets.rego and then referencing CEL expressions within the Rego). For simplicity and directness, let’s assume a future Gatekeeper version or a similar tool that allows direct CEL evaluation for admission control. The spirit of the CEL expression would look like this (conceptually, not actual Gatekeeper syntax for CEL):
input.review.object.spec.parameters.storageGB >= 10 &&
input.review.object.spec.parameters.instanceClass in ["small", "medium", "large"] &&
input.parameters.allowedRegions.contains(input.review.object.spec.parameters.region)
This CEL expression checks three conditions:
storageGBmust be 10 or greater.instanceClassmust be one of "small", "medium", or "large".regionmust be present in theallowedRegionslist provided to the constraint.
Now, we create a Constraint that uses this ConstraintTemplate and specifies the actual values for our policies.
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: PGInstancePolicy
metadata:
name: pginstance-storage-class-region-policy
spec:
match:
kinds:
- apiGroups: ["database.example.com"]
kinds: ["PGInstance"]
parameters:
allowedStorageGB: [10, 20, 50, 100] # Note: The CEL logic above is a floor, not an exact list match.
allowedInstanceClasses: ["small", "medium", "large"]
allowedRegions: ["us-east-1", "us-west-2"]
When a user creates or updates a PGInstance claim, Gatekeeper intercepts the request. It looks up the PGInstancePolicy constraint, finds the k8spspgruntime ConstraintTemplate, and evaluates the CEL (or Rego with CEL expressions) against the claim’s spec.parameters.
If the PGInstance claim is:
apiVersion: database.example.com/v1alpha1
kind: PGInstance
metadata:
name: my-prod-db
spec:
parameters:
storageGB: 5
instanceClass: xxlarge
region: eu-central-1
Gatekeeper will reject it with an error message like:
admission webhook "validation.gatekeeper.sh" denied the request: [pginstance-storage-class-region-policy] Storage must be at least 10GB.; [pginstance-storage-class-region-policy] Instance class 'xxlarge' is not allowed. Must be one of: small, medium, large.; [pginstance-storage-class-region-policy] Region 'eu-central-1' is not allowed. Must be one of: us-east-1, us-west-2.
If the claim is valid:
apiVersion: database.example.com/v1alpha1
kind: PGInstance
metadata:
name: my-staging-db
spec:
parameters:
storageGB: 50
instanceClass: medium
region: us-west-2
The admission webhook will allow the request to proceed, and Crossplane will then instantiate the underlying Composed resource (e.g., an AWS RDS instance).
The core idea is that Crossplane defines the shape and intent of your managed infrastructure via XRDs, but CEL (via a policy engine like Gatekeeper) provides the enforcement of specific business or operational rules on how those resources can be provisioned. This separation ensures that your claims are not only syntactically correct according to the schema but also semantically compliant with your organization’s policies.
The real power here is that you’re not just validating against the schema defined in the XRD; you’re adding custom, dynamic validation logic that can span multiple fields, compare values, and even reference external lists of allowed values. This allows for sophisticated governance over your cloud resources provisioned through Crossplane.
The next step after implementing these basic validation policies is to explore using CEL to enforce more complex relationships between parameters, such as ensuring that a specific instanceClass can only be provisioned in certain regions or that storageGB must be a multiple of 10.