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.
