İçeriğe atla

AWS Maliyet Optimizasyonu Araç Seti - Production Workload'lar için Pratik Stratejiler

AWS maliyet çalışmasının sırası: önce Cost Explorer ve Budgets ile görünürlük, sonra Compute Optimizer ile right-sizing, ardından commitment kararı.

Ayhan Sipahi Ayhan Sipahi

AWS maliyet çalışmasının bir varsayılan sırası var: önce görünürlük, sonra right-sizing, en son commitment. Cost Explorer ve Budgets paranın nereye gittiğini gösterir, Compute Optimizer kimsenin kullanmadığı kapasiteyi işaretler, Savings Plans veya Reserved Instances de bu iki adımdan geriye kalanı indirime bağlar. Sırayı ters çeviren ekipler, aslında silmeleri gereken kapasiteyi indirimli fiyattan satın almış olur.

Ekipleri bu işe iten belirtiler production hesaplarında birbirine benziyor: trafikte karşılığı olmadan oynayan aylık faturalar, yalnızca mesai saatlerinde gerekirken 7/24 açık duran development kaynakları ve CPU’su boşta dururken tam kapasite faturalanan instance’lar.

Tekrar Eden Beş Maliyet Sürücüsü#

AWS’nin native maliyet araçları yeterli; eksik bir özellik nadiren engel oluyor. Asıl soru hangi belirtiye hangi aracın cevap verdiği ve bunları hangi sırayla uygulayacağın.

Neredeyse her production hesabında beş pattern tekrar ediyor:

  • Maliyet öngörülemezliği: İş büyümesine karşılık gelmeden önemli ölçüde değişen aylık faturalar
  • Idle kaynak israfı: İş saatleri dışında bütçe yakan non-production kaynaklar
  • Over-provisioned instance’lar: Nadiren kullanılan kapasite için ödeme
  • Commitment paralizi: Reserved Instances, Savings Plans veya Spot Instances arasında seçim yapma zorluğu
  • Attribution eksikliği: Hangi projelerin veya ekiplerin AWS harcamasını yönlendirdiğini track edememe

Temel: Cost Explorer ve AWS Budgets#

Maliyetleri optimize etmeden önce görünürlüğe ihtiyacın var. Cost Explorer ve AWS Budgets, paranın nereye gittiğini anlamak ve anomalileri erken yakalamak için temeli kuruyor.

Cost Explorer’a Yakından Bakış#

Cost Explorer 12 aylık geçmiş veri ve 12 aya kadar forecasting sunuyor. İşte maliyet trendlerini analiz etmek ve anomalileri tespit etmek için pratik bir implementation:

import boto3
from datetime import datetime, timedelta

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

def analyze_cost_trends(months_back=3):
    """
    Servisler arasında maliyet trendlerini analiz et ve anomalileri tespit et
    Ay bazında %20'den fazla maliyet değişikliği olan servisleri döndürür
    """
    end_date = datetime.now().date()
    start_date = (datetime.now() - timedelta(days=30 * months_back)).date()

    response = ce_client.get_cost_and_usage(
        TimePeriod={
            'Start': start_date.strftime('%Y-%m-%d'),
            'End': end_date.strftime('%Y-%m-%d')
        },
        Granularity='MONTHLY',
        Metrics=['UnblendedCost'],
        GroupBy=[
            {'Type': 'DIMENSION', 'Key': 'SERVICE'},
        ]
    )

    # Sonuçları işle
    cost_by_service = {}
    for result in response['ResultsByTime']:
        period = result['TimePeriod']['Start']
        for group in result['Groups']:
            service = group['Keys'][0]
            cost = float(group['Metrics']['UnblendedCost']['Amount'])

            if service not in cost_by_service:
                cost_by_service[service] = []
            cost_by_service[service].append({
                'period': period,
                'cost': cost
            })

    # %20'den fazla maliyet artışı olan servisleri tespit et
    trending_services = []
    for service, costs in cost_by_service.items():
        if len(costs) >= 2:
            recent_cost = costs[-1]['cost']
            previous_cost = costs[-2]['cost']

            if previous_cost > 0:
                change_pct = ((recent_cost - previous_cost) / previous_cost) * 100

                if abs(change_pct) > 20:
                    trending_services.append({
                        'service': service,
                        'change': change_pct,
                        'current_cost': recent_cost,
                        'previous_cost': previous_cost
                    })

    return sorted(trending_services, key=lambda x: abs(x['change']), reverse=True)

Bu script AWS servisleri arasında maliyet anomalilerini tespit ediyor. Haftalık çalıştırmak, yanlış configure edilmiş Auto Scaling group’ları veya unutulmuş test kaynaklarını önemli maliyet birikmeden yakalar.

Takip Edilmeyen Maliyetleri Bulmak#

En ucuz optimizasyon çoğu zaman kimsenin takip etmediği harcamayı bulmak. Tag’lenmemiş harcama toplamın içinde saklanır ve tag’e göre filtrelenen hiçbir dashboard onu ayrı göstermez:

def analyze_cost_allocation_gaps():
    """
    Cost allocation için düzgün tag'lenmemiş maliyetleri tespit et
    """
    end_date = datetime.now().date()
    start_date = (datetime.now() - timedelta(days=30)).date()

    # Tag'lere göre maliyetleri kontrol et
    tagged_response = ce_client.get_cost_and_usage(
        TimePeriod={
            'Start': start_date.strftime('%Y-%m-%d'),
            'End': end_date.strftime('%Y-%m-%d')
        },
        Granularity='MONTHLY',
        Metrics=['UnblendedCost'],
        GroupBy=[
            {'Type': 'TAG', 'Key': 'Project'},
        ]
    )

    total_tagged_cost = 0
    for result in tagged_response['ResultsByTime']:
        for group in result['Groups']:
            if group['Keys'][0]:  # Project tag'i var
                total_tagged_cost += float(group['Metrics']['UnblendedCost']['Amount'])

    # Toplam maliyeti al
    total_response = ce_client.get_cost_and_usage(
        TimePeriod={
            'Start': start_date.strftime('%Y-%m-%d'),
            'End': end_date.strftime('%Y-%m-%d')
        },
        Granularity='MONTHLY',
        Metrics=['UnblendedCost']
    )

    total_cost = float(total_response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])
    untagged_cost = total_cost - total_tagged_cost
    untagged_percentage = (untagged_cost / total_cost) * 100

    return {
        'total_cost': total_cost,
        'tagged_cost': total_tagged_cost,
        'untagged_cost': untagged_cost,
        'untagged_percentage': untagged_percentage
    }

Cost allocation tag’lerine güvenmeden önce Billing Console’da aktif et. Tag’ler yalnızca aktivasyondan sonraki maliyetleri track ediyor; retroaktif tag’leme diye bir şey yok.

AWS Budgets ve Otomatik Aksiyonlar#

Budget’lar proaktif maliyet monitoring sağlıyor. İşte birden fazla alert eşiği olan production-ready bir implementation:

import boto3

budgets_client = boto3.client('budgets')

def create_department_budget_with_alerts(
    account_id: str,
    department: str,
    monthly_limit: float
):
    """
    Birden fazla alert eşiği ve SNS notification'larıyla budget oluştur
    %70 = Info, %90 = Warning, %100 = Critical, Forecasted = Predictive
    """
    budget_name = f"{department}-monthly-budget"

    budgets_client.create_budget(
        AccountId=account_id,
        Budget={
            'BudgetName': budget_name,
            'BudgetLimit': {
                'Amount': str(monthly_limit),
                'Unit': 'USD'
            },
            'TimeUnit': 'MONTHLY',
            'BudgetType': 'COST',
            'CostFilters': {
                'TagKeyValue': [f'user:Department${department}']
            }
        },
        NotificationsWithSubscribers=[
            # %70 eşiği - Info alert
            {
                'Notification': {
                    'NotificationType': 'ACTUAL',
                    'ComparisonOperator': 'GREATER_THAN',
                    'Threshold': 70.0,
                    'ThresholdType': 'PERCENTAGE',
                    'NotificationState': 'ALARM'
                },
                'Subscribers': [
                    {
                        'SubscriptionType': 'SNS',
                        'Address': f'arn:aws:sns:us-east-1:{account_id}:budget-alerts-info'
                    }
                ]
            },
            # %90 eşiği - Warning alert
            {
                'Notification': {
                    'NotificationType': 'ACTUAL',
                    'ComparisonOperator': 'GREATER_THAN',
                    'Threshold': 90.0,
                    'ThresholdType': 'PERCENTAGE',
                    'NotificationState': 'ALARM'
                },
                'Subscribers': [
                    {
                        'SubscriptionType': 'SNS',
                        'Address': f'arn:aws:sns:us-east-1:{account_id}:budget-alerts-warning'
                    }
                ]
            },
            # %100 eşiği - Critical alert
            {
                'Notification': {
                    'NotificationType': 'ACTUAL',
                    'ComparisonOperator': 'GREATER_THAN',
                    'Threshold': 100.0,
                    'ThresholdType': 'PERCENTAGE',
                    'NotificationState': 'ALARM'
                },
                'Subscribers': [
                    {
                        'SubscriptionType': 'SNS',
                        'Address': f'arn:aws:sns:us-east-1:{account_id}:budget-alerts-critical'
                    },
                    {
                        'SubscriptionType': 'EMAIL',
                        'Address': f'{department}-leads@company.com'
                    }
                ]
            },
            # Aşma tahmini - Predictive alert
            {
                'Notification': {
                    'NotificationType': 'FORECASTED',
                    'ComparisonOperator': 'GREATER_THAN',
                    'Threshold': 100.0,
                    'ThresholdType': 'PERCENTAGE',
                    'NotificationState': 'ALARM'
                },
                'Subscribers': [
                    {
                        'SubscriptionType': 'SNS',
                        'Address': f'arn:aws:sns:us-east-1:{account_id}:budget-forecast-alerts'
                    }
                ]
            }
        ]
    )

    print(f"Budget oluşturuldu: {budget_name} - ${monthly_limit} aylık limit")

Budget eşikleri günde yaklaşık üç kez değerlendiriliyor; bu da bir anomaliyi günlük email özetinden çok daha önce yakalıyor. FORECASTED alert’ler AWS’nin tahmin modelini kullanıyor ve bir şey üretebilmek için en az 5 haftalık geçmiş veriye ihtiyaç duyuyor.

Kaçınılması gereken yaygın hatalar:

  • Çok fazla budget oluşturmak alert fatigue’e neden olur; ana maliyet merkezlerine odaklan
  • FORECASTED alert’leri 5+ haftalık geçmiş veri gerektirdiğini anlamadan kullanmak
  • Tag’lere göre budget filtrelemeden önce cost allocation tag’lerini aktif etmemek
  • Tag’lenmemiş kaynak maliyetlerini göz ardı etmek; tag’e göre filtrelenen her raporda görünmez kalırlar

Compute Optimizer ile Right-Sizing#

AWS Compute Optimizer, CloudWatch metriklerini analiz etmek ve optimal instance type’ları, Lambda memory konfigürasyonları ve EBS volume’ları önermek için machine learning kullanıyor. Bu tipik olarak düşük implementation riski ile %20-40 maliyet tasarrufu sağlıyor.

Otomatik Right-Sizing Uygulaması#

import boto3
from typing import List, Dict
from dataclasses import dataclass

compute_optimizer = boto3.client('compute-optimizer')
ec2_client = boto3.client('ec2')

@dataclass
class RightsizingRecommendation:
    instance_id: str
    current_type: str
    recommended_type: str
    current_cost_monthly: float
    recommended_cost_monthly: float
    savings_monthly: float
    cpu_utilization_avg: float
    memory_utilization_avg: float

def get_underutilized_instances(
    max_cpu_threshold: float = 40.0,
    lookback_days: int = 14
) -> List[RightsizingRecommendation]:
    """
    Underutilized instance'lar için Compute Optimizer önerilerini getir
    Default: 14 gün boyunca ortalama <%40 CPU kullanan instance'lar
    """
    recommendations = []

    # EC2 instance önerilerini al
    paginator = compute_optimizer.get_paginator('get_ec2_instance_recommendations')

    for page in paginator.paginate():
        for rec in page.get('instanceRecommendations', []):
            instance_id = rec['instanceArn'].split('/')[-1]
            current_instance_type = rec['currentInstanceType']

            # Utilization metriklerini al
            cpu_util = next(
                (m['value'] for m in rec.get('utilizationMetrics', [])
                 if m['name'] == 'CPU'),
                0.0
            )

            memory_util = next(
                (m['value'] for m in rec.get('utilizationMetrics', [])
                 if m['name'] == 'MEMORY'),
                0.0
            )

            # Instance underutilized mı kontrol et
            if cpu_util < max_cpu_threshold:
                # En iyi öneri seçeneğini al
                if rec.get('recommendationOptions'):
                    best_option = rec['recommendationOptions'][0]

                    # Tasarrufu hesapla
                    estimated_savings = best_option.get('estimatedMonthlySavings', {}).get('value', 0)

                    recommendations.append(RightsizingRecommendation(
                        instance_id=instance_id,
                        current_type=current_instance_type,
                        recommended_type=best_option['instanceType'],
                        current_cost_monthly=estimated_savings,
                        recommended_cost_monthly=0,
                        savings_monthly=estimated_savings,
                        cpu_utilization_avg=cpu_util,
                        memory_utilization_avg=memory_util
                    ))

    # Potansiyel tasarrufa göre sırala (en yüksek önce)
    return sorted(recommendations, key=lambda x: x.savings_monthly, reverse=True)

Right-Sizing Önerilerini Uygulama#

Öneriyi uygulamanın dikkatli yolu şöyle; instance’ın önce durdurulmuş olması gerekiyor:

def apply_rightsizing_recommendation(
    instance_id: str,
    new_instance_type: str,
    dry_run: bool = True
) -> Dict:
    """
    Instance type değiştirerek right-sizing önerisini uygula
    Instance'ın önce durdurulması gerekir
    """
    try:
        response = ec2_client.describe_instances(InstanceIds=[instance_id])
        instance_state = response['Reservations'][0]['Instances'][0]['State']['Name']

        if instance_state != 'stopped':
            return {
                'success': False,
                'message': f'Instance durdurulmuş olmalı. Mevcut durum: {instance_state}'
            }

        ec2_client.modify_instance_attribute(
            InstanceId=instance_id,
            InstanceType={'Value': new_instance_type},
            DryRun=dry_run
        )

        return {
            'success': True,
            'message': f'{instance_id} başarıyla {new_instance_type} olarak değiştirildi',
            'instance_id': instance_id,
            'new_type': new_instance_type
        }

    except Exception as e:
        return {
            'success': False,
            'message': f'Değişiklik başarısız: {str(e)}'
        }

Bu önerileri hızlı yaymaktan çok güvenli yaymak önemli. Önce development’ta test et, sonra production instance’larının %10’una uygula, bir hafta monitor et, ardından kapsamı genişlet.

Lambda Memory Optimizasyonu#

Compute Optimizer Lambda fonksiyonlarını da analiz ediyor. İşte Lambda-spesifik öneriler nasıl alınır:

def get_lambda_optimization_recommendations():
    """
    Lambda memory konfigürasyon önerilerini al
    Performans etkisi olmadan memory azaltılabilen fonksiyonları döndürür
    """
    paginator = compute_optimizer.get_paginator('get_lambda_function_recommendations')

    recommendations = []

    for page in paginator.paginate():
        for rec in page.get('lambdaFunctionRecommendations', []):
            function_arn = rec['functionArn']
            current_memory = rec['currentMemorySize']

            if rec.get('recommendationOptions'):
                best_option = rec['recommendationOptions'][0]
                recommended_memory = best_option['memorySize']

                # Sadece öneri mevcut'tan farklıysa dahil et
                if recommended_memory != current_memory:
                    estimated_savings = best_option.get('estimatedMonthlySavings', {})

                    recommendations.append({
                        'function_arn': function_arn,
                        'function_name': function_arn.split(':')[-1],
                        'current_memory_mb': current_memory,
                        'recommended_memory_mb': recommended_memory,
                        'savings_monthly': estimated_savings.get('value', 0),
                        'savings_currency': estimated_savings.get('currency', 'USD')
                    })

    return sorted(recommendations, key=lambda x: x['savings_monthly'], reverse=True)

Compute Optimizer default olarak son 14 günlük CloudWatch metriklerini analiz ediyor. Aylık kullanım pattern’leri olan production workload’lar için enhanced infrastructure metrics’i aktif et (kaynak başına saat başına $0.0003360215, kaynak başına ayda ~$0.25 ekler) ve lookback period’u 93 güne çıkar.

Compute Optimizer ile yaygın hatalar:

  • Maintenance window olmadan iş saatlerinde öneriler uygulama
  • Önerilen instance type’ların uyumluluk için test edilmemesi (bazı type’lar tüm özellikleri desteklemez)
  • “Under-provisioned” uyarılarını göz ardı etme; maliyet tasarrufu performanstan ödün vermemeli
  • Production workload’lar için enhanced metrics aktif etmeme; 14 gün aylık spike’ları kaçırabilir

Commitment Stratejisi: Savings Plans vs Reserved Instances#

Reserved Instances, Savings Plans veya on-demand’da kalma arasında seçim yapmak, workload karakteristiklerini anlamayı gerektiriyor. İşte bir karar framework’ü:

Hayir

Evet

Yuksek Ongoerulebilir Ayni Instance Family

Esnek Birden Fazla Servis

Sadece EC2 Family Degisebilir

Workload Analizi

Workload 1-3 Yil Stabil mi?

On-Demand veya Spot Instances Kullan

Kullanim Paterni Ongoeruelebilir mi?

Reserved Instances %72'ye Kadar Tasarruf

Compute Savings Plans %66'ya Kadar Tasarruf

EC2 Instance Savings Plans %72'ye Kadar Tasarruf

Odeme Secenegi?

All Upfront Maximum Indirim

Partial Upfront Dengeli

No Upfront Minimum Indirim

Önerileri Programatik Olarak Almak#

AWS’nin commitment önerilerini koddan nasıl alırsın:

import boto3
from typing import List, Dict
from dataclasses import dataclass

ce_client = boto3.client('ce')

@dataclass
class CommitmentRecommendation:
    recommendation_type: str  # 'RI' veya 'SavingsPlan'
    service: str
    term: str  # '1_YEAR' veya '3_YEAR'
    payment_option: str
    monthly_commitment: float
    estimated_savings: float
    estimated_savings_percentage: float

def get_savings_plan_recommendations(
    term_years: int = 1,
    payment_option: str = 'NO_UPFRONT'
) -> List[CommitmentRecommendation]:
    """
    Savings Plans satın alma önerilerini al
    Son 30 günlük kullanım pattern'lerine dayalı
    """
    response = ce_client.get_savings_plans_purchase_recommendation(
        SavingsPlansType='COMPUTE_SP',  # veya 'EC2_INSTANCE_SP'
        TermInYears=f'{term_years}_YEAR',
        PaymentOption=payment_option,
        LookbackPeriodInDays='THIRTY_DAYS',
        AccountScope='PAYER'
    )

    recommendations = []

    for rec in response.get('SavingsPlansPurchaseRecommendation', {}).get('SavingsPlansPurchaseRecommendationDetails', []):
        savings_details = rec.get('SavingsPlansDetails', {})

        recommendations.append(CommitmentRecommendation(
            recommendation_type='SavingsPlan',
            service='Compute',
            term=f'{term_years}_YEAR',
            payment_option=payment_option,
            monthly_commitment=float(rec.get('HourlyCommitmentToPurchase', 0)) * 730,
            estimated_savings=float(rec.get('EstimatedMonthlySavingsAmount', 0)),
            estimated_savings_percentage=float(rec.get('EstimatedSavingsPercentage', 0))
        ))

    return recommendations

Karşılaştırma Çerçevesi#

Değişken stabiliteye sahip workload’lar için karşılaştırma motoru:

def compare_commitment_options(
    monthly_spend: float,
    workload_stability: str  # 'stable', 'variable', 'mixed'
) -> Dict:
    """
    Workload karakteristiklerine göre commitment stratejilerini karşılaştır
    """
    if workload_stability == 'stable':
        return {
            'recommendation': 'Reserved Instances',
            'reason': 'Stabil, öngörülebilir workload\'lar için en yüksek indirim',
            'expected_savings': monthly_spend * 0.72,  # 3 yıllık RI ile %72'ye kadar
            'flexibility': 'Düşük - instance family ve region\'a kilitli',
            'best_for': 'Production veritabanları, sürekli çalışan servisler'
        }
    elif workload_stability == 'variable':
        return {
            'recommendation': 'Compute Savings Plans',
            'reason': 'Instance family, region ve compute servisleri arasında esneklik',
            'expected_savings': monthly_spend * 0.66,
            'flexibility': 'Yüksek - EC2, Fargate, Lambda\'ya uygulanır',
            'best_for': 'Çok servisli mimariler, evrimleşen workload\'lar'
        }
    else:
        stable_portion = monthly_spend * 0.6
        variable_portion = monthly_spend * 0.4
        return {
            'recommendation': 'Hibrit Strateji',
            'reason': 'Baseline için RI, esneklik için Savings Plans birleştir',
            'breakdown': {
                'reserved_instances': {
                    'monthly_commitment': stable_portion,
                    'savings': stable_portion * 0.72
                },
                'savings_plans': {
                    'monthly_commitment': variable_portion,
                    'savings': variable_portion * 0.66
                }
            },
            'expected_total_savings': (stable_portion * 0.72) + (variable_portion * 0.66),
            'flexibility': 'Dengeli - her iki senaryo için optimize'
        }

Üç commitment tipinin farkı:

  • Reserved Instances: %72’ye kadar tasarruf, ama belirli instance family ve region’a kilitli. İhtiyaçlar değişirse RI Marketplace’te satılabilir.
  • Compute Savings Plans: %66’ya kadar tasarruf, herhangi bir region veya instance family’de EC2, Fargate ve Lambda’ya uygulanır. Maximum esneklik.
  • EC2 Instance Savings Plans: %72’ye kadar tasarruf, instance family ve region içinde esnek. RI’lar ve Compute SP’ler arası orta yol.
  • Ödeme seçenekleri: All Upfront (en yüksek indirim), Partial Upfront (dengeli), No Upfront (en düşük indirim ama sermaye commitment’ı yok)

Savings Plans satın alması artık geri dönülmez bir karar olmaktan çıktı. AWS kısıtlamalarla birlikte 7 günlük iade penceresi sunuyor (saatlik commitment $100 veya altı, iadeler aynı takvim ayı içinde, yılda en fazla 10 iade); bu da ilk satın almanın stresini azaltıyor.

Commitment’larla yaygın hatalar:

  • Baseline yerine peak kullanıma dayalı over-commitment; kullanılmayan commitment’lar sonucu
  • Teknoloji evrimini düşünmeden 3 yıllık term’ler seçmek; instance type’lar hızla gelişiyor
  • Satın alma sonrası RI/SP kullanımını monitor etmemek; underutilized commitment’lar para israfı
  • Net strateji olmadan RI’ları ve SP’leri karıştırmak; coverage gap’leri veya overlap’lere yol açabilir

Temkinli bir ramp, hataların maliyetini düşük tutar: 1. ayda baseline kullanımın %40’ını kapsa, utilization %95’i aşarsa %60’a çıkar, uzun vadede %70-80 coverage hedefle. Kalan %20-30 esneklik ve büyüme için on-demand kalsın.

Batch Workload’lar için Spot Instances#

Spot Instances %70-90 maliyet tasarrufu sunuyor ama interruption-resilient mimari gerektiriyor. İşte ne zaman ve nasıl etkili kullanılır:

Hayir

Evet

Kisa Task'ler 2 Saatten Az

Uzun Sureli Persistence Gerekli

Workload Siniflandirmasi

Interruption Toleransli mi?

On-Demand veya Reserved Instances Kullan

Beklenen Sure?

Spot Instances %70-90 Tasarruf

Checkpointing ile Spot

Instance Diversification

Birden Fazla Instance Type Birden Fazla AZ

Capacity-Optimized Allocation Strategy

2 Dakika Uyari Interruption Handler

Çeşitlendirme ile Spot Fleet#

Spot Instance dayanıklılığının anahtarı instance type’ları ve availability zone’ları arasında diversification:

import boto3
from typing import List, Dict

ec2_client = boto3.client('ec2')
autoscaling_client = boto3.client('autoscaling')

def create_diversified_spot_fleet(
    target_capacity: int,
    instance_types: List[str],
    subnets: List[str],
    user_data_script: str
) -> str:
    """
    Diversified Spot instance'larla EC2 Auto Scaling group oluştur
    Interruption'ları minimize etmek için capacity-optimized allocation strategy kullanır
    """
    # Launch template konfigürasyonu
    launch_template_overrides = []

    for instance_type in instance_types:
        for subnet in subnets:
            launch_template_overrides.append({
                'InstanceType': instance_type,
                'SubnetId': subnet,
                'WeightedCapacity': 1.0
            })

    # Mixed instances policy ile Auto Scaling group oluştur
    asg_config = {
        'AutoScalingGroupName': 'spot-optimized-asg',
        'MinSize': target_capacity,
        'MaxSize': target_capacity * 2,
        'DesiredCapacity': target_capacity,
        'VPCZoneIdentifier': ','.join(subnets),
        'MixedInstancesPolicy': {
            'InstancesDistribution': {
                'OnDemandBaseCapacity': 0,  # Tüm Spot instance'lar
                'OnDemandPercentageAboveBaseCapacity': 0,
                'SpotAllocationStrategy': 'capacity-optimized',
                'SpotInstancePools': len(instance_types) * len(subnets)
            },
            'LaunchTemplate': {
                'LaunchTemplateSpecification': {
                    'LaunchTemplateName': 'spot-fleet-template',
                    'Version': '$Latest'
                },
                'Overrides': launch_template_overrides
            }
        },
        'Tags': [
            {
                'Key': 'CostOptimization',
                'Value': 'SpotInstances',
                'PropagateAtLaunch': True
            }
        ]
    }

    response = autoscaling_client.create_auto_scaling_group(**asg_config)

    return asg_config['AutoScalingGroupName']

Buradaki her şey çeşitlendirmeye bağlı. Capacity-optimized allocation strategy kullan ve 10+ instance type ile 3+ availability zone arasında dağıt. Tek bir instance type’a sabitlenmiş fleet tek bir havuz için yarışır; o havuz daraldığı anda geri alınır.

Kesinti Yönetimi#

Spot instance’lar sonlandırmadan önce EventBridge üzerinden 2 dakika uyarı verir. Graceful shutdown için Lambda fonksiyonu:

# Spot kesintisi için Lambda fonksiyonu
import boto3
import json

ec2_client = boto3.client('ec2')
sqs_client = boto3.client('sqs')

def lambda_handler(event, context):
    '''
    EC2 Spot Instance kesinti uyarılarını işle (2 dakika bildirim)
    Strateji: Task'ları boşalt ve işi kuyruğa geri gönder
    '''
    detail = event.get('detail', {})
    instance_id = detail.get('instance-id')
    instance_action = detail.get('instance-action')

    if not instance_id:
        return {'statusCode': 400, 'body': 'Olayda instance ID yok'}

    # Instance detaylarını ve tag'lerini al
    response = ec2_client.describe_instances(InstanceIds=[instance_id])
    instance = response['Reservations'][0]['Instances'][0]
    task_queue_url = get_tag_value(instance.get('Tags', []), 'TaskQueueUrl')

    if task_queue_url:
        sqs_client.send_message(
            QueueUrl=task_queue_url,
            MessageBody=json.dumps({
                'action': 'drain_instance',
                'instance_id': instance_id,
                'interruption_time': detail.get('time')
            })
        )

    return {'statusCode': 200, 'body': f'{instance_id} kesintisi işlendi'}

def get_tag_value(tags: list, key: str) -> str:
    for tag in tags:
        if tag['Key'] == key:
            return tag['Value']
    return None

Uzun Süren İşler için Checkpoint#

2 saatten uzun job’lar için checkpoint ile kesinti sonrası kaldığı yerden devam et:

def save_checkpoint(state, bucket, prefix='checkpoints'):
    """Job checkpoint'ini S3'e kaydet - Spot kesintisi sonrası kurtarma için"""
    checkpoint_key = f"{prefix}/{state.job_id}/checkpoint-{state.current_step}.pkl"
    s3_client.put_object(
        Bucket=bucket,
        Key=checkpoint_key,
        Body=pickle.dumps(state),
        ServerSideEncryption='AES256'
    )

def restore_checkpoint(job_id, bucket, prefix='checkpoints'):
    """Kesintiye uğrayan job için son checkpoint'i geri yükle"""
    response = s3_client.list_objects_v2(Bucket=bucket, Prefix=f"{prefix}/{job_id}/")
    latest = sorted(response['Contents'], key=lambda x: x['LastModified'], reverse=True)[0]
    obj = s3_client.get_object(Bucket=bucket, Key=latest['Key'])
    return pickle.loads(obj['Body'].read())

Spot nereye oturur:

  • İyi uyum: batch işleme, CI/CD runner’ları, ML training, veri analizi, container workload’ları
  • Kötü uyum: fallback’i olmayan user-facing uygulamalar, checkpoint’siz stateful uygulamalar
  • Çeşitlendirme: benzer CPU/memory oranına sahip type’ları seç (genel amaçlı compute için m5, m5a, m5n, m6i, m6a)
  • Checkpoint sıklığı: 30 dakikadan uzun job’lar için 5-10 dakikada bir

S3 Depolama Optimizasyonu#

S3 depolama maliyetleri Intelligent-Tiering ve lifecycle policy’leri ile %40-95 azaltılabilir. Etkili implementation:

def create_intelligent_lifecycle_policy(
    bucket_name: str,
    prefix: str = '',
    enable_deep_archive_tier: bool = True
) -> Dict:
    """
    Intelligent-Tiering'e geçiş ve opsiyonel deep archive tier ile
    S3 lifecycle policy oluştur
    """
    lifecycle_rules = [
        {
            'ID': 'transition-to-intelligent-tiering',
            'Filter': {'Prefix': prefix},
            'Status': 'Enabled',
            'Transitions': [{'Days': 0, 'StorageClass': 'INTELLIGENT_TIERING'}]
        },
        {
            'ID': 'cleanup-incomplete-multipart-uploads',
            'Filter': {'Prefix': prefix},
            'Status': 'Enabled',
            'AbortIncompleteMultipartUpload': {'DaysAfterInitiation': 7}
        },
        {
            'ID': 'expire-old-versions',
            'Filter': {'Prefix': prefix},
            'Status': 'Enabled',
            'NoncurrentVersionExpiration': {'NoncurrentDays': 90}
        }
    ]

    s3_client.put_bucket_lifecycle_configuration(
        Bucket=bucket_name,
        LifecycleConfiguration={'Rules': lifecycle_rules}
    )

    if enable_deep_archive_tier:
        s3_client.put_bucket_intelligent_tiering_configuration(
            Bucket=bucket_name,
            Id='deep-archive-config',
            IntelligentTieringConfiguration={
                'Id': 'deep-archive-config',
                'Status': 'Enabled',
                'Tierings': [
                    {'Days': 90, 'AccessTier': 'ARCHIVE_ACCESS'},
                    {'Days': 180, 'AccessTier': 'DEEP_ARCHIVE_ACCESS'}
                ]
            }
        )

    return {'bucket': bucket_name, 'rules_applied': len(lifecycle_rules)}

Intelligent-Tiering pratikte ne yapıyor:

  • Otomatik erişim katmanları: Frequent Access, Infrequent Access (30 gün), Archive Instant Access (90 gün), opsiyonel Archive Access (90 gün), opsiyonel Deep Archive Access (180 gün)
  • Tasarruf: Archive Instant Access ile %68’e kadar, Deep Archive ile %95’e kadar
  • Monitoring ücreti: izlenen 1.000 obje başına $0.0025; büyük objelerde ihmal edilebilir, 128KB eşiğinin hemen üstündeki objelerde depolama ücretiyle başa baş
  • Otomatik katmanlama için minimum obje boyutu: 128KB; daha küçük objeler Frequent Access katmanında kalır, izlenmez ve monitoring ücreti ödemez
  • Frequent, Infrequent ve Archive Instant Access katmanlarında retrieval ücreti yok

Lambda Maliyet Optimizasyonu#

Lambda maliyeti üç bileşenden oluşur: request, duration (GB-saniye) ve opsiyonel provisioned concurrency. Her birini nasıl optimize edeceğin:

Power Tuning ile Memory Optimizasyonu#

AWS Lambda Power Tuning (açık kaynak) veri odaklı memory optimizasyonu sağlıyor:

def run_lambda_power_tuning(
    function_name: str,
    power_values: List[int] = [128, 256, 512, 1024, 1536, 2048, 3008],
    num_invocations: int = 10,
    strategy: str = 'balanced'  # 'cost', 'speed' veya 'balanced'
) -> Dict:
    """
    Optimal memory konfigürasyonunu bulmak için AWS Lambda Power Tuning çalıştır
    GitHub: alexcasalboni/aws-lambda-power-tuning
    """
    input_payload = {
        'lambdaARN': f'arn:aws:lambda:us-east-1:123456789012:function:{function_name}',
        'powerValues': power_values,
        'num': num_invocations,
        'payload': {},
        'parallelInvocation': True,
        'strategy': strategy
    }
    response = stepfunctions_client.start_execution(
        stateMachineArn='arn:aws:states:us-east-1:123456789012:stateMachine:lambda-power-tuner',
        input=json.dumps(input_payload)
    )

    # Execution bitene kadar bekle, sonra sonucu oku
    execution_arn = response['executionArn']
    stepfunctions_client.get_waiter('execution_succeeded').wait(executionArn=execution_arn)
    output = json.loads(
        stepfunctions_client.describe_execution(executionArn=execution_arn)['output']
    )

    return {'recommended_memory': output.get('power'), 'optimization_summary': output}

Provisioned Concurrency Maliyet Analizi#

Provisioned concurrency cold start’ı ortadan kaldırıyor; sıcak tutulan her GB için ayda yaklaşık $11 ödüyorsun. Bu takasın ne zaman değdiği:

def calculate_lambda_costs(config: LambdaCostBreakdown) -> Dict:
    """
    Provisioned concurrency ile ve olmadan Lambda maliyetlerini hesapla
    Provisioned concurrency'nin maliyet-etkin olup olmadığını belirlemeye yardımcı olur
    """
    price_per_request = 0.20 / 1_000_000
    price_per_gb_second = 0.0000166667
    provisioned_price_per_gb_second = 0.0000041667

    memory_gb = config.memory_mb / 1024
    duration_seconds = config.avg_duration_ms / 1000

    request_cost = config.invocations_monthly * price_per_request
    compute_cost = config.invocations_monthly * duration_seconds * memory_gb * price_per_gb_second
    total_on_demand = request_cost + compute_cost

    provisioned_cost = 0
    if config.provisioned_concurrency:
        seconds_per_month = 730 * 3600
        provisioned_cost = (config.provisioned_concurrency * memory_gb *
                           seconds_per_month * provisioned_price_per_gb_second)

    total_with_provisioned = request_cost + compute_cost + provisioned_cost
    return {
        'on_demand_cost_monthly': round(total_on_demand, 2),
        'provisioned_cost_monthly': round(total_with_provisioned, 2),
        # 1.5x, "gecikme primi ödemeye değer" için başlangıç eşiği;
        # kendi latency bütçene göre ayarla
        'recommendation': 'Provisioned concurrency kullan' if total_with_provisioned < total_on_demand * 1.5 else 'On-demand\'da kal'
    }

Lambda faturasını asıl hareket ettirenler:

  • Memory allocation CPU ve network payını da belirliyor; daha fazla memory çoğu zaman daha kısa duration ve daha düşük toplam demek
  • Pratikte 1024-1536MB aralığı maliyet/performans dengesi için iyi bir başlangıç, ama karar vermeden önce ölç
  • Compute Savings Plans, Lambda duration ücretlerine de uygulanıyor (%17’ye kadar indirim)
  • Kodun kendi çalışma süresini kısaltmak genelde memory tuning’i geçer ve aylık ek maliyeti yoktur
  • Provisioned concurrency, sıkı latency bütçesi olan user-facing API’lere aittir; seyrek çalışan fonksiyonlara değil

Maliyet Tahsisi ve Etiketleme#

Kapsamlı tagging ekipler, projeler ve environment’lar arasında maliyet attribution sağlar. Production-ready yaklaşım:

def create_tagging_compliance_rule():
    """
    Tag'lenmemiş kaynakları tespit eden AWS Config kuralı oluştur
    Environment, Project, CostCenter tag'lerini zorunlu kılar
    """
    config_client.put_config_rule(ConfigRule={
        'ConfigRuleName': 'required-tags-compliance',
        'Description': 'Kaynakların gerekli cost allocation tag\'lerine sahip olduğunu kontrol et',
        'Source': {'Owner': 'AWS', 'SourceIdentifier': 'REQUIRED_TAGS'},
        'InputParameters': '{"tag1Key":"Environment","tag2Key":"Project","tag3Key":"CostCenter"}',
        'Scope': {
            'ComplianceResourceTypes': [
                'AWS::EC2::Instance', 'AWS::RDS::DBInstance', 'AWS::S3::Bucket',
                'AWS::Lambda::Function', 'AWS::DynamoDB::Table'
            ]
        }
    })
    return {'rule_name': 'required-tags-compliance'}

Maliyet Tahsis Raporlaması#

Tag’lere göre gruplanmış aylık maliyet raporları chargeback/showback için:

def generate_cost_allocation_report(
    month: str,
    group_by_tags: List[str] = ['Project', 'Environment', 'CostCenter']
) -> Dict:
    """
    Tag'lere göre gruplanmış maliyet tahsis raporu oluştur
    Dikkat gerektiren tag'lenmemiş maliyetleri tespit eder
    """
    start_date = datetime.strptime(f'{month}-01', '%Y-%m-%d')
    if start_date.month == 12:
        end_date = start_date.replace(year=start_date.year + 1, month=1, day=1)
    else:
        end_date = start_date.replace(month=start_date.month + 1, day=1)
    end_date = end_date - timedelta(days=1)

    cost_by_tags = {}
    for tag_key in group_by_tags:
        response = ce_client.get_cost_and_usage(
            TimePeriod={'Start': start_date.strftime('%Y-%m-%d'), 'End': end_date.strftime('%Y-%m-%d')},
            Granularity='MONTHLY',
            Metrics=['UnblendedCost'],
            GroupBy=[{'Type': 'TAG', 'Key': tag_key}]
        )
        tag_costs = {}
        for result in response['ResultsByTime']:
            for group in result['Groups']:
                tag_value = group['Keys'][0].split('$')[1] if '$' in group['Keys'][0] else 'Untagged'
                tag_costs[tag_value] = float(group['Metrics']['UnblendedCost']['Amount'])
        cost_by_tags[tag_key] = tag_costs

    total_response = ce_client.get_cost_and_usage(
        TimePeriod={'Start': start_date.strftime('%Y-%m-%d'), 'End': end_date.strftime('%Y-%m-%d')},
        Granularity='MONTHLY', Metrics=['UnblendedCost'])
    total_cost = float(total_response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])
    return {'month': month, 'total_cost': round(total_cost, 2), 'breakdown_by_tags': cost_by_tags}

Ayakta kalan tag’leme kuralları:

  • Gerekli tag’ler: Environment (production/staging/development), Project, CostCenter, Owner
  • Cost allocation tag’lerini kullanmadan önce Billing Console’da aktif et; geriye dönük hiçbir şey izlenmiyor
  • İzin verilen tag değerlerini sabitle; production / prod / Production tek bir maliyet merkezini sessizce üçe böler
  • Tag’lenmemiş kaynakları AWS Config ile tespit et ve kaynak oluşturulurken zorla; elle düzeltmek pahalı
  • Hedef: <%10 tag’lenmemiş kaynak maliyeti

Optimizasyon Teknikleri Karşılaştırması#

TeknikTasarrufUygulama ÇabasıRiskEn İyi Kullanım
Right-Sizing (Compute Optimizer)%20-40DüşükDüşükOver-provisioned instance’lar
Savings Plans%66’ya kadarDüşükOrtaÖngörülebilir baseline
Reserved Instances%72’ye kadarDüşükOrtaStabil workload’lar
Spot Instances%70-90OrtaOrtaFault-tolerant workload’lar
Lambda memory optimizasyonu%20-50DüşükDüşükLambda-ağır mimariler
S3 Intelligent-Tiering%40-95DüşükDüşükBüyük depolama, karışık erişim
Instance scheduling%70OrtaDüşükNon-production environment’lar

Sıranın İşlemediği Durumlar#

Önce görünürlük, sonra right-sizing, en son commitment sırası production hesaplarının çoğunda geçerli; sebebi de sıralama riski. Ölçülmemiş ve fazla boyutlandırılmış kullanımın üzerine alınan bir Savings Plan, yanlış baseline’ı bir ila üç yıl boyunca sabitliyor. Önce yapılan bir right-sizing turu ise satın alman gereken commitment’ı doğrudan küçültüyor.

Bu sırayı bozmayı haklı çıkaran üç durum var. Şekli senin kontrolün dışında sabitlenmiş bir workload (belirli bir instance family’ye bağlı lisanslı bir veritabanı gibi) doğrudan Reserved Instance alabilir; right-sizing onu zaten oynatmayacak. Acil bir harcama tavanı altındaki bir hesap önce No Upfront commitment alıp right-sizing’i sonraya bırakabilir; indirim aktive edildiği saatten itibaren işliyor ve No Upfront peşin sermaye istemiyor. Kesintiye zaten dayanıklı bir batch katmanı da hiçbir ölçüm yapılmadan Spot’a geçebilir; %70-90 indirim, right-sizing’in orada bulacağı her şeyi gölgede bırakır.

Hangi noktadan girersen gir, ilk satın almadan önce cost allocation tag’lerini aktif et. Tag’ler geriye dönük çalışmıyor; onlarsız geçen her ay, sonradan kimsenin kime yazacağını bilemeyeceği bir harcama ayı demek.

Kaynaklar#

İlgili yazılar