Crossplane: Infrastructure Management Through Kubernetes
There's a question I keep coming back to when I'm managing infrastructure: why does provisioning an RDS instance live in a completely different mental model than deploying the application that uses it? GitOps for apps, Terraform for infra, two separate workflows, two separate state management stories. Crossplane is the project trying to collapse that gap.
I've been running it in a staging environment for a few months. Here's what I actually think.
The Core Idea
Crossplane's proposition is: Kubernetes is already a control plane. It has a reconciliation loop, it has a declarative API, it has RBAC, it has status tracking, it has GitOps tooling. Why run Terraform state files alongside your cluster when you could manage infrastructure the same way you manage Deployments?
In practice, this means your cloud resources — RDS instances, S3 buckets, IAM roles, VPCs — are Kubernetes custom resources. You declare what you want, Crossplane reconciles toward that desired state, and the status is visible via kubectl get.
Updated March 2026: Crossplane v2 shipped in 2025 with significant improvements: a cleaner provider architecture, better composition model (Functions replaced the patch-and-transform approach), and stronger support for dependency management between resources. The core concepts here still apply, but the composition YAML syntax has changed. The provider model is largely the same conceptually.
Providers
Providers are how Crossplane talks to cloud APIs. There's an AWS provider, GCP provider, Azure provider, and others. Each provider installs a set of CRDs corresponding to cloud resources.
kubectl apply -f - <<EOF
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-aws
spec:
package: crossplane/provider-aws:v0.18.1
EOF
After the provider is installed and configured with credentials, you have CRDs like RDSInstance, S3Bucket, IAMRole available in your cluster. You can create one directly:
apiVersion: database.aws.crossplane.io/v1beta1
kind: RDSInstance
metadata:
name: my-postgres
spec:
forProvider:
region: us-east-1
dbInstanceClass: db.t3.micro
engine: postgres
engineVersion: "13.3"
masterUsername: adminuser
skipFinalSnapshotBeforeDeletion: true
publiclyAccessible: false
writeConnectionSecretToRef:
namespace: default
name: my-postgres-conn
Apply this, and Crossplane provisions an RDS instance in AWS. The connection details land in a Kubernetes Secret. This is already useful — your app's Secret is populated automatically when the database is ready.
Composite Resources: Building Abstractions
Raw provider resources are too low-level to hand to application teams. Nobody wants to fill out 40 fields for an RDS instance. This is where Composite Resources (XRDs) come in.
An XRD is a custom API for your organization. You define it once, and application teams use a simplified interface. The XRD + Composition combination maps that simplified interface to real provider resources.
Here's a PostgreSQLInstance XRD:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.database.myorg.com
spec:
group: database.myorg.com
names:
kind: XPostgreSQLInstance
plural: xpostgresqlinstances
claimNames:
kind: PostgreSQLInstance
plural: postgresqlinstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
storageGB:
type: integer
size:
type: string
enum: [small, medium, large]
required: [storageGB, size]
The Composition maps the XRD parameters to real AWS resources:
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: postgresqlinstances.aws.database.myorg.com
spec:
compositeTypeRef:
apiVersion: database.myorg.com/v1alpha1
kind: XPostgreSQLInstance
resources:
- name: rdsinstance
base:
apiVersion: database.aws.crossplane.io/v1beta1
kind: RDSInstance
spec:
forProvider:
region: us-east-1
engine: postgres
engineVersion: "13.3"
skipFinalSnapshotBeforeDeletion: true
publiclyAccessible: false
patches:
- fromFieldPath: spec.parameters.storageGB
toFieldPath: spec.forProvider.allocatedStorage
- fromFieldPath: spec.parameters.size
toFieldPath: spec.forProvider.dbInstanceClass
transforms:
- type: map
map:
small: db.t3.micro
medium: db.t3.small
large: db.t3.medium
Now an application team can claim a database with:
apiVersion: database.myorg.com/v1alpha1
kind: PostgreSQLInstance
metadata:
name: orders-db
namespace: orders-service
spec:
parameters:
storageGB: 20
size: small
writeConnectionSecretToRef:
name: orders-db-conn
They don't know what cloud they're on. They don't know instance types. They declare what they need, and the platform team's Composition determines how that maps to real resources. This is the abstraction model Crossplane is designed for.
Crossplane vs Terraform
Same goal — infrastructure as code — very different approach.
Terraform: HCL DSL, separate state files (local or remote in S3/Terraform Cloud), plan/apply workflow, mature ecosystem (providers for everything), excellent documentation, most infrastructure engineers know it. The apply step is manual or CI-triggered, not continuously reconciling.
Crossplane: Kubernetes CRDs, state lives in etcd, continuous reconciliation loop (like a Deployment controller), GitOps-native (just commit the YAML), application teams can self-service through Claims. The provider ecosystem is smaller and less mature. YAML is more verbose than HCL for complex resources.
The honest comparison: Terraform is simpler and more accessible for most teams. If your team knows Terraform, there's no compelling reason to switch. Crossplane's advantage is specifically for teams who are already deep in Kubernetes and want a unified control plane — one GitOps workflow for everything, one RBAC model, one observability story.
Who It's Actually For
Crossplane makes sense if:
- You're already running Kubernetes and investing in it long-term
- You want application teams to self-service infrastructure through a controlled API (the XRD/Claim model is genuinely good for this)
- You're building an internal developer platform and want a single control plane
- Your organization is buying into GitOps across the board
It probably doesn't make sense if:
- Your team is comfortable with Terraform and doesn't have the Kubernetes expertise to operate Crossplane
- You need mature provider coverage — Terraform's providers are far more complete
- You want a gradual adoption path — Crossplane is all-or-nothing at the platform level
The Composition model is genuinely elegant once you understand it. But the learning curve is steep, the YAML is verbose, and the operational burden of running the providers is real. I'm not running it in production yet. I'm running it in staging and watching how it matures. Terraform is still doing the job in production.
That's an honest assessment and I'll update it when it changes.