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-node içindeki cache: 'npm' direktifi her seferinde npm 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.

Bir yanıt yazın

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