Terraform ile Load Balancer Yapılandırması

Altyapıyı kod olarak yönetmek bir kez tadına varınca geri dönmek istemiyorsun. Özellikle load balancer gibi karmaşık bileşenleri elle yapılandırmak yerine Terraform ile birkaç satır kodla tanımlamak, hem hız hem de tutarlılık açısından sizi farklı bir seviyeye taşıyor. Bu yazıda gerçek dünya senaryoları üzerinden Terraform ile load balancer yapılandırmasını adım adım ele alacağız.

Load Balancer Neden Terraform ile Yönetilmeli?

Klasik yöntemde bir load balancer kurduğunuzda şunlar oluyor: AWS Console’a giriyorsunuz, target group oluşturuyorsunuz, listener tanımlıyorsunuz, health check ayarlıyorsunuz. Bir sorun olduğunda “ben bunu nasıl kurmuştum?” sorusu kaçınılmaz hale geliyor. Üstelik staging ile production arasındaki farkları takip etmek neredeyse imkansız oluyor.

Terraform devreye girdiğinde tüm bu yapılandırma .tf dosyalarında yaşıyor. Git geçmişine bakarak “bu kural ne zaman eklendi, kim ekledi” sorularının cevabını anında bulabiliyorsunuz. Daha da önemlisi, aynı yapılandırmayı farklı ortamlara kolayca kopyalayabiliyorsunuz.

Bu yazıda AWS Application Load Balancer (ALB) üzerinden ilerleyeceğiz. Ancak anlatacağımız mantık GCP ve Azure için de büyük ölçüde geçerli.

Ortam Hazırlığı ve Gereksinimler

Başlamadan önce birkaç şeyin hazır olması gerekiyor:

  • Terraform 1.5+ kurulu olmalı
  • AWS CLI yapılandırılmış ve geçerli credentials mevcut olmalı
  • Temel VPC ve subnet yapısı hazır olmalı
  • EC2 instance’larınız ya da ECS servisiniz çalışıyor olmalı

Provider tanımlamalarıyla başlayalım:

# versions.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket = "my-terraform-state-bucket"
    key    = "infra/loadbalancer/terraform.tfstate"
    region = "eu-west-1"
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "Terraform"
      Project     = var.project_name
    }
  }
}

Remote state kullanımı kritik. Ekip ortamında çalışıyorsanız state dosyasının S3’te tutulması ve DynamoDB ile lock mekanizmasının aktif olması şart.

Değişken Tanımlamaları

Yapılandırmayı esnek tutmak için değişkenleri ayrı bir dosyada topluyoruz:

# variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "eu-west-1"
}

variable "environment" {
  description = "Deployment environment (dev/staging/prod)"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "project_name" {
  description = "Project name used in resource naming"
  type        = string
}

variable "vpc_id" {
  description = "VPC ID where ALB will be deployed"
  type        = string
}

variable "public_subnet_ids" {
  description = "List of public subnet IDs for ALB"
  type        = list(string)
}

variable "private_subnet_ids" {
  description = "List of private subnet IDs for target instances"
  type        = list(string)
}

variable "certificate_arn" {
  description = "ACM certificate ARN for HTTPS listener"
  type        = string
  default     = ""
}

variable "health_check_path" {
  description = "Health check endpoint path"
  type        = string
  default     = "/health"
}

variable "target_port" {
  description = "Application port on target instances"
  type        = number
  default     = 8080
}

Security Group Yapılandırması

Load balancer için güvenlik grubu tanımı oldukça önemli. Açık bırakmanız gerekenler ile kısıtlamanız gerekenler arasındaki dengeyi doğru kurmanız gerekiyor:

# security_groups.tf
resource "aws_security_group" "alb" {
  name        = "${var.project_name}-${var.environment}-alb-sg"
  description = "Security group for Application Load Balancer"
  vpc_id      = var.vpc_id

  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTPS from anywhere"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description = "All outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-alb-sg"
  }
}

resource "aws_security_group" "app" {
  name        = "${var.project_name}-${var.environment}-app-sg"
  description = "Security group for application instances"
  vpc_id      = var.vpc_id

  ingress {
    description     = "Traffic from ALB only"
    from_port       = var.target_port
    to_port         = var.target_port
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-app-sg"
  }
}

Dikkat etmeniz gereken nokta: uygulama instance’larının security group’u sadece ALB’den gelen trafiği kabul ediyor. Bu sayede instance’lara doğrudan erişimi engellenmiş oluyor.

Application Load Balancer Ana Kaynak

Asıl ALB kaynağını oluşturalım:

# alb.tf
resource "aws_lb" "main" {
  name               = "${var.project_name}-${var.environment}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = var.public_subnet_ids

  enable_deletion_protection = var.environment == "prod" ? true : false
  enable_http2               = true
  idle_timeout               = 60

  access_logs {
    bucket  = aws_s3_bucket.alb_logs.bucket
    prefix  = "${var.project_name}-${var.environment}"
    enabled = true
  }

  tags = {
    Name = "${var.project_name}-${var.environment}-alb"
  }
}

# ALB access logs için S3 bucket
resource "aws_s3_bucket" "alb_logs" {
  bucket        = "${var.project_name}-${var.environment}-alb-logs-${data.aws_caller_identity.current.account_id}"
  force_destroy = var.environment != "prod"
}

resource "aws_s3_bucket_lifecycle_configuration" "alb_logs" {
  bucket = aws_s3_bucket.alb_logs.id

  rule {
    id     = "expire-old-logs"
    status = "Enabled"

    expiration {
      days = var.environment == "prod" ? 90 : 30
    }
  }
}

data "aws_caller_identity" "current" {}

data "aws_elb_service_account" "main" {}

resource "aws_s3_bucket_policy" "alb_logs" {
  bucket = aws_s3_bucket.alb_logs.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          AWS = data.aws_elb_service_account.main.arn
        }
        Action   = "s3:PutObject"
        Resource = "${aws_s3_bucket.alb_logs.arn}/${var.project_name}-${var.environment}/AWSLogs/*"
      }
    ]
  })
}

Production ortamında enable_deletion_protection = true olması kritik. Birisi yanlışlıkla terraform destroy çalıştırdığında load balancer’ın silinmesini bu şekilde engelliyorsunuz.

Target Group ve Health Check Yapılandırması

Target group, ALB’nin trafiği yönlendireceği hedefleri tanımlar. Health check ayarları burada hayati önem taşıyor:

# target_groups.tf
resource "aws_lb_target_group" "app" {
  name        = "${var.project_name}-${var.environment}-tg"
  port        = var.target_port
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "instance"

  health_check {
    enabled             = true
    healthy_threshold   = 2
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    path                = var.health_check_path
    matcher             = "200,201"
    port                = "traffic-port"
    protocol            = "HTTP"
  }

  stickiness {
    type            = "lb_cookie"
    cookie_duration = 86400
    enabled         = false
  }

  deregistration_delay = 30

  tags = {
    Name = "${var.project_name}-${var.environment}-tg"
  }

  lifecycle {
    create_before_destroy = true
  }
}

# Blue/Green deployment için ikinci target group
resource "aws_lb_target_group" "app_green" {
  count = var.enable_blue_green ? 1 : 0

  name        = "${var.project_name}-${var.environment}-tg-green"
  port        = var.target_port
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "instance"

  health_check {
    enabled             = true
    healthy_threshold   = 2
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    path                = var.health_check_path
    matcher             = "200,201"
  }

  deregistration_delay = 30

  lifecycle {
    create_before_destroy = true
  }
}

deregistration_delay = 30 değerine dikkat edin. Bu değer instance devre dışı bırakılırken mevcut bağlantıların tamamlanması için beklenen süreyi ifade ediyor. Default 300 saniyedir ama çoğu uygulama için 30 saniye yeterli. Bu sayede deployment sürelerinizi önemli ölçüde kısaltıyorsunuz.

Listener Yapılandırması

HTTP ve HTTPS listener’larını oluşturalım. HTTP trafiğini otomatik olarak HTTPS’e yönlendirmek standart pratik:

# listeners.tf
# HTTP -> HTTPS redirect
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"

    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# HTTPS listener
resource "aws_lb_listener" "https" {
  count = var.certificate_arn != "" ? 1 : 0

  load_balancer_arn = aws_lb.main.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = var.certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

# Path-based routing kuralları
resource "aws_lb_listener_rule" "api" {
  listener_arn = aws_lb_listener.https[0].arn
  priority     = 100

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }

  condition {
    path_pattern {
      values = ["/api/*"]
    }
  }
}

resource "aws_lb_listener_rule" "static_assets" {
  listener_arn = aws_lb_listener.https[0].arn
  priority     = 200

  action {
    type = "redirect"

    redirect {
      host        = "cdn.${var.domain_name}"
      path        = "/#{path}"
      status_code = "HTTP_301"
    }
  }

  condition {
    path_pattern {
      values = ["/static/*", "/assets/*"]
    }
  }
}

# Maintenance modu için fixed response
resource "aws_lb_listener_rule" "maintenance" {
  listener_arn = aws_lb_listener.https[0].arn
  priority     = 999

  action {
    type = "fixed-response"

    fixed_response {
      content_type = "application/json"
      message_body = jsonencode({
        error   = "Service temporarily unavailable"
        message = "We are performing scheduled maintenance. Please try again in a few minutes."
      })
      status_code = "503"
    }
  }

  condition {
    http_header {
      http_header_name = "X-Maintenance-Mode"
      values           = ["true"]
    }
  }
}

SSL policy seçimi önemli. ELBSecurityPolicy-TLS13-1-2-2021-06 güncel ve güvenli bir seçenek. Eski tarayıcı desteği gerektirmiyorsanız bu policy’yi kullanın.

Auto Scaling ile Entegrasyon

Load balancer’ı Auto Scaling Group ile entegre etmek gerçek dünya senaryolarında kaçınılmaz:

# autoscaling.tf
resource "aws_autoscaling_group" "app" {
  name                = "${var.project_name}-${var.environment}-asg"
  vpc_zone_identifier = var.private_subnet_ids
  target_group_arns   = [aws_lb_target_group.app.arn]
  health_check_type   = "ELB"

  min_size         = var.environment == "prod" ? 2 : 1
  max_size         = var.environment == "prod" ? 10 : 3
  desired_capacity = var.environment == "prod" ? 2 : 1

  health_check_grace_period = 300

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  instance_refresh {
    strategy = "Rolling"
    preferences {
      min_healthy_percentage = 50
      instance_warmup        = 300
    }
  }

  tag {
    key                 = "Name"
    value               = "${var.project_name}-${var.environment}-app"
    propagate_at_launch = true
  }
}

resource "aws_autoscaling_policy" "scale_out" {
  name                   = "${var.project_name}-${var.environment}-scale-out"
  autoscaling_group_name = aws_autoscaling_group.app.name
  adjustment_type        = "ChangeInCapacity"
  scaling_adjustment     = 1
  cooldown               = 300
}

resource "aws_autoscaling_policy" "scale_in" {
  name                   = "${var.project_name}-${var.environment}-scale-in"
  autoscaling_group_name = aws_autoscaling_group.app.name
  adjustment_type        = "ChangeInCapacity"
  scaling_adjustment     = -1
  cooldown               = 300
}

CloudWatch Alarm ve Monitoring

Load balancer’ı izlemeden yönetmek kör uçmakla eşdeğer. Temel alarmları mutlaka tanımlayın:

# monitoring.tf
resource "aws_cloudwatch_metric_alarm" "alb_5xx_errors" {
  alarm_name          = "${var.project_name}-${var.environment}-alb-5xx-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "HTTPCode_ELB_5XX_Count"
  namespace           = "AWS/ApplicationELB"
  period              = "60"
  statistic           = "Sum"
  threshold           = "10"
  alarm_description   = "ALB 5XX error rate is too high"
  treat_missing_data  = "notBreaching"

  dimensions = {
    LoadBalancer = aws_lb.main.arn_suffix
  }

  alarm_actions = [aws_sns_topic.alerts.arn]
  ok_actions    = [aws_sns_topic.alerts.arn]
}

resource "aws_cloudwatch_metric_alarm" "target_response_time" {
  alarm_name          = "${var.project_name}-${var.environment}-response-time-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "3"
  metric_name         = "TargetResponseTime"
  namespace           = "AWS/ApplicationELB"
  period              = "60"
  extended_statistic  = "p95"
  threshold           = "2"
  alarm_description   = "P95 response time exceeds 2 seconds"

  dimensions = {
    LoadBalancer = aws_lb.main.arn_suffix
    TargetGroup  = aws_lb_target_group.app.arn_suffix
  }

  alarm_actions = [aws_sns_topic.alerts.arn]
}

resource "aws_cloudwatch_metric_alarm" "unhealthy_hosts" {
  alarm_name          = "${var.project_name}-${var.environment}-unhealthy-hosts"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "UnHealthyHostCount"
  namespace           = "AWS/ApplicationELB"
  period              = "60"
  statistic           = "Average"
  threshold           = "0"
  alarm_description   = "There are unhealthy hosts in the target group"

  dimensions = {
    LoadBalancer = aws_lb.main.arn_suffix
    TargetGroup  = aws_lb_target_group.app.arn_suffix
  }

  alarm_actions = [aws_sns_topic.alerts.arn]
}

resource "aws_sns_topic" "alerts" {
  name = "${var.project_name}-${var.environment}-alb-alerts"
}

Outputs Tanımlamaları

Diğer Terraform modülleri veya CI/CD pipeline’ınız için output değerlerini tanımlayın:

# outputs.tf
output "alb_dns_name" {
  description = "DNS name of the Application Load Balancer"
  value       = aws_lb.main.dns_name
}

output "alb_arn" {
  description = "ARN of the Application Load Balancer"
  value       = aws_lb.main.arn
}

output "alb_zone_id" {
  description = "Zone ID of the ALB, used for Route53 alias records"
  value       = aws_lb.main.zone_id
}

output "target_group_arn" {
  description = "ARN of the main target group"
  value       = aws_lb_target_group.app.arn
}

output "alb_security_group_id" {
  description = "Security group ID of the ALB"
  value       = aws_security_group.alb.id
}

output "app_security_group_id" {
  description = "Security group ID for application instances"
  value       = aws_security_group.app.id
}

Gerçek Dünya Senaryosu: Sıfır Kesintili Deployment

Bir e-ticaret sitesinde çalıştığınızı düşünün. Akşam 23:00’da yeni bir uygulama versiyonu deploy edeceksiniz. Auto Scaling instance refresh ile bu işlemi nasıl yapacağınızı görelim:

# Yeni deployment tetiklemek için launch template versiyonunu güncelle
# terraform.tfvars içinde ami_id değişkenini yeni AMI ile güncelle

# Plan çalıştır
terraform plan -var-file="prod.tfvars" -out=deployment.plan

# Planı gözden geçir, sonra uygula
terraform apply deployment.plan

# Instance refresh durumunu takip et
aws autoscaling describe-instance-refreshes 
  --auto-scaling-group-name "myapp-prod-asg" 
  --query 'InstanceRefreshes[0].{Status:Status,PercentageComplete:PercentageComplete}' 
  --output table

Instance refresh sırasında ALB health check’leri aktif çalışıyor. Yeni instance sağlıklı duruma geçmeden eski instance devre dışı bırakılmıyor. Bu sayede kullanıcılar kesinti yaşamıyor.

Sık Yapılan Hatalar ve Çözümleri

Yıllar içinde karşılaştığım tipik hatalar şunlar:

Health check yanlış yapılandırması: Uygulama /health endpoint’i döndürürken health check path’i / olarak tanımlanmış. 200 yerine 302 redirect gelince instance unhealthy olarak işaretleniyor. matcher parametresini uygulamanıza göre ayarlayın.

Stickiness sorunları: Session tabanlı uygulamalarda stickiness kapatık bırakılınca kullanıcılar her request’te farklı instance’a düşüyor ve session kaybı yaşanıyor. Ya stickiness aktif edin ya da Redis gibi external session storage kullanın.

SSL policy eskimesi: Yıllar önce tanımladığınız SSL policy artık eski TLS versiyonlarına izin veriyor olabilir. Periyodik olarak güncel policy ile karşılaştırın ve gerekiyorsa güncelleyin.

Access log bucket policy eksikliği: ALB access log’ları için S3 bucket policy eksik olduğunda loglar yazılmaz ama Terraform hata vermez. Bucket policy’sini doğruladığınızdan emin olun.

CI/CD Pipeline Entegrasyonu

GitLab CI ile nasıl entegre edeceğinizi gösterelim:

# .gitlab-ci.yml
stages:
  - validate
  - plan
  - apply

variables:
  TF_DIR: "infra/loadbalancer"
  AWS_DEFAULT_REGION: "eu-west-1"

terraform-validate:
  stage: validate
  image: hashicorp/terraform:1.6
  script:
    - cd $TF_DIR
    - terraform init -backend=false
    - terraform validate
    - terraform fmt -check -recursive
  only:
    - merge_requests

terraform-plan:
  stage: plan
  image: hashicorp/terraform:1.6
  script:
    - cd $TF_DIR
    - terraform init
    - terraform plan
        -var-file="${CI_ENVIRONMENT_NAME}.tfvars"
        -out=plan.tfplan
        -no-color 2>&1 | tee plan.log
  artifacts:
    paths:
      - $TF_DIR/plan.tfplan
      - $TF_DIR/plan.log
    expire_in: 1 hour
  environment:
    name: $CI_ENVIRONMENT_NAME

terraform-apply:
  stage: apply
  image: hashicorp/terraform:1.6
  script:
    - cd $TF_DIR
    - terraform init
    - terraform apply -auto-approve plan.tfplan
  dependencies:
    - terraform-plan
  when: manual
  only:
    - main

Modüler Yapı Oluşturma

Birden fazla projede ALB kullanıyorsanız reusable modül oluşturmak mantıklı:

# modules/alb/main.tf yapısı
# modules/
#   alb/
#     main.tf
#     variables.tf
#     outputs.tf
#     README.md

# Modülü çağırmak için:
module "app_alb" {
  source = "../../modules/alb"

  project_name      = "myapp"
  environment       = "prod"
  vpc_id            = data.terraform_remote_state.network.outputs.vpc_id
  public_subnet_ids = data.terraform_remote_state.network.outputs.public_subnet_ids
  certificate_arn   = data.aws_acm_certificate.main.arn
  health_check_path = "/api/health"
  target_port       = 8080
}

Sonuç

Terraform ile load balancer yönetmek başlangıçta zahmetli görünebilir ama bir kez doğru kurduğunuzda size büyük özgürlük sağlıyor. Tüm yapılandırma kod olarak git’te yaşıyor, değişiklikler izlenebilir, ortamlar arasındaki tutarsızlıklar ortadan kalkıyor.

Bu yazıda ele aldığımız konuları özetleyelim:

  • Provider ve backend yapılandırması ile güvenli state yönetimi
  • Security group tasarımıyla en az yetki prensibi
  • Health check ayarlarının doğru yapılandırılması
  • Listener kuralları ile path-based routing
  • Auto Scaling entegrasyonu ile sıfır kesintili deployment
  • CloudWatch alarmları ile proaktif izleme
  • CI/CD pipeline entegrasyonu ile otomatik deployment süreci

Bir sonraki adım olarak Terragrunt ile multi-environment yönetimini veya Terraform Cloud ile ekip çalışmasını incelemenizi öneririm. Sorularınız ve yorumlarınız için aşağıdaki yorum bölümünü kullanabilirsiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir