Kullanılmayan Bulut Kaynaklarını Tespit ve Temizleme

Bulut faturanıza baktığınızda “bu kadar mı ödüyorum?” diye irkildiniz mi hiç? Çoğu şirket, bulut maliyetlerinin yüzde otuzuna yakın bir kısmını tamamen kullanılmayan ya da fazla boyutlandırılmış kaynaklar için harcıyor. Bir EC2 instance’ı durdurdunuz ama EBS volume’ü hâlâ tickling ediyor, aylık fatura kesiyor. Load balancer kuruldu, arkasındaki servisler kapandı, ama load balancer kendisi şakır şakır çalışıyor. Bu yazıda bu “hayalet” kaynakları nasıl bulacağınızı ve temizleyeceğinizi, hem AWS hem de genel bulut prensipleri üzerinden ele alacağız.

Neden Kaynak İsrafı Bu Kadar Yaygın?

Bulut ortamlarında kaynak israfı genellikle kasıtlı değil, dikkatssizlikten ya da iş akışı eksikliklerinden kaynaklanır. Geliştirici bir test ortamı kurar, işi bitince instance’ı durdurur ama volume’ü, Elastic IP’yi, snapshot’ları silmeyi unutur. Bir CI/CD pipeline’ı her build’de yeni bir kaynak yaratır ama eskilerini temizlemez. DevOps ekibi bir servis migrate eder, eski ortamı “bir süre daha tutalım” der ve o süre asla bitmez.

Asıl sorun şu: Bulut kaynaklarını silmek, on-premise ortamda bir sunucuyu kapatmaktan farklı hissettiriyor. “Sil” butonuna basmak kolay ama “acaba bir şeye bağlı mıydı?” korkusu insanı duraksatıyor. Bu belirsizlik, zamanla dev bir kaynak mezarlığına dönüşüyor.

AWS Üzerinde Kullanılmayan Kaynakları Tespit Etmek

EC2 Instance’ları ve EBS Volume’leri

İlk durağımız EC2. Durdurulmuş instance’lar para yemez ama onlara bağlı EBS volume’leri yemaya devam eder. Aşağıdaki AWS CLI komutu ile hesabınızdaki tüm bölgelerdeki durdurulmuş instance’ları ve onlara bağlı volume’leri listeleyebilirsiniz:

#!/bin/bash
# Durdurulmuş EC2 instance'ları ve EBS volume'lerini listele

REGIONS=$(aws ec2 describe-regions --query 'Regions[].RegionName' --output text)

for REGION in $REGIONS; do
    echo "=== Bölge: $REGION ==="
    
    # Durdurulmuş instance'lar
    aws ec2 describe-instances 
        --region $REGION 
        --filters "Name=instance-state-name,Values=stopped" 
        --query 'Reservations[].Instances[].[InstanceId,InstanceType,LaunchTime,Tags[?Key==`Name`].Value|[0]]' 
        --output table 2>/dev/null
    
    # Herhangi bir instance'a bağlı olmayan volume'ler
    aws ec2 describe-volumes 
        --region $REGION 
        --filters "Name=status,Values=available" 
        --query 'Volumes[].[VolumeId,Size,CreateTime,VolumeType]' 
        --output table 2>/dev/null
done

Bu scripti çalıştırdığınızda muhtemelen şaşıracaksınız. Özellikle birden fazla geliştirici ekibi olan ortamlarda düzinelerce “available” durumundaki volume görmek normal.

Kullanılmayan Elastic IP Adresleri

Elastic IP’ler, bir instance’a veya network interface’e bağlı olmadıkları zaman ücretlendirilir. Kulağa küçük gelebilir ama yüzlerce EIP’nin biriktiği ortamlarda aylık yüzlerce dolara ulaşabiliyor:

#!/bin/bash
# Kullanılmayan Elastic IP'leri tespit et

REGIONS=$(aws ec2 describe-regions --query 'Regions[].RegionName' --output text)
TOTAL_EIP=0

for REGION in $REGIONS; do
    UNASSOCIATED=$(aws ec2 describe-addresses 
        --region $REGION 
        --query 'Addresses[?AssociationId==null].[PublicIp,AllocationId]' 
        --output text 2>/dev/null)
    
    if [ -n "$UNASSOCIATED" ]; then
        echo "=== $REGION - Kullanılmayan EIP'ler ==="
        echo "$UNASSOCIATED"
        COUNT=$(echo "$UNASSOCIATED" | wc -l)
        TOTAL_EIP=$((TOTAL_EIP + COUNT))
    fi
done

echo ""
echo "Toplam kullanılmayan EIP sayısı: $TOTAL_EIP"
echo "Tahmini aylık maliyet: $(echo "$TOTAL_EIP * 3.6" | bc) USD"

Load Balancer’ları Kontrol Etmek

Load balancer’lar, arkalarında hiç hedef olmasa bile çalışmaya devam eder. Bu özellikle Application Load Balancer’larda sık karşılaşılan bir durum. Bir servis kapatıldığında target group boşalır ama ALB yerinde durur:

#!/bin/bash
# Boş target group'lara sahip Load Balancer'ları bul

REGION=${1:-"eu-west-1"}

echo "=== Boş Target Group'lar ==="
aws elbv2 describe-target-groups 
    --region $REGION 
    --query 'TargetGroups[*].[TargetGroupName,TargetGroupArn,LoadBalancerArns]' 
    --output json | python3 -c "
import json, sys
data = json.load(sys.stdin)
for tg in data:
    name, arn, lbs = tg
    # Hiç load balancer'a bağlı olmayan target group'lar
    if not lbs:
        print(f'Orphan TG: {name}')
    else:
        # Target health kontrol et
        import subprocess
        result = subprocess.run(
            ['aws', 'elbv2', 'describe-target-health', '--region', '$REGION', '--target-group-arn', arn],
            capture_output=True, text=True
        )
        health_data = json.loads(result.stdout)
        if not health_data.get('TargetHealthDescriptions'):
            print(f'Boş TG (LB bağlı ama hedef yok): {name}')
"

RDS Snapshot ve Eski AMI Temizliği

RDS snapshot’ları ve eski AMI’lar, bulut faturasının en sinsi köşelerinden birini oluşturur. “Yedeği silersek ne olur?” korkusuyla kimse bunlara dokunmaz ve zamanla terabaytlarca eski snapshot birikir:

#!/bin/bash
# 90 günden eski RDS snapshot'larını listele

REGION=${1:-"eu-west-1"}
THRESHOLD_DAYS=90
THRESHOLD_DATE=$(date -d "$THRESHOLD_DAYS days ago" +%Y-%m-%dT%H:%M:%S 2>/dev/null || 
                 date -v-${THRESHOLD_DAYS}d +%Y-%m-%dT%H:%M:%S)

echo "=== $THRESHOLD_DAYS günden eski manuel RDS Snapshot'ları ==="

aws rds describe-db-snapshots 
    --region $REGION 
    --snapshot-type manual 
    --query "DBSnapshots[?SnapshotCreateTime<='$THRESHOLD_DATE'].[DBSnapshotIdentifier,DBInstanceIdentifier,SnapshotCreateTime,AllocatedStorage]" 
    --output text | while read SNAP_ID DB_ID CREATE_TIME SIZE; do
        echo "Snapshot: $SNAP_ID | DB: $DB_ID | Tarih: $CREATE_TIME | Boyut: ${SIZE}GB"
        MONTHLY_COST=$(echo "$SIZE * 0.095" | bc)
        echo "  -> Tahmini aylık maliyet: $MONTHLY_COST USD"
    done

Maliyet Görünürlüğü için AWS Cost Explorer Kullanımı

CLI ve script’lerin yanı sıra, AWS Cost Explorer ile de kaynak israfını tespit edebilirsiniz. Ancak Cost Explorer’ın en güçlü tarafını bence programatik erişimde:

#!/usr/bin/env python3
"""
Son 30 günde en çok maliyet üreten servis ve kaynak kombinasyonlarını bul
"""

import boto3
from datetime import datetime, timedelta

client = boto3.client('ce', region_name='us-east-1')

end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')

response = client.get_cost_and_usage(
    TimePeriod={'Start': start_date, 'End': end_date},
    Granularity='MONTHLY',
    Metrics=['UnblendedCost'],
    GroupBy=[
        {'Type': 'DIMENSION', 'Key': 'SERVICE'},
        {'Type': 'TAG', 'Key': 'Environment'}
    ]
)

print(f"Dönem: {start_date} - {end_date}")
print("=" * 60)

results = response['ResultsByTime'][0]['Groups']
sorted_results = sorted(results, 
                        key=lambda x: float(x['Metrics']['UnblendedCost']['Amount']), 
                        reverse=True)

for group in sorted_results[:15]:
    service = group['Keys'][0]
    env_tag = group['Keys'][1] if len(group['Keys']) > 1 else 'Tag Yok'
    cost = float(group['Metrics']['UnblendedCost']['Amount'])
    
    if cost > 1.0:  # 1 USD altını gösterme
        print(f"Servis: {service:<40} | Ortam: {env_tag:<15} | Maliyet: ${cost:.2f}")

Bu script’i çalıştırdığınızda “Environment” tag’i olmayan kaynakların ne kadar tuttuğunu görünce muhtemelen tagging policy oluşturmak isteyeceksiniz. Tag’i olmayan kaynak, sahibi belli olmayan kaynak demektir.

Otomatik Temizleme: Ne Zaman, Nasıl?

Tespit ettik, peki temizleyecek miyiz? Burada dikkatli olmak gerekiyor. Asla doğrudan production ortamında silme scripti çalıştırma. Her temizleme işlemi önce listeleme modunda çalışmalı, onay alınmalı, sonra silinmelidir.

Güvenli bir temizleme workflow’u şöyle olabilir:

#!/bin/bash
# Güvenli EBS volume temizleme scripti
# DRY_RUN=true ile önce test et, sonra gerçeği çalıştır

REGION=${1:-"eu-west-1"}
DRY_RUN=${2:-"true"}
LOG_FILE="/var/log/ebs-cleanup-$(date +%Y%m%d-%H%M%S).log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "EBS Temizleme başlatıldı. Bölge: $REGION | Dry Run: $DRY_RUN"

# Kullanılmayan volume'leri topla
VOLUMES=$(aws ec2 describe-volumes 
    --region $REGION 
    --filters "Name=status,Values=available" 
    --query 'Volumes[?CreateTime<=`'"$(date -d '30 days ago' +%Y-%m-%d)"'`].[VolumeId,Size]' 
    --output text)

if [ -z "$VOLUMES" ]; then
    log "Temizlenecek volume bulunamadı."
    exit 0
fi

TOTAL_GB=0
while read -r VOL_ID SIZE; do
    [ -z "$VOL_ID" ] && continue
    log "Bulundu: $VOL_ID (${SIZE}GB)"
    TOTAL_GB=$((TOTAL_GB + SIZE))
    
    if [ "$DRY_RUN" = "false" ]; then
        # Silmeden önce snapshot al (güvenlik için)
        SNAP_ID=$(aws ec2 create-snapshot 
            --region $REGION 
            --volume-id $VOL_ID 
            --description "pre-deletion-backup-$(date +%Y%m%d)" 
            --query 'SnapshotId' 
            --output text)
        log "Snapshot alındı: $SNAP_ID -> $VOL_ID silinecek"
        
        aws ec2 delete-volume --region $REGION --volume-id $VOL_ID
        log "SİLİNDİ: $VOL_ID"
    else
        log "[DRY RUN] Silinecekti: $VOL_ID"
    fi
done <<< "$VOLUMES"

MONTHLY_SAVINGS=$(echo "$TOTAL_GB * 0.10" | bc)
log "Toplam: ${TOTAL_GB}GB | Tahmini aylık tasarruf: $${MONTHLY_SAVINGS}"
log "Log dosyası: $LOG_FILE"

Bu script’in önemli bir özelliği: silmeden önce snapshot alıyor. “Yanlışlıkla sildim, geri getiremem” felaketine karşı güvenlik ağı. Evet, bu snapshot’lar da para yer ama geçici. Bir hafta sonra onları da temizleyebilirsiniz.

Tagging Policy ve Önleyici Tedbirler

Temizlemek reaktif bir yaklaşım. Asıl hedef, bu kaynakların birikmesini engellemek. Bunun için zorunlu tagging policy şart:

Her kaynağın şu tag’leri taşıması gerekiyor olmalı:

  • Owner: Kaynağı kimin oluşturduğu (e-posta adresi ideal)
  • Environment: production, staging, development, test
  • Project: Hangi projeye ait olduğu
  • ExpiryDate: Test ortamları için zorunlu, ne zaman silinecek?
  • CostCenter: Hangi departman ödüyor?

AWS Organizations ve Service Control Policy kullanıyorsanız, tag’siz kaynak yaratmayı tamamen engelleyebilirsiniz. Ama daha pratik bir başlangıç noktası olarak, tag’siz kaynakları tespit edip sahiplerine mail atmak bile büyük fark yaratır.

Aşağıdaki script, tag’siz kaynakları tespit eder ve bir rapor oluşturur:

#!/bin/bash
# Tag'siz kaynakları tespit et ve rapor oluştur

REGION=${1:-"eu-west-1"}
REQUIRED_TAGS=("Owner" "Environment" "Project")
REPORT_FILE="/tmp/untagged-resources-$(date +%Y%m%d).txt"

echo "Tag'siz Kaynak Raporu - $(date)" > $REPORT_FILE
echo "Bölge: $REGION" >> $REPORT_FILE
echo "Kontrol edilen tag'ler: ${REQUIRED_TAGS[*]}" >> $REPORT_FILE
echo "=" >> $REPORT_FILE

check_tags() {
    local RESOURCE_ID=$1
    local RESOURCE_TYPE=$2
    local TAGS_JSON=$3
    
    for REQUIRED_TAG in "${REQUIRED_TAGS[@]}"; do
        HAS_TAG=$(echo "$TAGS_JSON" | python3 -c "
import json, sys
tags = json.load(sys.stdin)
keys = [t.get('Key', '') for t in tags]
print('yes' if '$REQUIRED_TAG' in keys else 'no')
" 2>/dev/null)
        
        if [ "$HAS_TAG" = "no" ]; then
            echo "MISSING_TAG[$REQUIRED_TAG] | $RESOURCE_TYPE | $RESOURCE_ID" >> $REPORT_FILE
        fi
    done
}

# EC2 Instance'ları kontrol et
echo "EC2 instance'ları kontrol ediliyor..."
aws ec2 describe-instances 
    --region $REGION 
    --query 'Reservations[].Instances[].[InstanceId,Tags]' 
    --output json | python3 -c "
import json, sys
data = json.load(sys.stdin)
for item in data:
    inst_id = item[0]
    tags = item[1] if item[1] else []
    tag_keys = [t['Key'] for t in tags]
    missing = [t for t in ['Owner','Environment','Project'] if t not in tag_keys]
    if missing:
        print(f'EC2 | {inst_id} | Eksik tag: {missing}')
" | tee -a $REPORT_FILE

TOTAL_ISSUES=$(grep -c "MISSING_TAG|EC2|EBS" $REPORT_FILE 2>/dev/null || echo 0)
echo ""
echo "Rapor hazır: $REPORT_FILE"
echo "Toplam sorunlu kaynak: $TOTAL_ISSUES"

Lambda ile Otomatik Scheduler: Hayalet Kaynakları Önlemek

Geliştirme ortamlarında geceleri ve hafta sonları instance’ların çalışması genellikle gereksiz. Bir Lambda fonksiyonu ile mesai saatleri dışında development instance’larını otomatik olarak durdurabilirsiniz. Bu tek başına yüzde otuz ile elli arasında tasarruf sağlayabilir:

import boto3
import os
from datetime import datetime

def lambda_handler(event, context):
    """
    Her gün 20:00'de development instance'larını durdur
    Her gün 08:00'de yeniden başlat
    CloudWatch Events ile tetiklenir
    """
    ec2 = boto3.client('ec2')
    action = event.get('action', 'stop')  # 'stop' veya 'start'
    
    # Sadece development ortam instance'larını hedef al
    filters = [
        {'Name': 'tag:Environment', 'Values': ['development', 'dev', 'test']},
        {'Name': 'instance-state-name', 
         'Values': ['running'] if action == 'stop' else ['stopped']}
    ]
    
    response = ec2.describe_instances(Filters=filters)
    
    instance_ids = []
    for reservation in response['Reservations']:
        for instance in reservation['Instances']:
            instance_ids.append(instance['InstanceId'])
    
    if not instance_ids:
        print(f"Hiç uygun instance bulunamadı. Aksiyon: {action}")
        return {'statusCode': 200, 'body': 'No instances found'}
    
    print(f"İşlem yapılacak instance'lar: {instance_ids}")
    
    if action == 'stop':
        ec2.stop_instances(InstanceIds=instance_ids)
        print(f"{len(instance_ids)} instance durduruldu")
    elif action == 'start':
        ec2.start_instances(InstanceIds=instance_ids)
        print(f"{len(instance_ids)} instance başlatıldı")
    
    return {
        'statusCode': 200,
        'body': f"{action} işlemi tamamlandı. Etkilenen: {len(instance_ids)} instance"
    }

Bu Lambda’yı iki ayrı CloudWatch Events rule’u ile tetikleyin: biri stop için (20:00 UTC), biri start için (08:00 UTC). Event payload’ında {"action": "stop"} veya {"action": "start"} gönderin.

Gerçek Dünya Senaryosu: Bir Startup’ın 3000 Dolarlık Tasarruf Hikayesi

Bir müşteri şirketle çalışırken şöyle bir tablo gördüm: 15 kişilik bir startup, AWS faturası ayda 8.000 dolara çıkmış ve neden bu kadar olduğunu anlamıyorlardı. Bir haftalık inceleme sonucunda:

  • 23 adet “available” durumda EBS volume (toplam 2.3 TB) bulundu. Hepsi ya silinmiş ya da migrate edilmiş instance’lardan kalma.
  • 11 adet kullanılmayan Elastic IP adresi.
  • 4 yıllık RDS snapshot’lar birikmiş, kimse silmemiş. Sadece bu snapshot’lar aylık 600 dolar yiyordu.
  • 3 adet ALB, arkalarında hiç sağlıklı target olmadan çalışıyordu.
  • Development ortamı 7/24 ayakta, hafta sonları dahil.

Temizlik ve scheduler sonrasında aylık fatura 4.800 dolara düştü. Üç bin iki yüz dolar tasarruf, aylık. Yıllık hesaplarsanız, bu rakamın ne anlama geldiğini tahmin edebilirsiniz.

Periyodik Temizlik Rutini Oluşturmak

Bu işi bir kere yapmak yetmez. Aylık ya da iki haftada bir çalışan bir rutin şart. Önerdiğim kontrol listesi:

  • Her ayın ilk Pazartesi günü kullanılmayan EBS volume’leri kontrol et
  • 90 günden eski snapshot’ları incele, ihtiyaç olmayanlara son ver
  • Elastic IP listesini gözden geçir
  • Tag’siz kaynaklar raporunu ekip liderine mail at
  • AWS Trusted Advisor önerilerini incele (Business veya Enterprise support plan gerektirir)
  • Development ortamı scheduler’larının çalışıp çalışmadığını doğrula
  • Cost Explorer’da önceki aya göre anormal artışları tespit et

Bu rutini bir runbook olarak dokümante edin ve ekipte rotation ile yapın. Böylece hem bilgi paylaşımı olur hem de sorumlu tek kişi olmaz.

AWS Trusted Advisor ve Compute Optimizer

Eğer Business ya da Enterprise support plan kullanıyorsanız, Trusted Advisor’ın “Cost Optimization” kategorisi harika bir başlangıç noktası. Kullanılmayan veya düşük kullanımlı EC2 instance’larını, idle load balancer’ları ve kullanılmayan Elastic IP’leri otomatik olarak tespit eder.

AWS Compute Optimizer ise farklı bir açıdan yaklaşıyor: mevcut kaynaklarınızın kullanım metriklerini analiz edip “bu instance aslında daha küçük olabilir” veya “şu EBS volume’ü gp3’e geçirirsen hem ucuzlar hem hızlanır” gibi öneriler sunuyor. Tamamen ücretsiz (basic tier), aktif etmeniz yeterli.

Compute Optimizer’dan maksimum fayda için şunları yapın:

  • En az 14 günlük metrik birikimine izin verin
  • Enhanced Infrastructure Metrics aktif edin (ücretli ama daha doğru)
  • Önerileri AWS CLI ile otomatik çekip raporlara dahil edin

Sonuç

Bulut maliyet optimizasyonu tek seferlik bir proje değil, sürekli bir disiplin. Teknik araçlar ve script’ler işin sadece bir kısmı. Asıl mesele, ekip içinde bir “bulut sahiplik kültürü” oluşturmak: herkes oluşturduğu kaynaktan sorumlu, tag’siz kaynak yaratılmıyor, test ortamları için expiry date konuluyor.

Bugün yapabileceğiniz en pratik üç adım:

  • Hemen şimdi: Yukarıdaki available EBS ve kullanılmayan EIP script’ini çalıştırın, sonuçlara bakın.
  • Bu hafta: Tagging policy tanımlayın, mevcut kaynaklara geriye dönük tag’leme yapın.
  • Bu ay: Development ortamları için start/stop scheduler kurun ve maliyet anomaly detection aktif edin.

Faturanızdaki “sessiz gürültü”yü kesmek hem para kazandırır hem de ortamınızı daha yönetilebilir hale getirir. Kullanılmayan kaynak, sizi yavaşlatan teknik borçtan farksız; ne kadar erken temizlerseniz o kadar iyi.

Bir yanıt yazın

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