Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Infrastructure as Code By James Joyner IV · · 11 min read

Crossplane Composition Functions: When Patch-and-Transform Runs Out

Patch-and-transform compositions hit a wall fast. Composition Functions let you express real logic in code, with loops and conditionals, for your control plane.

  • #iac
  • #ai
  • #crossplane
  • #compositions
  • #kubernetes

Patch-and-transform compositions are where most Crossplane journeys start, and where most of them get stuck. The model is elegant for simple mappings: take a field from the claim, transform it, patch it onto a managed resource. But the first time you need to create a variable number of subnets based on an input list, or conditionally include a resource only when a flag is set, you discover that patch-and-transform has no loops, no conditionals worth the name, and no way to express “it depends.” You end up with sprawling YAML, duplicated resource blocks, and string templating that nobody can read six months later.

Composition Functions are Crossplane’s answer. Instead of declaring patches, you run a pipeline of functions that receive the observed and desired state and return the resources to compose — written in real code, or in purpose-built DSLs like KCL and Go templates. This guide walks through when to reach for them, how the pipeline model works, and how to keep the added power from becoming added risk.

The breaking point for patch-and-transform

Here is the kind of requirement that patch-and-transform handles badly. A platform team exposes a NetworkClaim where a consumer specifies a list of availability zones, and each zone should get its own subnet managed resource. With patch-and-transform you cannot iterate over a list to produce N resources — you have to hardcode a maximum and patch each one, leaving empty resources when fewer zones are requested. It works until it doesn’t, and the YAML becomes unmaintainable.

A function pipeline expresses this directly. Here is the same composition declaring a pipeline step instead of inline patches:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: network.platform.example.org
spec:
  compositeTypeRef:
    apiVersion: platform.example.org/v1alpha1
    kind: XNetwork
  mode: Pipeline
  pipeline:
    - step: render-subnets
      functionRef:
        name: function-go-templating
      input:
        apiVersion: gotemplating.fn.crossplane.io/v1beta1
        kind: GoTemplate
        source: Inline
        inline:
          template: |
            {{- range $i, $az := .observed.composite.resource.spec.zones }}
            ---
            apiVersion: ec2.aws.upbound.io/v1beta1
            kind: Subnet
            metadata:
              annotations:
                crossplane.io/composition-resource-name: subnet-{{ $i }}
            spec:
              forProvider:
                availabilityZone: {{ $az }}
                cidrBlock: 10.0.{{ $i }}.0/24
            {{- end }}
    - step: auto-ready
      functionRef:
        name: function-auto-ready

The range loop produces exactly as many Subnet resources as there are zones in the claim — no hardcoded ceiling, no empty placeholders. The function-auto-ready step then marks the composite ready once its resources reconcile. This is the kind of logic that patch-and-transform simply cannot express cleanly.

Composing functions in a pipeline

The pipeline model is the mental shift. Each function receives the current state — what the consumer asked for and what already exists — and returns a desired state, passing its output to the next function. You can chain a templating function that generates resources, a function that injects standard labels, and a validation function that rejects bad combinations, each doing one job. This is the same composability that makes Unix pipes powerful, applied to control-plane logic.

Because functions are ordinary programs (often distributed as OCI images), you can write them in Go, Python, or use community functions for templating, patch-and-transform compatibility, and readiness. The trade-off is operational: every function is now a dependency you version, scan, and trust, which is why pinning function versions and treating them as supply-chain artifacts matters.

Using AI to draft the pipeline, then verifying

This is exactly the kind of task where an LLM accelerates the boring part and a human catches the dangerous part. A useful prompt:

You are a Crossplane platform engineer. Convert this patch-and-transform Composition to Pipeline mode using function-go-templating. The claim has a spec.zones list and a spec.size enum. Produce one Subnet per zone and map size to an instance class. Show the full Composition and explain which behaviors changed.

A capable model will return something close to the pipeline above. What it returned for one team looked correct but defaulted the CIDR math in a way that would have overlapped subnets across two claims:

Here’s the converted pipeline. Note: I derived cidrBlock from the loop index only, so two XNetwork instances would both allocate 10.0.0.0/24. You’ll want to incorporate a per-claim base CIDR from the claim spec to avoid collisions.

That self-flagged caveat is the whole point of the AI-drafts, human-verifies loop. The model produced the structure in seconds and even surfaced the collision risk — but the engineer is the one who decides how CIDRs are allocated across the fleet, because that’s a blast-radius decision no model should make unsupervised. Always render the composition offline with crossplane render and diff the output before trusting it.

Keeping the power in check

Function pipelines remove the guardrails that patch-and-transform’s verbosity accidentally provided. A loop that miscounts produces fifty resources instead of three; a templating bug repoints every managed resource. Three practices keep this safe:

  • Render before merge. crossplane render evaluates the full pipeline offline and emits the composed YAML. Gate every composition change on a golden-file diff in CI so output changes are visible in review.
  • Pin function versions. Functions are OCI images; an unpinned latest means your control plane’s behavior can change without a commit. Treat them like any other dependency.
  • Validate in the pipeline. Add a function step that rejects invalid input combinations with a clear message, so a bad claim fails fast instead of producing broken infrastructure.

For the testing discipline that makes this safe, see our Crossplane Composition testing prompt and the Composition Functions prompt for generating the pipelines themselves. The broader Infrastructure as Code category covers the rest of the control-plane toolchain.

When to make the jump

Don’t migrate everything. Patch-and-transform is still the right tool for simple, fixed-shape compositions, and it’s easier to review. Reach for functions when you need iteration, conditionals, or logic that string templating can’t express without becoming unreadable. The signal is usually obvious: when your composition YAML has more comments explaining the workarounds than actual resource definitions, the pipeline model will be simpler despite the added moving parts. Start with one composition, get the render-test loop solid, and expand from there.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.