Crossplane vs. Terraform: The Evolution from State Files to Control Planes
The debate between Terraform and Crossplane is usually framed as a choice between two tools. This is a fundamental misunderstanding. Terraform is a CLI-driven state manager; Crossplane is a Kubernetes-native control plane. The difference isn't in the syntax—it's in the operational model of how infrastructure is reconciled, who owns the state, and how the "Last Mile" of provisioning is executed.
1. The Philosophical Divide: Edge-Triggered vs. Level-Triggered
To understand the difference, we have to look at how these tools perceive the world. Terraform is edge-triggered. It triggers a change when you run a command. It compares the current state file to the desired configuration, calculates a diff, and pushes a set of API calls to the provider. If you don't run the command, nothing happens.
Crossplane is level-triggered. It behaves like the Kubernetes controller manager. It doesn't "push" a change; it continuously observes the "level" (the current state) of the cloud resource and compares it to the "level" defined in the K8s API. If a developer manually changes a setting in the AWS Console, Crossplane doesn't wait for a CLI command to fix it; it detects the drift in the next reconciliation loop and reverts the resource to the desired state.
2. The Implementation Gap: S3 with Lifecycle Rules
Let's look at a concrete requirement: an S3 bucket with a lifecycle rule to move objects to Glacier after 30 days. This highlights the difference between resource-level management and platform-level abstraction.
Terraform: The Resource View
Terraform defines the resource as a set of properties. It is a one-to-one mapping of HCL to API calls. The developer must know exactly which resource types (Bucket, LifecycleConfiguration) are needed to achieve the goal.
resource "aws_s3_bucket" "logs" {
bucket = "platform-monkey-logs"
}
resource "aws_s3_bucket_lifecycle_configuration" "logs_lifecycle" {
bucket = aws_s3_bucket.logs.id
rule {
id = "archive-to-glacier"
status = "Enabled"
transition {
days = 30
storage_class = "GLACIER"
}
}
}
The Win: Absolute transparency. The SRE knows exactly which API calls are being made.
Crossplane: The Composition View
Crossplane allows the platform team to create a Composite Resource (XR). The developer no longer defines a "Bucket" and a "Lifecycle Configuration"; they define a XStorageBucket, and the platform team's Composition handles the wiring.
# The Composition (The "Internal API" for the company)
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: s3-standard-archive
spec:
compositeTypeRef:
apiVersion: platform.monkey.io/v1alpha1
kind: XStorageBucket
resources:
- base:
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
spec:
forProvider:
region: us-east-1
- base:
apiVersion: s3.aws.upbound.io/v1beta1
kind: BucketLifecycleConfiguration
spec:
forProvider:
rule:
transition: { days: 30, storageClass: GLACIER }
The Win: Developer Velocity. The dev just requests an XStorageBucket. The "Gold Standard" for lifecycle rules is baked into the platform, not the developer's memory.
3. Under the Hood: Upjet and the "Hidden" Terraform
One of the most misunderstood parts of Crossplane is how it actually interacts with the cloud. It doesn't rewrite every AWS/Azure/GCP API from scratch. Instead, it uses Upjet.
Upjet is a transpilation engine that allows Crossplane to leverage the massive ecosystem of Terraform providers. It effectively treats Terraform providers as "libraries" rather than "CLIs."
The Upjet Pipeline: From HCL to CRD
- Schema Extraction: Upjet reads the Terraform provider's schema (Go types and .tf definitions) to understand what properties a resource has.
- CRD Generation: It automatically generates a Kubernetes Custom Resource Definition (CRD) that mirrors the Terraform resource. This is why you see providers like
s3.aws.upbound.io. - The Provider Execution: When you create a resource in K8s, the Crossplane provider (powered by Upjet) invokes the Terraform provider binary internally to perform the API calls.
The Technical Trade-off: You are adding a layer of translation. If a new feature is released in a Terraform provider, you must wait for the Upjet-based Crossplane provider to be updated. You are trading "Immediate Access" for "Control Plane Consistency."
4. The "Hybrid" Model: Executing Terraform via Crossplane
A common question is: \"Why choose? Can't I just use both?\" The answer is yes, and in many large-scale environments, this is the correct architectural choice. This is the Bootstrapping vs. Lifecycle model.
In this model, Terraform is used for the Foundational Layer (The "Blast Radius" zone). You use Terraform to build the VPCs, the Hub-and-Spoke networks, and the Kubernetes clusters themselves. Why? Because these resources change rarely and require absolute, immutable precision.
Once the cluster is alive, you hand over the keys to Crossplane for the Application Layer (The "Velocity" zone). Databases, S3 buckets, and Redis instances are now managed as K8s resources. This allows the platform team to define Compositions that developers can self-serve without needing access to the master Terraform state file.
5. Operational Audit: State Files vs. etcd
The real divergence is in the "Source of Truth" and how it fails.
Terraform: The State File
Terraform relies on a state file (usually in an S3 bucket with a DynamoDB lock). It is a snapshot. If the state file is corrupted or the lock is stuck, the entire pipeline stops. To detect drift, you must run a plan—a process that is expensive in terms of time and API calls.
Crossplane: The etcd Loop
Crossplane uses the Kubernetes API (etcd) as its state store. There is no "state file" to lock. Drift detection is a background process that happens every few minutes. The "state" is the live cluster. This makes Crossplane inherently more resilient to "corrupted state" but makes it dependent on the health of the K8s control plane.
Final Verdict: The Decision Matrix
Stop choosing based on "which tool is better." Choose based on your Organizational Cognitive Load.
- Choose Terraform if: You have a small, highly skilled SRE team that manages a static set of infrastructure. You prefer the absolute predictability of a state file and a "push" workflow.
- Choose Crossplane if: You are building an Internal Developer Platform (IDP). You want to provide "T-shirt sized" infrastructure to 50+ developers and you want the system to automatically fix drift without human intervention.
- Choose the Hybrid Model if: You are at enterprise scale. Use Terraform to build the "foundation" (VPCs, Clusters) and Crossplane to provide the "self-service" (Databases, Buckets) to the developers.