Crossplane Composition Functions: Building Cloud Platforms in 2026
Imagine declaring a multi-cloud database with a single Kubernetes manifest—and having it automatically provision permissions, backups, and network policies without writing Go templates or Patch & Transform logic. That’s Crossplane Composition Functions in production today. This shift from declarative patching to functional pipelines has become the backbone of serious internal developer platforms (IDPs). In this post, we’ll dissect the gRPC-based function model, compare SDK choices, and show you how to build platform abstractions that scale across cloud providers. Whether you’re migrating from Patch & Transform or architecting your first IDP, understanding composition functions is essential for Kubernetes-native infrastructure in 2026. What this post covers: the reference architecture, function anatomy, a real CloudSQL example, SDK trade-offs, failure modes, and production deployment patterns.
Why Composition Functions Replaced Patch & Transform
Crossplane’s original composition model—Patch & Transform (P&T)—was powerful but rigid. Developers wrote CEL expressions or Go templates inline within CompositionResources, leading to massive, hard-to-test YAML files. Logic was tangled with resource declarations. Debugging meant parsing through nested substitutions and field-path errors. By Crossplane v1.14 (November 2023), the project introduced Composition Functions as a GA feature, and v1.17 made pipeline mode the default.
Functions invert the problem. Instead of patching fields into managed resources, functions run as external gRPC services, receiving the entire composition state as structured input and returning modified desired state. Each function is a discrete, testable stage in a pipeline. You can write a function once, package it as an OCI image, and reuse it across dozens of compositions. The function-patch-and-transform itself became a function (for backward compatibility), illustrating the model’s elegance.
This shift enables better separation of concerns: platform engineers own the composition functions; developers own the claims. Functions run on a schedule (default 60s timeout, configurable), are deployed to the cluster as Function resources, and communicate over Unix sockets via gRPC. The result is deterministic, debuggable, and composable platform logic.
Reference Architecture for a Functions-Based Composition
The canonical flow is straightforward but powerful:

A developer applies an XRClaim (e.g., a PostgresqlInstance claim). The Crossplane controller looks up the corresponding Composite Resource (XR) definition and its active Composition. The Composition specifies mode: Pipeline, listing a sequence of functions. Each function receives a RunFunctionRequest containing:
- observed: current state of all managed resources
- desired: the target resource template after function processing
- context: metadata like the claim name, target cloud region, owner references
The function processes this state and returns a modified desired state. The next function in the pipeline receives this updated state as its input. After all functions complete, the Crossplane controller reconciles the final desired state into managed resources (CloudSQL instances, IAM roles, Secrets, etc.).
The key advantage: functions are composable stages, not monolithic templates. A “fetch-secret” function can run before a “render-cloud-config” function; both remain reusable and testable in isolation.
Anatomy of a Composition Function
Under the hood, every Crossplane function is a gRPC service speaking the Crossplane-defined protocol:

The interface is minimal:
service Function {
rpc RunFunction(RunFunctionRequest) returns (RunFunctionResponse);
}
RunFunctionRequest contains:
observed_composite: the current state of the XR and its managed resourcesdesired_composite: the desired state after earlier pipeline stagesinput: function-specific configuration from the Composition manifestcontext: claim name, target region, namespace, etc.
RunFunctionResponse returns:
desired_composite: updated desired stateresults: array of log messages, warnings, errorsfatal: boolean to halt the pipeline on error
The Crossplane controller orchestrates the pipeline: function 1 input = claimed state; function 1 output = function 2 input. If a function sets fatal: true, the reconciliation loop pauses and retries on the next sync period.
Functions are deployed as Function resources:
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-templating
spec:
package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.1.0
The package field points to an OCI image containing the function binary and configuration. Crossplane pulls the image, spawns the function as a sidecar, and communicates over a Unix socket at /tmp/crossplane-function-<hash>.sock. This design avoids network latency and sidesteps service discovery.
Supported runtimes include:
- Go SDK (function-go, fastest, type-safe)
- Python SDK (function-python, good for ML/data pipelines)
- KCL SDK (function-kcl, declarative + strong validation)
- function-go-templating (Go text/template for simple cases, zero compilation)
The Crossplane controller enforces a 60-second timeout per function call. Longer operations should offload to async cloud jobs and poll status.
Walkthrough: Building an Internal Developer Platform XRD
Let’s build a practical example: a XPostgresInstance that developers claim, and the platform provisions CloudSQL, IAM bindings, and a connection secret.
Step 1: Define the Claim Type
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresinstances.mycompany.dev
spec:
group: mycompany.dev
names:
kind: XPostgresInstance
plural: xpostgresinstances
claimNames:
kind: PostgresInstance
plural: postgresinstances
versions:
- name: v1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
region:
type: string
default: "us-central1"
tier:
type: string
default: "db-f1-micro"
databaseVersion:
type: string
default: "POSTGRES_15"
backupLocation:
type: string
default: "us"
vaultSecret:
type: string
description: "Name of Vault secret containing admin password"
required:
- parameters
status:
type: object
properties:
ready:
type: boolean
connectionSecretRef:
type: object
properties:
name:
type: string
namespace:
type: string
This XRD exposes three main parameters: region, instance tier, and database version. The claim status includes a reference to the provisioned connection secret.
Step 2: Write a Composition with Function Pipeline
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: postgres-cloudsql
labels:
crossplane.io/xrd: xpostgresinstances.mycompany.dev
spec:
compositeTypeRef:
apiVersion: apiextensions.crossplane.io/v1
kind: XPostgresInstance
mode: Pipeline
pipeline:
- step: fetch-vault-secret
functionRef:
name: function-vault
input:
apiVersion: vault.function.crossplane.io/v1beta1
kind: VaultLookup
metadata:
name: fetch-admin-password
spec:
secretPath: "secret/data/postgres/admin"
secretField: "password"
- step: render-cloudsql
functionRef:
name: function-go-templating
input:
apiVersion: templating.function.crossplane.io/v1
kind: GoTemplate
metadata:
name: render-cloudsql-instance
spec:
source: Inline
inline:
template: |
{{- $adminPassword := .context.Vault.password -}}
apiVersion: sql.gcp.crossplane.io/v1beta1
kind: CloudSQLInstance
metadata:
name: {{ .observed.composite.metadata.name }}-cloudsql
namespace: crossplane-system
spec:
forProvider:
region: {{ .observed.composite.spec.parameters.region }}
databaseVersion: {{ .observed.composite.spec.parameters.databaseVersion }}
tier: {{ .observed.composite.spec.parameters.tier }}
backupConfiguration:
enabled: true
binaryLogEnabled: true
location: {{ .observed.composite.spec.parameters.backupLocation }}
settings:
availabilityType: REGIONAL
ipConfiguration:
ipv4Enabled: true
authorizedNetworks:
- value: "0.0.0.0/0"
requireSsl: true
providerConfigRef:
name: gcp-provider
status: {}
---
apiVersion: sql.gcp.crossplane.io/v1beta1
kind: User
metadata:
name: {{ .observed.composite.metadata.name }}-admin
spec:
forProvider:
instanceRef:
name: {{ .observed.composite.metadata.name }}-cloudsql
password:
secretRef:
name: cloudsql-admin-password
namespace: crossplane-system
key: password
providerConfigRef:
name: gcp-provider
- step: create-connection-secret
functionRef:
name: function-auto-ready
input:
apiVersion: meta.function.crossplane.io/v1
kind: AutoReady
metadata:
name: mark-ready
spec:
managedResources:
- apiVersion: sql.gcp.crossplane.io/v1beta1
kind: CloudSQLInstance
- apiVersion: sql.gcp.crossplane.io/v1beta1
kind: User
This pipeline has three stages:
- fetch-vault-secret: Retrieve the admin password from a Vault instance outside Kubernetes.
- render-cloudsql: Use Go templating to materialize CloudSQL instance and user manifests based on the claim spec.
- create-connection-secret: Mark the composition ready once both managed resources are ready.
Each function is independent, testable, and reusable across multiple compositions.
Step 3: Developer Experience
A developer creates a claim:
apiVersion: mycompany.dev/v1
kind: PostgresInstance
metadata:
name: my-analytics-db
namespace: data-engineering
spec:
parameters:
region: "us-west1"
tier: "db-custom-4-16384"
databaseVersion: "POSTGRES_15"
backupLocation: "us-west1"
vaultSecret: "secret/data/postgres/admin"
Within seconds, the Crossplane controller:
1. Recognizes the claim matches XPostgresInstance.
2. Instantiates the Composition and pipeline.
3. Runs each function in sequence.
4. Reconciles the final desired state (CloudSQL instance, user, and connection secret).
5. Updates the claim status with connection details.
The developer never touches GCP console, writes Terraform, or manages IAM roles. The platform owns complexity; developers own simplicity.

Function SDKs Compared: Go vs Python vs KCL vs Templating
Choosing a runtime involves trade-offs. Crossplane provides multiple SDKs to match different use cases:
Go SDK (function-go)
Best for: High performance, type safety, complex logic.
package main
import (
"context"
fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
"google.golang.org/grpc"
)
func (s *Server) RunFunction(ctx context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
// Type-safe access to observed and desired state
desired := req.DesiredComposite
observed := req.ObservedComposite
// Mutate desired state
// ...
return &fnv1.RunFunctionResponse{
DesiredComposite: desired,
Results: []*fnv1.Result{
{
Severity: fnv1.Severity_SEVERITY_NORMAL,
Message: "Successfully processed composition",
},
},
}, nil
}
Go SDK advantages:
– Compile-time type checking.
– Minimal reflection overhead; latency typically <50ms per function call.
– Access to rich ecosystem (AWS SDK, GCP client libraries).
– Native support for complex logic (loops, conditionals, error handling).
Go SDK disadvantages:
– Requires compilation and container build.
– Larger binary size (~30-50MB compressed).
– Steeper learning curve for platform teams without Go experience.
Python SDK (function-python)
Best for: Data transformation, ML-driven logic, rapid prototyping.
import json
from function.proto import v1pb2 as fnv1
def run_function(request: fnv1.RunFunctionRequest) -> fnv1.RunFunctionResponse:
desired = request.desired_composite
observed = request.observed_composite
# Mutation logic
# ...
return fnv1.RunFunctionResponse(
desired_composite=desired,
results=[
fnv1.Result(
severity=fnv1.Severity.SEVERITY_NORMAL,
message="Processed composition"
)
]
)
Python advantages:
– Easy to learn; familiar to DevOps teams.
– Strong for data pipelines, parsing, and transformation.
– Rapid iteration; minimal compilation.
Python disadvantages:
– Slower startup; cold-start latency can exceed 500ms.
– Larger memory footprint.
– GIL contention under high concurrency.
KCL SDK (function-kcl)
Best for: Declarative validation, schema enforcement, policy-as-code.
import k8s
import crossplane.pkg
_adminPassword = context.vault_secret.password
cloudsql_instance = k8s.core.v1.ObjectMeta {
name = context.claim_name + "-cloudsql"
namespace = "crossplane-system"
}
resources = [
{
apiVersion = "sql.gcp.crossplane.io/v1beta1"
kind = "CloudSQLInstance"
metadata = cloudsql_instance
spec = {
forProvider = {
region = context.region
databaseVersion = context.databaseVersion
tier = context.tier
}
}
}
]
KCL advantages:
– Declarative, human-readable; reduces imperative bugs.
– Built-in validation and type checking.
– Policy enforcement (e.g., “all CloudSQL instances must have backups enabled”).
– Fast execution (~100ms).
KCL disadvantages:
– Smaller ecosystem; fewer third-party integrations.
– Steeper learning curve for teams unfamiliar with KCL syntax.
function-go-templating
Best for: Simple cases, zero compilation, Go text/template syntax.
{{ $region := .observed.composite.spec.parameters.region }}
apiVersion: sql.gcp.crossplane.io/v1beta1
kind: CloudSQLInstance
metadata:
name: {{ .observed.composite.metadata.name }}-cloudsql
spec:
forProvider:
region: {{ $region }}
tier: db-f1-micro
Templating advantages:
– No compilation; instant feedback.
– Familiar Go template syntax.
– Ideal for simple field substitution.
Templating disadvantages:
– No type safety; errors emerge at runtime.
– Limited to Go template semantics; no loops or conditionals.
– Scales poorly for complex multi-resource orchestration.

Trade-offs and Failure Modes
While Composition Functions unlock powerful capabilities, they introduce operational complexity:
Reconciliation Latency
Each function call adds latency to the reconciliation loop. A typical composition without functions reconciles in 50-100ms. Adding three functions (vault lookup, template rendering, status check) can push latency to 250-400ms, especially under high load. Vault lookups and external API calls compound the cost. For time-sensitive applications (auto-scaling, active-passive failover), this added latency matters.
Mitigation: Use function caching, run functions on dedicated node pools with high memory, and avoid blocking I/O in the function hot path.
Debugging Across Function Boundaries
When a composition fails, the error may originate in function A, propagate as corruption through function B, and surface as a constraint violation in function C. Stack traces span multiple gRPC boundaries. Tools like crossplane render help, but tracing a failure across the entire pipeline requires logging discipline.
Mitigation: Instrument each function with structured logging; include function name, input hash, and result status in logs. Adopt distributed tracing (e.g., OpenTelemetry) early.
Package Supply Chain
Functions are OCI images pulled from registries. A compromised function image can inject malicious resources into the cluster. Pinning image digests (SHA256) instead of tags is mandatory. Scanning images for vulnerabilities before promotion to production is essential.
Mitigation: Use image signatures (Cosign), enforce admission policies (Kyverno, OPA/Gatekeeper), and scan all function images with a SBOM tool.
Cold Start Overhead
Python and larger Go binaries suffer cold-start latency (first invocation after a controller restart can be 500ms+). Container runtimes and image caching mitigate this, but it’s a real cost in high-churn environments.
Mitigation: Use function-go or function-go-templating for latency-sensitive compositions. Pre-pull function images on nodes. Monitor crossplane_function_run_duration_seconds to identify outliers.

Production Recommendations
1. Pin Function Digests
Never use image tags; always pin by SHA256 digest:
spec:
package: xpkg.upbound.io/crossplane-contrib/function-go-templating@sha256:a3f9b8c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7
This ensures reproducible function execution across restarts and clusters.
2. Dedicated Node Pool for Functions
Crossplane controllers and functions should not contend for CPU. Provision a dedicated node pool for crossplane-system namespace:
nodeSelector:
workload: crossplane-functions
tolerations:
- key: crossplane.io/function
operator: Equal
value: "true"
effect: NoSchedule
3. Monitor Function Latency
Crossplane exports Prometheus metrics:
crossplane_function_run_duration_seconds{function="function-go-templating"} 0.145
crossplane_function_call_count{function="function-go-templating"} 42
Alert if any function consistently exceeds 1 second. Use Prometheus histograms to track p50, p95, p99 latencies.
4. Validate Before Applying
Use crossplane render to execute the composition locally and inspect the generated resources:
crossplane render xpostgresinstances.mycompany.dev \
--claim-file my-claim.yaml \
--composition-file my-composition.yaml
This catches logic errors before deployment.
5. Composition Revisions
Never modify a Composition in place. Use composition revisions to safely roll out changes:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: postgres-cloudsql
spec:
compositeTypeRef:
apiVersion: apiextensions.crossplane.io/v1
kind: XPostgresInstance
publishedVersion: "1.0.0"
mode: Pipeline
# ...
Increment the version, deploy the new Composition alongside the old one, and gradually migrate claims via compositionSelector labels.
6. Function Health Checks
Deploy function health check sidecar:
apiVersion: v1
kind: Pod
metadata:
name: function-go-templating
spec:
containers:
- name: function
image: xpkg.upbound.io/crossplane-contrib/function-go-templating@sha256:...
livenessProbe:
grpc:
port: 9443
initialDelaySeconds: 10
periodSeconds: 30
gRPC liveness probes ensure stuck functions are restarted.
FAQ
Q: Are Composition Functions production ready?
A: Yes. Composition Functions reached GA in v1.14 (November 2023) and are battle-tested in production IDPs at scale. However, they add operational complexity; teams should be comfortable with gRPC debugging and image supply chain management.
Q: Do I need Composition Functions or can I still use Patch & Transform?
A: Patch & Transform still works (it’s implemented as a built-in function now). Use P&T for simple compositions; migrate to functions when logic becomes unwieldy, testability matters, or you need code reuse across multiple compositions. Functions are worth the investment if your IDP scales to 20+ claim types.
Q: How do I debug a failing function?
A: Enable debug logging on the Crossplane controller: --debug flag. Inspect function logs with kubectl logs -f deployment/crossplane -c crossplane -n crossplane-system. Use crossplane render to test the composition locally. Add structured logging (Zap, Slog) to the function itself, including request/response hashes.
Q: What’s the latency cost?
A: A single function call adds 50-200ms under normal load; each additional function multiplies this. Vault lookups and external API calls can add 500ms+. For most workloads, this is acceptable; provision headroom in your reconciliation SLAs.
Q: Crossplane vs Terraform vs Pulumi for IDPs?
A: Crossplane is Kubernetes-native; resources are CRDs, not state files. Terraform is cloud-agnostic and mature; Pulumi is programmatic. Choose Crossplane if your team is invested in Kubernetes; choose Terraform for multi-cloud with less k8s friction; choose Pulumi for teams that prefer Python/Go over HCL. Crossplane’s function model is closest to Pulumi’s multi-language SDK ecosystem.
Q: Can I write functions in TypeScript?
A: Not yet. The SDK ecosystem covers Go, Python, and KCL. TypeScript support is on the roadmap but requires a JavaScript runtime sidecar and adds startup overhead. For now, Go or Python are the practical choices.
Further Reading
For deeper dives into Crossplane, check out our companion posts on internal developer platforms and service mesh observability. If you’re building real-time systems with Crossplane, see our tutorial on MQTT clusters in Kubernetes.
External references:
- Crossplane Composition Functions documentation — official Crossplane docs on function architecture and SDK usage.
- Crossplane Contrib function library — open-source function implementations (KCL, templating, auto-ready, patch, etc.).
- CNCF blog: Crossplane and the future of infrastructure as code — context on Crossplane’s role in the CNCF ecosystem and IDP evolution.
Written by the iotdigitaltwinplm.com platform-engineering desk. Last updated 2026-04-24.
