Crossplane for Platform APIs
Crossplane lets a platform team define its own Kubernetes APIs and implement them as compositions of real infrastructure. A developer creates a small Database or WebService object; Crossplane expands it into the cloud resources, IAM, networking and Kubernetes objects behind it and keeps them reconciled.
That makes it one of the main engines for the self-service layer of an internal developer platform. This note is about the platform-design side. Installation, providers and AWS authentication are covered in Crossplane on EKS.
Two audiences, two layers
developer writes platform team writes Crossplane manages
┌──────────────────┐ ┌──────────────────────────────────┐ ┌──────────────────────────┐
│ kind: Database │ │ XRD: the schema of "Database" │ │ RDS instance │
│ spec: │ ───► │ Composition: how to build one │ ──► │ subnet group, SG │
│ size: small │ │ (a pipeline of functions) │ │ KMS key, parameter group │
│ engine: 16 │ │ │ │ Secret with credentials │
└──────────────────┘ └──────────────────────────────────┘ └──────────────────────────┘
the platform API the implementation managed resources
The XRD (CompositeResourceDefinition) is the contract with developers. The Composition is the implementation and can change without the contract changing — which is the property that solves the “day-two” problem described in golden paths: tighten a default in one Composition and every existing instance is reconciled to it.
Designing the API
The hard part of Crossplane is not YAML. It is deciding what the developer-facing API should look like.
Ask for intent, not implementation.
| Leaky | Intent-based |
|---|---|
instanceClass: db.r7g.large | size: medium |
multiAZ: true, backupRetentionPeriod: 14 | tier: production |
subnetIds: […], vpcSecurityGroupIds: […] | Nothing — derived from the environment |
storageEncrypted: true, kmsKeyId: … | Nothing — always on, not negotiable |
Guidelines:
- Expose what legitimately varies between uses: size, engine version, perhaps a tier. Decide everything else.
- Never expose a security or compliance setting as optional. If encryption is required, it is not a field.
- Use enums with meaningful names.
small | medium | largecan be remapped to new instance generations later; a raw instance type cannot. - Report useful status: an endpoint, the name of the Secret holding credentials, a ready condition.
- Keep it cloud-neutral only if you will really run on several clouds. Otherwise the abstraction costs more than it returns.
- Fewer fields is better. Every field is a promise you must keep across versions.
A worked example: Crossplane v2 Namespaced Control Plane
In Crossplane v2, platform APIs can be defined with namespaced scope (scope: Namespaced), allowing product teams to create composite resources directly inside their team namespaces without requiring cluster-wide privileges. Upbound Provider AWS v2 provides matching namespaced managed resources under the *.aws.m.upbound.io API groups.
1. The CompositeResourceDefinition (XRD)
The platform team defines the contract in the cluster:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: databases.platform.example.com
spec:
group: platform.example.com
names:
kind: Database
plural: databases
scope: Namespaced
claimNames:
kind: DatabaseClaim
plural: databaseclaims
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [size]
properties:
size:
type: string
enum: [small, medium, large]
engineVersion:
type: string
enum: ["15", "16", "17"]
default: "16"
tier:
type: string
enum: [development, production]
default: development
status:
type: object
properties:
endpoint:
type: string
port:
type: integer
secretName:
type: string
ready:
type: boolean2. The Composition with Complete Supporting Resources
The platform team writes a Composition that synthesizes the entire infrastructure graph shown in the architecture diagram: KMS encryption key, Security Group, Subnet Group, Parameter Group, RDS Instance, and exported credentials secret.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: database-aws-postgresql
labels:
provider: aws
engine: postgresql
spec:
compositeTypeRef:
apiVersion: platform.example.com/v1alpha1
kind: Database
mode: Pipeline
pipeline:
- step: render-resources
functionRef:
name: function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: |
{{ $xr := .observed.composite.resource }}
{{ $name := $xr.metadata.name }}
{{ $ns := $xr.metadata.namespace }}
{{ $prod := eq $xr.spec.tier "production" }}
{{ $sizes := dict "small" "db.t4g.medium" "medium" "db.r7g.large" "large" "db.r7g.2xlarge" }}
---
# 1. KMS Encryption Key (Namespaced v2 API)
apiVersion: kms.aws.m.upbound.io/v1beta1
kind: Key
metadata:
name: {{ $name }}-key
namespace: {{ $ns }}
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: kms-key
spec:
forProvider:
description: "KMS storage encryption key for {{ $name }}"
deletionWindowInDays: {{ if $prod }}30{{ else }}7{{ end }}
enableKeyRotation: true
tags:
Environment: {{ $xr.spec.tier }}
ManagedBy: crossplane
---
# 2. Database Security Group (Namespaced v2 API)
apiVersion: ec2.aws.m.upbound.io/v1beta1
kind: SecurityGroup
metadata:
name: {{ $name }}-sg
namespace: {{ $ns }}
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: db-sg
spec:
forProvider:
description: "Ingress security group for PostgreSQL {{ $name }}"
vpcIdSelector:
matchLabels:
role: platform-vpc
---
# 3. DB Subnet Group (Namespaced v2 API)
apiVersion: rds.aws.m.upbound.io/v1beta1
kind: SubnetGroup
metadata:
name: {{ $name }}-subnet-group
namespace: {{ $ns }}
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: db-subnet-group
spec:
forProvider:
description: "Subnets allocated for {{ $name }}"
subnetIdSelectors:
- matchLabels:
tier: private-database
---
# 4. PostgreSQL Parameter Group (Namespaced v2 API)
apiVersion: rds.aws.m.upbound.io/v1beta1
kind: ParameterGroup
metadata:
name: {{ $name }}-pg
namespace: {{ $ns }}
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: db-parameter-group
spec:
forProvider:
family: "postgres{{ $xr.spec.engineVersion }}"
description: "Optimized PostgreSQL {{ $xr.spec.engineVersion }} parameters"
parameter:
- name: "log_connections"
value: "1"
- name: "rds.force_ssl"
value: "1"
---
# 5. RDS Instance (Namespaced v2 API)
apiVersion: rds.aws.m.upbound.io/v1beta1
kind: Instance
metadata:
name: {{ $name }}-instance
namespace: {{ $ns }}
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: db-instance
spec:
forProvider:
region: eu-west-1
engine: postgres
engineVersion: {{ $xr.spec.engineVersion | quote }}
instanceClass: {{ index $sizes $xr.spec.size }}
allocatedStorage: {{ if $prod }}100{{ else }}20{{ end }}
maxAllocatedStorage: {{ if $prod }}1000{{ else }}100{{ end }}
multiAz: {{ $prod }}
backupRetentionPeriod: {{ if $prod }}14{{ else }}1{{ end }}
storageEncrypted: true
kmsKeyIdSelector:
matchControllerRef: true
dbSubnetGroupNameSelector:
matchControllerRef: true
vpcSecurityGroupIdSelectors:
- matchControllerRef: true
parameterGroupNameSelector:
matchControllerRef: true
deletionProtection: {{ $prod }}
skipFinalSnapshot: {{ not $prod }}
autoMinorVersionUpgrade: true
publiclyAccessible: false
writeConnectionSecretToRef:
name: {{ $name }}-db-conn
namespace: {{ $ns }}
- step: automatically-detect-readiness
functionRef:
name: function-auto-ready3. The Developer’s Self-Service Request
Developers submit an intent-driven resource in their assigned namespace:
# xr.yaml
apiVersion: platform.example.com/v1alpha1
kind: Database
metadata:
name: orders
namespace: payments
spec:
size: small
tier: production
engineVersion: "16"Specifying tier: production silently provisions a Multi-AZ cluster, 14 days of encrypted point-in-time recovery, dedicated KMS key rotation, deletion protection, and parameter-enforced SSL. The developer never configures IAM roles, VPC subnet IDs, or cloud passwords.
4. Offline Verification with crossplane render
Compositions can be verified offline in CI before deploying to a control plane. Create a functions.yaml declaration:
# functions.yaml
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-go-templating
spec:
package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.4.1
---
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-auto-ready
spec:
package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.2.1Run the offline render command:
crossplane render xr.yaml composition.yaml functions.yamlThe CLI evaluates the templating pipeline locally, validates schema constraints, and prints the 5 generated AWS managed resources (Key, SecurityGroup, SubnetGroup, ParameterGroup, Instance) with the targeted credentials secret.
Composition functions
A Composition is a pipeline of functions, each a small program that receives the desired state and returns a modified one.
| Function | Write logic in | Use for |
|---|---|---|
| Patch and transform | Declarative patches | Simple field mapping |
| Go templating | Go templates | Conditionals and loops, familiar to Helm users |
| KCL, CUE, Pkl | A configuration language | Typed, testable configuration logic |
| Python, Go, TypeScript SDKs | A general-purpose language | Complex logic, external lookups |
| Auto-ready, status, sequencer | — | Readiness, status propagation, ordering |
Start with templating or patch-and-transform. Reach for a real language only when the logic demands it — and when it does, unit-test it.
Tenancy and guardrails
Crossplane’s providers hold powerful cloud credentials. Anyone who can create an object that Crossplane acts on can make it create infrastructure.
- Developers get RBAC on your XR kinds only, in their own namespaces — never on raw managed resources such as
InstanceorRole. See RBAC. - Namespaced XRs give natural per-team isolation and work with ordinary Kubernetes tooling.
- One
ProviderConfigper environment or account, selected by the Composition, not by the developer. - Admission policy for what the schema cannot express: quotas, naming, which tiers a namespace may request — policy management.
deletionPolicy: Orphanand deletion protection on anything stateful. Deleting a namespace must not delete a production database.- Scope and bound the provider’s IAM role — IAM.
Evolving an API
Your XRD is an API with consumers, and deserves the same care as any other — release and versioning.
- Additive changes are safe: a new optional field with a default.
- Breaking changes need a new version (
v1alpha1→v1beta1), both served for a migration period. - Changing a Composition changes every instance. Roll out with composition revisions: pin existing resources to the current revision, try the new one on a few, then promote. A careless edit can trigger replacement of live databases across the organisation.
- Test compositions in CI:
crossplane renderproduces the output for a given input offline, so you can diff it and assert on it before anything reaches a cluster. - Package and version XRDs, Compositions and functions as a Configuration, and deliver them through GitOps like any other artifact.
When to choose something simpler
Crossplane is powerful and has a real learning curve and operational weight.
| Situation | Consider instead |
|---|---|
| A few infrastructure types that rarely change | Terraform modules in a pipeline |
| AWS only, and resources already have Kubernetes controllers | ACK with kro |
| The abstraction is only Kubernetes objects | A Helm chart with a small values schema, or kro |
| You need a reviewed plan before every infrastructure change | Terraform or OpenTofu |
| A small team with no one to own a control plane | A managed PaaS |
It earns its place when many teams need self-service infrastructure with centrally enforced standards, across accounts or clouds, with continuous reconciliation. The comparison table is in Crossplane on EKS.
Operating it
- The control-plane cluster is now critical infrastructure. Back it up, monitor provider health, and alert on resources that stay unsynced.
- Install only the provider families you use; CRD count affects API server memory.
- Debug top-down:
crossplane beta trace database orders -n paymentsshows the whole tree and where it is stuck. - Surface failures to developers in the XR’s status and events — they cannot and should not read provider logs.
Related
- Platform engineering overview
- Golden paths
- Infrastructure as code
- CustomResourceDefinitions and operators
- Argo CD — delivering XRs and Compositions