TrademarkTrademark
Features
Documentation

Terraform vs GitOps for Kubernetes: Where the Line Should Fall

Terraform and a TACO are the right tool for the Kubernetes cluster and its cloud dependencies. They are the wrong tool for in-cluster workloads. Here is where the line should fall, and the failure modes that show up when you cross it.
Sebastian StadilJune 23, 2026
Terraform vs GitOps for Kubernetes: Where the Line Should Fall
Key takeaways
  • Use Terraform (and your TACO) up to the cluster edge: the managed control plane, node pools, IAM/IRSA, networking, and cloud-side add-ons. Hand in-cluster workloads to a GitOps controller like Argo CD or Flux.
  • Kubernetes reconciles continuously through controllers; Terraform applies on a trigger. Argo CD polls Git every 120s plus up to 60s of jitter, and Flux reconciles on a per-resource interval, so drift inside the cluster is corrected in minutes rather than at the next pipeline run.
  • In-cluster controllers (HPAs, operators, mutating webhooks) routinely change fields after creation. Terraform reads those changes as drift and tries to revert them unless you keep adding lifecycle.ignore_changes exceptions.
  • The kubernetes_manifest resource needs API access at plan time, so you cannot create a cluster and apply manifests to it in one run, and you cannot install a CRD and a custom resource of it in the same apply.
  • Bootstrap the GitOps controller with Terraform, then let it own application delivery. That keeps each tool inside the operating model it was built for.

If you run Terraform through a TACO, sooner or later someone asks the obvious question. We already manage the cluster this way, so why not manage what runs inside it the same way? The Terraform Kubernetes and Helm providers are right there. The TACO already holds the state, runs the policy checks, keeps the audit log. It looks like one less tool to run.

Here's the catch. The cluster and the things running inside it work in two different ways, and Terraform only fits one of them well. So the question isn't really "Terraform or GitOps." It's where you draw the line between them.

Where is the line?

Draw it at the cluster edge.

Everything cloud-shaped sits on the outside: the managed control plane on EKS, GKE, or AKS, the node pools, IAM roles and IRSA bindings, VPCs and subnets, and the managed add-ons your provider exposes as first-class resources. This is day-0 and day-1 infrastructure. It barely changes, it can take down a lot when it breaks, and it wants exactly what a TACO gives you: state, plan review, policy enforcement, and an audit trail. Terraform is the right tool here, and running it through a TACO is the right way to run it.

Everything Kubernetes-shaped sits on the inside: Deployments, Services, ConfigMaps, Helm releases, custom resources. That's where a GitOps controller belongs. The rest of this post is about what breaks when you drag Terraform across the line into that second box.

Should you use Terraform to deploy Kubernetes resources?

You can. The providers work. But once you do it at any scale, four things start to grind.

The reconciliation models don't match. Kubernetes runs on controllers, which the docs describe as control loops that watch cluster state and keep nudging it toward the state you declared. A GitOps tool stretches that same loop out to Git. Argo CD polls the repo every 120 seconds plus up to 60 seconds of jitter, so every two to three minutes or so. Flux reconciles on a .spec.interval you set per resource, down to a minute or less. Terraform does none of this. It runs when a pipeline or a person tells it to. So when someone runs kubectl edit on a Deployment, or a controller changes it, GitOps closes the gap within minutes and Terraform has no idea until the next run. You can put runs on a schedule, but then you're faking a control loop with a cron job, and you're paying for a TACO run every time.

Other controllers are editing the same objects. A cluster is full of things that change resources after you create them. A HorizontalPodAutoscaler rewrites spec.replicas. Operators reconcile whatever they own. Mutating admission webhooks inject sidecars and fill in default fields. Kubernetes even tells you not to set spec.replicas on a Deployment that an HPA manages. Terraform, managing that same Deployment, reads every one of those edits as drift and plans to put it back. The usual fix is lifecycle { ignore_changes = [...] }, and it works, but on a busy cluster the list of fields you have to ignore keeps growing. Once you're teaching Terraform to ignore most of what Kubernetes does to a resource, that resource is telling you it belongs to a different tool.

The providers get awkward at the workload layer. The kubernetes_manifest resource needs API access to the cluster at plan time, not just apply time. HashiCorp's own docs say the cluster has to be reachable when Terraform plans, so you can't create a cluster and apply manifests to it in the same apply. That same plan-time check means you can't install a CRD and a custom resource of that CRD in one apply, because the custom resource fails to plan with a "no matches for kind" error before the CRD exists. The Helm provider, meanwhile, shows you no diff of the rendered manifests at plan time by default. There's an opt-in experiment that stores the rendered manifest so the full diff shows up in the plan, but it's still flagged experimental, it tends to dump the whole manifest instead of the actual change, and it has known reliability issues. None of this is the TACO's fault. The TACO just inherits all of it.

State turns into a second source of truth, with secrets in it. Kubernetes already keeps the state of in-cluster objects in etcd. Manage those same objects in Terraform and now you've got a second copy that can drift from the live cluster. And Terraform writes every managed attribute into state in plain text, including the data in a kubernetes_secret. HashiCorp says so directly. So Kubernetes Secrets managed through Terraform end up sitting in your state file unencrypted, and that state file lives wherever your TACO keeps it. Setting sensitive = true only hides the value in CLI output. It doesn't encrypt anything in state.

Build the cluster and its dependencies with Terraform, use Terraform to install and bootstrap the GitOps controller, then let the controller take over from there.

That handoff is the whole trick. Terraform stands up EKS, the node groups, the IAM, and then does its last job inside the cluster: it installs Argo CD or Flux. After that, application manifests live in Git and the controller reconciles them on its own loop. We walk through the provider mechanics of that boundary in Mastering Kubernetes with Terraform, and what it's actually like to run the controller in the no-nonsense guide to ArgoCD. If you're still picking a controller, our GitOps tools comparison covers the field.

There's a gray zone worth calling out. Cluster-level platform components like the ingress controller, cert-manager, the metrics server, and the CNI sit close to the edge, and people sometimes install them with the Helm provider while they're bringing the cluster up. That's defensible, since they're part of making the cluster usable rather than part of shipping an app. But the moment you start reconfiguring them on an application cadence, they're workloads, and they want to move inside the GitOps boundary too.

Where does Scalr fit in this?

Scalr is a pure-play Terraform and OpenTofu TACO, so it lives squarely on the infrastructure side of the line. It runs and governs the Terraform that provisions your clusters and their cloud dependencies, with the state management, RBAC, policy, and audit that layer needs, and it can run the bootstrap that installs your GitOps controller. What it isn't, on purpose, is your application-delivery engine. Scalr doesn't try to be the thing reconciling your Deployments every few minutes. That job goes to Argo CD or Flux, and a healthy setup runs both tools, each one inside the model it was built for.

So the case against managing Kubernetes through a TACO is really a case against making one tool span a boundary where the two sides work differently. Keep Terraform and your TACO on the cluster and its cloud dependencies. Hand the workloads to a controller built to reconcile them.

Frequently asked questions

Should you use Terraform to deploy Kubernetes resources?

For the cluster and its cloud dependencies, yes. Terraform is well suited to provisioning the managed control plane (EKS, GKE, AKS), node pools, IAM and IRSA, networking, and cloud-side add-ons. For in-cluster workloads such as Deployments, Services, and Helm releases, a GitOps controller like Argo CD or Flux is the better fit because it reconciles continuously instead of on a pipeline trigger. The practical rule is Terraform to the cluster edge, GitOps inside it.

What is the difference between Terraform and GitOps for Kubernetes?

Terraform is a run-triggered, point-in-time tool: it plans and applies when a pipeline or person tells it to. GitOps controllers run a continuous control loop that watches Git and drives live cluster state back toward the declared state on a short interval. Kubernetes itself is built on that same continuous-reconciliation model, which is why GitOps tools sit naturally inside the cluster and Terraform sits naturally at the cloud boundary around it.

Can a TACO like Scalr, Spacelift, or env0 manage Kubernetes?

A TACO runs and governs your Terraform or OpenTofu, so it manages whatever Terraform manages. That makes it a strong fit for provisioning and governing the cluster and its cloud dependencies, with state, RBAC, policy, and audit. It is not an application-delivery tool, so it should not be the thing reconciling your in-cluster Deployments many times a day. Use the TACO for the infrastructure layer and a GitOps controller for the workload layer.

Why does Terraform keep showing drift on my Kubernetes resources?

Because other controllers are changing them. A HorizontalPodAutoscaler edits replica counts, operators mutate the resources they own, and mutating admission webhooks inject fields. Terraform sees those post-creation changes as drift and plans to revert them. You can suppress this with lifecycle.ignore_changes, but on a busy cluster you end up maintaining a growing list of exceptions, which is a sign the workload layer belongs to a different tool.

What is the recommended pattern for Terraform and GitOps together?

Provision the cluster and its dependencies with Terraform, use Terraform to install and bootstrap the GitOps controller, then let Argo CD or Flux own application delivery from Git. This keeps each tool inside the operating model it was designed for and avoids two control planes fighting over the same objects.
About the author
Sebastian StadilCEO at Scalr
Sebastian Stadil is the CEO of Scalr with 15+ years of DevOps experience. He started with AWS in 2004 and advised early Microsoft Azure and Google Cloud.