Blog
June 23, 2021 Marie H.

Crossplane: Infrastructure Management Through Kubernetes

Crossplane: Infrastructure Management Through Kubernetes

Photo by <a href="https://unsplash.com/@iamdeivids?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Deivids Vasiljevs</a> on <a href="https://unsplash.com/?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Unsplash</a>

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.