AWS Lambda ile API Gateway Entegrasyonu: Sunucusuz API Geliştirme Rehberi

Sunucusuz mimari dünyasına adım atmak, özellikle AWS ekosisteminde, başta biraz ürkütücü gelebilir. Ama bir kez alıştığınızda, Lambda ve API Gateway ikilisinin ne kadar güçlü bir kombinasyon olduğunu anlıyorsunuz. Bu yazıda sıfırdan başlayıp production’a kadar giden bir yolculuğu birlikte yaşayacağız.

Lambda ve API Gateway Neden Bu Kadar Popüler?

Klasik bir web uygulaması düşünün: EC2 instance’ı ayağa kaldırıyorsunuz, üzerine uygulama sunucusu kuruyorsunuz, load balancer yapılandırıyorsunuz, auto scaling ayarlıyorsunuz ve sonunda trafiğin olmadığı gece 3’te bile o sunucular para yiyor. Lambda’da ise sadece kodunuz çalıştığında ödeme yapıyorsunuz. Ayda 1 milyon istek için AWS Lambda’nın ücretsiz katmanı zaten yeterli oluyor çoğu proje için.

API Gateway ise Lambda fonksiyonlarınızı dış dünyaya açan kapı görevi görüyor. HTTP endpoint’leri oluşturuyor, authentication yönetiyor, rate limiting yapıyor ve tüm bunları yönetilen bir servis olarak sunuyor. İkisini birleştirdiğinizde, altyapı yönetmeden tam özellikli bir REST API kurabiliyorsunuz.

Ortam Hazırlığı

Başlamadan önce şunlara ihtiyacınız var:

  • AWS CLI kurulu ve yapılandırılmış
  • Python 3.11+ veya Node.js 18+ (örneklerimizde Python kullanacağız)
  • AWS hesabı ve yeterli IAM yetkileri
  • Tercihen AWS SAM CLI (Serverless Application Model)

AWS CLI’yi yapılandırmak için:

aws configure
# AWS Access Key ID: your-key-here
# AWS Secret Access Key: your-secret-here
# Default region name: eu-west-1
# Default output format: json

# Yapılandırmayı test edelim
aws sts get-caller-identity

SAM CLI kurulumu Ubuntu/Debian üzerinde:

# SAM CLI kurulumu
pip install aws-sam-cli

# Versiyon kontrolü
sam --version

# Yeni proje başlatma
sam init --runtime python3.11 --name my-api-project

İlk Lambda Fonksiyonunuzu Yazın

En basit Lambda fonksiyonu şu şekilde görünüyor. Bu fonksiyon API Gateway’den gelen istekleri karşılayacak:

import json
import logging
from datetime import datetime

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    """
    API Gateway'den gelen HTTP isteklerini işler
    """
    logger.info(f"Gelen event: {json.dumps(event)}")
    
    # HTTP metodunu al
    http_method = event.get('httpMethod', 'UNKNOWN')
    
    # Query string parametrelerini al
    query_params = event.get('queryStringParameters', {}) or {}
    
    # Request body'sini al
    body = event.get('body', '{}')
    if isinstance(body, str):
        try:
            body = json.loads(body)
        except json.JSONDecodeError:
            body = {}
    
    # İstek işleme
    name = query_params.get('name', 'Dünya')
    
    response_body = {
        'message': f'Merhaba, {name}!',
        'method': http_method,
        'timestamp': datetime.utcnow().isoformat(),
        'request_id': context.aws_request_id
    }
    
    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
        },
        'body': json.dumps(response_body, ensure_ascii=False)
    }

Bu fonksiyonu elle deploy etmek yerine SAM template kullanacağız. Proje yapımız şu şekilde olsun:

my-api-project/
├── template.yaml
├── src/
│   ├── handlers/
│   │   ├── hello.py
│   │   ├── users.py
│   │   └── products.py
│   └── utils/
│       ├── db.py
│       └── auth.py
├── tests/
└── requirements.txt

SAM Template ile Altyapı Tanımlama

template.yaml dosyası tüm altyapınızı kod olarak tanımlar. Bu, Infrastructure as Code’un ta kendisi:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Description: Production-ready API projesi

Globals:
  Function:
    Timeout: 30
    MemorySize: 256
    Runtime: python3.11
    Environment:
      Variables:
        ENVIRONMENT: !Ref Environment
        LOG_LEVEL: INFO

Parameters:
  Environment:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - staging
      - prod

Resources:
  # API Gateway tanımı
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Environment
      Cors:
        AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
        AllowHeaders: "'Content-Type,Authorization,X-Api-Key'"
        AllowOrigin: "'*'"
      Auth:
        ApiKeyRequired: true
        UsagePlan:
          CreateUsagePlan: PER_API
          Quota:
            Limit: 10000
            Period: MONTH
          Throttle:
            BurstLimit: 100
            RateLimit: 50

  # Hello endpoint fonksiyonu
  HelloFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/handlers/
      Handler: hello.lambda_handler
      Description: Basit merhaba endpoint'i
      Events:
        HelloGet:
          Type: Api
          Properties:
            RestApiId: !Ref MyApi
            Path: /hello
            Method: GET

Outputs:
  ApiUrl:
    Description: API endpoint URL
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}"

Deploy etmek için:

# Build al
sam build

# Deploy (ilk seferinde guided mod)
sam deploy --guided --stack-name my-api-prod --region eu-west-1

# Sonraki deploylar için
sam deploy --stack-name my-api-prod --region eu-west-1

Gerçek Dünya Senaryosu: Kullanıcı Yönetim API’si

Şimdi daha gerçekçi bir örneğe geçelim. Bir e-ticaret uygulaması için kullanıcı yönetim API’si yapalım. Bu senaryo DynamoDB, JWT authentication ve proper error handling içerecek:

import json
import boto3
import logging
import os
from datetime import datetime, timedelta
import hashlib
import hmac
import base64

logger = logging.getLogger()
logger.setLevel(logging.INFO)

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ.get('USERS_TABLE', 'Users'))

def lambda_handler(event, context):
    http_method = event['httpMethod']
    path = event['path']
    
    try:
        # Router mantığı
        if path == '/users' and http_method == 'GET':
            return get_users(event)
        elif path == '/users' and http_method == 'POST':
            return create_user(event)
        elif path.startswith('/users/') and http_method == 'GET':
            user_id = path.split('/')[-1]
            return get_user(user_id)
        elif path.startswith('/users/') and http_method == 'PUT':
            user_id = path.split('/')[-1]
            return update_user(user_id, event)
        elif path.startswith('/users/') and http_method == 'DELETE':
            user_id = path.split('/')[-1]
            return delete_user(user_id)
        else:
            return error_response(404, 'Endpoint bulunamadı')
            
    except Exception as e:
        logger.error(f"Beklenmeyen hata: {str(e)}", exc_info=True)
        return error_response(500, 'Sunucu hatası')

def get_users(event):
    query_params = event.get('queryStringParameters', {}) or {}
    limit = int(query_params.get('limit', 20))
    
    response = table.scan(Limit=min(limit, 100))
    users = response.get('Items', [])
    
    # Hassas verileri temizle
    sanitized = [{k: v for k, v in u.items() if k != 'password_hash'} 
                 for u in users]
    
    return success_response({'users': sanitized, 'count': len(sanitized)})

def create_user(event):
    body = json.loads(event.get('body', '{}'))
    
    # Validasyon
    required_fields = ['email', 'name', 'password']
    for field in required_fields:
        if field not in body:
            return error_response(400, f'{field} alanı zorunludur')
    
    # Email kontrolü
    existing = table.get_item(Key={'email': body['email']})
    if 'Item' in existing:
        return error_response(409, 'Bu email zaten kayıtlı')
    
    user_item = {
        'email': body['email'],
        'name': body['name'],
        'password_hash': hash_password(body['password']),
        'created_at': datetime.utcnow().isoformat(),
        'active': True
    }
    
    table.put_item(Item=user_item)
    
    # Response'dan password'u çıkar
    user_item.pop('password_hash')
    return success_response(user_item, status_code=201)

def hash_password(password):
    salt = os.urandom(32)
    key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
    return base64.b64encode(salt + key).decode()

def success_response(data, status_code=200):
    return {
        'statusCode': status_code,
        'headers': {'Content-Type': 'application/json'},
        'body': json.dumps(data, ensure_ascii=False, default=str)
    }

def error_response(status_code, message):
    return {
        'statusCode': status_code,
        'headers': {'Content-Type': 'application/json'},
        'body': json.dumps({'error': message}, ensure_ascii=False)
    }

Lambda Authorizer ile JWT Authentication

Production ortamında API’nizi korumak için Lambda Authorizer kullanmanız şart. Bu yapı, her istekten önce çalışıp token’ı doğrular:

import json
import os
import logging
import jwt  # PyJWT kütüphanesi

logger = logging.getLogger()
logger.setLevel(logging.INFO)

SECRET_KEY = os.environ['JWT_SECRET_KEY']

def lambda_handler(event, context):
    """
    API Gateway Lambda Authorizer
    """
    token = event.get('authorizationToken', '')
    method_arn = event.get('methodArn', '')
    
    # Bearer token'ı temizle
    if token.startswith('Bearer '):
        token = token[7:]
    
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        user_id = payload.get('user_id')
        user_role = payload.get('role', 'user')
        
        logger.info(f"Doğrulama başarılı: user_id={user_id}")
        
        # IAM policy oluştur
        policy = generate_policy(user_id, 'Allow', method_arn)
        policy['context'] = {
            'user_id': str(user_id),
            'user_role': user_role,
            'email': payload.get('email', '')
        }
        
        return policy
        
    except jwt.ExpiredSignatureError:
        logger.warning("Token süresi dolmuş")
        raise Exception('Unauthorized')
    except jwt.InvalidTokenError as e:
        logger.warning(f"Geçersiz token: {str(e)}")
        raise Exception('Unauthorized')

def generate_policy(principal_id, effect, resource):
    return {
        'principalId': principal_id,
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [{
                'Action': 'execute-api:Invoke',
                'Effect': effect,
                'Resource': resource
            }]
        }
    }

Bu authorizer’ı template.yaml’a eklemek için:

# Lambda Authorizer tanımı
AuthorizerFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: src/handlers/
    Handler: authorizer.lambda_handler
    Environment:
      Variables:
        JWT_SECRET_KEY: !Sub "{{resolve:ssm:/myapp/${Environment}/jwt-secret}}"

# API'ye authorizer ekle
MyApi:
  Type: AWS::Serverless::Api
  Properties:
    StageName: !Ref Environment
    Auth:
      DefaultAuthorizer: JWTAuthorizer
      Authorizers:
        JWTAuthorizer:
          FunctionArn: !GetAtt AuthorizerFunction.Arn
          Identity:
            Header: Authorization
            ReauthorizeEvery: 300

Monitoring ve Hata Yönetimi

Production’da ne olduğunu bilmeden işinizi yapamazsınız. CloudWatch ile proper monitoring kuralım:

# Log group'larını listele
aws logs describe-log-groups 
  --log-group-name-prefix /aws/lambda/my-api 
  --region eu-west-1

# Son 30 dakikanın hatalarını çek
aws logs filter-log-events 
  --log-group-name /aws/lambda/MyApiFunction 
  --filter-pattern "ERROR" 
  --start-time $(date -d '30 minutes ago' +%s000) 
  --region eu-west-1

# Lambda metriklerini gör
aws cloudwatch get-metric-statistics 
  --namespace AWS/Lambda 
  --metric-name Errors 
  --dimensions Name=FunctionName,Value=MyApiFunction 
  --start-time $(date -d '1 hour ago' -u +%Y-%m-%dT%H:%M:%S) 
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) 
  --period 300 
  --statistics Sum 
  --region eu-west-1

CloudWatch Alarm kurmak için:

# Error rate alarmı
aws cloudwatch put-metric-alarm 
  --alarm-name "LambdaErrorRate-MyApi" 
  --alarm-description "Lambda hata oranı yüksek" 
  --metric-name Errors 
  --namespace AWS/Lambda 
  --statistic Sum 
  --period 300 
  --threshold 10 
  --comparison-operator GreaterThanThreshold 
  --evaluation-periods 2 
  --dimensions Name=FunctionName,Value=MyApiFunction 
  --alarm-actions arn:aws:sns:eu-west-1:123456789:alarm-notifications 
  --region eu-west-1

# Duration alarmı (timeout riski için)
aws cloudwatch put-metric-alarm 
  --alarm-name "LambdaDuration-MyApi" 
  --alarm-description "Lambda çok uzun sürüyor" 
  --metric-name Duration 
  --namespace AWS/Lambda 
  --statistic Average 
  --period 300 
  --threshold 25000 
  --comparison-operator GreaterThanThreshold 
  --evaluation-periods 1 
  --dimensions Name=FunctionName,Value=MyApiFunction 
  --alarm-actions arn:aws:sns:eu-west-1:123456789:alarm-notifications 
  --region eu-west-1

Cold Start Problemi ve Çözümleri

Lambda’nın en bilinen sorunu cold start’tır. Fonksiyon uzun süre kullanılmadığında, AWS container’ı durdurur ve sonraki istekte yeniden başlatmak zorunda kalır. Bu 1-3 saniyelik gecikmeye yol açabilir. Birkaç çözüm yolu var:

Provisioned Concurrency en etkili çözüm. Fonksiyonunuzun her zaman hazır instance’ları olur:

# Provisioned concurrency ayarla
aws lambda put-provisioned-concurrency-config 
  --function-name MyApiFunction 
  --qualifier prod 
  --provisioned-concurrent-executions 5 
  --region eu-west-1

# Mevcut durumu kontrol et
aws lambda get-provisioned-concurrency-config 
  --function-name MyApiFunction 
  --qualifier prod 
  --region eu-west-1

Kod seviyesinde optimizasyon da kritik. İmportları minimize edin, global scope’ta bağlantıları tutun:

import json
import boto3
import logging

# Bu satırlar cold start sırasında bir kez çalışır
# Her istek için değil
logger = logging.getLogger()
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Users')

# Bağlantı havuzu için connection reuse
import urllib3
http = urllib3.PoolManager()

def lambda_handler(event, context):
    # Burada sadece iş mantığı var
    # Bağlantı kurma maliyeti yok
    pass

CI/CD Pipeline Entegrasyonu

Manuel deploy yapmak production için doğru yaklaşım değil. GitHub Actions ile otomatik deploy kuralım:

# .github/workflows/deploy.yml içeriği
# (Bu bash bloğunda gösteriyoruz ama gerçekte YAML)

# Pipeline'ı test etmek için local runner
act -j deploy --secret-file .env.secrets

# SAM pipeline init
sam pipeline init --bootstrap

# Pipeline'ı özelleştirmek için mevcut konfigürasyonu gör
cat .aws-sam/pipeline/pipelineconfig.toml

GitHub Actions workflow’u şu adımları takip etmeli:

  • Lint ve test: pytest ile unit testleri çalıştır, flake8 ile kod kalitesini kontrol et
  • Build: sam build ile deployment paketini oluştur
  • Staging deploy: main branch’e her push’ta staging’e deploy et
  • Prod deploy: Tag push’larında production’a deploy et
  • Smoke test: Deploy sonrası kritik endpoint’leri test et

Performans Testleri ve Load Testing

Deploy ettiniz, çalışıyor, peki yük altında ne oluyor? Artillery ile load test yapalım:

# Artillery kurulumu
npm install -g artillery

# Basit load test config
cat > load-test.yaml << 'EOF'
config:
  target: 'https://your-api-id.execute-api.eu-west-1.amazonaws.com/prod'
  phases:
    - duration: 60
      arrivalRate: 10
      name: "Isınma"
    - duration: 120
      arrivalRate: 50
      name: "Normal yük"
    - duration: 60
      arrivalRate: 100
      name: "Yüksek yük"
  defaults:
    headers:
      Authorization: 'Bearer your-test-token'
      Content-Type: 'application/json'

scenarios:
  - name: "Kullanıcı listeleme"
    weight: 70
    flow:
      - get:
          url: '/users?limit=10'
  - name: "Kullanıcı oluşturma"
    weight: 30
    flow:
      - post:
          url: '/users'
          json:
            email: '{{ $randomEmail() }}'
            name: 'Test Kullanıcı'
            password: 'TestPass123!'
EOF

# Load testi çalıştır
artillery run load-test.yaml --output results.json

# Rapor oluştur
artillery report results.json

Sık Karşılaşılan Sorunlar ve Çözümleri

CORS Sorunu: En çok kafa yoran problem. API Gateway’de CORS ayarlasanız bile Lambda’dan dönen response’da header’lar olmayabilir:

# Her response'a mutlaka bu header'ları ekleyin
def get_cors_headers():
    return {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Api-Key',
        'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS'
    }

# OPTIONS isteklerini ayrıca handle edin
def lambda_handler(event, context):
    if event['httpMethod'] == 'OPTIONS':
        return {'statusCode': 200, 'headers': get_cors_headers(), 'body': ''}

Timeout Sorunları: Lambda maksimum 15 dakika çalışabilir ama API Gateway 29 saniyede timeout yapar. Uzun işlemleri asenkron yapın, SQS veya Step Functions kullanın.

IAM Yetki Hataları: Lambda’nın DynamoDB, S3 gibi servislere erişmesi için IAM rolüne gerekli policy’leri eklemeyi unutmayın:

# Lambda rolünün mevcut policy'lerini görüntüle
aws iam list-attached-role-policies 
  --role-name MyApiFunction-Role 
  --region eu-west-1

# DynamoDB erişimi ekle
aws iam attach-role-policy 
  --role-name MyApiFunction-Role 
  --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess

Payload Boyutu Limiti: API Gateway request/response limiti 10MB. Büyük dosyalar için S3 pre-signed URL kullanın, doğrudan Lambda üzerinden geçirmeyin.

Maliyet Optimizasyonu

Serverless ucuz ama yanlış kullanırsanız fatura şişebilir:

  • Memory ayarını doğru yapın: Daha fazla memory hem RAM hem CPU artırır. Lambda Power Tuning aracıyla optimal memory bulun
  • Gereksiz invocation’lardan kaçının: Dead letter queue ve retry policy’leri dikkatlice ayarlayın
  • X-Ray’i seçici kullanın: Her request’i trace etmek maliyet oluşturur, sampling rate’i ayarlayın
  • Log retention süresini sınırlayın: CloudWatch logları sonsuz tutmak pahalı. 30 gün genellikle yeterli
# Log retention süresini ayarla
aws logs put-retention-policy 
  --log-group-name /aws/lambda/MyApiFunction 
  --retention-in-days 30 
  --region eu-west-1

# Tüm Lambda log grupları için toplu ayar
for lg in $(aws logs describe-log-groups 
  --log-group-name-prefix /aws/lambda 
  --query 'logGroups[].logGroupName' 
  --output text --region eu-west-1); do
  aws logs put-retention-policy 
    --log-group-name $lg 
    --retention-in-days 30 
    --region eu-west-1
  echo "$lg için retention 30 güne ayarlandı"
done

Sonuç

AWS Lambda ve API Gateway kombinasyonu, doğru senaryolarda gerçekten dönüştürücü bir etkiye sahip. Startup projelerinden kurumsal uygulamalara kadar geniş bir yelpazede kullanılabiliyor. Ancak her araç gibi, bunların da trade-off’ları var.

Cold start, vendor lock-in, 15 dakika execution limiti ve stateless yapı kısıtlamaları bazı kullanım senaryolarında sorun yaratabilir. Uzun süreli hesaplama gerektiren işler, persistent bağlantı gerektiren uygulamalar (WebSocket hariç) veya çok büyük monolitik uygulamalar için Lambda her zaman doğru tercih olmayabilir.

Ama REST API yazmak, webhook endpoint’leri yönetmek, zamanlanmış görevler çalıştırmak veya event-driven mimariler kurmak için Lambda ve API Gateway ikilisi gerçekten güçlü bir seçenek. Üstelik altyapı yönetiminden kurtulmanın verdiği rahatlık, zaman içinde ne kadar değerli olduğunu anlıyorsunuz.

SAM ile başlayın, test yazın, monitoring kurun ve maliyeti takip edin. Bu dört prensibi uygularsanız, serverless yolculuğunuz çok daha pürüzsüz geçecektir. Sorularınız olursa yorumlarda buluşalım.

Bir yanıt yazın

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