Skip to content

The Edge of the Cyber World See the latest

Apps

How to Set Up Crossplane: 13 Steps, v2.4.1 [2026]

Crossplane just crossed a threshold that most Kubernetes add-ons never reach: full CNCF Graduated status, granted October 28, 2025, putting it in the same governance tier as Kubernetes, Prometheus, and Helm. The project’s maintainer, Upbound, says more than 1,000 organizations now run it in production, including Nike, Autodesk, and NASA’s Science Cloud, and puts cumulative Crossplane downloads above 100 million. If you have been provisioning AWS, Azure, and GCP resources through three separate Terraform state files and a pile of CI scripts, this tutorial shows you how to replace that setup with a single Kubernetes-native control plane instead. By the end you will have a working Crossplane v2.4.1 installation that can provision real cloud infrastructure from Kubernetes manifests, plus a composition you can extend for your own platform.

This tutorial covers 13 hands-on steps, from creating a control plane cluster through installing multi-cloud providers, building a Composition, locking it down with namespace-scoped RBAC, and tearing it all down cleanly. Every command below was checked against Crossplane’s current documented v2.4 requirements and the provider package versions Upbound’s package Marketplace listed as of September 2026, so version numbers you see here should match what you get if you run these commands today rather than a stale snapshot from an earlier Crossplane release.

What Crossplane Actually Does (and Why It’s Not Just “Terraform on Kubernetes”)

Crossplane extends the Kubernetes API so that cloud resources become Kubernetes objects. Instead of running terraform apply from a pipeline, you submit a YAML manifest describing an S3 bucket, an Azure Resource Group, or a GCP Cloud SQL instance, and a Crossplane controller continuously reconciles that manifest against the real cloud provider. That reconciliation loop is the core difference: Terraform, OpenTofu, and Pulumi are plan-and-apply tools that run on a schedule or a trigger, while Crossplane behaves like any other Kubernetes controller, constantly checking whether the live state still matches the declared state and correcting drift without a human running a command.

The practical effect is that Crossplane resources show up in kubectl get output alongside your Deployments and Services. Platform teams use this to build internal developer platforms: a developer submits a lightweight “claim” (for example, requesting a Postgres database), and a Crossplane Composition translates that claim into the dozen underlying cloud resources needed to satisfy it, all without the developer touching a cloud console or writing HCL.

Crossplane v2, released August 13, 2025, pushed this idea further by letting Compositions assemble any Kubernetes object, not just Crossplane-managed cloud resources. A single Composition can now produce a Deployment, a ConfigMap, and an RDS instance from one claim, which means the abstraction a developer requests can span both application and infrastructure layers in one API call. That capability comes from Composition Functions, small programs that calculate desired state and can implement conditionals, loops, and templating logic that a static patch-and-transform YAML file never could.

Crossplane vs. Terraform, OpenTofu, and Pulumi: How the Architectures Differ

Adoption numbers for infrastructure-as-code tools vary by survey and are not directly comparable, since sample sizes and question wording differ. A 2025 Firefly State of IaC report put Terraform at 62% current usage with OpenTofu at 12%. A separate DevOps.com-cited survey found roughly 60% current Terraform usage alongside more than 40% usage each for OpenTofu and Pulumi, and 40% Crossplane usage with 60% of respondents planning to adopt it. Treat both figures as directional rather than a precise market-share ranking. GitHub stars offer a rougher but consistent proxy: as of September 2026, Terraform sits around 49,700 stars, OpenTofu around 30,200, Pulumi around 25,700, and Crossplane on GitHub shows around 12,100. Crossplane’s lower star count reflects that it is a newer, more specialized tool aimed at platform engineering teams rather than general-purpose IaC users.

Tool Execution Model State Storage Primary Use Case ~GitHub Stars (Sep 2026)
Crossplane v2.4.1 Continuous reconciliation (Kubernetes controller) Kubernetes etcd (as CRDs) Internal developer platforms, self-service infra APIs ~12,100
Terraform Plan-and-apply, triggered Remote/local state file General-purpose multi-cloud provisioning ~49,700
OpenTofu Plan-and-apply, triggered Remote/local state file Open-source Terraform fork, license-driven adoption ~30,200
Pulumi Plan-and-apply, triggered Pulumi service or self-hosted backend IaC in general-purpose languages (TypeScript, Go, Python) ~25,700

None of these tools is strictly better across the board. Terraform and OpenTofu remain the default choice for one-shot infrastructure provisioning where a human or a CI job triggers changes deliberately. Crossplane earns its keep when you want infrastructure to self-heal continuously, or when you’re building a platform where application teams request resources through a Kubernetes-native API rather than learning HCL. Several teams run both: Terraform for foundational networking and IAM, Crossplane for the self-service layer application teams interact with directly.

Prerequisites and Version Requirements

Before starting, confirm you have the following. Crossplane’s official v2.4 installation documentation requires an actively supported Kubernetes version and Helm 3.2.0 or later. It does not pin one fixed minor version, so use whatever currently supported Kubernetes release your cluster provider offers and a matching kubectl client.

  • Kubernetes cluster: any actively supported version (a local kind or minikube cluster works for this tutorial, while a managed EKS, AKS, or GKE cluster works for production)
  • kubectl: a version matching your cluster’s Kubernetes release, on your PATH
  • Helm: version 3.2.0 or later (Crossplane installs via the official Helm chart)
  • Crossplane CLI: the crossplane binary, used later to convert v1 compositions and inspect installed packages
  • Cloud accounts: an AWS account with an IAM user or role capable of creating S3 buckets, plus (optionally) Azure and GCP accounts if you want to follow the multi-cloud steps
  • ~90 minutes: enough time to complete all 13 steps, including waiting for provider pods to become healthy

One version note worth flagging up front: this tutorial targets Crossplane v2.4.1, the release Crossplane’s GitHub repository lists as current for September 2026. Crossplane v2.0 shipped August 13, 2025 and made managed resources namespaced by default, a meaningful architectural shift from v1’s cluster-scoped model. If you’re following an older tutorial that shows cluster-scoped claims without a namespace, you’re looking at v1-era syntax. It still works under v2 for backward compatibility, but new deployments should use the namespaced model shown below.

Step 1: Provision a Kubernetes Cluster for Your Control Plane

Crossplane needs a Kubernetes cluster to run in, but that cluster does not need to host your application workloads. Many platform teams run a dedicated “control plane cluster” that exists purely to manage cloud infrastructure, separate from the clusters running application pods. For this tutorial, a local cluster is enough to prove the setup works before you point it at production credentials.

# Local option: create a kind cluster
kind create cluster --name crossplane-control-plane

# Confirm you're pointed at the right context
kubectl config current-context
kubectl get nodes

If you’re deploying against EKS, AKS, or GKE instead, swap this step for your normal cluster-creation workflow (eksctl create cluster, az aks create, or gcloud container clusters create) and run kubectl get nodes to confirm connectivity before moving on. Crossplane itself is cloud-agnostic, and nothing about this step differs by provider once the cluster exists.

Step 2: Install Crossplane With Helm

Crossplane ships as a Helm chart hosted on Upbound’s chart repository. Add the repository, update your local index, and install into a dedicated namespace so Crossplane’s controllers and CRDs stay isolated from application workloads.

helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update

kubectl create namespace crossplane-system

helm install crossplane crossplane-stable/crossplane 
  --namespace crossplane-system 
  --version 2.4.1

Pin the chart version explicitly with --version rather than letting Helm grab whatever is newest. Crossplane’s provider families and core chart version independently, so a tutorial or CI pipeline that assumes “latest” can silently pick up breaking changes between the time you write the manifest and the time it runs. If you’re deploying into a shared cluster with resource quotas already in place, add --set resourcesCrossplane.limits.cpu=500m and a matching memory limit to the install command. The default chart values are conservative but worth reviewing against your cluster’s actual quota before a first install fails silently on a resource-constrained namespace.

Step 3: Verify the Crossplane Installation

Before installing any provider, confirm the core Crossplane controllers are healthy. This step catches most environment-level problems (insufficient cluster resources, image pull failures, webhook issues) before you add cloud provider complexity on top.

kubectl get pods -n crossplane-system

# Expected output (names will vary):
# NAME                                       READY   STATUS    RESTARTS   AGE
# crossplane-7d6f9c9b8d-x2f7k                1/1     Running   0          45s
# crossplane-rbac-manager-6f8b7c5d4f-p9k2q   1/1     Running   0          45s

kubectl get crds | grep crossplane.io

If both pods show Running with 1/1 ready and the CRD list includes entries like compositeresourcedefinitions.apiextensions.crossplane.io, the core installation succeeded. If a pod is stuck in Pending, check node resource availability first. Crossplane’s controllers are lightweight but still need scheduling headroom.

Step 4: Install the AWS Provider Family

Crossplane splits cloud provider support into “provider families,” where a base family package handles authentication and shared configuration, and individual service packages (EC2, S3, RDS, and so on) install separately so you only pull in the CRDs you actually use. The current AWS family package is upbound/provider-family-aws, at v2.7.2 as of September 2026, built on the Terraform AWS provider v6.3.0 under the hood.

cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-family-aws
spec:
  package: xpkg.upbound.io/upbound/provider-family-aws:v2.7.2
EOF

cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws-s3
spec:
  package: xpkg.upbound.io/upbound/provider-aws-s3:v2.7.2
EOF

# Provider installation is asynchronous - watch until healthy
kubectl get providers

Pin the exact version tag rather than using :latest. Upbound’s Marketplace listing for the AWS family shows the version can change independently of Crossplane core, so a floating tag makes your setup non-reproducible. Wait for the INSTALLED and HEALTHY columns in kubectl get providers to both read True before continuing. This can take one to two minutes as the provider pod pulls its image and registers CRDs.

Step 5: Configure AWS Credentials and ProviderConfig

Option A: Static Credentials (Fastest for Testing)

For a quick local test, a static access key works, though it should never be used for a production control plane. Create a Kubernetes Secret holding an AWS credentials file, then reference it from a ProviderConfig object.

# Create a credentials file locally, then load it as a Secret
kubectl create secret generic aws-creds 
  -n crossplane-system 
  --from-file=creds=./aws-credentials.conf

cat <<EOF | kubectl apply -f -
apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: aws-provider-config
spec:
  credentials:
    source: Secret
    secretRef:
      namespace: crossplane-system
      name: aws-creds
      key: creds
EOF

Option B: Workload Identity (Recommended for Production)

Crossplane’s own documentation recommends workload identity or OIDC federation over long-lived static keys for AWS, Azure, and GCP alike. On EKS, that means configuring an OIDC identity provider and binding the Crossplane provider’s service account to an IAM role via IRSA, so credentials are issued per-request rather than stored as a static secret. This adds setup time but removes an entire class of credential-leak risk, and it’s the approach used by most of the production deployments Upbound has documented publicly.

Step 6: Provision Your First AWS Resource

With the provider healthy and credentials configured, create a managed resource directly. This is the simplest possible Crossplane object: a single S3 bucket, declared as YAML instead of created through the AWS console or a terraform apply.

cat <<EOF | kubectl apply -f -
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
  name: crossplane-tutorial-bucket
spec:
  forProvider:
    region: us-east-1
  providerConfigRef:
    name: aws-provider-config
EOF

kubectl get buckets.s3.aws.upbound.io

# Expected output once reconciled:
# NAME                         READY   SYNCED   EXTERNAL-NAME                AGE
# crossplane-tutorial-bucket   True    True     crossplane-tutorial-bucket   38s

Both READY and SYNCED need to read True. SYNCED means Crossplane successfully talked to the AWS API. READY means the resource matches the desired spec. If either stays False, run kubectl describe bucket crossplane-tutorial-bucket and check the Conditions section, which usually names the exact API error (permission denied, region mismatch, name collision).

Step 7: Add the Azure and GCP Provider Families

The same provider-family pattern applies to Azure and GCP. Both maintain independent version numbers from the AWS family and from each other, so check current tags before pinning them in a real deployment.

Cloud Package Version (Sep 2026) Underlying Terraform Provider
AWS upbound/provider-family-aws v2.7.2 Terraform AWS provider v6.3.0
Azure upbound/provider-family-azure v2.7.2 Terraform AzureRM provider
GCP upbound/provider-family-gcp v3.0.3 Terraform Google provider v6.43.0
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-family-azure
spec:
  package: xpkg.upbound.io/upbound/provider-family-azure:v2.7.2
EOF

cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-family-gcp
spec:
  package: xpkg.upbound.io/upbound/provider-family-gcp:v3.0.3
EOF

kubectl get providers

Note that the GCP v2 provider line removed External Secret Store support when it upgraded to the newer Terraform Google provider, so if a GCP-specific pitfall shows up during a version upgrade, check the release notes for that provider family specifically rather than assuming it matches AWS or Azure’s changelog.

Step 8: Build a Composition With Composition Functions

Individual managed resources like the S3 bucket above are useful, but the real value of Crossplane shows up in Compositions, which bundle multiple managed resources behind one API a developer can call without knowing the underlying cloud details. Crossplane v2 requires composition functions for this. The native “patch-and-transform” mode used in v1 Compositions was removed, and functions now calculate the desired composed resources through a pipeline.

Before a Composition can exist, Crossplane needs to know the shape of the claim it’s satisfying. That’s the job of a CompositeResourceDefinition (XRD): it registers a new custom API, in this case XStorageBucket, and tells Kubernetes what fields a claim against that API can set. Apply the XRD first, then the function, then the Composition that ties them together.

cat <<EOF | kubectl apply -f -
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xstoragebuckets.platform.example.org
spec:
  scope: Namespaced
  group: platform.example.org
  names:
    kind: XStorageBucket
    plural: xstoragebuckets
  versions:
    - name: v1alpha1
      served: true
      referenceable: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                region:
                  type: string
                  default: us-east-1
EOF

kubectl get xrd
# Install the function-patch-and-transform function, the closest
# equivalent to v1's built-in patch-and-transform behavior
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
  name: function-patch-and-transform
spec:
  package: xpkg.upbound.io/crossplane-contrib/function-patch-and-transform:v0.8.2
EOF

kubectl get functions

Once the function reports healthy, reference it inside a Composition’s pipeline. A minimal Composition that provisions a storage bucket behind a simplified API looks like this:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: storage-bucket-aws
spec:
  compositeTypeRef:
    apiVersion: platform.example.org/v1alpha1
    kind: XStorageBucket
  mode: Pipeline
  pipeline:
    - step: patch-and-transform
      functionRef:
        name: function-patch-and-transform
      input:
        apiVersion: pt.fn.crossplane.io/v1beta1
        kind: Resources
        resources:
          - name: bucket
            base:
              apiVersion: s3.aws.upbound.io/v1beta1
              kind: Bucket
              spec:
                forProvider:
                  region: us-east-1

If you have existing v1 Compositions written with native patch-and-transform syntax, don’t rewrite them by hand. The Crossplane CLI includes a conversion command: crossplane beta convert pipeline-composition rewrites a v1-style Composition into the function-pipeline format automatically, which is faster and less error-prone than a manual port for anything beyond a trivial example.

Step 9: Create a Namespaced Claim (Crossplane v2 Scoping)

Crossplane v2’s biggest structural change from v1 is that managed resources and composite resources are namespaced by default, matching how the rest of Kubernetes scopes objects. In v1, a developer’s claim and the underlying composite resource lived at the cluster level. In v2, a claim can live in the same namespace as the application it supports, which makes RBAC and multi-tenancy far more natural.

kubectl create namespace team-checkout

cat <<EOF | kubectl apply -f -
apiVersion: platform.example.org/v1alpha1
kind: XStorageBucket
metadata:
  name: checkout-uploads
  namespace: team-checkout
spec:
  compositionRef:
    name: storage-bucket-aws
EOF

kubectl get xstoragebuckets -n team-checkout

A developer on the checkout team can now request storage without ever touching an AWS credential, an HCL file, or a ticket queue. They apply a manifest scoped to their own namespace, and RBAC (covered next) determines exactly what they’re allowed to request. This is also where Crossplane v2’s namespaced model pays off operationally: because the claim lives in team-checkout rather than at the cluster root, you can apply standard Kubernetes tooling that already understands namespaces, including resource quotas, network policies, and audit log filtering, to the infrastructure-request layer without writing any Crossplane-specific tooling to do it.

Step 10: Set Up RBAC for Multi-Tenant Platform Teams

Because v2 composite resources are namespaced, standard Kubernetes RBAC now applies directly. Grant application teams permission to create claims in their own namespace without granting them access to the underlying managed resources or provider credentials, which stay cluster-scoped and restricted to the platform team.

cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: storage-bucket-requester
  namespace: team-checkout
rules:
  - apiGroups: ["platform.example.org"]
    resources: ["xstoragebuckets"]
    verbs: ["get", "list", "watch", "create", "update", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: checkout-team-storage
  namespace: team-checkout
subjects:
  - kind: Group
    name: checkout-team
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: storage-bucket-requester
  apiGroup: rbac.authorization.k8s.io
EOF

Verify the boundary works as intended by impersonating the group: kubectl auth can-i create buckets.s3.aws.upbound.io --as-group=checkout-team should return no, while the equivalent check against xstoragebuckets should return yes. That gap between “can request the abstraction” and “cannot touch the raw cloud resource” is the entire point of building a platform API on top of Crossplane.

Step 11: Add Observability for Reconciliation Failures

Because Crossplane resources reconcile continuously rather than running once, silent drift or repeated reconciliation failures can go unnoticed without monitoring. Two lightweight additions cover most of what you need: Kubernetes Events and a Prometheus scrape target.

# Watch events for a specific claim in real time
kubectl get events -n team-checkout --field-selector involvedObject.name=checkout-uploads --watch

# Crossplane's core pod exposes Prometheus metrics on :8080/metrics
kubectl port-forward -n crossplane-system deploy/crossplane 8080:8080
curl -s localhost:8080/metrics | grep crossplane_pkg

If you already run Prometheus in-cluster, add a ServiceMonitor pointed at the Crossplane deployment and alert on a sustained Ready=False or Synced=False condition rather than a single failed reconciliation, since transient cloud API throttling can cause a one-off failure that resolves on the next reconcile loop.

Step 12: Migrate Existing v1 Compositions to Function Pipelines

If you’re adding new provider families to an existing Crossplane v1 deployment rather than starting fresh, plan the migration path explicitly. Crossplane’s v2 upgrade guidance states that v1 Compositions using spec.mode: Resources (the native patch-and-transform mode) must be migrated to function pipelines, since that mode was removed in v2. Existing claims and cluster-scoped managed resources keep working during the transition, so this is not a forced big-bang cutover, but it is a required step before you can take advantage of new v2 features like namespaced composites.

# Convert a v1-style Composition file to the pipeline format
crossplane beta convert pipeline-composition 
  --input=old-composition.yaml 
  --output=migrated-composition.yaml

# Review the diff before applying
diff old-composition.yaml migrated-composition.yaml
kubectl apply -f migrated-composition.yaml

Test the converted Composition against a non-production claim first. The conversion tool handles the mechanical translation reliably, but any custom patch logic that relied on v1-specific behavior is worth a manual review before it touches production infrastructure.

Step 13: Tear Down and Clean Up Resources

Because Crossplane manages real cloud resources, deleting the Kubernetes object also deletes the underlying infrastructure by default. Clean up in reverse order: claims first, then compositions, then providers, so nothing is left orphaned in AWS, Azure, or GCP billing you later.

# Delete the claim (this deletes the underlying cloud resource too)
kubectl delete xstoragebucket checkout-uploads -n team-checkout

# Confirm the bucket is gone in AWS before removing providers
kubectl get buckets.s3.aws.upbound.io

# Remove providers and functions once no managed resources reference them
kubectl delete provider provider-aws-s3 provider-family-aws
kubectl delete function function-patch-and-transform

# Uninstall Crossplane itself
helm uninstall crossplane -n crossplane-system
kubectl delete namespace crossplane-system

Always verify the cloud-side resource is actually gone (check the AWS console or run aws s3 ls) before deleting the provider that manages it. If a provider is removed while a managed resource still references it, Crossplane can no longer reconcile that resource’s deletion, leaving it orphaned in the cloud account with no Kubernetes object tracking it.

When Crossplane Is the Wrong Tool for the Job

None of the above is an argument that every team should replace Terraform with Crossplane. If your infrastructure changes are infrequent, reviewed by a human before every apply, and don’t need a self-service layer for other teams, the operational overhead of running a Kubernetes control plane just to manage a handful of cloud resources is hard to justify. Crossplane adds a cluster you now have to patch, upgrade, and monitor, on top of whatever clusters already run your applications. For a five-person startup provisioning a VPC and a database once a quarter, that overhead outweighs the benefit.

Crossplane also assumes your team is comfortable thinking in Kubernetes primitives: CRDs, controllers, reconciliation loops, and RBAC. Teams without existing Kubernetes operational experience face a steeper learning curve adopting Crossplane than adopting Terraform, which has a much larger base of tutorials, Stack Overflow answers, and hired talent already familiar with HCL. The GitHub star gap mentioned earlier (roughly 12,100 for Crossplane versus 49,700 for Terraform) is a rough proxy for how much community troubleshooting content exists for each, and that gap shows up directly when you hit an obscure error message at 11pm.

The clearest signal that Crossplane is worth the investment is a genuine multi-team self-service need: more than a handful of application teams requesting infrastructure regularly enough that a shared, versioned API pays for itself in reduced ticket volume and faster provisioning. If that description doesn’t match your situation yet, Terraform or OpenTofu, run through a normal CI pipeline, remains the more boring and more appropriate choice.

Common Crossplane Setup Pitfalls

Most Crossplane setup failures fall into a handful of repeatable categories. Knowing them ahead of time saves the debugging cycle of staring at a healthy-looking provider pod while resources silently fail to reconcile.

  • Provider pod healthy, resources still failing. A running provider pod only means the controller started. It says nothing about whether the ProviderConfig’s credentials are valid. Always check the managed resource’s Conditions, not just provider pod status.
  • ProviderConfig references the wrong Secret key. The Secret must contain a key matching exactly what the ProviderConfig’s key field specifies. A typo here produces an authentication error that looks identical to a genuinely invalid credential.
  • Using :latest tags on provider packages. Provider family versions move independently of Crossplane core and of each other. An unpinned tag means your manifest can produce different behavior on two different days.
  • Creating managed resources before the provider reports healthy. Provider installation is asynchronous. Applying a Bucket manifest seconds after applying the Provider object often fails simply because the CRD hasn’t registered yet.
  • Missing namespace-scoped RBAC after upgrading to v2. Teams that upgrade from v1 sometimes keep cluster-scoped RBAC rules that no longer match v2’s namespaced composite resources, silently breaking developer self-service until the Role and RoleBinding are updated.
  • Applying v1-style native Compositions on a v2 install. Since v2 removed the native patch-and-transform mode, a Composition written for v1 without a function pipeline will fail to reconcile until it’s converted with crossplane beta convert pipeline-composition.
  • Deleting a provider while managed resources still reference it. This orphans the underlying cloud resource. Always confirm zero managed resources depend on a provider before removing it.

Troubleshooting Guide

The table below covers the errors you’re most likely to hit during and after setup, along with the fastest way to confirm the cause.

Symptom Likely Cause Fix
Provider stuck at INSTALLED=False Image pull failure or registry rate limit Run kubectl describe provider <name> and check Events for pull errors
Provider INSTALLED=True, HEALTHY=False Provider pod crash-looping Check logs with kubectl logs -n crossplane-system deploy/provider-aws-s3
Managed resource SYNCED=False Invalid or missing cloud credentials Verify the Secret referenced by ProviderConfig exists and has the correct key
Managed resource SYNCED=True, READY=False Cloud API rejected the spec (e.g. invalid region, name collision) Run kubectl describe on the resource and read the Conditions message
Composition fails with “no function response” Function package not installed or unhealthy Check kubectl get functions and confirm HEALTHY=True
Claim created but no composite resource appears compositionRef name mismatch or missing CompositeResourceDefinition Confirm the XRD is applied and the Composition name matches exactly
“forbidden” error creating a claim in a namespace Missing or misconfigured RBAC Role/RoleBinding Run kubectl auth can-i create <kind> --as-group=<group> -n <namespace>
v1 Composition fails after upgrading to v2 Native patch-and-transform mode was removed in v2 Convert with crossplane beta convert pipeline-composition
Deleted claim, but cloud resource still exists Provider was removed before the managed resource finished deleting Reinstall the provider, confirm deletion completes, then remove it again

Advanced Tips for Production Crossplane Deployments

Once the basic setup works, a few practices separate a proof-of-concept from something you’d trust with production infrastructure. First, split your control plane cluster from your workload clusters. Running Crossplane’s controllers on the same cluster as the applications it provisions infrastructure for creates an awkward dependency loop if that cluster ever needs to be rebuilt.

Second, favor workload identity over static credentials everywhere it’s available. EKS IRSA, GKE Workload Identity, and Azure AD Workload Identity all let Crossplane’s provider pods assume cloud permissions without a long-lived secret sitting in etcd. This closes off one of the more common paths to a credential leak in Kubernetes-based platforms.

Third, treat Compositions as versioned, tested artifacts, not one-off YAML. Since Compositions define the actual API your application teams depend on, changing one without a review process can silently alter what “requesting a database” means for every team using it. Several platform teams keep Compositions in the same Git repository as application Helm charts and require pull-request review before a Composition change reaches the cluster.

Fourth, if you’re evaluating whether to self-host Crossplane or use Upbound’s commercial offering, know that Crossplane itself remains fully open source and self-hostable, and nothing about the tutorial above requires a paid account. Upbound’s commercial tier adds hosted control planes, a private package registry, and support, billed on a consumption model with a Team plan that includes a $150 monthly usage credit before per-resource-hour billing kicks in. Most teams start self-hosted and evaluate the managed offering only once they’re running enough Compositions that operating the control plane itself becomes a burden.

Finally, budget real time for the v1-to-v2 migration if you’re on an existing deployment. The namespaced-by-default model is the right long-term direction for multi-tenant platforms, but converting Compositions, auditing RBAC, and re-testing claims across every team using the platform is not a same-afternoon task on anything beyond a small deployment.

Complete Working Project: A Self-Service Storage API

Putting the steps above together gives you a working, if minimal, self-service platform API. The pieces are: Crossplane core installed via Helm, the AWS provider family and S3 service package, a function-patch-and-transform pipeline, a Composition mapping a simplified XStorageBucket claim to a real S3 Bucket, and a namespace-scoped RBAC rule limiting the checkout team to requesting that abstraction. A developer on that team never sees an AWS credential, a Terraform state file, or an HCL module. They apply one manifest, and within roughly 30-60 seconds kubectl get xstoragebuckets -n team-checkout shows READY: True.

Extending this to Azure and GCP means repeating steps 7 through 10 with the Azure and GCP provider families and a Composition that maps the same XStorageBucket claim to an Azure Storage Account or a GCP Cloud Storage bucket instead. Because the claim’s API stays identical across all three implementations, you can also build a single Composition that picks the target cloud based on a field in the claim spec, giving application teams one consistent request format regardless of which cloud actually backs it. That pattern, sometimes called a “compositional abstraction layer,” is where most of Crossplane’s value shows up once a platform grows past a single cloud provider.

To confirm the full loop actually works end to end, run through this checklist after completing all 13 steps: the provider pods report HEALTHY=True, the XRD shows up under kubectl get xrd, the Composition shows up under kubectl get compositions, and applying the claim manifest produces a real bucket visible from the AWS CLI or console within roughly a minute. If every one of those checks passes, you have a working, minimal version of the same pattern platform teams at much larger scale use to run hundreds of Compositions covering databases, message queues, and compute across three clouds from one Git-reviewed set of YAML files.

From here, the natural next additions are a second Composition (a managed Postgres database is a common second target), a CI check that runs crossplane render against sample claims before merging a Composition change, and a policy engine such as Kyverno or OPA Gatekeeper to enforce guardrails like “no publicly readable S3 buckets” at the cluster level, independent of what any individual Composition author writes.

Frequently Asked Questions

Is Crossplane a replacement for Terraform?

Not necessarily a full replacement. Crossplane and Terraform solve overlapping but different problems: Terraform excels at one-shot, plan-reviewed provisioning of foundational infrastructure, while Crossplane excels at building a continuously reconciled, self-service API for application teams. Many organizations run both, using Terraform for networking and IAM foundations and Crossplane for the developer-facing platform layer on top. Survey data on how common that split is remains thin and inconsistent between sources, so treat it as an emerging pattern rather than an established best practice with hard numbers behind it.

Do I need to pay for Upbound to use Crossplane?

No. Crossplane is fully open source and can be self-hosted for free using the Helm chart shown in this tutorial. Upbound’s commercial offering is optional and adds hosted control planes, a private package registry, and support on a consumption-based pricing model.

What’s the difference between Crossplane v1 and v2?

The headline change is scope: v2 makes managed resources and composite resources namespaced by default, where v1 used cluster-scoped resources. V2 also requires composition functions for all Compositions, removing the native patch-and-transform mode that v1 supported directly, and it extends Compositions so they can assemble any Kubernetes object rather than only Crossplane-managed cloud resources. Existing v1-style resources continue working under v2 for backward compatibility, so the upgrade is not a forced rewrite, but new deployments should use v2’s namespaced model and function pipelines, and the Crossplane CLI’s conversion command can automate most of the mechanical migration work for teams moving existing Compositions over.

Which Kubernetes version does Crossplane require?

Crossplane’s official documentation requires an actively supported Kubernetes version and Helm 3.2.0 or later, without pinning one specific minor version. In practice, use whatever release your managed Kubernetes provider currently supports, and keep kubectl matched to that cluster version.

Can Crossplane manage resources across AWS, Azure, and GCP at the same time?

Yes. Each cloud has its own provider family (provider-family-aws, provider-family-azure, provider-family-gcp), and you can install as many as you need on a single Crossplane installation. A single Composition can even target multiple clouds within one claim, which is common for platform teams building a cloud-agnostic self-service API.

Why is my managed resource stuck with SYNCED=False?

SYNCED=False almost always means Crossplane could not successfully call the cloud provider’s API, most commonly due to invalid or missing credentials in the referenced ProviderConfig Secret. Run kubectl describe on the resource and read the Conditions section, which typically includes the underlying API error message.

Is Crossplane production-ready?

Crossplane reached CNCF Graduated status on October 28, 2025, the same governance tier as Kubernetes and Prometheus, and Upbound reports more than 1,000 organizations running it in production, including Nike, Autodesk, and NASA’s Science Cloud. That said, production readiness for your specific use case still depends on building proper RBAC boundaries, workload identity, and tested Compositions, as covered in the advanced tips above.

How much does it cost to run Crossplane itself?

Crossplane the open-source project has no licensing cost. You pay only for the underlying Kubernetes cluster and whatever cloud resources it provisions. Upbound’s optional commercial tier uses consumption-based pricing, with published per-resource-month rates in the roughly $0.95 to $1.10 range depending on usage tier, after a $150 monthly usage credit on the Team plan. Factor in the cost of the control plane cluster itself when budgeting: a small managed Kubernetes cluster dedicated to running Crossplane typically costs less per month than the engineering time saved by not maintaining separate Terraform pipelines per cloud, but that math depends heavily on how many teams and Compositions you’re actually running.

Related Coverage

Source: Tech Insider