Terraform and Crossplane both manage cloud infrastructure as code. The difference is fundamental: Terraform runs on-demand (plan β apply), Crossplane runs continuously (Kubernetes control loop). This shapes everything β drift handling, GitOps, team workflows, and failure modes.
Architecture
Terraform
Developer β terraform plan β Review β terraform apply β Cloud API β Done
β
State file (S3/TFC)Terraform is imperative-triggered: you run a command, it computes a diff, you approve, it applies. Between runs, nothing watches your infrastructure.
Crossplane
Developer β kubectl apply β Kubernetes β Crossplane Controller β Cloud API
β β
etcd (state) β Reconciliation loop β Actual stateCrossplane is continuously reconciling: a controller watches desired state in etcd and actual state in the cloud, fixing any drift automatically.
Feature comparison
| Feature | Crossplane | Terraform |
|---|---|---|
| Execution model | Continuous reconciliation | On-demand plan/apply |
| Language | YAML (Kubernetes manifests) | HCL |
| State storage | Kubernetes etcd | State file (S3, TFC, local) |
| Drift detection | Automatic, continuous | Manual (terraform plan) |
| Drift correction | Automatic | Manual (terraform apply) |
| Provider ecosystem | ~100 providers | 4,000+ providers |
| Composability | Compositions (XRDs) | Modules |
| GitOps | Native (ArgoCD/Flux apply YAML) | Requires wrapper (Atlantis, TFC) |
| RBAC | Kubernetes RBAC | TFC teams / custom |
| Secret management | Kubernetes Secrets / External Secrets | Variables, Vault integration |
| Preview changes | Limited (dry-run) | terraform plan (detailed) |
| Import existing | Yes (observe-only) | terraform import |
| Destroy | Delete the resource YAML | terraform destroy |
| Learning curve | Kubernetes + Crossplane concepts | HCL (moderate) |
Drift handling
This is the most important practical difference:
Terraform: drift is invisible until you check
# Someone manually changed a security group in AWS console
# Terraform doesn't know until you run:
terraform plan
# Plan shows drift:
# ~ aws_security_group.web
# ~ ingress.0.cidr_blocks: ["10.0.0.0/8"] β ["0.0.0.0/0"]
# You manually fix it:
terraform applyBetween terraform apply runs, infrastructure can drift silently. If your team runs Terraform weekly, drift goes undetected for days.
Crossplane: drift is corrected automatically
# Someone manually changed the security group
# Crossplane detects the difference within seconds
# and reverts it to the desired state in the YAML
# You see it in events:
kubectl describe securitygroup.ec2.aws.upbound.io/web
# Events:
# Synced: True (drift detected and corrected)Crossplane closes the gap between desired and actual state continuously. For security-critical infrastructure, this is a significant advantage.
GitOps integration
Crossplane + ArgoCD (native)
# ArgoCD Application manages Crossplane resources
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure
spec:
source:
repoURL: https://github.com/myorg/infra
path: crossplane/production
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
selfHeal: trueInfrastructure is just more Kubernetes YAML β ArgoCD syncs it like any other resource.
Terraform + Atlantis (wrapper needed)
# atlantis.yaml
version: 3
projects:
- name: production
dir: terraform/production
workflow: default
autoplan:
enabled: trueTerraform requires a wrapper tool (Atlantis, Terraform Cloud, Spacelift) to integrate with GitOps workflows. It works but is an additional system to manage.
Platform engineering with Crossplane
Crossplaneβs killer feature for platform teams is Compositions β custom abstractions that hide cloud complexity:
# Platform team defines the API
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xdatabases.platform.example.com
spec:
group: platform.example.com
names:
kind: XDatabase
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
spec:
properties:
size:
type: string
enum: [small, medium, large]
engine:
type: string
enum: [postgres, mysql]
---
# Composition maps the API to real cloud resources
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: xdatabase-aws
spec:
compositeTypeRef:
apiVersion: platform.example.com/v1alpha1
kind: XDatabase
resources:
- name: rds
base:
apiVersion: rds.aws.upbound.io/v1beta1
kind: Instance
spec:
forProvider:
engine: postgres
instanceClass: db.t3.mediumDevelopers request:
apiVersion: platform.example.com/v1alpha1
kind: XDatabase
metadata:
name: orders-db
spec:
size: medium
engine: postgresThey do not need to know about AWS RDS instance classes, subnet groups, or security groups. The platform team handles that in the Composition.
Terraform modules offer similar abstraction but lack the continuous reconciliation and Kubernetes-native RBAC.
When they work together
Many organizations use both:
- Terraform for foundational infrastructure β VPCs, accounts, DNS zones, Kubernetes clusters themselves
- Crossplane for application infrastructure β databases, caches, queues, buckets requested by development teams
This hybrid approach works because Terraform excels at one-time setup and Crossplane excels at ongoing management.
Decision guide
Choose Crossplane when:
- You are building an internal developer platform with self-service APIs
- Drift correction must be automatic, not manual
- Your team already uses Kubernetes and GitOps (ArgoCD/Flux)
- You want Kubernetes RBAC for infrastructure access control
- Infrastructure is managed by the same pipeline as applications
Choose Terraform when:
- You need the broadest provider ecosystem (4,000+ providers)
- Your team already knows HCL and has existing Terraform codebases
- You need detailed plan previews before any change
- Terraform Cloud/Enterprise features matter (Sentinel policies, cost estimation, VCS integration)
- Infrastructure changes are infrequent and do not need continuous reconciliation