Mert Özen Her Satırda Daha İleri
DevOps Jenkins — #11

Jenkins'i Kod Olarak Yönetmek: Configuration as Code (JCasC), plugins.txt ve Yedekleme

Mert Özen 8 Kas 2026 7 dk 5 görüntülenme
Jenkins'i Kod Olarak Yönetmek: Configuration as Code (JCasC), plugins.txt ve Yedekleme

Jenkins'i Git'teki Dockerfile, pin'li plugins.txt ve JCasC YAML'ı ile kod olarak yönetir, JENKINS_HOME'da yalnız durumu yedeklersin. YAML'ın yapısı, export'un şifreli secret tuzağı, elle yapılan değişikliğin reload'da geri alınması, --available-updates, 2.580.1'den beri war'da gömülü plugin olmaması, 1,7 MB'lık yedek, gerçek restore testi ve LTS upgrade akışı.

Sürüm notu: Bu yazıdaki örnekler Jenkins 2.580.2 LTS (Long-Term Support), Configuration as Code plugin'i 2147, Job DSL (Domain-Specific Language) plugin'i 3732, jenkins-plugin-cli ve Docker 29 ile denenmiştir.

Önce Tahmin Et

Controller'ı JCasC (Jenkins Configuration as Code) ile yönetiyorsun ve YAML'da (YAML Ain't Markup Language) numExecutors: 0 yazıyor. Bir admin bir gün script console'dan ya da arayüzden controller'ın executor sayısını 2 yapıyor; "geçici, bir build'i hızlıca koşturup geri alacağım". Sonra unutuyor. Bir hafta sonra biri JCasC'yi yeniden uyguluyor. Executor sayısı ne olur? Ve o bir hafta boyunca gerçek yapılandırmanın Git'tekiyle aynı olmadığını kim biliyordu?

Neyi Kod Olarak Yönetiyoruz?

Bu seri boyunca controller'ı hiç elle yapılandırmadık. Her şey dört dosyadan geliyor: controller image'ını tarif eden Dockerfile, sürümü pin'li plugins.txt, sistem ayarlarını tarif eden casc/jenkins.yaml ve job'ları tarif eden casc/jobs.groovy. Bu dosyalar bir Git repository'sinde durur ve her değişiklik PR (Pull Request) ile gelir. Secret'lar repository'de değil, container'a read-only mount edilen dosyalarda. JENKINS_HOME ise yalnız durumu, yani build geçmişini, fingerprint'leri, artifact'ları ve şifreleme anahtarlarını tutuyor.

11-config-as-code-tr.png

Bu ayrımın getirisi şu: controller'ın diski yansa, yeni bir makinede aynı image ve aynı YAML ile dakikalar içinde aynı Jenkins ayağa kalkar. Kaybolan şey yalnız yedeklenmeyen durum olur.

JCasC YAML'ının Anatomisi

Bu serideki jenkins.yaml 117 satır. Kök anahtarlar plugin'lere göre ayrılıyor: jenkins (çekirdek: security realm, yetkilendirme, node'lar), credentials, unclassified (plugin'lerin global ayarları: Jenkins URL'i (Uniform Resource Locator), Gitea sunucusu, global library) ve jobs. Önceki yazılarda parça parça gördük; bir araya gelince yapısı şu:

jenkins:
  systemMessage: "Bookstore CI - managed by JCasC, manual changes will be overwritten"
  numExecutors: 0
  securityRealm: ...
  authorizationStrategy: ...
  crumbIssuer: standard
  nodes: ...
credentials:
  system:
    domainCredentials: ...
unclassified:
  globalLibraries: ...
  location:
    url: http://jenkwork-jenkins:8080/
  giteaServers: ...
jobs:
  - file: /var/jenkins_casc/jobs.groovy

Doğru anahtar adlarını bulmanın en güvenilir yolu iki tane. Birincisi, controller'ın kendi şema dokümantasyonu: /manage/configuration-as-code/reference, kurulu plugin'lere göre üretiliyor. İkincisi, çalışan bir controller'ın mevcut yapılandırmasını dışa aktarmak (POST /manage/configuration-as-code/export). Bizim controller'ın export'unun başı:

configuration-as-code:
  deprecated: "reject"
  restricted: "reject"
  unknown: "reject"
  version: "1"

unknown: "reject" ikinci yazıda controller'ın neden hiç açılmadığını açıklıyor: tanınmayan bir alan uyarı değil, hata. Export'un bir de tuzağı var. Credential bölümü şöyle görünüyor:

          password: "{AQAAABAAAAAwBA8uGfqi9ezchmCFiUSAprkjFl+SWQSYDCVNSHP57lWkRk1rRmx2pbSz1p1VgFWhNKD2D0LMaDvX9AoBroHgyA==}"

Değer şifreli, ama bu controller'ın anahtarıyla şifreli. Export'u olduğu gibi Git'e koymak, secret'ları anahtarı bilen herkesin çözebileceği biçimde repository'ye koymak demek; ayrıca başka bir controller bu değeri çözemez. Export'u şema öğrenmek ve karşılaştırmak için kullan, kaynak olarak değil. Kaynak YAML'da secret'lar her zaman ${readFile:/run/secrets/...} veya environment variable referansı olmalı.

Drift: Elle Yapılan Değişiklik Ne Olur?

Baştaki sorunu denedim. Script console'dan executor sayısını değiştirdim, sonra JCasC'yi yeniden uyguladım:

numExecutors=2
reload HTTP 302
numExecutors=0

JCasC YAML'da tanımlı olanı her uygulamada yeniden yazıyor; elle yapılan değişiklik bir sonraki reload'a ya da controller restart'ına kadar yaşıyor. Bu iyi bir şey: Git tek doğruluk kaynağı. Ama aradaki bir hafta boyunca gerçek durum Git'ten farklıydı ve bunu kimse görmedi. Bu yüzden iki kural koy: arayüzden yapılandırma değişikliği yapma (systemMessage'daki uyarı bunun için), ve reload'u düzenli olarak bir pipeline'dan tetikle, ki drift uzun yaşamasın. Bir uyarı daha: YAML'da tanımlamadığın bir ayar elle değiştirilirse JCasC ona dokunmaz; drift yalnız tanımlı alanlarda düzelir.

plugins.txt ve Image

plugins.txt'teki her satır id:version biçiminde pin'li. Sürüm yazmazsan jenkins-plugin-cli image build edildiği gündeki en son sürümü alır ve iki gün arayla build edilen iki image farklı plugin'lerle çıkar. Pin'li liste bunu önlüyor, ama güncel kalmayı da senin işin yapıyor. Aracın kendisi bunda yardım ediyor:

$ jenkins-plugin-cli --plugin-file plugins.txt --available-updates --output txt

Bu komut listedeki her plugin'i yazılabilecek en güncel sürümle döndürür. Eski bir sürüm yazdığım küçük bir dosyada (git:5.10.0) çıktı git:5.11.0 oldu; bizim plugins.txt'te ise her satır aynı döndü, yani yazım anında hepsi günceldi. Çıktıyı dosyaya yazıp diff'lemek, plugin güncellemesi için bir PR açmanın en basit yolu.

Plugin'leri image'a gömmenin bir sonucu daha var: Jenkins'in arayüzündeki plugin yöneticisinden plugin kurmak ya da güncellemek artık anlamsız, çünkü bir sonraki image ile kaybolur. Plugin değişikliği her zaman plugins.txt'te bir PR.

2.580.1 ile Değişen: Gömülü Plugin'ler

LTS (Long-Term Support) changelog'una göre 2.580.1 ile JUnit, Mailer, Matrix Authorization Strategy, Matrix Project, OWASP (Open Worldwide Application Security Project) Markup Formatter, Bouncy Castle API (Application Programming Interface), Instance Identity ve birkaç plugin daha jenkins.war'a gömülü olarak gelmiyor. Eskiden Jenkins çok eski sürümlerden yapılan upgrade'lerde bu "detached" plugin'leri otomatik kurardı; artık kurmuyor ve changelog bunların plugin yöneticisinden kurulmasını söylüyor. Resmi image'daki war'ın içine baktım:

$ unzip -l /usr/share/jenkins/jenkins.war | grep -cE "\.(hpi|jpi)$"
0

War'da tek bir plugin arşivi yok. Bunun pratik anlamı: junit ya da matrix-auth'a dayanan bir kurulumun bu plugin'leri plugins.txt'te açıkça listelemesi gerekiyor. Bizim listede ikisi de vardı; olmasaydı JCasC'deki globalMatrix bölümü uygulanamaz ve controller açılışta hata verirdi.

Yedekleme: JENKINS_HOME'da Ne Var?

Bizim controller'ın JENKINS_HOME'u, en büyükten küçüğe:

188M	plugins
66M	war
27M	.cache
5.5M	jobs
3.4M	updates
324K	fingerprints
300K	caches
88K	logs
44K	secrets
24K	nodes
20K	users
20K	.java

Yerin büyük kısmı yedeklenmesi gerekmeyen şeyler: plugins ve war image'dan geliyor, .cache ve caches yeniden oluşuyor. Asıl değerli olan küçük: jobs (build geçmişi, log'lar, test sonuçları, arşivlenen artifact'lar), fingerprints, users ve secrets. Yedeği controller'ın volume'ünden, gereksizleri dışarıda bırakarak aldım:

tar -czf /backup/jenkins-home-20261108.tgz -C /src \
    --exclude=./war --exclude=./plugins --exclude=./workspace \
    --exclude=./caches --exclude=./.cache .
-rw-r--r-- 1 root root 1.7M Nov  8 04:59 jenkins-home-20261108.tgz

Dikkat: arşivde secrets/ ve secret.key var. Beşinci yazıda gördüğümüz gibi credential'lar bu anahtarlarla şifreli; yani bu yedek, bütün credential'ların çözülebilir bir kopyası. Yedeği secret gibi sakla: şifreli ve erişimi kısıtlı bir yerde. Tersini de unutma: secrets/'ı yedeklemezsen geri yüklenen controller credential'larını çözemez.

Yedek, geri yüklenemiyorsa yedek değildir. Arşivi yeni bir volume'e açıp aynı image ve aynı JCasC ile ikinci bir controller başlattım (18403 portunda) ve API'ye sordum:

{"_class":"org.jenkinsci.plugins.workflow.job.WorkflowJob","builds":[{"_class":"org.jenkinsci.plugins.workflow.job.WorkflowRun","number":5},{"_class":"org.jenkinsci.plugins.workflow.job.WorkflowRun","number":4},{"_class":"org.jenkinsci.plugins.workflow.job.WorkflowRun","number":3},{"_class":"org.jenkinsci.plugins.workflow.job.WorkflowRun","number":2},{"_class":"org.jenkinsci.plugins.workflow.job.WorkflowRun","number":1}],"lastBuild":{"_class":"org.jenkinsci.plugins.workflow.job.WorkflowRun","number":5,"result":"SUCCESS"}}
{"_class":"org.jenkinsci.plugins.workflow.job.WorkflowRun","artifacts":[{"relativePath":"bookstore-1.4.5.tgz"}]}

Build geçmişi ve artifact geri geldi; yedeğe hiç girmeyen plugins klasörü de image'dan yeniden dolduruldu (96 plugin). Bu restore testi, bir sonraki bölümdeki upgrade stratejisinin de temeli.

LTS Upgrade Stratejisi

LTS hattında her dört haftada bir düzeltme, her on iki haftada bir yeni baseline geliyor. Benim önerdiğim akış:

  • Changelog'u ve LTS upgrade guide'ını oku; Java gereksinimi, kaldırılan özellik ve gömülü plugin değişiklikleri burada yazar.
  • Dockerfile'da tag'i değiştir, jenkins-plugin-cli --available-updates ile plugins.txt'i güncelle, ikisini aynı PR'da gönder.
  • Image'ı build et ve production'ın son yedeğiyle bir test controller'ı aç: yukarıdaki restore testinin aynısı.
  • Test controller'ında birkaç gerçek pipeline koştur (agent'sız da olsa linter ve Replay ile); JCasC'nin hatasız uygulandığını log'dan doğrula.
  • Production'ı bir bakım penceresinde güncelle; geri dönüş planın eski image tag'i ve son yedek.

Security advisory'leri takip et: Jenkins projesi çekirdek ve plugin'ler için advisory'leri jenkins.io'da yayımlıyor, controller da kurulu bir plugin'in bilinen bir açığı varsa yönetim sayfasında uyarı gösteriyor. Bir advisory geldiğinde yapacağın şey aynı akış, yalnız daha hızlı.

Üretimde Dikkat

Plugin hijyeni: listedeki her plugin'in bir sahibi ve bir sebebi olsun. Kullanılmayan plugin'i plugins.txt'ten çıkarmak, bir sonraki upgrade'i ve bir sonraki advisory'yi küçültür. Bakımcısı bırakmış (adoption bekleyen) plugin'leri plugins.jenkins.io'da kontrol et.

JCasC değişikliklerini production'a gitmeden test et. En basit test, değiştirilmiş YAML ile image'ı CI'da (Continuous Integration) bir kez açıp /login'in 200 dönmesini beklemek; ikinci yazıdaki boot hatası tam olarak bu testin yakalayacağı türden.

Controller'ın kendisini de izle: disk, heap, queue uzunluğu, offline agent'lar. Kod olarak yönetmek controller'ı yeniden kurmayı kolaylaştırır, ama çökmesini engellemez.

Sık Düşülen Tuzaklar

Export'u Git'e koymak. İçindeki şifreli değerler o controller'ın anahtarına bağlı; hem sızıntı riski hem taşınamaz bir dosya.

Pin'siz plugins.txt. Aynı Dockerfile'dan farklı günlerde farklı Jenkins'ler çıkar.

Plugin'i arayüzden güncellemek. Bir sonraki image build'inde değişiklik kaybolur, kimse nedenini anlamaz.

Restore'u hiç denememek. secrets/ eksik, izinler yanlış ya da arşiv bozuk: bunların hepsi ancak restore sırasında ortaya çıkar.

Gömülü plugin'lere güvenmek. 2.580.1'den sonra war'da plugin yok; listede olmayan bir plugin upgrade sonrası "kayboldu" gibi görünür.

Küçük Bir Deneme

★☆☆ Kendi controller'ından JCasC export'u al ve kendi YAML'ınla karşılaştır. Export'ta olup senin YAML'ında olmayan, yani elle ya da varsayılanla belirlenmiş ayarları listele.

★★☆ plugins.txt'teki bir plugin'i bilerek eski bir sürüme çek, --available-updates çıktısını al ve diff ile yalnız değişen satırı gösteren küçük bir script yaz.

★★★ Haftalık çalışan bir Jenkins job'u tasarla: JENKINS_HOME'un yedeğini alsın, yedeği yeni bir volume'e açıp geçici bir controller başlatsın, API'den son build'i sorgulayıp doğrulasın ve geçici controller'ı silsin. Yedeğin şifreli nerede tutulacağına da karar ver.

Elimizde artık kodla tarif edilmiş, yedeklenen ve test edilerek upgrade edilen bir Jenkins var. Serinin son yazısında ters yöne bakıyoruz: yıllar içinde elle büyümüş eski bir Jenkins'i nasıl okursun ve buraya nasıl taşırsın? Freestyle'dan Pipeline'a, Blue Ocean'dan Pipeline Graph View'a, Java 17'den 21'e.