Serverless Mimaride CI/CD Pipeline Kurulumu
Serverless dünyasına adım attığınızda ilk aklınıza gelen şey muhtemelen “fonksiyon yazıyorum, deploy ediyorum, bitti” oluyor. Ama işin gerçeği, üretim ortamında serverless uygulamaları yönetmek geleneksel VM ya da container tabanlı sistemlerden çok farklı değil; hatta bazı açılardan daha karmaşık. CI/CD pipeline kurulumu bu karmaşıklığın tam ortasında oturuyor. Doğru yapılandırıldığında hayatınızı inanılmaz kolaylaştırıyor, yanlış yapıldığında ise “fonksiyon mu deploy oldu, olmadı mı?” belirsizliğiyle uğraşıyorsunuz.
Bu yazıda AWS Lambda üzerinde bir serverless CI/CD pipeline kuracağız. GitHub Actions, Serverless Framework ve AWS SAM kullanacağız. Gerçek dünyada karşılaştığım sorunları ve çözümlerini de paylaşacağım.
Serverless CI/CD Neden Farklıdır?
Geleneksel bir uygulama için CI/CD kuruyorsanız mantık basittir: kodu build et, test et, paketi sunucuya gönder, servisi yeniden başlat. Serverless’ta bu süreç biraz farklı işliyor.
Her fonksiyon bağımsız bir deploy birimi. Bir projede 20-30 Lambda fonksiyonunuz varsa ve sadece birini güncelliyorsanız, diğerlerini neden deploy edesiniz ki? Partial deployment yönetimi serverless CI/CD’nin en kritik konularından biri.
Bunun yanı sıra şunları da göz önünde bulundurmanız gerekiyor:
- Environment yönetimi: dev, staging, prod ortamlarının birbirinden tamamen izole olması
- Secret yönetimi: API anahtarları, veritabanı şifreleri fonksiyonlara nasıl geçiyor?
- Rollback mekanizması: bir fonksiyon hatalıysa hızlıca geri dönebiliyor muyuz?
- Cold start optimizasyonu: deploy sonrası fonksiyonların ısınması
- Versiyon yönetimi: Lambda alias ve version konseptinin pipeline’a entegrasyonu
Proje Yapısı ve Başlangıç
Önce proje iskeletini oluşturalım. Ben Node.js kullanacağım ama Python veya Go için de aynı prensipler geçerli.
mkdir serverless-cicd-demo
cd serverless-cicd-demo
# Serverless Framework kurulumu
npm install -g serverless
# Proje başlatma
serverless create --template aws-nodejs --path my-service
cd my-service
# Gerekli plugin'leri kur
npm init -y
npm install --save-dev
serverless-offline
serverless-prune-plugin
serverless-plugin-warmup
jest
@types/jest
Şimdi serverless.yml dosyamızı yapılandıralım:
cat > serverless.yml << 'EOF'
service: my-api-service
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs18.x
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'eu-west-1'}
memorySize: 256
timeout: 30
environment:
NODE_ENV: ${self:provider.stage}
DB_SECRET_ARN: ${ssm:/myapp/${self:provider.stage}/db-secret-arn}
iam:
role:
statements:
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:GetParameters
Resource:
- arn:aws:ssm:${self:provider.region}:*:parameter/myapp/${self:provider.stage}/*
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- arn:aws:secretsmanager:${self:provider.region}:*:secret:myapp/*
functions:
getUser:
handler: src/handlers/user.get
events:
- httpApi:
path: /users/{id}
method: GET
tags:
Version: ${env:GIT_COMMIT_SHA, 'local'}
createUser:
handler: src/handlers/user.create
events:
- httpApi:
path: /users
method: POST
processOrder:
handler: src/handlers/order.process
events:
- sqs:
arn: !GetAtt OrderQueue.Arn
batchSize: 10
plugins:
- serverless-offline
- serverless-prune-plugin
custom:
prune:
automatic: true
number: 3
EOF
GitHub Actions ile Pipeline Kurulumu
Ana pipeline dosyamızı oluşturalım. Bu pipeline üç aşamadan oluşacak: test, staging deploy ve prod deploy.
mkdir -p .github/workflows
cat > .github/workflows/deploy.yml << 'EOF'
name: Serverless CI/CD Pipeline
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
env:
AWS_REGION: eu-west-1
NODE_VERSION: '18'
jobs:
test:
name: Test & Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run unit tests
run: npm run test:unit -- --coverage
- name: Upload coverage report
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
deploy-staging:
name: Deploy to Staging
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_STAGING }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGING }}
aws-region: ${{ env.AWS_REGION }}
- name: Install dependencies
run: npm ci
- name: Install Serverless Framework
run: npm install -g serverless@3
- name: Deploy to staging
run: |
export GIT_COMMIT_SHA=${GITHUB_SHA::8}
serverless deploy --stage staging --verbose
env:
SERVERLESS_ACCESS_KEY: ${{ secrets.SERVERLESS_ACCESS_KEY }}
- name: Run integration tests
run: npm run test:integration
env:
API_ENDPOINT: ${{ steps.deploy.outputs.endpoint }}
deploy-production:
name: Deploy to Production
needs: [test, deploy-staging]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_PROD }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }}
aws-region: ${{ env.AWS_REGION }}
- name: Install dependencies
run: npm ci
- name: Install Serverless Framework
run: npm install -g serverless@3
- name: Deploy to production
run: |
export GIT_COMMIT_SHA=${GITHUB_SHA::8}
serverless deploy --stage prod --verbose
- name: Notify Slack on success
if: success()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Production deploy başarılı! Commit: ${{ github.sha }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
EOF
IAM Rol ve Yetki Yönetimi
Pipeline’ın AWS’e deploy edebilmesi için doğru IAM yetkilerine ihtiyacı var. “Admin yetkisi ver geç” demek kolay ama production ortamında bu ciddi bir güvenlik açığı. En az yetki prensibine göre hareket edelim.
cat > ci-iam-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cloudformation:CreateStack",
"cloudformation:UpdateStack",
"cloudformation:DeleteStack",
"cloudformation:DescribeStacks",
"cloudformation:ListStackResources",
"cloudformation:ValidateTemplate"
],
"Resource": "arn:aws:cloudformation:*:*:stack/my-api-service-*/*"
},
{
"Effect": "Allow",
"Action": [
"lambda:CreateFunction",
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"lambda:PublishVersion",
"lambda:CreateAlias",
"lambda:UpdateAlias",
"lambda:GetFunction",
"lambda:ListFunctions",
"lambda:AddPermission",
"lambda:RemovePermission"
],
"Resource": "arn:aws:lambda:*:*:function:my-api-service-*"
},
{
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-api-service-*",
"arn:aws:s3:::my-api-service-*/*"
]
},
{
"Effect": "Allow",
"Action": [
"apigateway:GET",
"apigateway:POST",
"apigateway:PUT",
"apigateway:DELETE",
"apigateway:PATCH"
],
"Resource": "arn:aws:apigateway:*::/*"
},
{
"Effect": "Allow",
"Action": [
"iam:CreateRole",
"iam:DeleteRole",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:PutRolePolicy",
"iam:DeleteRolePolicy",
"iam:PassRole",
"iam:GetRole"
],
"Resource": "arn:aws:iam::*:role/my-api-service-*"
}
]
}
EOF
# Policy oluştur ve CI kullanıcısına ata
aws iam create-policy
--policy-name serverless-cicd-policy
--policy-document file://ci-iam-policy.json
aws iam attach-user-policy
--user-name github-actions-ci
--policy-arn arn:aws:iam::YOUR_ACCOUNT_ID:policy/serverless-cicd-policy
Environment Variable ve Secret Yönetimi
Serverless uygulamalarda secret yönetimi kritik. AWS SSM Parameter Store ve Secrets Manager ikisini de kullanıyorum; hangisi için ne kullanacağınızı bilmek önemli.
SSM Parameter Store: Konfigürasyon değerleri, feature flag’ler, endpoint URL’leri için idealdir. Ucuz ve hızlıdır.
Secrets Manager: Veritabanı şifreleri, API anahtarları gibi gerçek sırlar için kullanın. Otomatik rotation destekler.
# Staging parametrelerini SSM'e kaydet
aws ssm put-parameter
--name "/myapp/staging/db-host"
--value "staging-db.cluster-xyz.eu-west-1.rds.amazonaws.com"
--type "String"
--overwrite
aws ssm put-parameter
--name "/myapp/staging/api-base-url"
--value "https://api-staging.mycompany.com"
--type "String"
--overwrite
# Gerçek sırları Secrets Manager'a
aws secretsmanager create-secret
--name "myapp/staging/database"
--secret-string '{
"username": "app_user",
"password": "super-secret-password-here",
"dbname": "myapp_staging",
"port": "5432"
}'
# Production için
aws secretsmanager create-secret
--name "myapp/prod/database"
--secret-string '{
"username": "app_user_prod",
"password": "even-more-secret-password",
"dbname": "myapp_prod",
"port": "5432"
}'
Canary Deployment ile Güvenli Prod Deploy
Production’a direkt %100 traffic geçmek yerine canary deployment kullanmak iyi bir pratik. AWS Lambda bunu alias traffic shifting ile destekliyor.
cat > .github/workflows/canary-deploy.yml << 'EOF'
name: Canary Production Deploy
on:
workflow_dispatch:
inputs:
canary_weight:
description: 'Canary traffic percentage (1-50)'
required: true
default: '10'
jobs:
canary-deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_PROD }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }}
aws-region: eu-west-1
- name: Deploy new version
id: deploy
run: |
serverless deploy --stage prod
NEW_VERSION=$(aws lambda publish-version
--function-name my-api-service-prod-getUser
--query 'Version' --output text)
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Configure canary traffic
run: |
CANARY_WEIGHT=${{ github.event.inputs.canary_weight }}
STABLE_WEIGHT=$((100 - CANARY_WEIGHT))
NEW_VERSION=${{ steps.deploy.outputs.new_version }}
aws lambda update-alias
--function-name my-api-service-prod-getUser
--name live
--routing-config "AdditionalVersionWeights={$NEW_VERSION=$CANARY_WEIGHT}"
--function-version $NEW_VERSION
echo "Canary aktif: %$CANARY_WEIGHT traffic yeni versiyona gidiyor"
- name: Monitor canary (5 dakika)
run: |
echo "Canary izleniyor..."
sleep 300
# Error rate kontrol et
ERROR_RATE=$(aws cloudwatch get-metric-statistics
--namespace AWS/Lambda
--metric-name Errors
--dimensions Name=FunctionName,Value=my-api-service-prod-getUser
--start-time $(date -d '5 minutes ago' -u +%Y-%m-%dT%H:%M:%SZ)
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ)
--period 300
--statistics Sum
--query 'Datapoints[0].Sum'
--output text)
if [ "$ERROR_RATE" -gt "10" ]; then
echo "HATA: Error rate yüksek, rollback yapılıyor!"
exit 1
fi
- name: Promote to 100% or rollback
if: success()
run: |
NEW_VERSION=${{ steps.deploy.outputs.new_version }}
aws lambda update-alias
--function-name my-api-service-prod-getUser
--name live
--function-version $NEW_VERSION
--routing-config "AdditionalVersionWeights={}"
echo "Deploy başarılı! %100 traffic yeni versiyona geçti."
- name: Rollback on failure
if: failure()
run: |
aws lambda update-alias
--function-name my-api-service-prod-getUser
--name live
--routing-config "AdditionalVersionWeights={}"
echo "Rollback tamamlandı!"
EOF
Monitoring ve Alerting Pipeline Entegrasyonu
Deploy sonrası monitoring kurmayı unutmak en sık yapılan hatalardan biri. Pipeline içine CloudWatch alarm kurulumunu entegre edelim.
cat > scripts/setup-monitoring.sh << 'EOF'
#!/bin/bash
set -e
STAGE=$1
FUNCTION_PREFIX="my-api-service-${STAGE}"
SNS_TOPIC_ARN=$2
functions=("getUser" "createUser" "processOrder")
for func in "${functions[@]}"; do
FUNCTION_NAME="${FUNCTION_PREFIX}-${func}"
echo "Alarm kuruluyor: $FUNCTION_NAME"
# Error rate alarmı
aws cloudwatch put-metric-alarm
--alarm-name "${FUNCTION_NAME}-errors"
--alarm-description "Lambda error rate yüksek: $FUNCTION_NAME"
--metric-name Errors
--namespace AWS/Lambda
--dimensions Name=FunctionName,Value=$FUNCTION_NAME
--period 60
--evaluation-periods 2
--threshold 5
--comparison-operator GreaterThanThreshold
--statistic Sum
--alarm-actions $SNS_TOPIC_ARN
--ok-actions $SNS_TOPIC_ARN
# Duration alarmı (timeout riski)
aws cloudwatch put-metric-alarm
--alarm-name "${FUNCTION_NAME}-duration"
--alarm-description "Lambda çok yavaş çalışıyor: $FUNCTION_NAME"
--metric-name Duration
--namespace AWS/Lambda
--dimensions Name=FunctionName,Value=$FUNCTION_NAME
--period 60
--evaluation-periods 3
--threshold 25000
--comparison-operator GreaterThanThreshold
--statistic p95
--alarm-actions $SNS_TOPIC_ARN
# Throttle alarmı
aws cloudwatch put-metric-alarm
--alarm-name "${FUNCTION_NAME}-throttles"
--alarm-description "Lambda throttle yaşıyor: $FUNCTION_NAME"
--metric-name Throttles
--namespace AWS/Lambda
--dimensions Name=FunctionName,Value=$FUNCTION_NAME
--period 60
--evaluation-periods 1
--threshold 0
--comparison-operator GreaterThanThreshold
--statistic Sum
--alarm-actions $SNS_TOPIC_ARN
echo "Alarmlar kuruldu: $FUNCTION_NAME"
done
echo "Tüm alarmlar başarıyla kuruldu!"
EOF
chmod +x scripts/setup-monitoring.sh
Gerçek Dünya Senaryosu: Çoklu Repo ile Mono-Repo Karşılaştırması
Müşterilerimden birinde ciddi bir sorunla karşılaştım. 15 farklı Lambda fonksiyonu, her biri ayrı repoda. Her güncelleme ayrı pipeline çalıştırıyor, dependency versiyonları birbirinden farklılaşıyor, bir fonksiyon güncellendi mi güncellenmedi mi belli değil. Klasik bir kaos ortamı.
Çözüm olarak mono-repo yapısına geçtik. Tüm fonksiyonlar tek repoda ama her biri bağımsız deploy edilebilir.
# Mono-repo yapısı için gelişmiş pipeline
cat > .github/workflows/smart-deploy.yml << 'EOF'
name: Smart Deploy - Only Changed Functions
on:
push:
branches: [main, develop]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
user-service: ${{ steps.changes.outputs.user-service }}
order-service: ${{ steps.changes.outputs.order-service }}
notification-service: ${{ steps.changes.outputs.notification-service }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
user-service:
- 'services/user/**'
- 'shared/**'
- 'package.json'
order-service:
- 'services/order/**'
- 'shared/**'
- 'package.json'
notification-service:
- 'services/notification/**'
- 'shared/**'
- 'package.json'
deploy-user-service:
needs: detect-changes
if: needs.detect-changes.outputs.user-service == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Deploy user service
run: |
cd services/user
serverless deploy --stage ${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
deploy-order-service:
needs: detect-changes
if: needs.detect-changes.outputs.order-service == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Deploy order service
run: |
cd services/order
serverless deploy --stage ${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
EOF
Pipeline Performansını Artırma
Pipeline’ın her push’ta 10-15 dakika sürmesi geliştiricilerin motivasyonunu öldürür. Bazı pratik optimizasyonlar:
- npm cache kullanımı:
actions/setup-nodeiçindekicache: 'npm'direktifi her seferindenpm ci‘ın sıfırdan çalışmasını önler. Ortalama 2-3 dakika kazandırır. - Lambda layer kullanımı: node_modules’u layer olarak deploy edin, kod paketiniz küçülsün. Deploy süresi yarıya düşer.
- Parallel job’lar: Birbirinden bağımsız fonksiyonları paralel deploy edin.
- Artifact cache: Build çıktısını job’lar arasında paylaşın.
- Conditional deployment: Değişmeyen fonksiyonları deploy etmeyin. Yukarıdaki
dorny/paths-filterörneği bu sorunu çözüyor.
Pratikte bu optimizasyonlarla 15 dakikalık pipeline’ları 4-5 dakikaya indirdiğimi gördüm.
Sık Karşılaşılan Sorunlar ve Çözümleri
“Deployment package too large” hatası: Lambda’nın 50MB zip sınırı var. serverless-webpack veya serverless-esbuild kullanın. Production dependency’lerini dikkatli yönetin.
“CloudFormation stack is in ROLLBACK_COMPLETE state” hatası: Stack’i manuel olarak silip tekrar deploy etmeniz gerekir. Bu genellikle ilk deployment sırasında bir kaynak oluşturulamadığında olur.
# Stuck stack'i temizle
aws cloudformation delete-stack
--stack-name my-api-service-staging
# Stack silinmesini bekle
aws cloudformation wait stack-delete-complete
--stack-name my-api-service-staging
# Tekrar deploy et
serverless deploy --stage staging
Cold start problemi: Yeni deploy sonrası ilk istekler yavaş gelir. WarmUp plugin’i veya EventBridge scheduled event ile fonksiyonları canlı tutabilirsiniz. Ancak bu ek maliyet demek, değerlendirin.
Concurrent Lambda limit: AWS hesabınızda varsayılan 1000 concurrent execution limiti var. CI/CD sırasında paralel test çalıştırıyorsanız ve o anda production trafiği de varsa throttle yiyebilirsiniz. Reserved concurrency ile fonksiyon bazında limit koyun.
Sonuç
Serverless CI/CD kurulumu ilk bakışta karmaşık görünüyor ama doğru yapıldığında gerçekten “push et, unut” deneyimi yaşıyorsunuz. Özetlemek gerekirse:
- Güvenlik önce gelir: CI kullanıcısına minimum yetki verin, secret’ları asla kod içine yazmayın.
- Canary deployment hayat kurtarır: Production’a direkt geçmek yerine kademeli traffic shifting yapın.
- Sadece değişeni deploy edin: Mono-repo’da paths-filter ile gereksiz deploy’ların önüne geçin.
- Monitoring pipeline’ın parçası olsun: Deploy sonrası alarm kurulumu otomatik olmalı.
- Rollback mekanizmanızı test edin: Sorun çıktığında panikle rollback yapmak yerine, test ortamında rollback’i deneyin.
En büyük tavsiyem şu: Pipeline’ı bir seferde mükemmel yapmaya çalışmayın. Önce çalışan bir şey yapın, sonra staging’de test edin, sonra iyileştirin. Serverless’ın güzelliği de zaten bu; küçük adımlarla ilerleyebilirsiniz.
