
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.
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.
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.
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.
