Terraform ile Veritabanı Altyapısı Yönetimi: RDS ve Managed Database Kurulumu

Veritabanı altyapısını elle kurmak, configure etmek ve yönetmek… Kim bilir kaç kez sabah 3’te bir production RDS instance’ını patlattıktan sonra düzeltmeye çalıştın. Ya da “şu an hangi veritabanı hangi parametrelerle ayakta?” sorusuna kimse cevap veremedi. İşte tam bu noktada Terraform devreye giriyor ve tüm bu kaosa bir son veriyor. Bu yazıda AWS RDS ve genel managed database servislerini Terraform ile nasıl yöneteceğini, gerçek dünya senaryolarıyla birlikte ele alacağız.

Neden Veritabanı Altyapısı İçin Terraform?

Veritabanları, altyapının en kritik ve aynı zamanda en riskli parçasıdır. Bir EC2 instance’ını yanlış configure ettiysen silersin yenisini ayağa kaldırırsın. Ama bir veritabanını silmek… O iş bambaşka.

Terraform ile veritabanı yönetiminin asıl gücü şuradan geliyor: her şey kod olarak saklanıyor, versiyon kontrolüne giriyor ve tekrar üretilebilir hale geliyor. Hangi parametreyi ne zaman değiştirdiğini, kim değiştirdiğini, neden değiştirdiğini git history’den görebiliyorsun. Bu, özellikle compliance gereksinimleri olan ortamlarda paha biçilmez bir özellik.

Ayrıca “drift detection” meselesi var. Biri console’dan girip bir parametre değiştirdi mi? terraform plan çalıştırdığında bunu anında görüyorsun. Manuel değişiklikler artık gizlenemiyor.

Temel Yapı ve Proje Organizasyonu

Başlamadan önce proje yapısını düzgün kurmak lazım. Veritabanı altyapısını ayrı bir modül olarak organize etmek, hem reusability hem de maintainability açısından kritik.

terraform-db/
├── modules/
│   └── rds/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       └── security.tf
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── prod/
│       ├── main.tf
│       ├── terraform.tfvars
│       └── backend.tf
└── shared/
    └── parameter_groups/
        └── main.tf

Bu yapıyla dev ve prod ortamları aynı modülü kullanıyor ama farklı değişkenlerle. Bir değişikliği önce dev’de test edip sonra prod’a uyguluyorsun.

VPC ve Subnet Grubu Hazırlığı

RDS kurulumunun en sık atlanan kısmı network hazırlığıdır. Veritabanını public erişime açmak en büyük güvenlik hatalarından biri. Önce subnet grubunu düzgün kuralım:

# network.tf
resource "aws_db_subnet_group" "main" {
  name        = "${var.environment}-db-subnet-group"
  description = "${var.environment} ortami icin DB subnet grubu"
  subnet_ids  = var.private_subnet_ids

  tags = {
    Name        = "${var.environment}-db-subnet-group"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_security_group" "rds" {
  name        = "${var.environment}-rds-sg"
  description = "RDS instance icin guvenlik grubu"
  vpc_id      = var.vpc_id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [var.app_security_group_id]
    description     = "Sadece uygulama katmanindan PostgreSQL erisimi"
  }

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

  tags = {
    Name        = "${var.environment}-rds-sg"
    Environment = var.environment
  }
}

Burada dikkat çeken nokta: ingress kuralında sadece uygulama security group’undan gelen trafiğe izin veriyoruz. CIDR block kullanmak yerine security group referansı kullanmak çok daha güvenli bir yaklaşım.

RDS Instance Tanımı

Şimdi asıl RDS kaynağına geçelim. PostgreSQL üzerinden gideceğiz çünkü production’da en çok tercih edilen veritabanlarından biri:

# main.tf - RDS Instance
resource "aws_db_instance" "main" {
  identifier = "${var.environment}-${var.db_name}-postgres"

  # Motor ve versiyon
  engine               = "postgres"
  engine_version       = "15.4"
  instance_class       = var.instance_class
  allocated_storage    = var.allocated_storage
  max_allocated_storage = var.max_allocated_storage
  storage_type         = "gp3"
  storage_encrypted    = true
  kms_key_id          = aws_kms_key.rds.arn

  # Veritabani ayarlari
  db_name  = var.db_name
  username = var.db_username
  password = random_password.db_password.result

  # Network
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]
  publicly_accessible    = false
  port                   = 5432

  # Parametre grubu
  parameter_group_name = aws_db_parameter_group.postgres15.name

  # Backup ayarlari
  backup_retention_period   = var.backup_retention_days
  backup_window            = "03:00-04:00"
  maintenance_window       = "Mon:04:00-Mon:05:00"
  copy_tags_to_snapshot    = true
  delete_automated_backups = false

  # Yuksek erisebilirlik
  multi_az = var.environment == "prod" ? true : false

  # Monitoring
  monitoring_interval = 60
  monitoring_role_arn = aws_iam_role.rds_monitoring.arn
  enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]

  # Silme korumasi
  deletion_protection = var.environment == "prod" ? true : false
  skip_final_snapshot = var.environment == "prod" ? false : true
  final_snapshot_identifier = var.environment == "prod" ? "${var.environment}-${var.db_name}-final-snapshot" : null

  apply_immediately = var.environment == "prod" ? false : true

  tags = {
    Name        = "${var.environment}-${var.db_name}-postgres"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

Birkaç önemli noktanın üzerinden geçelim:

  • multi_az: Production’da true, dev’de false. Koşullu ifadeyle environment’a göre otomatik ayarlanıyor.
  • deletion_protection: Production’da açık, bu sayede kazara terraform destroy ile veritabanını silemezsin.
  • apply_immediately: Production’da false çünkü değişiklikler maintenance window’da uygulanmalı.
  • storage_encrypted: Her zaman true olmalı, tartışmasız.

Şifre Yönetimi ve KMS

Veritabanı şifrelerini Terraform state dosyasında düz metin olarak saklamak felaket reçetesidir. Doğru yaklaşım:

# secrets.tf
resource "random_password" "db_password" {
  length           = 32
  special          = true
  override_special = "!#$%&*()-_=+[]{}<>:?"
  min_special      = 2
  min_upper        = 2
  min_lower        = 2
  min_numeric      = 2
}

resource "aws_secretsmanager_secret" "db_password" {
  name                    = "/${var.environment}/${var.db_name}/db-password"
  description             = "${var.environment} ortami ${var.db_name} DB sifresi"
  recovery_window_in_days = var.environment == "prod" ? 30 : 0
  kms_key_id              = aws_kms_key.secrets.arn

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id = aws_secretsmanager_secret.db_password.id
  secret_string = jsonencode({
    username = var.db_username
    password = random_password.db_password.result
    host     = aws_db_instance.main.address
    port     = aws_db_instance.main.port
    dbname   = aws_db_instance.main.db_name
  })
}

resource "aws_kms_key" "rds" {
  description             = "${var.environment} RDS sifreleme anahtari"
  deletion_window_in_days = 7
  enable_key_rotation     = true

  tags = {
    Environment = var.environment
    Purpose     = "rds-encryption"
  }
}

Şifreyi Secrets Manager’a tam bir connection string olarak kaydediyoruz. Uygulama bu secret’ı çektiğinde tüm bağlantı bilgilerine tek seferde ulaşıyor. Ayrıca KMS key rotation’ı aktif etmeyi unutma.

Parameter Group Optimizasyonu

Default parameter group ile production’a gitme. PostgreSQL için özelleştirilmiş bir parameter group şöyle görünmeli:

# parameter_groups.tf
resource "aws_db_parameter_group" "postgres15" {
  family = "postgres15"
  name   = "${var.environment}-postgres15-params"
  description = "${var.environment} ortami PostgreSQL 15 parametre grubu"

  parameter {
    name  = "log_connections"
    value = "1"
  }

  parameter {
    name  = "log_disconnections"
    value = "1"
  }

  parameter {
    name  = "log_duration"
    value = "1"
  }

  parameter {
    name  = "log_min_duration_statement"
    value = "1000"  # 1 saniyeden uzun sorgulari logla
  }

  parameter {
    name  = "shared_preload_libraries"
    value = "pg_stat_statements"
    apply_method = "pending-reboot"
  }

  parameter {
    name  = "pg_stat_statements.track"
    value = "ALL"
  }

  parameter {
    name  = "max_connections"
    value = "200"
    apply_method = "pending-reboot"
  }

  parameter {
    name  = "work_mem"
    value = "16384"  # 16MB
  }

  parameter {
    name  = "maintenance_work_mem"
    value = "131072"  # 128MB
  }

  parameter {
    name  = "checkpoint_completion_target"
    value = "0.9"
  }

  parameter {
    name  = "wal_buffers"
    value = "8192"  # 64MB
  }

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

apply_method = "pending-reboot" olan parametreler dikkat ister. Bu parametreler değiştirildiğinde instance’ın restart edilmesi gerekiyor. Terraform bunu bildirim olarak gösterecek, ama restart’ı otomatik yapmayacak.

Read Replica Kurulumu

Production ortamlarda okuma yükünü dağıtmak için read replica şart. Raporlama sorguları, analytics işlemleri bunlar üzerinden yapılabilir:

# read_replica.tf
resource "aws_db_instance" "read_replica" {
  count = var.create_read_replica ? 1 : 0

  identifier             = "${var.environment}-${var.db_name}-replica-${count.index + 1}"
  replicate_source_db    = aws_db_instance.main.identifier
  instance_class         = var.replica_instance_class
  publicly_accessible    = false
  auto_minor_version_upgrade = true
  storage_encrypted      = true
  kms_key_id            = aws_kms_key.rds.arn

  vpc_security_group_ids = [aws_security_group.rds.id]

  # Read replica icin backup gerekmez, kaynak instance'tan geliyor
  backup_retention_period = 0
  skip_final_snapshot     = true

  # Monitoring
  monitoring_interval = 60
  monitoring_role_arn = aws_iam_role.rds_monitoring.arn

  performance_insights_enabled = true
  performance_insights_retention_period = 7

  tags = {
    Name        = "${var.environment}-${var.db_name}-replica"
    Environment = var.environment
    Role        = "read-replica"
    ManagedBy   = "terraform"
  }
}

count = var.create_read_replica ? 1 : 0 kullanımı sayesinde dev ortamında bu kaynağı hiç oluşturmuyoruz. Terraform’un koşullu kaynak oluşturma özelliği bu tür senaryolar için biçilmiş kaftan.

Monitoring ve Alerting

Veritabanını kurmak yetmez, ne zaman yardım çığlığı attığını bilmen gerekir:

# monitoring.tf
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
  alarm_name          = "${var.environment}-${var.db_name}-cpu-high"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = "3"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/RDS"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "RDS CPU kullanimi %80 uzerinde"
  alarm_actions       = [aws_sns_topic.db_alerts.arn]
  ok_actions          = [aws_sns_topic.db_alerts.arn]

  dimensions = {
    DBInstanceIdentifier = aws_db_instance.main.id
  }
}

resource "aws_cloudwatch_metric_alarm" "storage_low" {
  alarm_name          = "${var.environment}-${var.db_name}-storage-low"
  comparison_operator = "LessThanOrEqualToThreshold"
  evaluation_periods  = "1"
  metric_name         = "FreeStorageSpace"
  namespace           = "AWS/RDS"
  period              = "300"
  statistic           = "Average"
  threshold           = "10737418240"  # 10GB
  alarm_description   = "RDS disk alani 10GB'in altina dustu"
  alarm_actions       = [aws_sns_topic.db_alerts.arn]

  dimensions = {
    DBInstanceIdentifier = aws_db_instance.main.id
  }
}

resource "aws_cloudwatch_metric_alarm" "connection_count" {
  alarm_name          = "${var.environment}-${var.db_name}-connections-high"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = "2"
  metric_name         = "DatabaseConnections"
  namespace           = "AWS/RDS"
  period              = "300"
  statistic           = "Average"
  threshold           = "150"
  alarm_description   = "Veritabani baglanti sayisi 150 uzerinde"
  alarm_actions       = [aws_sns_topic.db_alerts.arn]

  dimensions = {
    DBInstanceIdentifier = aws_db_instance.main.id
  }
}

resource "aws_sns_topic" "db_alerts" {
  name = "${var.environment}-db-alerts"
  kms_master_key_id = "alias/aws/sns"
}

resource "aws_sns_topic_subscription" "db_alerts_email" {
  topic_arn = aws_sns_topic.db_alerts.arn
  protocol  = "email"
  endpoint  = var.alert_email
}

Üç kritik alarm ekledik: CPU, disk alanı ve bağlantı sayısı. Bunlara ek olarak ReadLatency ve WriteLatency alarmlarını da düşünebilirsin.

Variables ve Outputs

Modülü kullanılabilir hale getirmek için iyi tanımlanmış variable ve output’lar şart:

# variables.tf
variable "environment" {
  description = "Ortam adi (dev, staging, prod)"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Ortam dev, staging veya prod olmali."
  }
}

variable "db_name" {
  description = "Veritabani adi"
  type        = string
  validation {
    condition     = can(regex("^[a-z][a-z0-9_]*$", var.db_name))
    error_message = "Veritabani adi kucuk harf, rakam ve alt cizgi icermeli."
  }
}

variable "instance_class" {
  description = "RDS instance sinifi"
  type        = string
  default     = "db.t3.medium"
}

variable "allocated_storage" {
  description = "Baslangic depolama alani (GB)"
  type        = number
  default     = 100
  validation {
    condition     = var.allocated_storage >= 20
    error_message = "Minimum 20GB depolama gerekli."
  }
}

variable "max_allocated_storage" {
  description = "Maksimum otomatik buyume limiti (GB)"
  type        = number
  default     = 500
}

variable "backup_retention_days" {
  description = "Backup saklanma suresi (gun)"
  type        = number
  default     = 7
}

variable "create_read_replica" {
  description = "Read replica olusturulsun mu?"
  type        = bool
  default     = false
}

# outputs.tf
output "db_instance_endpoint" {
  description = "RDS baglanti endpoint'i"
  value       = aws_db_instance.main.endpoint
  sensitive   = false
}

output "db_instance_arn" {
  description = "RDS instance ARN"
  value       = aws_db_instance.main.arn
}

output "secret_arn" {
  description = "DB sifresi icin Secrets Manager ARN"
  value       = aws_secretsmanager_secret.db_password.arn
}

output "read_replica_endpoint" {
  description = "Read replica endpoint'i"
  value       = var.create_read_replica ? aws_db_instance.read_replica[0].endpoint : null
}

Validation bloklarına dikkat et. variable içindeki validation’lar, yanlış değer girildiğinde terraform plan aşamasında hata verip seni uyarıyor. Production’a kadar gitmeden sorunları erkenden yakalıyorsun.

Gerçek Dünya: Uygulama

Modülü environment bazında nasıl kullanacaksın:

# environments/prod/main.tf
module "production_db" {
  source = "../../modules/rds"

  environment             = "prod"
  db_name                 = "myapp"
  db_username             = "myapp_admin"
  instance_class          = "db.r6g.xlarge"
  allocated_storage       = 200
  max_allocated_storage   = 1000
  backup_retention_days   = 30
  create_read_replica     = true
  replica_instance_class  = "db.r6g.large"
  vpc_id                  = data.terraform_remote_state.network.outputs.vpc_id
  private_subnet_ids      = data.terraform_remote_state.network.outputs.private_subnet_ids
  app_security_group_id   = data.terraform_remote_state.app.outputs.security_group_id
  alert_email             = "[email protected]"
}

Deployment sırası önemli. Önce network altyapısı (terraform apply ile VPC, subnet’ler), sonra veritabanı modülü. data.terraform_remote_state ile farklı Terraform state’lerinden çıktı çekiyoruz.

Sık Yapılan Hatalar

Terraform ile RDS yönetiminde en çok karşılaşılan sorunları bilmek, onları yaşamaktan seni kurtarır:

  • apply_immediately = true production’da: Değişiklik anında uygulanır, maintenance window’u bypass eder ve downtime’a yol açabilir. Her zaman false kullan.
  • State dosyasında hassas veri: Şifreler, connection string’ler… Bunları Secrets Manager veya Vault’ta tut, direkt value olarak verme.
  • prevent_destroy lifecycle kullanmamak: Kritik kaynaklar için lifecycle bloğuna prevent_destroy = true ekle.
  • Parametre group değişikliklerini küçümsemek: Bazı parametre değişiklikleri reboot gerektirir. Plan çıktısını dikkatlice oku.
  • Tek AZ’de production: Multi-AZ maliyet artırıyor ama bir AZ’nin düşmesi anında failover sağlıyor. Production için tartışmasız açık olmalı.
  • Monitoring olmadan deploy: Veritabanını kurdun güzel, ama alarm kurmadan production’a aldın. İlk sorun gecenin 2’sinde gözükünce anlarsın.

Sonuç

Terraform ile veritabanı altyapısı yönetimi ilk başta karmaşık görünse de doğru yapılandırıldığında inanılmaz bir güvenlik ve tutarlılık sağlıyor. En önemli noktaları şöyle özetleyebilirim:

Her ortam için ayrı tfvars dosyaları kullan ve aynı modülü paylaş. Şifreleri asla kod içinde tutma, Secrets Manager entegrasyonunu baştan kur. Production için deletion_protection, multi_az ve apply_immediately = false üçlüsü zorunlu. Parameter group’ları optimize et ve varsayılan ayarlarla gitme. Monitoring ve alerting’i veritabanıyla birlikte kur, sonraya bırakma.

Bu altyapıyı bir kez düzgün kurduktan sonra yeni bir ortam açmak dakikalar alıyor. Veritabanı konfigürasyonunu review edip onaylamak için git pull request akışını kullanabiliyorsun. Ve en önemlisi, sabah 3’te “hangi parametre ne zaman değişti?” sorusuna git log’dan anında cevap bulabiliyorsun.

Terraform state’ini remote backend’de tut (S3 + DynamoDB lock), state dosyasını asla git’e commit etme ve her ciddi değişiklikten önce terraform plan çıktısını bir ekip arkadaşına review ettir. Bu üç kural seni büyük belalardan korur.

Bir yanıt yazın

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