Infrastructure as Data, Not Scripts
Most infrastructure-as-code tools run to completion and then walk away. You apply a plan, the resources appear, and nothing watches them afterward. Drift creeps in, someone edits a console setting, and your declared state and real state quietly diverge until the next apply surprises everyone.
Crossplane takes a different stance. It turns your Kubernetes cluster into a universal control plane that provisions and continuously reconciles external infrastructure — databases, buckets, networks — as ordinary Kubernetes objects. The same control loop that keeps a Deployment at three replicas keeps your RDS instance matching its spec.
Crossplane is an open-source CNCF project licensed under Apache 2.0. This guide installs it, wires up a cloud provider, and then builds the part that actually matters for platform teams: a custom API your developers can call without knowing a single cloud SKU.
Note
The Building Blocks
Crossplane introduces a handful of concepts. They stack cleanly, so it is worth naming them before touching YAML. Each layer builds on the one below it.
- Providers. Packages that teach Crossplane how to talk to an external API — AWS, GCP, Azure, or SaaS systems. Installing one adds a library of managed-resource CRDs.
- Managed Resources (MRs). The granular Kubernetes objects a provider installs — an
S3 Bucketor anRDSInstance. One MR maps to one external resource, reconciled continuously. - Compositions.A template that bundles several managed resources into one higher-level unit — a “database” that is really an instance, a subnet group, and a security group.
- CompositeResourceDefinitions (XRDs). The schema for your own custom API. An XRD defines the fields a developer fills in; the Composition decides what those fields build.
- Composite Resources (XRs). Instances of the API you defined — what a developer actually creates to get infrastructure.
The theme is familiar to anyone building an internal platform: hide primitives behind a curated interface. It is the same instinct behind building developer portals with Backstage, only here the abstraction is a live Kubernetes API rather than a catalog entry.
Installing Crossplane
Crossplane ships as a Helm chart and runs inside its own namespace. You need a Kubernetes cluster and Helm v3.2.0 or later. The chart installs the core controllers and the package manager that later pulls in providers and functions.
# Add the stable Crossplane Helm repository
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
# Install into the crossplane-system namespace
helm install crossplane \
--namespace crossplane-system \
--create-namespace crossplane-stable/crossplane
# Confirm the pods are running
kubectl get pods -n crossplane-systemThat is the whole footprint: a couple of controller pods in crossplane-system. Everything else — cloud access, higher-level APIs — arrives later as packages you install declaratively, which keeps the whole control plane describable in Git.
Providers — Extending the Control Plane
A Provider is a Crossplane package that installs a controller and a set of managed-resource CRDs for one external system. You install one by applying a Provider object; the package manager pulls the image and registers the new resource kinds.
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: crossplane-contrib-provider-aws-s3
spec:
package: xpkg.crossplane.io/crossplane-contrib/provider-aws-s3:v2.0.0Providers are large, so the ecosystem splits big clouds into service-scoped packages — you install provider-aws-s3 rather than one monolith for every AWS service. Check readiness with kubectl get providers; the HEALTHY column reports Unknown during install and True once the controller is up.
Note
provider-aws-s3@sha256:... — so a moved tag can never change what runs. To customise the provider pod (extra args, replica counts), reference a DeploymentRuntimeConfig via spec.runtimeConfigRef; it is a beta feature enabled by default, and its runtime container is named package-runtime.Managed Resources — Cloud as Kubernetes Objects
Once a provider is healthy, its CRDs let you declare external infrastructure directly. A managed resource is the thinnest possible wrapper: one Kubernetes object, one cloud resource, reconciled forever. Edit the spec and Crossplane converges the real resource; delete the object and it tears the resource down.
In Crossplane v2, managed resources are namespaced by default, which aligns them with standard Kubernetes multi-tenancy. Namespaced provider kinds are distinguished by an .m group suffix — for example rds.aws.m.upbound.io/v1beta1 — so the same team can own its resources inside its own namespace.
apiVersion: s3.aws.m.upbound.io/v1beta1
kind: Bucket
metadata:
name: reports-bucket
namespace: analytics
spec:
forProvider:
region: eu-central-1
providerConfigRef:
name: defaultThe providerConfigRef points at a ProviderConfig that holds credentials and settings — how the provider authenticates to the cloud. Managing a raw fleet of these objects by hand is possible but misses the point. The real leverage comes from composing them, which is the next layer.
CompositeResourceDefinitions — Defining Your Platform API
This is where Crossplane stops being “Terraform inside Kubernetes” and becomes a platform tool. An XRD defines a brand-new API — its group, its kind, and the schema of fields a consumer may set. Developers then use that API without seeing a single provider resource underneath.
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
name: xdatabases.platform.example.org
spec:
scope: Namespaced
group: platform.example.org
names:
kind: XDatabase
plural: xdatabases
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
size:
type: string
region:
type: string
required:
- size
- regionA few rules matter. metadata.name must be the plural plus a dot plus the group. Crossplane recommends prefixing the composite kind with X. Each version needs both served: true and referenceable: true to be usable, and only one version may be referenceable at a time.
Note
apiextensions.crossplane.io/v2) adds a scope field that defaults to Namespaced. Use Cluster for cluster-scoped composites, or LegacyCluster for the v1-compatible mode that still supports claims. If you use the older v1 XRD API, scope defaults to LegacyCluster automatically.Compositions and Composition Functions
The XRD says what a developer can ask for; a Composition says how to build it. In Crossplane v2 a Composition is a pipeline of composition functions— each step is a small program that reads the composite’s spec and returns the desired managed resources.
Functions are themselves packages, installed like providers. The most common is function-patch-and-transform, which templates resources with field patches and value transforms without writing code.
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-patch-and-transform
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:v0.8.2The Composition wires that function into a pipeline. Its compositeTypeRef points at the API defined by your XRD, mode: Pipeline selects the function-based engine, and each step names a functionRef plus its input.
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xdatabases.platform.example.org
spec:
compositeTypeRef:
apiVersion: platform.example.org/v1alpha1
kind: XDatabase
mode: Pipeline
pipeline:
- step: render-resources
functionRef:
name: function-patch-and-transform
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
# resources and patches omitted for brevityNote that the Composition object itself still uses apiextensions.crossplane.io/v1, while the XRD uses v2 — two different resources, two different API versions. The compositeTypeRef uses your own API group, not a Crossplane one.
Note
Consuming the API
With the XRD and Composition in place, a developer creates a composite resource — an instance of the API you designed. They set the fields your schema exposed and nothing else. Crossplane runs the pipeline and provisions every underlying managed resource, reconciling them all continuously.
apiVersion: platform.example.org/v1alpha1
kind: XDatabase
metadata:
name: orders-db
namespace: payments
spec:
size: large
region: eu-central-1That short manifest is the entire developer-facing surface. No IAM policy, no subnet group, no engine version — those live in the Composition, owned by the platform team. This is exactly the “golden path” idea from platform engineering and internal developer platforms: a paved, opinionated route that is easier to take than to avoid.
What Changed in Crossplane v2
v2 is a meaningful break from v1, and skipping the details will bite you on an upgrade. The headline is the namespace-first model: both composite and managed resources are namespaced by default, and the old duplication between a cluster-scoped composite and its namespaced claim is gone — you create the resource directly in a namespace.
v2 also lets a Composition include any Kubernetes resource, not only Crossplane managed ones — a Deployment or a third-party custom resource can sit in the same composition as an RDSInstance. That is what lets a single manifest deploy an app and the infrastructure it needs together.
The removals are the sharp edges. v2 drops native patch-and-transform composition (you now use the function), the ControllerConfig type, external secret stores, composite-resource connection details, and the default registry for packages. Before upgrading, run crossplane beta upgrade check with the v1.20 CLI against your v1.x control plane to scan for exactly these features.
Note
Crossplane and GitOps
Because every Crossplane object is a Kubernetes manifest, the whole control plane fits a GitOps workflow with no special tooling. Argo CD or Flux syncs your providers, functions, XRDs, compositions, and composite resources from Git, and Crossplane reconciles the cloud to match. Infrastructure changes become pull requests.
This is a genuine advantage over an imperative apply. Continuous reconciliation means out-of-band console edits are corrected automatically, and the desired state is always the file in your repository — the same discipline we apply to Terraform at scale, but enforced by a live control loop rather than a scheduled plan.
Beyond Provisioning — Operations
v2 introduces an Operation type for declarative operational workflows — annotating resources with operational data, triggering maintenance tasks, or encoding custom policies. It is an alpha feature, so treat it as experimental and keep it out of critical paths until it stabilises.
The direction is clear, though: Crossplane wants to be the control plane for both provisioning and day-two operations. Paired with strong cost discipline — the kind we cover in Kubernetes cost optimization — a single reconciling API for your whole platform is a compelling place to standardise.
Production Checklist
- Pin packages by digest. Reference providers and functions by
@sha256digest, not a floating tag, so installs are reproducible. - Design the XRD as a real API. Expose only the fields a consumer should set, mark the rest
required, and version deliberately — an XRD is a contract. - Keep credentials in ProviderConfig. Never inline secrets in managed resources; reference a
ProviderConfigand manage its secret separately. - Scan before upgrading to v2. Run
crossplane beta upgrade checkto catch removed features — native patch-and-transform,ControllerConfig, external secret stores. - Verify provider namespaced support. The
.mnamespaced model depends on the provider; confirm yours supports it before adopting namespace-first. - Manage everything in Git. Sync providers, functions, XRDs, and compositions with Argo CD or Flux so the control plane is fully declarative.
Crossplane’s bet is that infrastructure belongs behind an API that never stops reconciling, and that platform teams should be able to publish that API themselves rather than hand developers raw cloud primitives. If your organisation already lives in Kubernetes and you are tired of drift and copy-pasted modules, it is worth a proof of concept.
Crossplane rarely stands alone. When Crossplane-provisioned clusters need to scale their own compute, pair it with Karpenter autoscaling so node pools grow and shrink with demand. For teams weighing whether to build platform abstractions in Crossplane or a general-purpose IaC tool, our guide to Terraform advanced patterns covers the trade-offs of modules and remote state, while Backstage developer platforms shows how a self-service portal can front the composite APIs you publish here.
Your infrastructure-as-code tool applies a plan and walks away so console edits silently drift your real state from your declared state, your developers copy-paste cloud modules they do not understand to stand up a database, or you want a self-service platform API but do not want to hand your teams raw IAM policies and subnet groups?
We design and implement Crossplane control planes — from installing the core controllers and the right service-scoped Provider packages pinned by digest, through ProviderConfig credential management kept out of resource specs, designing CompositeResourceDefinitions as real versioned platform APIs that expose only the fields a consumer should set, building Compositions as pipelines of composition functions in patch-and-transform, KCL, or Python so one manifest provisions a database, its security groups, and its networking together, adopting the Crossplane v2 namespace-first model where it fits and verifying each provider's namespaced support first, wiring the whole control plane into a GitOps workflow with Argo CD or Flux so every infrastructure change is a reviewed pull request that a live control loop reconciles and drift is corrected automatically, and running crossplane beta upgrade check before any v1-to-v2 migration so removed features surface before they break you. Let's talk.
Let's Talk