Terraform ile Kubernetes Cluster Kurulumu: Adım Adım Rehber
Kubernetes cluster yönetimi söz konusu olduğunda, elle yapılan kurulumların ne kadar can sıkıcı olduğunu hepimiz biliriz. Bir cluster’ı kurar, bir şeyi yanlış yapılandırırsın, sonra her şeyi silip baştan başlarsın. Terraform devreye girdiğinde bu döngü kırılıyor ve altyapın kod haline geliyor. Bu yazıda AWS üzerinde EKS (Elastic Kubernetes Service) kullanarak gerçek dünya senaryolarına uygun bir Kubernetes cluster kurulumunu Terraform ile adım adım gerçekleştireceğiz.
Ön Koşullar ve Ortam Hazırlığı
Başlamadan önce bazı araçların kurulu olması gerekiyor. Bu rehberde AWS EKS kullanacağız, ancak aynı mantık GKE veya AKS için de geçerli.
Sisteminizde şunların kurulu olduğundan emin olun:
- Terraform 1.5+:
terraform --versionile kontrol et - AWS CLI v2: Kimlik bilgilerinin yapılandırılmış olması şart
- kubectl: Cluster’a bağlanmak için
- helm: İsteğe bağlı ama işe yarıyor
AWS kimlik bilgilerini ayarlamak için:
aws configure
# AWS Access Key ID: [your-key]
# AWS Secret Access Key: [your-secret]
# Default region name: eu-west-1
# Default output format: json
# Kimlik doğrulamayı test et
aws sts get-caller-identity
Proje dizin yapısını organize tutmak önemli. Özellikle birden fazla ortam (dev, staging, prod) yönetiyorsan modüler bir yapı seni kurtarır.
mkdir -p kubernetes-terraform/{modules/{vpc,eks,node-groups},environments/{dev,prod}}
cd kubernetes-terraform
# Dizin yapısı:
# .
# ├── modules/
# │ ├── vpc/
# │ ├── eks/
# │ └── node-groups/
# └── environments/
# ├── dev/
# └── prod/
Terraform Backend Yapılandırması
State dosyasını yerel tutmak küçük projeler için işe yarasa da ekip ortamında felaket reçetesidir. S3 backend kullanmak zorundasın.
# Önce S3 bucket ve DynamoDB tablosu oluştur
aws s3api create-bucket
--bucket my-terraform-state-k8s
--region eu-west-1
--create-bucket-configuration LocationConstraint=eu-west-1
aws s3api put-bucket-versioning
--bucket my-terraform-state-k8s
--versioning-configuration Status=Enabled
aws dynamodb create-table
--table-name terraform-state-lock
--attribute-definitions AttributeName=LockID,AttributeType=S
--key-schema AttributeName=LockID,KeyType=HASH
--billing-mode PAY_PER_REQUEST
--region eu-west-1
environments/dev/backend.tf dosyası:
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state-k8s"
key = "dev/eks/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.23"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = "kubernetes-cluster"
}
}
}
VPC Modülü
EKS için özel bir VPC kurman gerekiyor. Mevcut VPC’yi kullanmak cazip gelir ama izolasyon açısından yeni bir VPC önerilir.
modules/vpc/main.tf:
# modules/vpc/main.tf
locals {
azs = slice(data.aws_availability_zones.available.names, 0, 3)
}
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.cluster_name}-vpc"
# EKS için zorunlu tag
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
}
}
resource "aws_subnet" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
availability_zone = local.azs[count.index]
tags = {
Name = "${var.cluster_name}-private-${local.azs[count.index]}"
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
"kubernetes.io/role/internal-elb" = "1"
}
}
resource "aws_subnet" "public" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index + 3)
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.cluster_name}-public-${local.azs[count.index]}"
"kubernetes.io/cluster/${var.cluster_name}" = "shared"
"kubernetes.io/role/elb" = "1"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.cluster_name}-igw"
}
}
resource "aws_eip" "nat" {
count = length(local.azs)
domain = "vpc"
tags = {
Name = "${var.cluster_name}-nat-eip-${count.index}"
}
}
resource "aws_nat_gateway" "main" {
count = length(local.azs)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.cluster_name}-nat-${count.index}"
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "private" {
count = length(local.azs)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
tags = {
Name = "${var.cluster_name}-private-rt-${count.index}"
}
}
resource "aws_route_table_association" "private" {
count = length(local.azs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
VPC Değişkenleri
modules/vpc/variables.tf:
variable "cluster_name" {
description = "EKS cluster adi"
type = string
}
variable "vpc_cidr" {
description = "VPC CIDR blogu"
type = string
default = "10.0.0.0/16"
}
variable "environment" {
description = "Ortam adi (dev, staging, prod)"
type = string
}
EKS Cluster Modülü
Asıl iş burada başlıyor. EKS modülü hem control plane’i hem de node group’ları yönetiyor.
modules/eks/main.tf:
# modules/eks/main.tf
data "aws_iam_policy_document" "eks_cluster_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["eks.amazonaws.com"]
}
}
}
resource "aws_iam_role" "eks_cluster" {
name = "${var.cluster_name}-cluster-role"
assume_role_policy = data.aws_iam_policy_document.eks_cluster_assume_role.json
}
resource "aws_iam_role_policy_attachment" "eks_cluster_policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
role = aws_iam_role.eks_cluster.name
}
resource "aws_security_group" "eks_cluster" {
name = "${var.cluster_name}-cluster-sg"
description = "EKS cluster security group"
vpc_id = var.vpc_id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.cluster_name}-cluster-sg"
}
}
resource "aws_eks_cluster" "main" {
name = var.cluster_name
version = var.kubernetes_version
role_arn = aws_iam_role.eks_cluster.arn
vpc_config {
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.eks_cluster.id]
endpoint_private_access = true
endpoint_public_access = var.public_access
public_access_cidrs = var.public_access_cidrs
}
enabled_cluster_log_types = [
"api",
"audit",
"authenticator",
"controllerManager",
"scheduler"
]
encryption_config {
provider {
key_arn = aws_kms_key.eks.arn
}
resources = ["secrets"]
}
depends_on = [
aws_iam_role_policy_attachment.eks_cluster_policy
]
tags = {
Name = var.cluster_name
}
}
resource "aws_kms_key" "eks" {
description = "${var.cluster_name} EKS Secret Encryption Key"
deletion_window_in_days = 7
enable_key_rotation = true
tags = {
Name = "${var.cluster_name}-eks-key"
}
}
# Node group IAM role
resource "aws_iam_role" "eks_nodes" {
name = "${var.cluster_name}-node-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}]
})
}
resource "aws_iam_role_policy_attachment" "eks_worker_node_policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
role = aws_iam_role.eks_nodes.name
}
resource "aws_iam_role_policy_attachment" "eks_cni_policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
role = aws_iam_role.eks_nodes.name
}
resource "aws_iam_role_policy_attachment" "eks_container_registry" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
role = aws_iam_role.eks_nodes.name
}
resource "aws_eks_node_group" "main" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "${var.cluster_name}-${each.key}"
node_role_arn = aws_iam_role.eks_nodes.arn
subnet_ids = var.private_subnet_ids
for_each = var.node_groups
instance_types = each.value.instance_types
capacity_type = each.value.capacity_type
scaling_config {
desired_size = each.value.desired_size
min_size = each.value.min_size
max_size = each.value.max_size
}
update_config {
max_unavailable = 1
}
labels = each.value.labels
dynamic "taint" {
for_each = lookup(each.value, "taints", [])
content {
key = taint.value.key
value = taint.value.value
effect = taint.value.effect
}
}
depends_on = [
aws_iam_role_policy_attachment.eks_worker_node_policy,
aws_iam_role_policy_attachment.eks_cni_policy,
aws_iam_role_policy_attachment.eks_container_registry,
]
lifecycle {
ignore_changes = [scaling_config[0].desired_size]
}
}
Ortam Yapılandırması
Modülleri hazırladıktan sonra dev ortamı için ana konfigürasyonu oluşturuyoruz.
environments/dev/main.tf:
# environments/dev/main.tf
locals {
cluster_name = "my-eks-${var.environment}"
}
module "vpc" {
source = "../../modules/vpc"
cluster_name = local.cluster_name
vpc_cidr = "10.0.0.0/16"
environment = var.environment
}
module "eks" {
source = "../../modules/eks"
cluster_name = local.cluster_name
kubernetes_version = "1.28"
vpc_id = module.vpc.vpc_id
private_subnet_ids = module.vpc.private_subnet_ids
public_access = true
public_access_cidrs = ["0.0.0.0/0"] # Prod'da kısıtla!
node_groups = {
general = {
instance_types = ["t3.medium"]
capacity_type = "ON_DEMAND"
desired_size = 2
min_size = 1
max_size = 5
labels = {
role = "general"
}
taints = []
}
spot = {
instance_types = ["t3.large", "t3a.large", "m5.large"]
capacity_type = "SPOT"
desired_size = 1
min_size = 0
max_size = 10
labels = {
role = "spot"
workload = "batch"
}
taints = [{
key = "spot"
value = "true"
effect = "NO_SCHEDULE"
}]
}
}
}
# Cluster'a kubectl erişimi için kubeconfig güncelle
resource "null_resource" "update_kubeconfig" {
triggers = {
cluster_name = module.eks.cluster_name
}
provisioner "local-exec" {
command = "aws eks update-kubeconfig --name ${module.eks.cluster_name} --region ${var.aws_region}"
}
depends_on = [module.eks]
}
environments/dev/variables.tf:
variable "aws_region" {
description = "AWS bolge"
type = string
default = "eu-west-1"
}
variable "environment" {
description = "Ortam adi"
type = string
default = "dev"
}
Cluster’ı Ayağa Kaldırma
Terraform kodları hazır olduğunda sıra uygulamaya geliyor. Bu adımları sırayla takip et.
cd environments/dev
# Terraform'u başlat
terraform init
# Neyin oluşturulacağını gör (her zaman önce bunu yap!)
terraform plan -out=tfplan
# Plan çıktısını incele, sonra uygula
terraform apply tfplan
# Yaklaşık 15-20 dakika sürer
# Bitmesini bekle ve çıktıları kontrol et
terraform output
# kubectl'in doğru cluster'a bağlı olduğunu doğrula
kubectl get nodes
kubectl get pods -A
# Node'ların durumunu kontrol et
kubectl get nodes -o wide
# Cluster bilgilerini görüntüle
kubectl cluster-info
Add-on’lar ve Cluster Tamamlama
Bare minimum EKS cluster çalışıyor ama production’a hazır değil. AWS Load Balancer Controller ve Cluster Autoscaler olmadan kullanışlı değil.
# Helm repo ekle
helm repo add eks https://aws.github.io/eks-charts
helm repo add autoscaler https://kubernetes.github.io/autoscaler
helm repo update
# AWS Load Balancer Controller kur
helm install aws-load-balancer-controller eks/aws-load-balancer-controller
-n kube-system
--set clusterName=my-eks-dev
--set serviceAccount.create=true
--set serviceAccount.name=aws-load-balancer-controller
# Cluster Autoscaler kur
helm install cluster-autoscaler autoscaler/cluster-autoscaler
--namespace kube-system
--set autoDiscovery.clusterName=my-eks-dev
--set awsRegion=eu-west-1
--set rbac.serviceAccount.create=true
# Metrics server kur (HPA için şart)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# Kurulumları doğrula
kubectl get deployment -n kube-system aws-load-balancer-controller
kubectl get deployment -n kube-system cluster-autoscaler
kubectl top nodes
Gerçek Dünya Senaryosu: Mavi-Yeşil Node Güncellemesi
Production’da node group’u güncellemek riskli bir operasyondur. Terraform ile bu işlemi kontrollü yapabilirsin.
# Mevcut node group'u taint ile işaretle
kubectl taint nodes -l role=general
maintenance=true:NoSchedule
# Terraform'da yeni node group ekle (main.tf'i güncelle)
# Ardından plan ve apply yap
terraform plan -target=module.eks.aws_eks_node_group.main
terraform apply -target=module.eks.aws_eks_node_group.main
# Eski node'ları drain et
for node in $(kubectl get nodes -l role=general --no-headers | awk '{print $1}'); do
kubectl drain $node
--ignore-daemonsets
--delete-emptydir-data
--force
done
# Eski node group'u Terraform'dan kaldır
# variables.tf'den eski node group'u sil
terraform apply
# Node'ların yeni gruba taşındığını doğrula
kubectl get nodes -o wide
kubectl get pods -A -o wide | grep -v Running
Ortamlar Arası Tutarlılık
Dev, staging ve prod arasında yapılandırma farklarını yönetmek için tfvars dosyalarını kullan.
# environments/prod/terraform.tfvars
aws_region = "eu-west-1"
environment = "prod"
# Bu değerleri prod için override et
# modules/eks içindeki node_groups prod değerleri:
# general:
# instance_types: ["m5.xlarge", "m5.2xlarge"]
# capacity_type: ON_DEMAND
# desired_size: 3
# min_size: 2
# max_size: 20
# spot:
# instance_types: ["m5.2xlarge", "m5.4xlarge"]
# capacity_type: SPOT
# desired_size: 5
# min_size: 2
# max_size: 50
# Prod planını ayrı workspace'de çalıştır
terraform workspace new prod
terraform workspace select prod
terraform plan -var-file="terraform.tfvars" -out=prod-tfplan
# Kritik: prod apply öncesi mutlaka review et
terraform show prod-tfplan | less
terraform apply prod-tfplan
Yaygın Hatalar ve Çözümleri
Terraform ile EKS kurarken sık karşılaşılan sorunlar şunlar:
- IAM yetki hataları:
aws sts get-caller-identityçalışıyorsa ama Terraform hata veriyorsa, kullanıcının EKS ve VPC izinlerini kontrol et.AdministratorAccessgeliştirme ortamında geçici çözüm, prod’da asgari yetki prensibini uygula.
- Subnet tag eksikliği: EKS load balancer’ların hangi subnet’e yerleşeceğini bu tag’lerle anlıyor.
kubernetes.io/role/elbvekubernetes.io/role/internal-elbtag’lerini subnet’lere eklemeyi unutma.
- Node’lar NotReady kalıyor: Genellikle CNI sorunu.
kubectl describe nodeile olayları incele. AWS VPC CNI plugin’inin düzgün çalışıp çalışmadığınıkubectl get pods -n kube-system | grep aws-nodeile kontrol et.
- State lock sorunu: Ekipten biri Terraform çalıştırırken başkası da apply yapmaya çalışırsa DynamoDB lock devreye girer.
terraform force-unlockile çözebilirsin ama dikkatli ol.
for_eachile node group sorunları: Node group map’te değişiklik yaparken Terraform tüm node group’u silip yeniden oluşturmak isteyebilir.lifecycle { prevent_destroy = true }ile bunu engelle ve manuel müdahale et.
- KMS key silme:
terraform destroyyaptığında KMS key’ler 7-30 gün arası bekleme süresine giriyor. Bu normal davranış,deletion_window_in_daysdeğerini düşürebilirsin ama 7 günün altına inemezsin.
Maliyet Optimizasyonu
Spot instance kullanımı maliyeti ciddi düşürür ama pod’ların ani kapanmaya hazır olması gerekir.
# Spot instance kesmelerini simüle et (test ortamında)
# aws-node-termination-handler kur
helm repo add eks https://aws.github.io/eks-charts
helm install aws-node-termination-handler
eks/aws-node-termination-handler
--namespace kube-system
--set enableSpotInterruptionDraining=true
--set enableRebalanceMonitoring=true
--set enableScheduledEventDraining=true
# Maliyet anomalisi için CloudWatch alarm oluştur
aws budgets create-budget
--account-id $(aws sts get-caller-identity --query Account --output text)
--budget file://budget.json
--notifications-with-subscribers file://notifications.json
Sonuç
Terraform ile EKS kurulumu başta karmaşık görünüyor, özellikle VPC, IAM ve node group konfigürasyonlarının birbirine bağımlılıkları nedeniyle. Ama modüler yapıya geçtiğinde her şey yerine oturuyor. Bu yazıda anlattığım yaklaşımın en büyük avantajı tekrar kullanılabilirlik; aynı modülleri dev, staging ve prod için birkaç değişkenle farklı konfigürasyonlarda kullanabiliyorsun.
Üretim ortamında dikkat etmen gereken en kritik noktalar şunlar: state dosyasını S3’te şifreli tut, public API endpoint erişimini kısıtla, node group’larını multi-AZ dağıt ve cluster autoscaler’ı mutlaka kur. KMS ile secret şifreleme ve CloudWatch log’larını etkinleştirmek de production checklist’inin vazgeçilmez parçaları.
Bir sonraki adım olarak ArgoCD veya Flux gibi GitOps araçlarını bu altyapının üzerine kurabilirsin. Terraform altyapıyı kuruyor, GitOps ise uygulama deployment’larını yönetiyor. İkisi birlikte tam anlamıyla code olarak altyapı ve uygulama yönetimi sağlıyor.
