GitLab CI/CD ile Kubernetes Entegrasyonu: Adım Adım Kurulum Rehberi

Modern yazılım geliştirme süreçlerinde Kubernetes ve GitLab CI/CD birlikte kullanımı artık neredeyse bir standart haline geldi. Uygulamalarını container’lara taşıyan ekiplerin büyük çoğunluğu, deploy süreçlerini otomatize etmek için bu ikiliye başvuruyor. Ancak bu entegrasyonu sağlıklı kurmak, birkaç config dosyası yazmaktan çok daha fazlasını gerektiriyor. Bu yazıda sıfırdan başlayarak production-ready bir GitLab CI/CD pipeline’ı kuracağız ve Kubernetes’e otomatik deployment yapacağız.

Genel Mimari ve Ön Gereksinimler

Başlamadan önce ne inşa edeceğimizi netleştirelim. Hedefimiz şu akışı otomatize etmek: kod GitLab’a push edilir, pipeline tetiklenir, Docker image build edilir, registry’e push edilir ve Kubernetes cluster’ına deploy edilir.

Bu yazı için şunlara ihtiyacın var:

  • Çalışan bir Kubernetes cluster’ı (EKS, GKE, AKS veya bare-metal)
  • GitLab instance’ı (self-hosted veya GitLab.com)
  • kubectl erişimi olan bir GitLab Runner
  • Container registry erişimi (GitLab Container Registry, Docker Hub veya başka bir registry)

GitLab Runner’ın Kubernetes cluster’ıyla iletişim kurabilmesi için ya runner’ı cluster içinde çalıştırman ya da kubeconfig dosyasını runner’a güvenli şekilde aktarman gerekiyor. İkinci yaklaşımı bu yazıda ele alacağız çünkü çoğu ekipte runner ayrı bir ortamda koşuyor.

GitLab CI/CD için Kubernetes Service Account Oluşturma

Güvenlik açısından en doğru yaklaşım, CI/CD pipeline’ı için minimum yetkili ayrı bir Kubernetes service account oluşturmaktır. cluster-admin yetkisi vermek kısa vadede kolay görünse de production’da büyük risk taşır.

Önce deployment yapılacak namespace’i ve service account’u oluşturalım:

# Namespace oluştur
kubectl create namespace production
kubectl create namespace staging

# CI/CD için service account oluştur
kubectl create serviceaccount gitlab-ci -n production
kubectl create serviceaccount gitlab-ci -n staging

# Role oluştur - sadece gerekli yetkiler
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: gitlab-ci-role
  namespace: production
rules:
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
  resources: ["services", "pods", "configmaps", "secrets"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["networking.k8s.io"]
  resources: ["ingresses"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]
EOF

# RoleBinding ile service account'a role ata
kubectl create rolebinding gitlab-ci-binding 
  --role=gitlab-ci-role 
  --serviceaccount=production:gitlab-ci 
  -n production

Service account token’ını almak için Kubernetes 1.24 ve sonrası için ayrı bir işlem gerekiyor. Artık token’lar otomatik oluşturulmuyor:

# Token secret oluştur
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: gitlab-ci-token
  namespace: production
  annotations:
    kubernetes.io/service-account.name: gitlab-ci
type: kubernetes.io/service-account-token
EOF

# Token değerini al
kubectl get secret gitlab-ci-token -n production 
  -o jsonpath='{.data.token}' | base64 --decode

# CA sertifikasını al
kubectl get secret gitlab-ci-token -n production 
  -o jsonpath='{.data.ca.crt}'

Bu değerleri GitLab’ın Settings > CI/CD > Variables bölümüne ekleyeceksin. Token’ı KUBE_TOKEN, CA sertifikasını KUBE_CA_PEM_FILE (file type olarak), cluster API URL’ini de KUBE_URL olarak kaydet. Bu değişkenleri masked ve protected olarak işaretlemeyi unutma.

Temel .gitlab-ci.yml Yapısı

Şimdi asıl işe gelelim. Gerçek dünyada kullandığım, production’da test edilmiş bir pipeline yapısı paylaşıyorum. Bu yapıyı basit bir Node.js uygulaması üzerinden göstereceğim ama mantık her dil için aynı.

# .gitlab-ci.yml
image: docker:24.0

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  KUBECTL_VERSION: "1.28.0"

services:
  - docker:24.0-dind

stages:
  - test
  - build
  - deploy-staging
  - deploy-production

# Her job'da kullanılacak ortak before_script
.kubectl_setup: &kubectl_setup
  before_script:
    - apk add --no-cache curl bash
    - curl -LO "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
    - chmod +x kubectl && mv kubectl /usr/local/bin/
    - kubectl version --client
    - mkdir -p ~/.kube
    - echo "$KUBE_CA_PEM_FILE" > ~/.kube/ca.pem
    - kubectl config set-cluster production-cluster
        --server="$KUBE_URL"
        --certificate-authority=~/.kube/ca.pem
    - kubectl config set-credentials gitlab-ci
        --token="$KUBE_TOKEN"
    - kubectl config set-context default
        --cluster=production-cluster
        --user=gitlab-ci
    - kubectl config use-context default

Bu şablonu kullanarak her job’da tekrar tekrar kubectl kurulum adımlarını yazmaktan kurtuluyoruz. YAML anchors (&kubectl_setup) bu konuda hayat kurtarıyor.

Test ve Build Stage’leri

# Test aşaması
unit-tests:
  stage: test
  image: node:20-alpine
  cache:
    key: "$CI_COMMIT_REF_SLUG"
    paths:
      - node_modules/
  script:
    - npm ci
    - npm run test:unit
    - npm run lint
  coverage: '/Liness*:s*(d+.?d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    expire_in: 1 week

# Docker image build ve registry'e push
build-image:
  stage: build
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build
        --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
        --build-arg VCS_REF=$CI_COMMIT_SHORT_SHA
        --cache-from $CI_REGISTRY_IMAGE:latest
        -t $IMAGE_TAG
        -t $CI_REGISTRY_IMAGE:latest
        .
    - docker push $IMAGE_TAG
    - docker push $CI_REGISTRY_IMAGE:latest
  rules:
    - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "staging"'
    - if: '$CI_COMMIT_TAG'

--cache-from parametresini kullanmak build sürelerini ciddi miktarda kısaltıyor. Özellikle büyük dependency ağacı olan projelerde bu fark 3-4 dakikadan 30 saniyeye inebiliyor.

Kubernetes Manifest Dosyaları

Pipeline’ın deploy ettiği Kubernetes manifestlerini projenin k8s/ dizininde tutuyorum. Bunu hem versiyon kontrolü hem de değişiklik takibi açısından şiddetle tavsiye ederim.

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: production
  labels:
    app: myapp
    version: "${IMAGE_TAG}"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: myapp
        version: "${IMAGE_TAG}"
    spec:
      serviceAccountName: myapp
      containers:
      - name: myapp
        image: "${IMAGE_TAG}"
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: "production"
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: myapp-secrets
              key: db-password
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10

maxUnavailable: 0 ayarı zero-downtime deployment için kritik. Bunu 1 yaparsanız rolling update sırasında kısa bir süre pod sayısı replica count’un altına düşer, bu da production’da sorun yaratabilir.

Staging ve Production Deploy Job’ları

# Staging deployment
deploy-staging:
  stage: deploy-staging
  image: alpine:3.18
  <<: *kubectl_setup
  environment:
    name: staging
    url: https://staging.myapp.com
  variables:
    KUBE_NAMESPACE: staging
    KUBE_URL: $STAGING_KUBE_URL
    KUBE_TOKEN: $STAGING_KUBE_TOKEN
  script:
    - |
      # Image tag'i manifest'e işle
      sed -i "s|${IMAGE_TAG}|$IMAGE_TAG|g" k8s/deployment.yaml
      sed -i "s|namespace: production|namespace: staging|g" k8s/deployment.yaml

      # Deploy et
      kubectl apply -f k8s/ -n staging

      # Rollout'u bekle
      kubectl rollout status deployment/myapp -n staging --timeout=5m

      # Deployment durumunu kontrol et
      kubectl get pods -n staging -l app=myapp
  rules:
    - if: '$CI_COMMIT_BRANCH == "staging"'

# Production deployment - manual onay gerektirir
deploy-production:
  stage: deploy-production
  image: alpine:3.18
  <<: *kubectl_setup
  environment:
    name: production
    url: https://myapp.com
  variables:
    KUBE_NAMESPACE: production
  script:
    - sed -i "s|${IMAGE_TAG}|$IMAGE_TAG|g" k8s/deployment.yaml
    - kubectl apply -f k8s/ -n production
    - kubectl rollout status deployment/myapp -n production --timeout=10m
    - |
      echo "=== Deployment Özeti ==="
      kubectl get deployment myapp -n production
      kubectl get pods -n production -l app=myapp
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
    - if: '$CI_COMMIT_TAG =~ /^v[0-9]+.[0-9]+.[0-9]+$/'
      when: manual

Production deploy’u when: manual yapması önemli bir nokta. Otomatik production deployment isteyen ekipler var ama benim gördüğüm kadarıyla gerçek senaryolarda bir gözden geçirme adımı her zaman değer katıyor.

Otomatik Rollback Mekanizması

En çok sorulan konulardan biri: “Deploy başarısız olursa ne yapacaksın?” İşte buna hazırlıklı olmak şart.

deploy-production-with-rollback:
  stage: deploy-production
  image: alpine:3.18
  <<: *kubectl_setup
  script:
    - |
      # Mevcut revision'ı kaydet
      CURRENT_REVISION=$(kubectl get deployment myapp -n production 
        -o jsonpath='{.metadata.annotations.deployment.kubernetes.io/revision}' 
        2>/dev/null || echo "0")

      echo "Mevcut revision: $CURRENT_REVISION"

      # Deploy et
      sed -i "s|${IMAGE_TAG}|$IMAGE_TAG|g" k8s/deployment.yaml
      kubectl apply -f k8s/ -n production

      # Rollout'u izle, başarısız olursa rollback
      if ! kubectl rollout status deployment/myapp -n production --timeout=5m; then
        echo "HATA: Deployment başarısız! Rollback başlatılıyor..."
        kubectl rollout undo deployment/myapp -n production

        # Rollback'in tamamlanmasını bekle
        kubectl rollout status deployment/myapp -n production --timeout=3m

        echo "Rollback tamamlandı. Lütfen logları inceleyin."

        # Son 50 satır log
        kubectl logs -n production 
          -l app=myapp 
          --tail=50 
          --previous 2>/dev/null || true

        exit 1
      fi

      echo "Deployment başarılı!"
      NEW_REVISION=$(kubectl get deployment myapp -n production 
        -o jsonpath='{.metadata.annotations.deployment.kubernetes.io/revision}')
      echo "Yeni revision: $NEW_REVISION"
  when: manual
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

Bu script’i production’da birkaç kez kullandım ve hayat kurtardı. Özellikle gece yarısı deployment sırasında otomatik rollback olmasa sabah kriz toplantısıyla başlıyordun.

Helm ile Deployment

Birçok ekip düz manifest yerine Helm kullanıyor. GitLab CI/CD ile Helm entegrasyonu da oldukça temiz:

deploy-with-helm:
  stage: deploy-production
  image: alpine/helm:3.12.0
  before_script:
    - apk add --no-cache curl bash
    - curl -LO "https://dl.k8s.io/release/v1.28.0/bin/linux/amd64/kubectl"
    - chmod +x kubectl && mv kubectl /usr/local/bin/
    - mkdir -p ~/.kube
    - echo "$KUBE_CA_PEM_FILE" > ~/.kube/ca.pem
    - kubectl config set-cluster k8s --server="$KUBE_URL" --certificate-authority=~/.kube/ca.pem
    - kubectl config set-credentials gitlab --token="$KUBE_TOKEN"
    - kubectl config set-context default --cluster=k8s --user=gitlab
    - kubectl config use-context default
  script:
    - |
      helm upgrade --install myapp ./helm/myapp 
        --namespace production 
        --create-namespace 
        --set image.tag=$CI_COMMIT_SHORT_SHA 
        --set image.repository=$CI_REGISTRY_IMAGE 
        --set replicaCount=3 
        --set ingress.host=myapp.com 
        --values helm/myapp/values-production.yaml 
        --wait 
        --timeout 10m 
        --atomic
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v[0-9]+.[0-9]+.[0-9]+$/'
      when: manual

--atomic flag’i burada kritik. Eğer deployment başarısız olursa Helm otomatik olarak önceki release’e rollback yapıyor. Bu, manuel rollback script yazmaktan çok daha güvenilir bir yaklaşım.

Image Scanning ve Güvenlik

Production’a deployment yapmadan önce container image’ını taramak artık bir lüks değil, zorunluluk. GitLab’ın built-in Container Scanning özelliğini veya Trivy gibi açık kaynak araçları kullanabilirsin:

container-scanning:
  stage: build
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  variables:
    GIT_STRATEGY: none
    TRIVY_USERNAME: "$CI_REGISTRY_USER"
    TRIVY_PASSWORD: "$CI_REGISTRY_PASSWORD"
    TRIVY_AUTH_URL: "$CI_REGISTRY"
    FULL_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  script:
    - trivy --version
    - trivy image --clear-cache
    - trivy image
        --exit-code 0
        --cache-dir .trivycache/
        --no-progress
        --format template
        --template "@/contrib/gitlab.tpl"
        --output gl-container-scanning-report.json
        "$FULL_IMAGE_NAME"
    # Kritik açıklar varsa pipeline'ı durdur
    - trivy image
        --exit-code 1
        --cache-dir .trivycache/
        --severity CRITICAL
        --no-progress
        "$FULL_IMAGE_NAME"
  cache:
    paths:
      - .trivycache/
  artifacts:
    reports:
      container_scanning: gl-container-scanning-report.json
  needs:
    - build-image
  allow_failure: false

--exit-code 1 ile CRITICAL severity’deki açıklar bulunursa pipeline duruyor. HIGH severity için allow_failure: true yapıp raporu artifact olarak saklayabilirsin, bu şekilde geliştiriciler bilgilendirilir ama deploy engellenmiyor.

Environment’a Özgü Konfigürasyonlar

GitLab’ın environments özelliği ile her branch farklı bir environment’a map edilebilir. Bu özellikle feature branch deployment’larında çok işe yarıyor:

deploy-feature:
  stage: deploy-staging
  image: alpine:3.18
  <<: *kubectl_setup
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    url: https://$CI_ENVIRONMENT_SLUG.staging.myapp.com
    on_stop: stop-feature
    auto_stop_in: 3 days
  variables:
    APP_NAMESPACE: "review-$CI_ENVIRONMENT_SLUG"
  script:
    - kubectl create namespace $APP_NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
    - |
      sed -i "s|${IMAGE_TAG}|$IMAGE_TAG|g" k8s/deployment.yaml
      sed -i "s|namespace: production|namespace: $APP_NAMESPACE|g" k8s/deployment.yaml
      sed -i "s|replicas: 3|replicas: 1|g" k8s/deployment.yaml
    - kubectl apply -f k8s/ -n $APP_NAMESPACE
    - kubectl rollout status deployment/myapp -n $APP_NAMESPACE --timeout=5m
  rules:
    - if: '$CI_COMMIT_BRANCH =~ /^feature//'

stop-feature:
  stage: deploy-staging
  image: alpine:3.18
  <<: *kubectl_setup
  environment:
    name: review/$CI_COMMIT_REF_SLUG
    action: stop
  variables:
    APP_NAMESPACE: "review-$CI_ENVIRONMENT_SLUG"
    GIT_STRATEGY: none
  script:
    - kubectl delete namespace $APP_NAMESPACE --ignore-not-found=true
  rules:
    - if: '$CI_COMMIT_BRANCH =~ /^feature//'
      when: manual

Bu yapıyla her feature branch’i kendi izole namespace’ine deploy edilip test edilebiliyor. auto_stop_in: 3 days ayarı ise unutulan review environment’larının boşa kaynak tüketmesini önlüyor.

Pipeline Performansını Artırma

Uzun süren pipeline’lar geliştirici deneyimini ciddi şekilde düşürüyor. Birkaç kritik optimizasyon:

  • Parallel job kullanımı: Bağımsız testleri parallel çalıştır. parallel: 4 ile aynı job birden fazla worker’da koşabilir.
  • Cache stratejisi: node_modules, pip cache, maven repository gibi dizinleri mutlaka cache’le. Cache key olarak $CI_COMMIT_REF_SLUG ve lock file hash’i kombine et.
  • needs keyword: Default olarak her stage bir öncekinin tamamlanmasını bekler. needs ile bu bağımlılığı fine-grained kontrol edebilirsin. Build job, test tamamlanmadan başlamayacak ama başka bağımsız testler paralel çalışabilir.
  • Docker layer caching: --cache-from ile önceki build’ın layer’larını kullan. Bunu registry üzerinden yapmak için image’ı hem commit SHA hem de latest tag’iyle push et.
  • Resource limit on runners: Küçük job’lar için büyük runner kullanmak kaynak israfı. GitLab Runner’ı concurrent ve limit ayarlarıyla doğru yapılandır.

Gerçek Dünya Senaryosu: Yaygın Hatalar ve Çözümleri

Production’da en çok karşılaştığım sorunlar:

ImagePullBackOff sonrası takılan deployment: Registry credential’larını Kubernetes secret olarak oluşturmayı unutmak. Her namespace için ayrı imagePullSecrets tanımlamak gerekiyor. Bunu pipeline içinde otomatize etmek için bir setup job eklenebilir.

Rollout timeout’larını yanlış ayarlamak: --timeout=5m yazdın ama pod’un ayağa kalkması 6 dakika sürüyor. Pipeline başarısız görünüyor ama aslında deployment devam ediyor. Readiness probe sürelerini ve timeout’ları birlikte hesapla.

Secret yönetimi: GitLab CI variable’larını doğrudan Kubernetes secret’larına aktarmak yerine HashiCorp Vault veya External Secrets Operator kullan. Bu hem audit trail hem de secret rotation açısından çok daha sağlıklı.

Namespace isolation eksikliği: Tüm environment’ları aynı namespace’e deploy etmek kaos yaratıyor. Her environment, her uygulama için ayrı namespace politikası belirle.

RBAC çok geniş yetkiler: “Çalışıyor, dokunma” mantığıyla cluster-admin verilen CI/CD service account’ları güvenlik açığı. Minimum yetki prensibine sadık kal.

Sonuç

GitLab CI/CD ile Kubernetes entegrasyonu doğru kurulduğunda gerçekten güçlü bir deployment pipeline’ı ortaya çıkıyor. Bu yazıda anlattığım yapıyı birkaç farklı ölçekte production ortamında çalıştırdım ve temel prensiplerin değişmediğini gördüm: minimum yetki, otomatik rollback, environment izolasyonu ve image güvenliği.

Başlangıç için tüm bu bileşenleri bir anda kurmaya çalışmana gerek yok. Önce basit bir build-and-deploy pipeline’ı kur, çalıştır. Sonra rollback mekanizması ekle, ardından security scanning’i dahil et. Adım adım ilerlemek hem öğrenme sürecini kolaylaştırıyor hem de sorun çıktığında nerede baktığını bilmeni sağlıyor.

Helm kullanıp kullanmama kararı ise ekibin Kubernetes deneyimine göre değişmeli. Eğer ekibinde Helm bilen birkaç kişi varsa --atomic flag’i ile gelen otomatik rollback özelliği tek başına yeterli bir gerekçe. Yoksa düz manifest + rollback script başlangıç için gayet yeterli.

Son olarak, bu pipeline’ları “bir kez kur unut” olarak görme. Kubernetes versiyonları değişiyor, kubectl API’leri deprecated oluyor, güvenlik gereksinimleri artıyor. Pipeline’larını da uygulama kodu gibi bakım gerektiren canlı sistemler olarak ele al.

Bir yanıt yazın

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