Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 059 / 100

Kubernetes provider vs Helm provider in Terraform & IaC

Manage cluster resources from Terraform.

Expona Admin7 min read

Introduction

When you reach the point of deploying applications into Kubernetes clusters via infrastructure as code, you'll quickly face a choice: should you use the Kubernetes provider vs Helm provider in Terraform & IaC? Both tools sit between your infrastructure code and your running cluster, but they solve fundamentally different problems. Understanding when to reach for each—and when to step back entirely—keeps your infrastructure predictable, maintainable, and aligned with your team's workflows.

The Kubernetes provider: direct resource management

The Terraform Kubernetes provider lets you declare Kubernetes resources directly in your IaC. You're writing manifests as code, creating deployments, services, configmaps, and secrets without leaving HCL. It's straightforward and works well for simple, static resources that won't change often.

Consider a scenario where your ops team provisions a cluster with aws_eks_cluster and then immediately needs to deploy a basic nginx ingress controller and a couple of namespaces. Using the Kubernetes provider, you can do that in the same state file:

hcl

provider "kubernetes" {
  host                   = aws_eks_cluster.main.endpoint
  cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
  token                  = data.aws_eks_auth.main.token
}

resource "kubernetes_namespace" "apps" {
  metadata {
    name = "apps"
  }
}

resource "kubernetes_deployment" "nginx" {
  metadata {
    name      = "nginx"
    namespace = kubernetes_namespace.apps.metadata[0].name
  }

  spec {
    replicas = 2

    selector {
      match_labels = {
        app = "nginx"
      }
    }

    template {
      metadata {
        labels = {
          app = "nginx"
        }
      }

      spec {
        container {
          image = "nginx:1.21"
          name  = "nginx"

          port {
            container_port = 80
          }
        }
      }
    }
  }
}

This approach gives you infrastructure-as-code governance over cluster internals. You can review changes in pull requests, track drift detection, and roll back if needed. The provider is mature, well-documented, and requires no additional tooling beyond Terraform itself.

However, the Kubernetes provider struggles with complexity. Large manifests become verbose in HCL. Managing hundreds of resources, complex templating logic, or resources that need frequent updates gets unwieldy fast. The provider also doesn't handle the conceptual grouping and packaging that teams expect from application deployments.

The Helm provider: packaged applications

Helm is Kubernetes' package manager. A Helm chart bundles together related resources—deployments, services, configmaps, rbac roles—with templating and sensible defaults. The Terraform Helm provider lets you install and manage charts without writing raw Kubernetes manifests.

Helm shines when you're deploying third-party applications or your own packaged services. Instead of maintaining dozens of individual Kubernetes resources, you install a chart and override values. For example, deploying a PostgreSQL database via the Bitnami chart:

hcl

provider "helm" {
  kubernetes {
    host                   = aws_eks_cluster.main.endpoint
    cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
    token                  = data.aws_eks_auth.main.token
  }
}

resource "helm_release" "postgres" {
  name       = "postgres"
  repository = "https://charts.bitnami.com/bitnami"
  chart      = "postgresql"
  version    = "12.0.0"
  namespace  = "databases"

  values = [
    yamlencode({
      auth = {
        username = "appuser"
        password = random_password.db_password.result
      }
      primary = {
        persistence = {
          size = "20Gi"
        }
      }
    })
  ]
}

Helm charts abstract complexity. You get a well-tested package with security best practices, resource limits, and monitoring hooks baked in. Overrides are cleaner than managing individual resources. Many teams use charts for infrastructure components—prometheus, grafana, cert-manager, external-dns—rather than maintaining their own manifests.

The tradeoff: Helm provider releases are opaque to Terraform's state. You can't easily see or drift-detect individual resources inside a release. If a chart is too rigid or poorly maintained, you're stuck working around its assumptions. And Helm charts still require understanding their structure to customize them effectively.

Keep cluster creation and applications separate

Here's the architectural principle that prevents pain: don't mix cluster provisioning with application deployment in the same Terraform state.

Cluster infrastructure—the EKS cluster, node groups, vpc, subnets, security groups—changes infrequently and has high blast radius if something breaks. Application deployments change weekly or daily and should be independent of cluster state changes. Mixing them means every app deploy risks touching cluster networking, every cluster upgrade risks triggering app redeployments.

Use one state file for cluster infrastructure:

hcl

# infrastructure/main.tf
provider "aws" {
  region = var.region
}

resource "aws_eks_cluster" "main" {
  name            = "prod-cluster"
  role_arn        = aws_iam_role.eks_cluster.arn
  vpc_config {
    subnet_ids = aws_subnet.main[*].id
  }
}

resource "aws_eks_node_group" "main" {
  cluster_name    = aws_eks_cluster.main.name
  node_role_arn   = aws_iam_role.node.arn
  subnet_ids      = aws_subnet.main[*].id
  desired_size    = 3
  min_size        = 2
  max_size        = 10
  instance_types  = ["t3.large"]
}

output "cluster_endpoint" {
  value = aws_eks_cluster.main.endpoint
}

output "cluster_ca" {
  value = aws_eks_cluster.main.certificate_authority[0].data
}

And a separate state for cluster add-ons and core services (if using Terraform at all):

hcl

# cluster-addons/main.tf
data "aws_eks_cluster_auth" "main" {
  name = var.cluster_name
}

provider "kubernetes" {
  host                   = var.cluster_endpoint
  cluster_ca_certificate = base64decode(var.cluster_ca)
  token                  = data.aws_eks_cluster_auth.main.token
}

provider "helm" {
  kubernetes {
    host                   = var.cluster_endpoint
    cluster_ca_certificate = base64decode(var.cluster_ca)
    token                  = data.aws_eks_cluster_auth.main.token
  }
}

# Essential cluster services only: ingress, cert-manager, monitoring
resource "helm_release" "ingress_nginx" {
  name       = "ingress-nginx"
  repository = "https://kubernetes.github.io/ingress-nginx"
  chart      = "ingress-nginx"
  version    = "4.8.0"
  namespace  = "ingress-nginx"
}

This separation lets infrastructure and platform teams own cluster lifecycle independently from application teams.

Prefer GitOps tools for application workloads

For applications themselves—your microservices, APIs, web frontends—neither the Kubernetes nor Helm provider should be your primary deployment mechanism. This is where GitOps tools like ArgoCD, Flux, or Helm (used directly, not through Terraform) excel.

Here's why: applications change frequently. Your CI/CD pipeline rebuilds images multiple times a day. GitOps tools watch a Git repository for changes and automatically sync the cluster to match. This decouples deployment from infrastructure provisioning, gives you a clear audit trail in Git, and lets application teams move independently of infrastructure Terraform cycles.

The right pattern: use Terraform to provision the cluster and bootstrap GitOps, then let GitOps manage apps:

hcl

# cluster-addons/main.tf - Bootstrap ArgoCD
resource "helm_release" "argocd" {
  name       = "argocd"
  repository = "https://argoproj.github.io/argo-helm"
  chart      = "argo-cd"
  version    = "5.40.0"
  namespace  = "argocd"

  values = [
    yamlencode({
      server = {
        service = {
          type = "LoadBalancer"
        }
      }
    })
  ]
}

# Point ArgoCD at your application Git repository
# Then your CI/CD updates manifests in Git
# ArgoCD automatically syncs them to the cluster

Your application team commits Helm charts or Kubernetes manifests to Git. ArgoCD watches that repository and keeps the cluster in sync. If something goes wrong, you roll back a Git commit, not a Terraform state. This is how mature teams separate concerns and scale.

Common mistakes

  • Managing all cluster resources in one state: Combining cluster infrastructure with add-ons and applications means you can't safely update infrastructure without risking app downtime.
  • Using Kubernetes provider for frequently-changing apps: If your app gets redeployed daily via CI/CD, Terraform state will constantly drift. You'll spend time fighting Terraform instead of shipping features.
  • Over-customizing Helm charts with Terraform: If you're heavily overriding chart values to the point they're unrecognizable, you should maintain your own manifests instead. It's clearer and more maintainable.
  • Ignoring authentication token expiry: Kubernetes provider auth tokens (especially short-lived ones from cloud providers) can expire mid-apply. Use data sources for tokens, not static values.
  • Deploying without version pinning: Always pin Helm chart versions. Letting Helm auto-upgrade means your cluster can change in unexpected ways outside your control.

Key takeaways

  • Use the Kubernetes provider for simple, static cluster resources that rarely change—namespaces, rbac roles, basic configmaps.
  • Use the Helm provider for packaged third-party applications and infrastructure components with predictable, templated deployments.
  • Keep cluster infrastructure in a separate Terraform state from add-ons and applications. Separate concerns reduce blast radius and allow independent update cycles.
  • For application workloads that change frequently, prefer GitOps tools (ArgoCD, Flux) over Terraform. It's the industry standard for a reason.
  • Always pin versions—cluster versions, chart versions, provider versions. Drift detection is only useful if you control what can drift.
  • Remember: infrastructure-as-code is powerful, but it's not the right tool for every problem. Know when to step back and use the right tool for the job.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles