Bu yazıda EKS cluster, Istanbul worker node group, Karpenter ile dinamik scaling, EBS CSI driver ve ALB Controller kurulumunu gerçek kod örnekleriyle anlatıyorum.
Control Plane Kısıtlaması
EKS control plane Local Zone subnet'lerini kabul etmez ve minimum 2 farklı standart AZ gerektirir. slice() ile ilk iki subnet seçilir:
# env/hepapi/eu-central-1/prod/eks/eks/terragrunt.hcl
terraform {
source = "tfr:///terraform-aws-modules/eks/aws//?version=${local.env_vars.locals.module_versions.eks}"
# apply sonrası kubeconfig otomatik güncellenir
after_hook "after_hook" {
commands = ["apply"]
execute = [
"aws", "eks", "update-kubeconfig",
"--region", "${include.root.locals.region}",
"--name", "${local.env_vars.locals.eks.cluster_name}",
"--profile", "${local.env_vars.locals.aws_profile}"
]
}
}
inputs = {
cluster_name = local.env_vars.locals.eks.cluster_name
cluster_version = "1.35"
vpc_id = dependency.vpc.outputs.vpc_id
# KRITIK: Local Zone control plane'de çalışmaz
# Sadece index 0 (eu-central-1a) ve 1 (eu-central-1b)
subnet_ids = slice(dependency.vpc.outputs.private_subnets, 0, 2)
authentication_mode = "API"
cluster_endpoint_private_access = true
cluster_endpoint_public_access = true
cluster_service_ipv4_cidr = "10.240.0.0/16"
enable_irsa = true
cluster_addons = {
coredns = {
addon_version = "v1.13.2-eksbuild.3"
}
eks-pod-identity-agent = {}
vpc-cni = {
addon_version = "v1.21.1-eksbuild.1"
configuration_values = jsonencode({
env = {
# Prefix delegation: node başına daha fazla pod IP'si
ENABLE_PREFIX_DELEGATION = "true"
WARM_PREFIX_TARGET = "1"
}
})
}
}
# Karpenter node discovery için security group tag'i
node_security_group_tags = {
"karpenter.sh/discovery/${local.env_vars.locals.eks.cluster_name}" = local.env_vars.locals.eks.cluster_name
}
}
SSO ile EKS Erişimi
EKS access entry için IAM user ARN değil, SSO permission set role ARN kullanın. Bu yaklaşımla o permission set'e sahip tüm kullanıcılar tek tanımla admin yetkisi alır ve her kullanıcı için ayrı access entry yazmaya gerek kalmaz.
SSO role ARN'ını bulun:
aws iam list-roles --profile hepapi-sso \
--query 'Roles[?contains(RoleName, `AWSReservedSSO`)].{Name:RoleName,Arn:Arn}' \
--output table
# env.hcl — access_entries
access_entries = {
hepapi = {
# Tüm SSO AdministratorAccess kullanıcıları bu role ile gelir
principal_arn = "arn:aws:iam::xxxxxxxxxxxxx:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_AdministratorAccess_xxxxxxxxxxxxxxxx"
policy_associations = {
policy = {
policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
access_scope = { type = "cluster" }
}
}
}
}
Node group yalnızca private_subnets[2] (Istanbul) subnet'ine kurulur:
# env/hepapi/eu-central-1/prod/eks/node-group/terragrunt.hcl
inputs = {
name = "hepapi-istanbul-node-group"
cluster_name = dependency.eks.outputs.cluster_name
cluster_version = dependency.eks.outputs.cluster_version
# Sadece Istanbul Local Zone subnet (index 2)
subnet_ids = slice(dependency.vpc.outputs.private_subnets, 2, 3)
instance_types = ["m7i.xlarge"]
capacity_type = "ON_DEMAND" # Local Zone'da Spot desteklenmez
min_size = 1
max_size = 2
desired_size = 1
block_device_mappings = {
xvda = {
device_name = "/dev/xvda"
ebs = {
volume_size = 80
volume_type = "gp3"
}
}
}
# SSM ile SSH yerine güvenli node erişimi
iam_role_additional_policies = {
AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
# Workload scheduling için node label'ları
# Bu label'lar Deployment nodeSelector'da kullanılır
labels = {
topology = "local-zone"
zone = "istanbul"
}
}
Node'un Istanbul'da çalıştığını doğrulayın:
kubectl get nodes -o custom-columns='NAME:.metadata.name,ZONE:.metadata.labels.topology\.kubernetes\.io/zone'
Static node group yetmediğinde Karpenter devreye girer. Istanbul için özel yapılandırma gerektiriyor - yanlış yapılandırılırsa Karpenter Frankfurt'ta node açar, Istanbul'da değil.
Karpenter IAM Modülü
# env/hepapi/eu-central-1/prod/eks/karpenter/karpenter-module/terragrunt.hcl
terraform {
source = "tfr:///terraform-aws-modules/eks/aws//modules/karpenter//?version=20.33.1"
}
inputs = {
cluster_name = dependency.eks.outputs.cluster_name
enable_v1_permissions = true
enable_irsa = true
enable_pod_identity = true
create_pod_identity_association = true
node_iam_role_additional_policies = {
AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
}
Karpenter Helm Chart
# env/hepapi/eu-central-1/prod/eks/karpenter/karpenter-controller-helm/terragrunt.hcl
inputs = {
name = "karpenter"
namespace = "kube-system"
chart_version = "1.1.2"
helm_repo_url = "oci://public.ecr.aws/karpenter"
sets = [
{ name = "serviceAccount.name",
value = dependency.karpenter-module.outputs.service_account },
{ name = "serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn",
value = dependency.karpenter-module.outputs.iam_role_arn },
{ name = "settings.interruptionQueue",
value = dependency.karpenter-module.outputs.queue_name },
{ name = "settings.clusterName",
value = dependency.eks.outputs.cluster_name },
{ name = "settings.clusterEndpoint",
value = dependency.eks.outputs.cluster_endpoint },
{ name = "replicas", value = "1" }
]
}
NodePool - Istanbul'a Özgü Yapılandırma
Bu NodePool tanımında iki kritik requirement var:
# modules/karpenter/karpenter_node_pool.tf
resource "kubectl_manifest" "karpenter_node_pool" {
yaml_body = <<-YAML
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: hepapi-istanbul
labels:
capacity-type: on-demand
spec:
template:
metadata:
labels:
capacity-type: on-demand
topology: local-zone
zone: istanbul
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: "karpenter.k8s.aws/instance-category"
operator: In
values: ["c", "m", "r"]
- key: "karpenter.k8s.aws/instance-generation"
operator: Gt
values: ["6"] # 7. nesil ve üzeri: c7i, m7i, r7i
- key: "kubernetes.io/arch"
operator: In
values: ["amd64"]
- key: "karpenter.sh/capacity-type"
operator: In
values: ["on-demand"] # Spot yok!
- key: "topology.kubernetes.io/zone"
operator: In
values: ["eu-central-1-ist-1a"] # Sadece Istanbul
limits:
cpu: 50
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 60s
YAML
}
Karpenter'ın doğru çalıştığını doğrulayın:
# Karpenter pod'u çalışıyor mu?
kubectl get pods -n kube-system -l app.kubernetes.io/name=karpenter
# NodePool durumu
kubectl get nodepool hepapi-istanbul
# Karpenter tarafından açılan node'lar
kubectl get nodes -l karpenter.sh/nodepool=hepapi-istanbul
Pod'ları Istanbul node'larına yönlendirmek için nodeSelector ekleyin:
spec:
nodeSelector:
topology: local-zone
zone: istanbul
Local Zone'da storage kurulumunda yapılan en yaygın hata: StorageClass'a hardcoded zone topology eklemek.
Yanlış yaklaşım:
# BUNU YAPMAYIN — pod başka node'a taşınırsa volume erişilemez
volumeBindingMode: Immediate
allowedTopologies:
- matchLabelExpressions:
- key: topology.kubernetes.io/zone
values: ["eu-central-1-ist-1a"]
Doğru yaklaşım: WaitForFirstConsumer - volume, pod'un schedule edildiği node'un zone'unda oluşturulur. Zone değişirse volume de değişir.
# modules/ebs-csi/main.tf
resource "helm_release" "aws_ebs_csi_driver" {
name = "aws-ebs-csi-driver"
namespace = "kube-system"
repository = "https://kubernetes-sigs.github.io/aws-ebs-csi-driver/"
chart = "aws-ebs-csi-driver"
version = var.chart_version
set = [
{ name = "controller.serviceAccount.create", value = "true" },
{ name = "controller.serviceAccount.name", value = "ebs-csi-controller-sa" },
{ name = "controller.serviceAccount.annotations.eks\\.amazonaws\\.com/role-arn",
value = var.ebs_csi_role_arn },
]
}
resource "kubectl_manifest" "storageclass" {
yaml_body = <<-YAML
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ${var.storageclassname}
provisioner: ebs.csi.aws.com
parameters:
tagSpecification_1: "Name=-"
tagSpecification_2: "Namespace="
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer # Pod'u takip et, zone hardcode etme
YAML
}
ALB'yi Istanbul Local Zone'da oluşturmak için alb.ingress.kubernetes.io/subnets annotation'ına Local Zone subnet adını vermek yeterli.
IAM IRSA Role
# modules/aws-alb-controller-role/main.tf
module "lb_role" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
version = "~> 5.52.0" # v6 dizin yapısını değiştirdi, pinlemek şart
role_name = "eks-lb-controller-${var.cluster_name}"
attach_load_balancer_controller_policy = true
oidc_providers = {
main = {
provider_arn = var.oidc_provider_arn
namespace_service_accounts = ["kube-system:aws-load-balancer-controller"]
}
}
}
resource "kubernetes_service_account" "service-account" {
metadata {
name = "aws-load-balancer-controller"
namespace = "kube-system"
annotations = {
"eks.amazonaws.com/role-arn" = module.lb_role.iam_role_arn
"eks.amazonaws.com/sts-regional-endpoints" = "true"
}
}
}
Helm Chart
# env/hepapi/eu-central-1/prod/eks/alb-controller/aws-alb-controller/terragrunt.hcl
inputs = {
name = "aws-load-balancer-controller"
namespace = "kube-system"
chart_version = "1.11.0"
helm_repo_url = "https://aws.github.io/eks-charts"
sets = [
{ name = "serviceAccount.create", value = "false" },
{ name = "serviceAccount.name", value = "aws-load-balancer-controller" },
{ name = "clusterName", value = dependency.eks.outputs.cluster_name },
{ name = "vpcId", value = dependency.vpc.outputs.vpc_id },
# eu-central-1 ECR mirror (us-east-1 değil!)
{ name = "image.repository",
value = "602401143452.dkr.ecr.eu-central-1.amazonaws.com/amazon/aws-load-balancer-controller" }
]
}
Ingress Örneği - Local Zone İçin Annotation'lar
ALB'yi Istanbul Local Zone'da oluşturmak için tek yapmanız gereken alb.ingress.kubernetes.io/subnets annotation'ına Local Zone subnet adını vermek. AWS Load Balancer Controller bu subnet'i okuyarak ALB'yi doğrudan Istanbul'da açar.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
namespace: default
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
# ZORUNLU: pod IP'lerine doğrudan yönlendirme için IP modunu kullan
alb.ingress.kubernetes.io/target-type: ip
# ALB'yi Istanbul Local Zone subnet'inde oluştur
alb.ingress.kubernetes.io/subnets: eu-central-1-ist-1a
alb.ingress.kubernetes.io/healthcheck-path: /health
spec:
ingressClassName: alb
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-service
port:
number: 80
← Önceki: Bölüm 2: Temel Altyapı
Serinin devamı → Bölüm 4: Production'a Taşımak - Sorunlar, Maliyet ve Best Practices için takipte kalın.