Production EKS için Terraform Modül Desenleri

2026-05-12 10 min

EKS için gördüğüm çoğu Terraform kodu tek bir küme için çalışır ve ikinci ortam ortaya çıktığı an dağılır. Aşağıdaki desenler Turna'da ve daha önceki projelerde birden fazla production kümesinde yakınsadığım şeyler — soyut "best practice"ler değil, gerçek ekiplerde kira ödeyen seçimler.

Modül sınırları sahipliği takip eder, AWS servislerini değil

İlk dürtü, her AWS servisi için bir modül yapmaktır: modules/vpc, modules/eks, modules/rds. Düzenli görünür, kötü yaşlanır. Gerçek sahiplik sınırları daha kaba. Bir platform ekibi "networking"i (VPC + subnet + endpoint + NAT + flow log) bölünmez bir bütün olarak sahiplenir. Bir parçayı diğerleri düşünmeden neredeyse hiç değiştirmezler.

Modülleri o sahipliğe göre yapılandırıyorum:

modules/
├── platform-network/     # VPC, subnet, endpoint, NAT, flow log
├── platform-cluster/     # EKS, addon'lar, IRSA, varsayılan node group'lar
├── platform-observability/ # Prometheus stack, Loki, alerting
└── app-database/         # RDS instance'ları, parameter group, IAM

Her modülün 5–15 input'u ve düz bir output yüzeyi var. Bir modül 20'den fazla input istiyorsa çok iş yapıyordur.

Stack başına değil, ortam başına Terraform workspace

Terraform workspace'leri cazip, çünkü bedava bir ortam ayar düğmesi gibi görünüyorlar. Prod'u dev'den ayırmak için kullanıldığında tuzaktırlar, çünkü aynı backend prefix'ini paylaşırlar ve tek bir hatalı workspace select, yanlış ortamı yok edebilir.

Bunun yerine kullandığım: ortam başına ayrı dizinler, her birinin kendi backend config'i ve kendi state dosyası.

envs/
├── dev/
│   ├── main.tf          # platform-* modüllerini dev-boyutlu input'larla çağırır
│   ├── backend.tf       # S3 key: env/dev/terraform.tfstate
│   └── terraform.tfvars
├── stage/
└── prod/

Workspace'ler kısa ömürlü stack varyantları için (bir feature branch'in preview kümesi, bir load test stack'i) hâlâ yararlı. Prod vs dev için ayrı dizin, ayrı backend.

Değişkenler: required, optional, derived

Onlarca saat kazandıran bir desen: değişkenleri niyete göre üç kovaya ayırın.

  • Required input'ların default'u yoktur. Çağıran kişi unutursa plan yüksek sesle başarısız olur. Örnek: cluster_name, vpc_cidr.
  • Optional input'ların çağıranların %80'i için işe yarayan makul bir default'u vardır. Örnek: node_group_instance_types = ["t3.large"].
  • Derived değerler variable değil, locals'tır. Örnek: locals { subnet_count = length(var.availability_zones) }.

Güvenlik-kritik düğmeler için asla optional kullanmam. Cluster logging'i açmak önemliyse, required yapın.

Remote state, state pas etmek değil

Eninde sonunda bir stack'ten (network ID, security group) diğerine (cluster, app) değer geçirmeniz gerekir. İki yol var:

  1. Data source yolu: data "terraform_remote_state" "network". Başka bir stack'in state'inin içine uzanır.
  2. Output-and-pass yolu: değeri SSM Parameter Store veya AWS Secrets Manager'a yazın, consumer'dan okuyun.

Varsayılan olarak ikincisini seçiyorum. Başka bir stack'in state'ine uzanmak modüllerinizi sıkıca eşler ve producer output'larını yeniden düzenlerse kırılır. SSM, kontrol ettiğiniz stabil bir kontrattır ve Terraform dışı araçlar için de çalışır.

resource "aws_ssm_parameter" "vpc_id" {
  name  = "/platform/network/${var.env}/vpc_id"
  type  = "String"
  value = aws_vpc.main.id
}

Consumer tarafında küçük bir data block onu çeker. State eşleşmesi yok.

Provider block kök'e ait, modüle değil

Kendi provider block'unu (region, profile, assume-role ile) tanımlayan bir modül, modülü iki farklı provider ile çağırana kadar kullanışlı görünür — sonra Terraform reddeder. Çözüm: required_providers'ı modülde tutun (tolere ettiği version aralığını bildirmek için) ama gerçek provider block'unu yalnızca kök yapılandırmaya koyun.

# modül içinde
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# kökte
provider "aws" {
  region = var.region
}

Modül versiyonlarını kilitleyin

Bir modülü tag olmadan Git referansıyla çağırmak, bir sonraki terraform init'in farklı bir commit çekebileceği anlamına gelir. Her zaman bir tag'e pin'leyin:

module "cluster" {
  source = "git::https://github.com/org/tf-modules.git//platform-cluster?ref=v1.4.2"
  # ...
}

Renovate veya Dependabot bu tag'leri bump etmek için PR açabilir. PR audit izidir.

Artık kullanmadıklarım

  • Tek dizinde ortam başına tfvars dosyaları. İlk gün ayrı dizinlerden kolay, 100. gün çok daha kötü. Erkenden geçin.
  • Yinelemeli modül iç içeliği. Başka bir modülü çağıran başka bir modülü çağıran bir modül, hiçbir insanın takip edemediği iz üretir. Maksimum iki seviye.
  • terraform-aws-modules EKS modülünün default'larını override etmeden kullanmak. Mükemmel başlangıç noktası; ilk seferinde kaynağı okumak zorunlu.
  • Ortam toggle'ı için count kullanmak. count = var.env == "prod" ? 1 : 0, adresleri değişkene bağlı olan ve değiştiğinde yeniden yaratılan kaynaklar üretir. Modül veya main.tf'te branch kullanın.

Tüm bunlarda değişmeyen ne

Gerçek EKS cluster config'i çoğunlukla sıkıcı. İki node group (system + workload), IRSA açık, CloudWatch logging açık, KMS şifreli secret'lar, private API endpoint. İlginç kod cluster'ın etrafındaki her şey — IAM policy'leri, addon orkestrasyonu, app başına IRSA role'leri. Terraform değerini orada kazanır.