Crossplane vs Terraform for Platform Teams Adopting GitOps
Crossplane reconciles infrastructure continuously, while Terraform only checks when you run it.

The Crossplane vs. Terraform decision comes down to one question: should infrastructure be managed by a control plane that runs all the time, or by a command-line tool you invoke when you need it? That question determines who or what is watching your systems the moment you stop typing commands.
The mechanics make the split clear. Terraform is a binary. You run it, it does its work, and when the process exits, nothing is left behind to keep an eye on things. Crossplane installs into a Kubernetes cluster as a set of controllers, and those controllers reconcile desired state against actual state on a loop that never stops, the same pattern Kubernetes already uses to keep Pods and Deployments alive.
That gap between an always-running control plane and a tool you invoke on demand is a different answer to a basic question: who is responsible for infrastructure in the hours between human actions? Everything the rest of this piece covers, drift handling, team autonomy, incident response, day-to-day operational load, flows from that one architectural fact. None of it is a separate decision.
What Terraform's plan-apply cycle leaves unhandled between runs
Terraform's workflow runs in three steps: write, plan, apply. It reads HCL configuration, computes a diff against a state file, shows that diff for review, and then applies it. Once the apply command finishes, the process ends and nothing remains running in the background.
That has a direct consequence between runs. Terraform holds no opinion about what happens to your infrastructure while it's not actively executing. If an engineer opens the AWS console at 2am and manually changes a security group rule, Terraform has no way of knowing. It won't notice until someone runs plan again, when the diff reveals the change.
Terraform keeps its state in a state file, usually stored in a remote backend like S3, Azure Blob Storage, Google Cloud Storage, or Terraform Cloud. Every later run checks against that file, not against the live state of the cloud itself. If the file and reality drift apart, Terraform only catches up when invoked.
That design has a collaboration cost at scale. Because the state file needs a lock while an apply is running, and applies can take minutes on large configurations, no other engineer or pipeline can apply changes during that window. Teams also run into a structural limit around Terraform's monolithic apply process: there's no standard way to change just one resource inside a large configuration without touching the rest of it. Over time, that pushes teams toward splitting configurations into smaller, more tightly coupled pieces just to keep blast radius manageable.
What Crossplane's control plane model requires to run
Crossplane takes a different starting point. It installs into a Kubernetes cluster as a set of controllers, and cloud resources become Kubernetes custom resources, an RDSInstance object, a Bucket object, each one representing a real piece of infrastructure. The control plane watches the desired state of those objects, compares it against actual cloud state, and acts on any difference it finds, continuously, without anyone running a command.
State lives in the cluster's own etcd store rather than in a separate file in cloud storage. Kubernetes itself becomes the source of truth, which removes the state-locking problem that slows down collaborative Terraform workflows. There's no single file to lock, because there's no single file.
Crossplane's providers map cloud resource types to Kubernetes CRDs. Coverage varies by cloud: the AWS provider covers hundreds of resource types, while GCP's provider covers more than 500 and Azure's covers more than 900, meaningfully less complete than AWS coverage in both cases.
The core abstraction is the Composition. A platform team defines a high-level API, called an XRD, and a template, called a Composition, that maps a developer's simple request, "give me a PostgreSQL database", to everything that actually has to exist to deliver it: the RDS instance, the parameter group, the subnet group, the Secrets Manager secret. The developer never has to see any of that.
None of this comes free. Adopting Crossplane means running and maintaining a Kubernetes cluster, full stop, before provisioning a single bucket. For a team that has never operated Kubernetes, that's a real operational prerequisite that deserves to be weighed honestly against whether the team needs Kubernetes for anything else.
Continuous Reconciliation: Drift, Incidents, and Day-Two Operations
Continuous reconciliation is the property that separates these two tools most sharply once they're actually running in production. Crossplane's control loops catch any gap between desired and actual state and correct it without anyone stepping in. Terraform's drift sits undetected until someone runs plan and looks.
That distinction affects reliability at scale in concrete ways. One platform team managed dozens of microservices across multiple regions, and found that under Terraform's passive model, AWS resources changed outside of Terraform simply stayed changed, with no self-correction, until the next apply happened to run. Across a large enough footprint, that silent drift turned into a real reliability risk rather than a theoretical one.
For security-sensitive infrastructure, that gap matters even more. A misconfigured security group under Crossplane gets corrected automatically. But under Terraform, it survives until the next scheduled run happens to catch it.
But continuous reconciliation cuts both ways, and the edge case deserves full weight, not just a footnote. During an incident, an engineer might need to make a fast, undocumented fix directly in the cloud console, so whatever process normally governs changes gets bypassed. Crossplane's control plane will reconcile that fix away, reverting it back to whatever the stored desired state says it should be. A team working this way needs to pause reconciliation first before making the emergency change, and that's a procedural step added under exactly the conditions where speed matters most.
Connection details for provisioned resources land in a Kubernetes Secret that workloads in the same cluster can mount directly. If your applications already run in Kubernetes, that's a genuine convenience. For teams whose applications live elsewhere, it changes nothing either way.
Team Autonomy and Self-Service Infrastructure at Scale
The clearest illustration of the self-service gap between these tools is a ticket queue disappearing. One platform team used to manage RDS instances, S3 buckets, and SQS queues through a Jira ticket loop, with each request taking anywhere from two hours to two days to fulfill. After adopting Crossplane, product teams provisioned their own cloud resources by submitting manifests through a pull request, reconciled automatically once merged. No tickets, no waiting.
That shift reflects something structural about how each tool handles access. Terraform modules raise the abstraction level of infrastructure code, but they don't raise the access control level with it. A developer using a Terraform module still has to learn HCL, still has to run or trigger Terraform directly, and still operates inside Terraform's collaboration constraints, including the state locking discussed earlier. Access control stays framed around raw cloud concepts: subnet groups, parameter groups, IAM roles.
Crossplane's XRD model changes that framing. Because it exposes a genuine Kubernetes API, a platform team can grant permissions at the API level itself, authorizing a team to create "a PostgreSQL database" as its own concept instead of managing access to the cloud primitives underneath it. Application developers interact with the abstraction the platform team built, with its implementation details hidden from them.
Terraform can reach a similar self-service outcome, but only by adding a wrapper on top, Atlantis, Terraform Cloud, Spacelift, or a custom CI pipeline. That wrapper works, but it's an additional system a platform team has to build, run, and maintain, and the underlying access control still lives at the cloud provider level rather than at the abstraction layer Crossplane builds natively. None of this makes Terraform inadequate for self-service. So Crossplane's architecture fits more naturally once an organization reaches a scale with many application teams that need independence from a central platform team's queue.
For teams building internal platforms, this is also a question about where operational burden should sit. Porter, which runs as a managed control plane inside a team's own AWS, GCP, or Azure account, shows how that same pattern, continuous reconciliation sitting alongside developer-facing abstractions, can simplify deployment without requiring a team to stand up and operate Kubernetes themselves.
Where Terraform's ecosystem maturity and operational simplicity still win
None of this makes Crossplane the right default for every team, and the honest comparison has to say so. Terraform requires no Kubernetes cluster. For a platform team not already running Kubernetes, standing up and maintaining a cluster purely as a prerequisite for infrastructure management is a real tax, and for smaller organizations, the break-even point where that tax pays for itself may never arrive.
Terraform's maturity also shows in specific, demanding workloads. Teams running multi-cloud GPU training with central compliance controls often find Terraform's toolchain more complete today: native state locking, mature policy-as-code integrations, and broad provider coverage give it an edge for AI workloads that span clouds under strict governance requirements.
Migration cost also depends on factors beyond the purely technical. A team with years of existing Terraform modules and accumulated institutional knowledge is sitting on real engineering time invested in that configuration. Walking away from it only makes sense if the problems Crossplane solves, drift correction, self-service at scale, native GitOps integration, are problems that team actually has. For a team running a handful of services in one cloud with a small, stable group of engineers, those problems may simply not exist yet, and Terraform's simplicity remains the better fit until they do. That matters even more for teams working across multiple clouds on compliance-heavy AI infrastructure, where Terraform's current toolchain often has the deeper bench.
How GitOps workflows integrate differently with each tool's runtime model
GitOps adoption is where the control-plane-versus-CLI distinction becomes concrete, determining how much extra machinery a team needs to build. Crossplane is natively compatible with GitOps because infrastructure is declared as Kubernetes manifests, the exact format GitOps tools already know how to sync. ArgoCD or Flux watches the Git repository, applies manifests to the cluster, and Crossplane's controllers take it from there, reconciling cluster state to cloud reality with no extra wrapper in between.
Terraform needs that wrapper. Atlantis, Terraform Cloud, Spacelift, or a custom CI pipeline has to sit between the Git repository and the terraform apply command for GitOps to work. The integration functions, but it's a separate system with its own configuration, its own failure modes, and its own operational surface area to maintain.
The properties a platform team actually wants from GitOps, Git as the single source of truth, changes proposed through pull requests, automatic reconciliation, a clean audit trail, come built into Crossplane's model from the start. In Terraform, those same properties get assembled piece by piece: CLI plus wrapper plus pipeline, each one a separate thing that can break.
Ephemeral environments make the gap concrete. When teams spin up and tear down short-lived environments at high frequency, they run directly into Terraform's state locking and unlocking behavior, a mechanism that wasn't built for Git-driven workflows firing many times a day. Crossplane's model has no equivalent locking constraint to run into.
For a platform team already running GitOps for application deployments, Crossplane extends that exact workflow to infrastructure without asking anyone to learn a new paradigm. The developer experience stays the same whether the pull request changes application code or a database claim. Continuous reconciliation plays the same role here that it plays in Porter's compliance automation and autoscaling: keeping systems aligned with desired state constantly, rather than catching up with it periodically.
The practical case for running both tools together
Many platform teams land on "use both," and that answer holds up under real conditions. A hybrid model works when each tool owns a distinct, non-overlapping domain. Terraform manages foundational primitives, VPCs, IAM, DNS, whole accounts, resources that change rarely and benefit from a human reviewing an explicit apply. Crossplane manages the higher-frequency, developer-facing resources, databases, queues, caches, where continuous reconciliation and Kubernetes-native access control actually pay off.
That split is architecturally sound because it tracks real change frequency. Account-level infrastructure gets provisioned once and rarely touched again. Database and queue provisioning happens constantly as application teams scale and ship.
The opposing case deserves equal weight, too: running two parallel infrastructure-as-code toolchains means two sets of documentation, two separate debugging paths when something breaks, two sets of provider versions to track, and two onboarding curricula for every new engineer. So for a small platform team, that overhead can easily outweigh whatever benefit comes from using each tool exactly where it fits best.
Ownership gets murkier too. When a Crossplane-managed database depends on a Terraform-managed network, a failure at that boundary is harder to diagnose, because the two tools share no dependency graph between them. Nobody can run one command and see the whole picture. The hybrid model rewards teams that have enough platform engineering capacity to maintain two systems well. It punishes teams that adopt it before they have that capacity, turning a clean division of labor into two half-maintained toolchains instead.
Which architecture fits which team: a decision framework
Four questions settle most of this. Is the team already running Kubernetes? Does the team need application developers to provision infrastructure themselves without going through the platform team every time? Does the team require drift detection and automatic correction, rather than drift that waits for the next manual run to surface? Is GitOps the primary deployment model the team is building toward, for applications and infrastructure alike?
If the answer to all four is yes, Crossplane is the architectural match. The team already has the Kubernetes prerequisite, so the Composition model delivers self-service without a ticket queue, continuous reconciliation handles drift without manual intervention, and the manifest-based approach fits GitOps by construction rather than by wrapper.
If the team isn't running Kubernetes, operates at a smaller scale, manages a wide range of uncommon resource types, or runs multi-cloud AI and GPU workloads where provider maturity and compliance tooling carry real weight, Terraform remains the lower-overhead choice, and that includes its open-source equivalent, which drops into the same GitOps pipeline patterns without modification and carries an openly licensed governance model under community foundation stewardship. The right architecture matches the team's actual scale, stack, and the workflow it's already building toward.

