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’defalse. Koşullu ifadeyle environment’a göre otomatik ayarlanıyor. - deletion_protection: Production’da açık, bu sayede kazara
terraform destroyile veritabanını silemezsin. - apply_immediately: Production’da
falseçünkü değişiklikler maintenance window’da uygulanmalı. - storage_encrypted: Her zaman
trueolmalı, 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 = trueproduction’da: Değişiklik anında uygulanır, maintenance window’u bypass eder ve downtime’a yol açabilir. Her zamanfalsekullan.- State dosyasında hassas veri: Şifreler, connection string’ler… Bunları Secrets Manager veya Vault’ta tut, direkt value olarak verme.
prevent_destroylifecycle kullanmamak: Kritik kaynaklar için lifecycle bloğunaprevent_destroy = trueekle.- 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.
