Amazon EKS in Production with Terraform›03 · The multi-provider chicken-and-egg
Learning Hub / Cloud — OpenStack, AWS & EKS / Amazon EKS in Production with Terraform

Lesson 03 of 18 · Build

The multi-provider chicken-and-egg

Terraform must create the cluster before it can talk to it. Solve the 'multi-provider chicken-and-egg': configure the Kubernetes and Helm providers from cluster outputs with exec authentication, and split stacks so plans never depend on a cluster that doesn't exist yet.

Advanced
Key wordskubernetes providerhelm providerexec authenticationaws eks get-tokenlayered stacksremote statetoken expiry

The problem

You want one terraform apply to create an EKS cluster and install things into it (the AWS Load Balancer Controller, Karpenter, namespaces). But the kubernetes and helm providers need the cluster's endpoint, CA and credentials at configuration time, before the cluster exists.

It's like writing a letter to a friend who's moving into a house that hasn't been built yet. You can't address the envelope until the house exists. The fix: build the house first, write the address down in a shared notebook, and only then post the letter.

Symptoms you'll see: plan errors about connecting to localhost, providers using the wrong cluster, or applies that fail after about 15 minutes with Unauthorized.

Fix 1: configure providers from outputs, with exec auth

provider "kubernetes" {
  host                   = module.eks.cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "aws"
    args        = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
  }
}

provider "helm" {
  kubernetes {
    host                   = module.eks.cluster_endpoint
    cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
    exec {
      api_version = "client.authentication.k8s.io/v1beta1"
      command     = "aws"
      args        = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
    }
  }
}
  • exec asks aws eks get-token for a token whenever the provider needs one, so long applies don't hit the ~15-minute expiry of a pre-fetched token.
  • The machine running Terraform needs the AWS CLI and credentials that map to cluster access (access entries, lesson 05).
  • (Newer major versions of the Helm provider changed the kubernetes block syntax; check the docs for the version you pin.)

This works in one stack when the cluster already exists. For the very first apply, or cluster replacement, it's still fragile.

live/prod/cluster/    → VPC refs, EKS, node groups, IAM, EKS add-ons      (aws provider only)
live/prod/platform/   → Helm releases, namespaces, CRDs, Karpenter config (kubernetes + helm providers)

The platform layer reads the cluster layer's outputs:

data "terraform_remote_state" "cluster" {
  backend = "s3"
  config = {
    bucket = "acme-tfstate-prod"
    key    = "eks/prod/cluster/terraform.tfstate"
    region = "eu-west-1"
  }
}

locals {
  cluster_name     = data.terraform_remote_state.cluster.outputs.cluster_name
  cluster_endpoint = data.terraform_remote_state.cluster.outputs.cluster_endpoint
  cluster_ca       = data.terraform_remote_state.cluster.outputs.cluster_certificate_authority_data
}

Apply the cluster layer, then the platform layer. Each has a smaller state, a clear owner and a clean plan.

Fix 3: hand in-cluster things to GitOps

Many teams let Terraform stop at "cluster + IAM + bootstrap Argo CD", and let GitOps install everything else (controllers, policies, apps) from Git (see GitOps with Argo CD). Terraform then never needs long-lived access inside the cluster.

What not to rely on

  • -target to bootstrap in two steps: fine in an emergency, but a sign the structure needs splitting.
  • depends_on between providers and modules: providers can't truly depend on resources; it hides the problem rather than solving it.

Try it: feel the problem, then fix it

  1. In a single stack, create an EKS cluster with the community terraform-aws-modules/eks/aws module and a helm_release (e.g. metrics-server) configured with the aws_eks_cluster_auth data source. Run the first plan and note what happens.
  2. Switch both providers to exec authentication; apply; confirm long operations don't fail on token expiry.
  3. Split into cluster/ and platform/ layers with remote state; move the Helm release to platform/ (use removed/import blocks or recreate it).
  4. Destroy the platform layer, then the cluster layer, in that order.

Going deeper: bootstrapping patterns

  • The identity running platform/ needs cluster access. Grant it with an access entry in the cluster layer (as code), never by editing aws-auth by hand.
  • Keep CRDs in their own step or layer (or let GitOps apply them first). Resources using a CRD fail to plan before the CRD exists.
  • Pin provider and module versions, and upgrade them deliberately. Provider upgrades can change behaviour across the whole stack.

Recap

  • Providers need the cluster before they can be configured: that's the chicken-and-egg.
  • Use exec authentication (aws eks get-token) to avoid token expiry during long applies.
  • Best structure: a cluster layer and a platform layer linked by remote state, or hand in-cluster work to GitOps.
  • Treat -target and provider depends_on as smells, not solutions.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.