Crossplane vs Terraform vs Pulumi in 2026: Control Plane or Plan/Apply?

Oct 20, 2026 · 12 min · Ajay Kumar

I keep getting asked whether a platform team should move from Terraform to Crossplane, usually by someone who has just watched a conference talk about control planes. My honest answer is that "Crossplane vs Terraform" is less a tool choice than a choice of execution model: a plan you review and apply, or a controller that never stops applying. This post compares Crossplane, Terraform/OpenTofu and Pulumi as they stand in October 2026, after Crossplane v2 reshaped its API and Pulumi started running HCL, and explains drift, cost and team fit so you can pick on mechanics rather than vibes. For licences, forks and the AI tooling around HCL, I already covered that ground in Infrastructure as Code in 2026, so I will not repeat it here.

Plan/apply vs control plane: the difference that matters

Terraform, OpenTofu and Pulumi share one model. You run a command, the engine reads state, refreshes it against the cloud APIs, computes a diff, shows it to you, and applies it if you agree. Between runs nothing happens. If somebody changes a security group in the console on Tuesday, your code does not know until the next plan.

Crossplane runs as a set of Kubernetes controllers. You submit a resource to the Kubernetes API, and a provider controller reconciles the real cloud object towards it, then keeps checking. The Crossplane docs are blunt about what that means: forProvider is the source of truth and Crossplane "overrides any changes made to an external resource outside of Crossplane" (managed resources docs). The AWS provider, for example, ships with a default poll interval of 10 minutes per resource and a full resync every hour, set as flags in its main.go.

That one difference cascades into everything else: where state lives (etcd instead of a state file), what review looks like (an API admission decision instead of a plan diff), how you do emergency changes (pause the resource first, or it will be reverted), and who can self-serve.

Three execution models for the same S3 bucket Terraform / OpenTofu Pulumi Crossplane Trigger a person or CI runs plan, then apply Trigger a person or CI runs pulumi up Trigger controllers reconcile continuously State state file in a backend (S3, HCP, ...) State Pulumi Cloud or self-managed backend State Kubernetes etcd plus the cloud API Drift seen at next plan; HCP health checks on Standard and Premium only Drift seen at refresh; scheduled drift runs on Pro and Enterprise plans Drift reverted on next poll (AWS default 10 min per resource, 1 h full resync) Review point the plan diff in a pull request Review point the preview diff in a pull request Review point Git plus API admission policy Underneath: the AWS, Azure and GCP Crossplane providers are generated from Terraform providers (Upjet)
Execution model comparison compiled from the Crossplane managed resources docs, the provider-upjet-aws flag defaults, the HCP Terraform health assessment docs and Pulumi's pricing page, all as of October 2026.

What changed in Crossplane v2?

Crossplane v2.0 shipped on 14 August 2025 and was the first major version in years. Four changes matter for this comparison.

Namespaced by default. Composite resources (XRs) and managed resources (MRs) now live in namespaces. In v1 you needed a separate Claim object to give a team a namespaced handle on a cluster-scoped composite; in v2 the XRD's scope defaults to Namespaced, with Cluster and LegacyCluster (claims, for v1 compatibility) as alternatives, per the XRD docs. Namespaced managed resources got new API groups, so the AWS bucket you compose is s3.aws.m.upbound.io, with the .m. marking the namespaced variant.

Compositions are function pipelines only. The old native patch-and-transform mode (mode: Resources) was removed. Every Composition is now mode: Pipeline, a sequence of composition functions packaged as OCI images. Patch-and-transform survives as a function, alongside templating functions and ones you write in Go or Python. The release notes point to crossplane beta convert pipeline-composition in the v1 CLI for migration.

Compose anything. Compositions can now include any Kubernetes resource, so one abstraction can create the RDS instance and the Deployment that uses it.

Day-two Operations and provider filtering. A new Operation type runs one-off, scheduled or event-driven tasks such as backups. Managed resource filtering lets you install only the CRDs you need from a provider, which answers the long-standing complaint about hundreds of CRDs slowing the API server.

Since then the project has shipped quarterly. v2.1 (5 November 2025) added a per-XR circuit breaker to stop controllers fighting over composed resources. v2.4.0 (20 August 2026) added scale to zero for safe-start providers and moved the CLI to cli.crossplane.io; the current patch is v2.4.2 from 22 September 2026. The same notes say v1.20 reaches end of life when v2.5 ships in November 2026, so if you are still on v1 this is the quarter to run crossplane beta upgrade check. Crossplane graduated in the CNCF on 6 November 2025, citing over 3,000 contributors from more than 450 organisations and adopters including Nike, Autodesk and SAP.

Here is what a minimal v2 abstraction looks like: a namespaced Bucket API for application teams, backed by one AWS bucket.

# Crossplane v2.4.2, function-patch-and-transform v0.8.2, provider-aws-s3 v2.9.0
# Install core: helm install crossplane crossplane-stable/crossplane \
#   --namespace crossplane-system --create-namespace --version 2.4.2
apiVersion: apiextensions.crossplane.io/v2
kind: CompositeResourceDefinition
metadata:
  name: buckets.platform.example.org
spec:
  scope: Namespaced            # the v2 default; teams get their own copies
  group: platform.example.org
  names:
    kind: Bucket
    plural: buckets
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              region:
                type: string
            required: ["region"]
---
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: bucket-aws
spec:
  compositeTypeRef:
    apiVersion: platform.example.org/v1alpha1
    kind: Bucket
  mode: Pipeline               # the only mode left in v2
  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.m.upbound.io/v1beta1   # .m. = namespaced MR
          kind: Bucket
          spec:
            forProvider:
              region: eu-west-1
        patches:
        - type: FromCompositeFieldPath
          fromFieldPath: spec.region
          toFieldPath: spec.forProvider.region
---
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.2

An application team then writes a five-line Bucket in its own namespace and never sees the AWS schema. That is the real product Crossplane sells: an internal API, not a nicer HCL.

The irony: Crossplane runs on Terraform providers

Most people comparing these tools miss this. The AWS, Azure and GCP Crossplane providers are generated by Upjet, which "transforms Terraform providers into Crossplane providers" and powers those three plus 50 or more community providers. So resource coverage and many provider bugs are shared with Terraform. What you are choosing is the loop around the provider, not a different understanding of the cloud.

Packaging changed too. Since 2025, Upbound's "Official Providers" are commercially licensed builds; the free, Apache-2.0 builds are the crossplane-contrib ones, released monthly and served from xpkg.crossplane.io, per Upbound's announcement. The community provider-upjet-aws is at v2.9.0 (8 October 2026). If a tutorial tells you to pull xpkg.upbound.io/upbound/provider-family-aws, check whether you need a subscription before you build on it.

Terraform and OpenTofu in 2026: still the default

Terraform is at 1.16.6 (14 October 2026) and OpenTofu at 1.13.1 (1 October 2026). For this comparison the relevant recent work is about adopting what already exists: Terraform 1.14 added list blocks and terraform query to discover resources and generate import configuration, and 1.16 allowed import blocks inside modules (1.16.0 notes). OpenTofu has an open issue to match the query feature. I walk through all of that in the sibling guide, How to Import Existing Resources into Terraform and OpenTofu.

Drift is the weak spot. Plain Terraform only sees drift when someone runs terraform plan (or plan -refresh-only). HCP Terraform's health assessments add scheduled drift detection and continuous validation, but only in the Standard and Premium editions (docs), and they report drift rather than fix it. Many teams simply run a nightly terraform plan -detailed-exitcode in CI and page on exit code 2. That works, and it keeps a human between detection and correction, which I consider a feature.

Pulumi in 2026: real languages, and now HCL too

Pulumi is Apache-2.0 and at v3.270.0 (16 October 2026), with about 25,800 GitHub stars. Its pitch is still that infrastructure is code in TypeScript, Python, Go, .NET or Java, with loops, types, unit tests and package managers rather than a configuration language.

The 2026 change is that Pulumi stopped asking Terraform users to rewrite. Its August 2026 release made HCL a supported Pulumi language, let Pulumi Cloud act as a Terraform/OpenTofu state backend with remote runs and policy on plans, and converts Terraform modules into components usable from every Pulumi language. The migration docs show pulumi package add hcl module terraform-aws-modules/vpc/aws 5.19.0 for module reuse and pulumi import --from terraform ./terraform.tfstate for adopting existing state. In other words, Pulumi is now competing with HCP Terraform as a place to run Terraform, not only with Terraform as a language.

Drift detection exists but is a paid feature: Pulumi's pricing page lists it on Pro ($400 a month, up to 2,000 managed resources) and Enterprise ($2,000 a month), not on Free or Essentials ($40). The CLI gained pulumi stack schedule new for drift schedules and pulumi stack drift status in v3.241.0 on 18 May 2026, the same release that took the Neo agent out of experimental.

Crossplane vs Terraform vs Pulumi: side-by-side comparison

Terraform OpenTofu Pulumi Crossplane
Licence BSL 1.1 MPL-2.0 Apache-2.0 Apache-2.0
Governance IBM (HashiCorp) Linux Foundation, CNCF Sandbox Pulumi Corp CNCF Graduated
Latest (Oct 2026) 1.16.6 1.13.1 3.270.0 2.4.2
Language HCL HCL TS, Python, Go, .NET, Java, YAML, HCL YAML plus functions (Go, Python, templates)
Execution plan, then apply plan, then apply preview, then up continuous reconcile
State state file state file (optional encryption) Pulumi Cloud or self-managed etcd plus cloud API
Drift next plan; HCP Standard/Premium health checks (docs) next plan refresh; scheduled on Pro/Enterprise (pricing) auto-reverted on poll (docs)
Needs a cluster no no no yes, and you must run it well
Best at broad, reviewed changes same, open governance app teams, testing, abstraction in code self-service APIs on Kubernetes

How does each tool handle drift, and do you want it fixed?

"Crossplane fixes drift automatically" sounds strictly better until the night someone hand-edits a load balancer during an incident and the controller quietly puts it back. Continuous reconciliation is excellent for things that should never deviate (encryption settings, public access blocks, tags) and dangerous for things humans legitimately touch under pressure. Crossplane gives you levers: spec.managementPolicies can be set to ["Observe"] for read-only adoption, an empty list pauses a resource, and the crossplane.io/poll-interval annotation changes the cadence per resource. But those levers have to be in your runbook before the incident, not discovered during it.

Plan/apply tools fail the other way: drift accumulates silently until a plan proposes destroying something a person created by hand. The fix is boring discipline, a scheduled plan with alerting and a rule that console changes get back-ported to code within a day. I wrote about the guardrail side of this in Policy as Code for AI Agents, and it applies equally to humans.

This matters more now that agents write infrastructure changes. A plan is a natural approval checkpoint for an agent's proposal. A control plane, by contrast, turns an agent's mistake into a rejected API call if your admission policy is good, or into an immediately reconciled outage if it is not.

Which should your team pick?

Pick Terraform or OpenTofu if you have a small or mixed team, infrastructure that changes weekly rather than hourly, and reviewers who read plans. It has the largest provider ecosystem, the most hiring signal and the fewest moving parts. OpenTofu if open governance or client-side state encryption matters to you; Terraform if you are buying HCP Terraform anyway.

Pick Pulumi if the people writing infrastructure are application developers who already live in TypeScript or Python, you want real unit tests and typed components, or you want a managed runner that also takes your existing HCL. Budget for the paid tiers if you want drift detection.

Pick Crossplane if you are a platform team building a self-service API for many product teams, you already operate Kubernetes competently, and you want infrastructure requests to go through the same GitOps and admission path as workloads (see Kubernetes as the Agent Control Plane for where that is heading). Do not pick it to avoid learning Terraform: you will be debugging Terraform-derived providers inside controllers.

Plenty of mature shops run two: Terraform for the foundations (accounts, networks, the cluster that hosts Crossplane), Crossplane for per-team resources on top. That split is sensible, because the control plane cannot create the cluster it runs in.

FAQ

Is Crossplane a replacement for Terraform?

For self-service resources on top of Kubernetes, it can be. For bootstrapping accounts, networks and the management cluster itself, most teams still use Terraform or OpenTofu, because Crossplane needs a running cluster before it can do anything.

Does Crossplane use Terraform under the hood?

Partly. The main AWS, Azure and GCP providers are generated from Terraform providers with Upjet and embed their resource logic, but there is no Terraform CLI, plan file or state backend for you to manage. The reconciliation loop is Crossplane's own.

Is Crossplane v2 backward compatible with v1?

Mostly. Claims and cluster-scoped resources still work through legacy modes, but native patch-and-transform compositions, ControllerConfig and external secret stores were removed. Run crossplane beta upgrade check from the v1.20 CLI before upgrading, and do it before v1.20 reaches end of life in November 2026.

Is Pulumi better than Terraform in 2026?

It is better for teams that want general-purpose languages and testing, and since August 2026 it can run HCL and host Terraform state, so the switching cost is lower. Terraform and OpenTofu remain the safer default for teams that value a small, widely understood toolchain.

This quarter I would do three things in this order: put a nightly plan -detailed-exitcode on every Terraform or OpenTofu root module that lacks one, get any Crossplane v1.20 control plane onto v2.4 before the November end of life, and only then pilot Crossplane for one narrow self-service resource (a bucket or a queue) with managementPolicies and pause procedures written down first. If the pilot does not remove tickets from the platform team's queue within a month, it is not worth the controllers. The state side of all this, backends, locking and encryption, is in Terraform State in 2026.

Related: Infrastructure as Code in 2026: Two Forks at 1.16 and 1.13, a $6.4B Owner, 912 Public State Files, and One Agent That Ran terraform destroy, Kubernetes as the Agent Control Plane in 2026 and Policy as Code for AI Agents in 2026.

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related