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 buildile 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.
