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)
kubectleriş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: 4ile aynı job birden fazla worker’da koşabilir. - Cache stratejisi:
node_modules,pip cache,maven repositorygibi dizinleri mutlaka cache’le. Cache key olarak$CI_COMMIT_REF_SLUGve lock file hash’i kombine et. - needs keyword: Default olarak her stage bir öncekinin tamamlanmasını bekler.
needsile 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-fromile önceki build’ın layer’larını kullan. Bunu registry üzerinden yapmak için image’ı hem commit SHA hem delatesttag’iyle push et. - Resource limit on runners: Küçük job’lar için büyük runner kullanmak kaynak israfı. GitLab Runner’ı
concurrentvelimitayarları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.
