GitLab'da bir depo açtın, kod içeride, ama her sürümde testleri yerelde çalıştırıp dosyaları sunucuya elle kopyalıyorsun. GitLab CI pipeline yazma işi, deponun köküne tek bir .gitlab-ci.yml dosyası koymakla başlar ve o dosya doğru yazıldığında testten dağıtıma kadar her şeyi senin yerine yapar.
Ama GitLab CI'ın iki tarafı var ve yeni başlayanların çoğu ikincisini atlıyor: pipeline tanımını yazmak yetmiyor, o pipeline'ı çalıştıracak bir runner da olması gerekiyor. GitLab.com'da paylaşımlı runner'lar hazır gelir, kendi GitLab sunucunda ise runner'ı sen kurarsın ve hangi executor'ı seçtiğin pipeline'ın nasıl davranacağını doğrudan belirler. Bu rehberde önce runner tarafını netleştireceğim, sonra üç aşamalı çalışan bir pipeline yazacağız, cache ile artifact ayrımını oturtacağız ve rules ile koşullu çalıştırmayı kuracağız.
.gitlab-ci.yml Dosyasının Anatomisi#
Dosya üç temel kavram üzerine kurulu. Stage (aşama), pipeline'ın sıralı adımlarıdır; aynı aşamadaki job'lar paralel çalışır, bir sonraki aşama ancak öncekinin tamamı başarılı olduğunda başlar. Job, gerçek işi yapan birimdir ve mutlaka bir script bloğu içerir. Runner ise job'u çalıştıran makinedir.
stages:
- test
- build
- deploy
# Tüm job'lar için varsayılan ayarlar
default:
image: node:22-alpine
interruptible: true # yeni push gelince eskisini iptal et
birim-testleri:
stage: test
script:
- npm ci
- npm test
Bir dosyada tanımlı tüm anahtar kelimeler job adı sayılır; bu yüzden stages, default, variables, workflow ve include dışındaki her üst seviye anahtar bir job'dır. Alt çizgi ile başlayan (.sablon: gibi) girdiler ise gizli şablonlardır, kendileri çalışmaz ama extends ile miras alınabilirler:
.node-tabani: # nokta ile başlar: çalışmaz, şablondur
image: node:22-alpine
before_script:
- npm ci --prefer-offline
lint:
extends: .node-tabani
stage: test
script:
- npm run lint
Bu şablon yaklaşımı, on job'lu bir dosyada aynı kurulum adımlarını on kez yazmaktan kurtarır ve dosyayı okunabilir tutar.
Runner Kurulumu ve Executor Seçimi#
GitLab.com kullanıyorsan paylaşımlı runner'lar hazırdır ve hiçbir şey kurmana gerek yoktur. Kendi GitLab sunucunu işletiyorsan ya da özel ağdaki bir sunucuya dağıtım yapacaksan kendi runner'ını kurarsın. AlmaLinux, Rocky veya Debian tabanlı bir sunucuda kurulum şöyle ilerler:
# Resmi depoyu ekle ve paketi kur (Debian/Ubuntu)
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
sudo apt-get install -y gitlab-runner
# Runner'ı projeye veya gruba kaydet
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--token "KAYIT_TOKENI" \
--executor "docker" \
--docker-image "alpine:latest" \
--description "clou-runner-1"
# Durumu kontrol et
sudo gitlab-runner status
sudo gitlab-runner list
Executor seçimi, pipeline davranışını belirleyen en önemli karardır:
| Executor | Nasıl çalışır | Ne zaman uygun |
|---|---|---|
docker | Her job ayrı konteynerde | Varsayılan tercih; izole ve temiz |
shell | Doğrudan sunucunun kabuğunda | Dağıtım job'ları, basit betikler |
docker+machine | İhtiyaca göre makine açar | Değişken yük, otomatik ölçekleme |
kubernetes | Her job bir pod | Zaten Kubernetes kullanıyorsan |
shell executor kolay görünür ama tehlikelidir: job'lar sunucunun kendisinde, aynı kullanıcıyla ve birbirinden izole olmadan çalışır. Bir job'un bıraktığı dosya diğerini etkiler, ayrıca depoya push yetkisi olan herkes sunucuda komut çalıştırabilir hâle gelir. Gerçekten gerekmedikçe docker executor kullan; Docker'ın temel kavramlarına ihtiyacın olursa Docker nedir yazısı iyi bir başlangıç.
Üç Aşamalı Çalışan Bir Pipeline#
Şimdi test eden, imaj derleyip GitLab'ın kendi kayıt defterine gönderen ve sunucuya dağıtan eksiksiz bir dosya yazalım:
stages: [test, build, deploy]
variables:
IMAJ: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
DOCKER_TLS_CERTDIR: "/certs" # dind için gerekli
testler:
stage: test
image: node:22-alpine
cache:
key:
files: [package-lock.json] # kilit dosyası değişince önbellek yenilenir
paths: [.npm/]
script:
- npm ci --cache .npm --prefer-offline
- npm run lint
- npm test -- --reporter=junit --outputFile=junit.xml
artifacts:
when: always
reports:
junit: junit.xml # sonuçlar MR ekranında görünür
imaj:
stage: build
image: docker:27
services:
- docker:27-dind # Docker in Docker
script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
- docker build -t "$IMAJ" -t "$CI_REGISTRY_IMAGE:latest" .
- docker push "$IMAJ"
- docker push "$CI_REGISTRY_IMAGE:latest"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
dagitim:
stage: deploy
image: alpine:latest
environment:
name: uretim
url: https://firmaniz.com
before_script:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "$SSH_ANAHTARI" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
- ssh-keyscan -H 185.12.34.56 >> ~/.ssh/known_hosts
script:
- ssh [email protected] "cd /srv/uygulama && docker compose pull && docker compose up -d"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual # düğmeye basılınca dağıt
Bu dosyada dikkat çekmek istediğim üç şey var. Birincisi $CI_REGISTRY_IMAGE ve $CI_REGISTRY_PASSWORD gibi değişkenler GitLab tarafından otomatik sağlanır; kayıt defteri için ayrıca token üretmene gerek yoktur. İkincisi imajı $CI_COMMIT_SHORT_SHA ile etiketlemek, hangi kodun yayında olduğunu kesinleştirir ve geri almayı tek adıma indirir. Üçüncüsü when: manual, üretime dağıtımı bir düğmeye bağlar — bunu otomatiğe almadan önce testlerine ne kadar güvendiğini dürüstçe sor.
Cache ile Artifact Arasındaki Fark#
Bu ikisi sürekli karıştırılıyor ve yanlış kullanıldığında ya pipeline yavaşlıyor ya da adımlar birbirinin çıktısını bulamıyor. Ayrım aslında net:
| Özellik | cache | artifacts |
|---|---|---|
| Amaç | Tekrar indirmemek | Aşamalar arası dosya taşımak |
| Kapsam | Pipeline'lar arası | Aynı pipeline içinde |
| Garantili mi | Hayır, "olsa iyi olur" | Evet, sonraki job mutlaka alır |
| Tipik içerik | node_modules, .npm, .m2 | Derleme çıktısı, rapor, paket |
| Arayüzde görünür mü | Hayır | Evet, indirilebilir |
Yani node_modules bir cache'tir: kaybolursa pipeline yine çalışır, sadece yavaşlar. Derlenmiş dist/ klasörü ise artifact'tır: derleme job'undan dağıtım job'una taşınması zorunludur, cache ile taşımaya çalışırsan bir gün eksik gelir ve nedenini bulmak saatler alır.
derle:
stage: build
script: npm run build
artifacts:
paths: [dist/]
expire_in: 1 week # depoyu şişirmemek için süre ver
dagit:
stage: deploy
# dist/ klasörü otomatik olarak buraya indirilir
script: rsync -az dist/ [email protected]:/var/www/firmaniz/
expire_in yazmayı unutmak, kendi GitLab sunucusunu işletenlerin en sık karşılaştığı disk sorunudur; her pipeline artifact bırakır ve bir yıl sonra disk dolar. Aynı disk baskısını Docker tarafında da görürsün; temizlik yöntemleri için Docker diski doldurdu, nasıl temizlenir yazısına bakabilirsin.
Değişkenler ve Gizli Bilgiler#
Şifreleri .gitlab-ci.yml içine yazmak, onları depoya yazmakla aynı şeydir. GitLab'ın değişken deposu proje ayarlarında Settings → CI/CD → Variables yolundadır ve üç önemli bayrak sunar:
- Masked (maskeli): değer job log'unda yıldızlarla gizlenir. Yalnızca belirli bir uzunluk ve karakter kümesini karşılayan değerler maskelenebilir; kısa ya da özel karakterli değerler maskelenemez ve GitLab bunu kaydederken söyler.
- Protected (korumalı): değişken yalnızca korumalı dal ve etiketlerde görünür. Üretim SSH anahtarını mutlaka korumalı yap, yoksa herhangi bir özellik dalından okunabilir.
- Expanded: değer içindeki
$işaretlerinin genişletilip genişletilmeyeceğini belirler; içinde$geçen bir parola varsa bunu kapatman gerekir.
Değişkenleri dosya olarak da verebilirsin (File tipi); bir kubeconfig ya da servis hesabı JSON'u için en temiz yol budur, çünkü job içinde geçici bir dosyaya yazılır ve yolunu değişkende bulursun.
Sık yapılan bir hata, korumalı değişkeni korumasız bir dalda kullanmaya çalışmaktır: değişken boş gelir, script beklenmedik biçimde ve genellikle anlaşılmaz bir hatayla düşer. Böyle bir durumda önce dalın korumalı olup olmadığını kontrol et. Sızan kimlik bilgilerinin sonuçlarını görmek istersen .git klasörü ve .env dosyası ifşası yazısı iyi bir uyarı niteliğinde.
rules ile Koşullu Çalıştırma#
Eski only/except anahtarları hâlâ çalışıyor ama artık rules kullanman öneriliyor; çok daha okunabilir ve birleştirilebilir. rules bir liste olarak değerlendirilir ve ilk eşleşen kural kazanır:
dagitim:
script: ./deploy.sh
rules:
# Etiket push'unda otomatik dağıt
- if: $CI_COMMIT_TAG
when: on_success
# Ana dalda elle onayla
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
allow_failure: true # manuel job pipeline'ı bloklamasın
# Diğer her durumda job'u hiç oluşturma
- when: never
Dosya değişikliğine göre çalıştırmak da mümkündür ve tek depoda birden çok servis tutuyorsan (monorepo) çok işe yarar:
api-testleri:
script: cd api && npm test
rules:
- changes:
- api/**/*
- package-lock.json
Pipeline'ın hiç oluşturulmamasını istediğin durumlar için üst seviye workflow bloğu vardır. Örneğin çekme isteği (merge request) pipeline'ı ile dal pipeline'ının aynı anda çalışıp iki kat kaynak tüketmesini engellemek:
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- when: never
Sık Yapılan Hatalar ve Tuzaklar#
Docker in Docker'ı ayrıcalıklı modda düşünmeden kullanmak. docker:dind servisi çalışması için runner'ın privileged = true ayarına ihtiyaç duyar ve bu, job'a sunucunun çekirdeği üzerinde geniş yetki verir. Paylaşımlı bir runner'da bu ayarı açmak, depoya push yetkisi olan herkese sunucuyu açmak demektir. Alternatif olarak Kaniko ya da Buildah gibi ayrıcalıksız derleyicileri değerlendirebilirsin.
Cache anahtarını sabit bırakmak. cache: key: "sabit" yazarsan tüm dallar aynı önbelleği paylaşır ve bir dalın bozuk node_modules klasörü diğerlerini de bozar. Anahtarı kilit dosyasının içeriğine ya da $CI_COMMIT_REF_SLUG değişkenine bağla.
Artifact yerine cache kullanmak. Derleme çıktısını cache ile taşımak çoğu zaman çalışır, sonra bir gün çalışmaz. Cache garanti vermez; aşamalar arası veri her zaman artifact ile taşınmalıdır.
before_script ile script ilişkisini yanlış kurmak. Job seviyesinde before_script tanımlarsan default bloğundaki before_script tamamen ezilir, birleşmez. Ortak kurulum adımlarının kaybolduğunu fark ettiğinde ilk buraya bak.
Zaman aşımı ve interruptible ayarını atlamak. Takılan bir job runner'ı saatlerce meşgul eder. Proje ayarlarından makul bir zaman aşımı belirle ve interruptible: true ile yeni push geldiğinde eski pipeline'ın iptal edilmesini sağla.
Dağıtım job'unu her dala açık bırakmak. rules olmadan yazılan bir dağıtım job'u, her özellik dalından üretime çıkar. Bu hata genellikle ancak bir kez yaşandıktan sonra öğrenilir.
Sıkça Sorulan Sorular#
GitLab CI ücretsiz mi#
GitLab.com'un ücretsiz planında aylık belirli bir dakika kotasıyla paylaşımlı runner kullanabilirsin; kota dolduğunda ya beklersin ya da kendi runner'ını bağlarsın. Kendi sunucunda kurduğun GitLab Community Edition'da ise CI özelliği tamamen ücretsizdir ve dakika sınırı yoktur — tek maliyet runner'ı çalıştıracak sunucudur.
Pipeline neden hiç başlamıyor#
Üç şeyi sırayla kontrol et. Birincisi, dosya adı tam olarak .gitlab-ci.yml mi ve deponun kökünde mi? İkincisi, projeye atanmış ve çevrimiçi bir runner var mı — Settings bölümündeki runner listesinde yeşil görünmeli. Üçüncüsü, workflow veya job seviyesindeki rules blokları pipeline'ı engelliyor olabilir; GitLab'ın CI Lint aracı dosyayı doğrulayıp hangi job'ların oluşacağını gösterir.
Cache ile artifact arasındaki fark nedir#
Cache, tekrar indirmemek için tutulan ve kaybolması pipeline'ı bozmayan geçici veridir; node_modules bunun tipik örneğidir. Artifact ise bir job'un ürettiği ve sonraki aşamaların kullanacağı dosyadır; taşınması garantilidir ve arayüzden indirilebilir. Derleme çıktısını her zaman artifact ile taşı, bağımlılıkları cache ile sakla.
GitLab Runner'ı hangi executor ile kurmalıyım#
Genel tavsiye docker executor'dır: her job temiz bir konteynerde başlar, job'lar birbirini kirletmez ve imajı job seviyesinde seçebilirsin. shell executor yalnızca sunucunun kendisinde iş yapması gereken dar kapsamlı dağıtım job'ları için, hem de ayrı ve yetkisi kısıtlı bir runner üzerinde tercih edilmelidir. Zaten Kubernetes kullanıyorsan kubernetes executor işleri tek yerde toplar.
GitHub Actions'tan GitLab CI'a geçiş zor mu#
Kavramlar büyük ölçüde örtüşüyor: workflow yerine pipeline, job yine job, step yerine script satırları, uses yerine hazır imajlar ve include şablonları. Asıl farklar üç yerde: GitLab'da aşamalar açıkça stages ile sıralanır, gizli değişkenler proje ayarlarında korumalı/maskeli bayraklarıyla yönetilir ve imaj derleme genellikle Docker in Docker üzerinden yapılır. GitHub Actions ile CI/CD kurulumu yazısındaki hattın karşılığını yazmak birkaç saatlik bir iştir.
Runner'ı aynı sunucuda mı çalıştırmalıyım#
Üretim uygulamanın çalıştığı sunucuda runner çalıştırmamanı öneririm. Derlemeler CPU ve disk tüketir, docker build adımları belleği zorlar ve bir pipeline hatası üretim servisini etkileyebilir. Ayrı ve mütevazı bir sunucu, hem izolasyon hem de öngörülebilir performans sağlar; dağıtımı oraya kurulu runner SSH ile yapar.
Kapanış#
İyi bir GitLab CI pipeline'ı birkaç net kararın toplamıdır: docker executor ile izole job'lar çalıştır, bağımlılıkları cache'e ve derleme çıktılarını artifact'a koy, gizli değişkenleri korumalı ve maskeli olarak tanımla, dağıtım job'unu rules ile ana dala ve gerekiyorsa manuel onaya bağla. Bir de expire_in yazmayı unutma; disk dolmadan önce fark etmek her zaman daha ucuzdur.
Kendi GitLab sunucunu ve runner'ını barındırmak için tam root erişimli VDS ya da yükü arttıkça büyüyen bulut sunucu paketlerimizi kullanabilirsin; runner'ı ayrı bir makinede tutmak istersen sanal sunucu seçeneği mütevazı bir başlangıç sunar. Sunucuların kurulumu, güncellemeleri ve yedeklenmesiyle uğraşmak istemiyorsan sunucu yönetimi ve yedekleme hizmetlerimiz bu yükü sizin yerinize üstlenir.