Crossplane vs Terraform: Composition vs HCL for Infrastructure as Code

Reading Time: 5 minutes

Kubernetes Ecosystem: From User to Contributor, Episode 7
← EP06: Crossplane · EP07: Crossplane vs Terraform · EP08: Karpenter →

11 min read


TL;DR

  • Crossplane vs Terraform is fundamentally a continuous-reconciliation model against a plan/apply model — not just two different syntaxes for the same idea
  • Crossplane needs a live Kubernetes cluster to run at all; Terraform needs nothing but a state file and network access to the providers it’s calling
  • Terraform’s provider registry is a decade deep and covers services Crossplane’s younger ecosystem hasn’t reached yet — SaaS tools, monitoring platforms, and services with no cloud-infrastructure angle at all
  • Crossplane’s Compositions give app teams a genuinely self-service, in-cluster API; Terraform’s modules give infra teams reusable code, but consuming a module still means running Terraform yourself
  • Recommendation: many real platform teams use both — Terraform (or CAPI, EP05) to bootstrap the cluster and its surrounding VPC/networking, then Crossplane running inside that cluster for the self-service, app-team-facing layer
  • Contribution opportunity: Crossplane’s provider coverage gap against Terraform’s registry is real, specific, and a legitimate place to build a brand-new provider

The Big Picture

TERRAFORM                                   CROSSPLANE
──────────                                   ──────────
terraform plan                               kubectl apply -f resource.yaml
  │  (shows what WOULD change)                    │
  ▼                                                ▼
terraform apply                              Crossplane controller reconciles
  │  (changes happen once, here)                   │  (continuously, forever,
  ▼                                                 │   not just at apply time)
State file (local or remote backend)               ▼
tracks what Terraform created                 Kubernetes etcd IS the state —
                                                the CR's status field tracks
No live cluster or control                     sync state
plane required to run this
                                               Requires a running Kubernetes
                                               cluster as the control plane

Crossplane vs Terraform is best understood through that control-flow difference first, before comparing any specific feature: Terraform changes things at discrete moments you trigger; Crossplane’s controllers are always watching, always correcting drift, for as long as the cluster runs.


The Fundamental Model Difference: Continuous Reconciliation vs Plan/Apply

Terraform’s model gives you an explicit review step — terraform plan shows exactly what will change before anything does, and nothing changes again until you run apply a second time. Crossplane’s model (covered in EP06) has no equivalent pause: once a Managed Resource or Composition claim exists, Crossplane’s controllers reconcile it toward the desired state continuously, including reverting manual out-of-band changes automatically.

Neither is objectively better — they’re suited to different operating assumptions. Terraform’s model fits teams who want a deliberate, reviewed change process. Crossplane’s fits teams who want infrastructure to behave like every other Kubernetes-native resource: self-healing, always converging, no separate “did anyone remember to re-apply” step.


State Management: etcd + CRDs vs Terraform State Files

# Terraform: state lives in a file (local or remote — S3, Terraform Cloud, etc.)
$ terraform state list
aws_s3_bucket.uploads
aws_db_instance.main

# Crossplane: "state" is just the live cluster's etcd — the CR's own status
$ kubectl get bucket uploads -o jsonpath='{.status.conditions}'
[{"type":"Ready","status":"True"},{"type":"Synced","status":"True"}]

Terraform’s state file is a single point of coordination that has to be locked correctly for concurrent runs to be safe — a well-understood but real operational concern (remote state backends, state locking, occasional manual state surgery after a botched apply). Crossplane sidesteps a separate state file entirely, but that means the health of your Kubernetes cluster’s etcd is the health of your infrastructure’s state — a different, not smaller, operational responsibility.


Composition vs Modules: Reusable Infrastructure Patterns Compared

Terraform modules are reusable code that whoever runs Terraform includes in their own configuration — genuinely reusable, but still something each consumer runs themselves. Crossplane Compositions (EP06) are reusable inside the cluster — an app team doesn’t run anything, they just create a custom resource the platform team already defined, and Crossplane’s controllers do the rest without the app team ever touching Terraform or Crossplane’s own tooling directly.

That’s the real practical difference for organizational self-service: Compositions remove the “app team needs to know how to run our IaC tool” step entirely. Modules still require the consumer to run Terraform, even if they didn’t write the module.


Ecosystem Maturity: Terraform’s Decade-Deep Provider Registry vs Crossplane’s Younger One

Terraform’s provider registry covers not just the major clouds but a long tail of SaaS platforms, monitoring tools, DNS providers, and internal enterprise systems that have no “cloud infrastructure” angle at all — a decade of community and vendor-contributed providers. Crossplane’s provider ecosystem, while actively growing and covering the major clouds thoroughly, has real, documented gaps once you look past core compute/storage/networking/database resources into more specialized or less common services.


The Recommendation: Which One, and When to Use Both

If your platform team is Kubernetes-native and wants to offer app teams a true self-service infrastructure API without teaching them a separate IaC tool: Crossplane. That’s the specific problem its Composition model solves better than anything Terraform offers.

If you need broad provider coverage beyond core cloud infrastructure, or you don’t want infrastructure lifecycle tied to a live Kubernetes control plane’s uptime: Terraform. Its registry depth and its independence from any running cluster are real advantages Crossplane doesn’t currently match.

The honest answer for a lot of real platform teams is both, at different layers. Use Terraform (or Cluster API, EP05) to bootstrap the Kubernetes cluster itself and its surrounding cloud networking — the layer that has to exist before Crossplane can run at all — then run Crossplane inside that cluster for the ongoing, self-service, app-team-facing infrastructure requests. This isn’t a compromise; it’s matching each tool to the layer it’s actually better suited for.


⚠ Production Gotchas

Don’t manage the same cloud resource with both Terraform and Crossplane simultaneously. Both tools will detect the other’s changes as drift and fight to revert them — pick one owner per resource, even when both tools are in use across your stack at different layers.

Terraform’s plan/apply gives you a review window Crossplane doesn’t — build your own review gate if you need one with Crossplane (a PR-based GitOps flow with required approval before a claim manifest merges is the common substitute).

Crossplane’s continuous reconciliation means a broken provider or a cloud API outage shows up as a stuck Synced: False condition, not a failed one-time command — monitoring needs to watch for stuck conditions over time, not just command exit codes the way Terraform CI pipelines typically do.


Quick Reference

Terraform Crossplane
Change model Plan → Apply (explicit) Continuous reconciliation
Requires a live cluster No Yes
State State file (local/remote) Kubernetes etcd + CR status
Reusable patterns Modules (you still run them) Compositions (app team just creates a claim)
Provider breadth Very broad, decade-deep Growing, strong on core cloud, gaps elsewhere
Manual drift Detected at next plan, not auto-reverted Auto-reverted on next reconcile

Contribution Opportunity: Building a Missing Crossplane Provider

The limitation: For a meaningful number of services Terraform has supported for years — smaller SaaS platforms, specialized monitoring tools, niche infrastructure services — there’s no Crossplane provider equivalent yet. Anyone wanting to manage that service the Crossplane way currently can’t, full stop.

Why it’s hard to fix: Building a new provider means implementing a real API client, defining CRD schemas that faithfully map the service’s actual parameters, and maintaining it as that service’s API evolves — real, ongoing engineering commitment, not a one-time script. That’s exactly why the ecosystem’s provider list still trails Terraform’s, despite Crossplane’s core reconciliation engine being mature: the core is one thing to maintain, but each provider is its own ongoing surface area.

What a contribution-shaped fix looks like: Crossplane’s provider-template repository exists specifically to make starting a new provider tractable — it scaffolds the boilerplate (code generation, CRD structure, controller wiring) so a new provider author focuses on the actual API mapping, not plumbing. Picking one service you already use via Terraform that has no Crossplane equivalent, and building a minimal provider covering just the 2-3 resource types you actually need, is a real, bounded, achievable contribution — and one the Crossplane community actively wants, given how directly it grows the ecosystem.


Key Takeaways

  • Crossplane’s continuous reconciliation and Terraform’s plan/apply are different operating models, not different syntaxes for the same thing — pick based on which review/change process fits your team
  • Crossplane requires a live cluster to function at all; Terraform doesn’t, which matters for bootstrapping order
  • Compositions remove the “app team has to run our IaC tool” step that Terraform modules still require
  • Terraform’s provider registry breadth remains a real advantage for anything beyond core cloud infrastructure
  • Many real platform teams run both at different layers — Terraform/CAPI to bootstrap the cluster, Crossplane inside it for self-service — and that’s a legitimate architecture, not indecision

What’s Next

Everything so far in this series has been about provisioning clusters and the infrastructure around them. EP08 shifts to what happens inside an already-running cluster when pods can’t be scheduled: Karpenter’s just-in-time node provisioning, and why it replaced the node-group model most teams started with.

Next: EP08 — Karpenter: Just-in-Time Node Provisioning for Kubernetes

Get EP08 in your inbox when it publishes → linuxcent.com/subscribe