Jenkins ile Artifact Yönetimi ve Nexus Entegrasyonu
CI/CD pipeline’larında en sık karşılaşılan sorunlardan biri artifact’ların nereye gideceğidir. Build çıktıları yerel workspace’te birikir, disk dolar, kim hangi versiyonu kullandığını bilmez, üretim ortamına hangi JAR dosyasının gönderileceği belirsiz kalır. Jenkins ile Nexus Repository Manager entegrasyonu bu kaosun önüne geçmenin en etkili yollarından biridir. Bu yazıda sıfırdan kurulumdan başlayarak gerçek dünya senaryolarına kadar konuyu derinlemesine ele alacağız.
Nexus Repository Manager Nedir ve Neden Gereklidir
Nexus, Sonatype tarafından geliştirilen bir artifact deposu yöneticisidir. Maven, npm, Docker, PyPI, NuGet gibi onlarca format destekler. Temel işlevi build süreçlerinden çıkan artifact’ları versiyonlayarak saklamak ve dağıtmaktır.
Nexus olmadan tipik bir ortamda şu sorunlar yaşanır:
- Her developer farklı bir Maven Central mirror kullanır, bant genişliği boşa harcar
- Build artifact’ları Jenkins workspace’inde kalır, agent yeniden başlayınca kaybolur
- Hangi versiyonun production’da olduğunu takip etmek zorlaşır
- Bağımlılık yönetimi merkezi değildir, “bende çalışıyor” sendromu baş gösterir
Nexus bu sorunları proxy repository, hosted repository ve group repository konseptleriyle çözer.
Proxy Repository: Uzak bir kaynağı (Maven Central gibi) önbelleğe alır. Paketler bir kez indirilir, sonraki taleplerde Nexus’tan gelir.
Hosted Repository: Kendi artifact’larınızı sakladığınız depodur. Releases ve snapshots için ayrı ayrı oluşturulur.
Group Repository: Birden fazla repository’yi tek endpoint altında birleştirir.
Ortam Hazırlığı ve Kurulum
Bu yazıda Ubuntu 22.04 LTS üzerinde çalışacağız. Jenkins zaten kurulu olduğunu varsayıyorum, Nexus kurulumundan başlayalım.
Nexus Java gerektiriyor, önce bunu halledelim:
sudo apt update
sudo apt install -y openjdk-11-jdk
# Java versiyonunu doğrula
java -version
# Nexus için ayrı bir kullanıcı oluştur
sudo useradd -r -m -U -d /opt/nexus -s /bin/bash nexus
Nexus’u indirip kuralım:
# Nexus'u indir
cd /tmp
wget https://download.sonatype.com/nexus/3/nexus-3.61.0-02-unix.tar.gz
# Arşivi aç
sudo tar -xzf nexus-3.61.0-02-unix.tar.gz -C /opt/
# Dizin adını düzenle ve izinleri ayarla
sudo mv /opt/nexus-3.61.0-02 /opt/nexus
sudo chown -R nexus:nexus /opt/nexus
sudo chown -R nexus:nexus /opt/sonatype-work
# Nexus'un hangi kullanıcıyla çalışacağını belirt
sudo sed -i 's/#run_as_user=""/run_as_user="nexus"/' /opt/nexus/bin/nexus.rc
Systemd service dosyası oluşturalım:
sudo tee /etc/systemd/system/nexus.service > /dev/null <<EOF
[Unit]
Description=Nexus Repository Manager
After=network.target
[Service]
Type=forking
LimitNOFILE=65536
ExecStart=/opt/nexus/bin/nexus start
ExecStop=/opt/nexus/bin/nexus stop
User=nexus
Restart=on-abort
TimeoutSec=600
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable nexus
sudo systemctl start nexus
# Servis durumunu kontrol et
sudo systemctl status nexus
Nexus başlamaya biraz zaman ayırır, 8081 portunda ayağa kalkar. İlk admin şifresini almak için:
cat /opt/sonatype-work/nexus3/admin.password
Bu şifreyle http://sunucu-ip:8081 adresine giriş yapıp şifreyi değiştirin.
Repository Yapısının Oluşturulması
Nexus web arayüzüne girdikten sonra repository yapımızı kuralım. Maven için tipik bir yapı şöyle olur:
- maven-central (proxy): Maven Central önbellekleme
- maven-releases (hosted): Kendi release artifact’larımız
- maven-snapshots (hosted): Geliştirme sürecindeki snapshot’lar
- maven-public (group): Hepsini bir araya getiren grup
Web arayüzü yerine Nexus REST API ile bu işlemi script’le yapabiliriz, bu daha tekrarlanabilir bir yaklaşım:
NEXUS_URL="http://localhost:8081"
ADMIN_PASS="yeni-admin-sifreniz"
# maven-releases hosted repository oluştur
curl -s -u admin:${ADMIN_PASS}
-X POST "${NEXUS_URL}/service/rest/v1/repositories/maven/hosted"
-H "Content-Type: application/json"
-d '{
"name": "maven-releases",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": true,
"writePolicy": "ALLOW_ONCE"
},
"maven": {
"versionPolicy": "RELEASE",
"layoutPolicy": "STRICT"
}
}'
# maven-snapshots hosted repository oluştur
curl -s -u admin:${ADMIN_PASS}
-X POST "${NEXUS_URL}/service/rest/v1/repositories/maven/hosted"
-H "Content-Type: application/json"
-d '{
"name": "maven-snapshots",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": true,
"writePolicy": "ALLOW"
},
"maven": {
"versionPolicy": "SNAPSHOT",
"layoutPolicy": "STRICT"
}
}'
writePolicy farkına dikkat edin. Release repository’de ALLOW_ONCE kullanıyoruz, yani aynı versiyonu iki kez yükleyemezsiniz. Bu kasıtlı bir tasarım kararıdır; üretim artifact’larının değişmezliğini garanti eder. Snapshot repository’de ALLOW var çünkü geliştirme sürecinde aynı snapshot versiyonu sürekli güncellenir.
Jenkins Tarafında Yapılandırma
Jenkins’te Nexus entegrasyonu için birkaç farklı yol var. En yaygın ikisi Maven’ın settings.xml dosyasını kullanmak ve Nexus Platform Plugin’i kullanmaktır.
Maven Settings.xml Yaklaşımı
Jenkins’te “Manage Jenkins > Managed Files” bölümünden (Config File Provider Plugin gerekli) bir Maven settings.xml tanımlayalım:
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>nexus-releases</id>
<username>deployment-user</username>
<password>${NEXUS_PASSWORD}</password>
</server>
<server>
<id>nexus-snapshots</id>
<username>deployment-user</username>
<password>${NEXUS_PASSWORD}</password>
</server>
</servers>
<mirrors>
<mirror>
<id>nexus</id>
<mirrorOf>*</mirrorOf>
<url>http://nexus-sunucu:8081/repository/maven-public/</url>
</mirror>
</mirrors>
<profiles>
<profile>
<id>nexus</id>
<repositories>
<repository>
<id>central</id>
<url>http://central</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>nexus</activeProfile>
</activeProfiles>
</settings>
Şifreleri düz metin olarak koymamak için Jenkins Credentials kullanıyoruz. ${NEXUS_PASSWORD} değişkeni pipeline içinde inject edileceğiz.
Nexus Deployment Kullanıcısı Oluşturma
Production ortamında admin kullanıcısını deployment için kullanmak iyi bir pratik değil. Nexus’ta minimal yetkili bir deployment kullanıcısı oluşturalım:
NEXUS_URL="http://localhost:8081"
ADMIN_PASS="admin-sifreniz"
# Deployment rolü oluştur
curl -s -u admin:${ADMIN_PASS}
-X POST "${NEXUS_URL}/service/rest/v1/security/roles"
-H "Content-Type: application/json"
-d '{
"id": "jenkins-deployment-role",
"name": "Jenkins Deployment Role",
"description": "CI/CD pipeline artifact deployment",
"privileges": [
"nx-repository-view-maven2-maven-releases-*",
"nx-repository-view-maven2-maven-snapshots-*"
],
"roles": []
}'
# Deployment kullanıcısı oluştur
curl -s -u admin:${ADMIN_PASS}
-X POST "${NEXUS_URL}/service/rest/v1/security/users"
-H "Content-Type: application/json"
-d '{
"userId": "jenkins-deployer",
"firstName": "Jenkins",
"lastName": "Deployer",
"emailAddress": "[email protected]",
"password": "guclu-bir-sifre",
"status": "active",
"roles": ["jenkins-deployment-role"]
}'
Gerçek Dünya Pipeline Senaryosu
Şimdi gerçek bir senaryo üzerine gidelim. Bir Java mikroservisi düşünün; feature branch’lerde snapshot olarak, main/master’da release olarak publish edilsin. Ayrıca hangi commit’ten hangi artifact’ın oluştuğu izlenebilir olsun.
pipeline {
agent any
environment {
NEXUS_URL = 'http://nexus-sunucu:8081'
NEXUS_CREDENTIALS = credentials('nexus-deployment-creds')
APP_NAME = 'payment-service'
MAVEN_OPTS = '-Xmx1024m'
}
tools {
maven 'Maven-3.9'
jdk 'JDK-11'
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
// Git commit hash'ini al, artifact metadata'sına ekleyeceğiz
env.GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
env.BUILD_BRANCH = env.GIT_BRANCH.replaceAll('origin/', '')
}
}
}
stage('Version Belirleme') {
steps {
script {
def pom = readMavenPom file: 'pom.xml'
env.PROJECT_VERSION = pom.version
env.IS_SNAPSHOT = pom.version.contains('SNAPSHOT')
echo "Versiyon: ${env.PROJECT_VERSION}"
echo "Snapshot mi: ${env.IS_SNAPSHOT}"
}
}
}
stage('Build ve Test') {
steps {
configFileProvider([configFile(fileId: 'nexus-maven-settings', variable: 'MAVEN_SETTINGS')]) {
sh """
mvn -s ${MAVEN_SETTINGS}
-Dbuild.number=${BUILD_NUMBER}
-Dgit.commit=${GIT_COMMIT_SHORT}
clean verify
-Dmaven.test.failure.ignore=false
"""
}
}
post {
always {
junit '**/target/surefire-reports/*.xml'
jacoco(
execPattern: '**/target/jacoco.exec',
classPattern: '**/target/classes',
sourcePattern: '**/src/main/java'
)
}
}
}
stage('Artifact Publish') {
when {
anyOf {
branch 'main'
branch 'develop'
expression { return env.BUILD_BRANCH.startsWith('release/') }
}
}
steps {
configFileProvider([configFile(fileId: 'nexus-maven-settings', variable: 'MAVEN_SETTINGS')]) {
sh """
mvn -s ${MAVEN_SETTINGS}
deploy
-DskipTests
-Dmaven.install.skip=true
"""
}
echo "Artifact başarıyla Nexus'a yüklendi: ${APP_NAME}-${PROJECT_VERSION}"
}
}
stage('Artifact Doğrulama') {
when {
branch 'main'
}
steps {
script {
// Artifact'ın gerçekten Nexus'ta olduğunu doğrula
def artifactGroup = 'com.sirket.payments'
def artifactPath = artifactGroup.replace('.', '/') + "/${APP_NAME}/${PROJECT_VERSION}"
def response = sh(
script: """
curl -s -o /dev/null -w "%{http_code}"
-u ${NEXUS_CREDENTIALS_USR}:${NEXUS_CREDENTIALS_PSW}
"${NEXUS_URL}/repository/maven-releases/${artifactPath}/"
""",
returnStdout: true
).trim()
if (response != '200') {
error "Artifact doğrulaması başarısız! HTTP: ${response}"
}
echo "Artifact doğrulandı, HTTP 200 OK"
}
}
}
}
post {
success {
slackSend(
color: 'good',
message: "✅ ${APP_NAME} v${PROJECT_VERSION} başarıyla Nexus'a yüklendi. Build: #${BUILD_NUMBER}"
)
}
failure {
slackSend(
color: 'danger',
message: "❌ ${APP_NAME} build/deploy başarısız. Build: #${BUILD_NUMBER}"
)
}
}
}
Snapshot Temizleme Stratejisi
Snapshot artifact’lar birikir ve disk doldurmaya başlar. Nexus’ta cleanup policy tanımlamak şart:
NEXUS_URL="http://localhost:8081"
ADMIN_PASS="admin-sifreniz"
# Snapshot cleanup policy oluştur
curl -s -u admin:${ADMIN_PASS}
-X POST "${NEXUS_URL}/service/rest/v1/cleanup-policies"
-H "Content-Type: application/json"
-d '{
"name": "snapshot-cleanup-30gun",
"format": "maven2",
"mode": "delete",
"notes": "30 gunden eski snapshot artifact temizle",
"criteria": {
"lastBlobUpdated": "30",
"lastDownloaded": "30",
"preRelease": "PRERELEASES"
}
}'
Bu policy’yi Nexus arayüzünden maven-snapshots repository’ye atayabilirsiniz. Compact blobstore görevini de haftada bir çalışacak şekilde scheduler’a ekleyin, yoksa disk alanı geri kazanılmaz.
Docker Artifact Yönetimi
Maven dışında Docker image’ları da Nexus’ta saklayabilirsiniz. Bu özellikle private registry ihtiyacı olan ekipler için kritik:
# Nexus'ta Docker hosted repository oluştur
curl -s -u admin:${ADMIN_PASS}
-X POST "${NEXUS_URL}/service/rest/v1/repositories/docker/hosted"
-H "Content-Type: application/json"
-d '{
"name": "docker-internal",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": true,
"writePolicy": "ALLOW"
},
"docker": {
"v1Enabled": false,
"forceBasicAuth": true,
"httpPort": 8082
}
}'
Jenkins pipeline’da Docker image push için:
stage('Docker Build ve Push') {
steps {
script {
def imageTag = "${NEXUS_URL}:8082/${APP_NAME}:${PROJECT_VERSION}-${GIT_COMMIT_SHORT}"
def latestTag = "${NEXUS_URL}:8082/${APP_NAME}:latest"
docker.withRegistry("http://${NEXUS_URL}:8082", 'nexus-docker-creds') {
def image = docker.build("${APP_NAME}:${PROJECT_VERSION}")
image.push("${PROJECT_VERSION}-${GIT_COMMIT_SHORT}")
// Sadece main branch'te latest tag'i güncelle
if (env.BUILD_BRANCH == 'main') {
image.push('latest')
}
}
echo "Docker image push tamamlandı: ${imageTag}"
}
}
}
Artifact Metadata ve Traceability
Hangi artifact’ın hangi Git commit’inden, hangi pipeline’dan geldiğini bilmek güvenlik ve debug açısından çok değerlidir. Maven’da bunu MANIFEST.MF dosyasına yazarız:
<!-- pom.xml içinde maven-jar-plugin yapılandırması -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<manifestEntries>
<Build-Number>${build.number}</Build-Number>
<Git-Commit>${git.commit}</Git-Commit>
<Build-Branch>${git.branch}</Build-Branch>
<Build-Timestamp>${maven.build.timestamp}</Build-Timestamp>
</manifestEntries>
</archive>
</configuration>
</plugin>
Bu sayede production’da çalışan JAR dosyasının içine bakarak hangi commit’ten geldiğini anlayabilirsiniz:
# JAR içindeki manifest'i oku
unzip -p uygulama.jar META-INF/MANIFEST.MF | grep -E "Build|Git"
Nexus’u İzleme ve Sağlık Kontrolü
Nexus’un düzgün çalışıp çalışmadığını Jenkins pipeline’ından veya harici monitoring sistemlerinden kontrol edebilirsiniz:
#!/bin/bash
# nexus-healthcheck.sh
NEXUS_URL="http://localhost:8081"
ADMIN_PASS="admin-sifreniz"
# Nexus durumunu kontrol et
STATUS=$(curl -s -o /dev/null -w "%{http_code}"
"${NEXUS_URL}/service/rest/v1/status")
if [ "$STATUS" != "200" ]; then
echo "KRITIK: Nexus yanit vermiyor! HTTP: $STATUS"
exit 2
fi
# Disk kullanimi kontrolu
DISK_INFO=$(curl -s -u admin:${ADMIN_PASS}
"${NEXUS_URL}/service/rest/v1/status/check" |
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('Available Space', 'N/A'))")
echo "Nexus durumu: OK"
echo "Disk bilgisi: ${DISK_INFO}"
# Repository erisilebilirlik kontrolu
for REPO in maven-releases maven-snapshots maven-public; do
REPO_STATUS=$(curl -s -o /dev/null -w "%{http_code}"
-u admin:${ADMIN_PASS}
"${NEXUS_URL}/repository/${REPO}/")
echo "Repository ${REPO}: HTTP ${REPO_STATUS}"
done
Bu script’i Jenkins’te her 30 dakikada çalışan bir monitoring job’ına ekleyebilirsiniz. Disk dolduğunda veya Nexus çöktüğünde anında haberdar olursunuz.
Yaygın Sorunlar ve Çözümleri
Problem: 401 Unauthorized hatası deployment sırasında
settings.xml içindeki server ID ile pom.xml içindeki distribution management ID’nin birebir eşleşmesi gerekir. Büyük küçük harf duyarlıdır.
Problem: Snapshot artifact’ı bulunamıyor
Maven local cache’i sorun yaratabilir. Jenkins agent’ta şunu deneyin:
mvn -s settings.xml -U clean install
# -U bayragi snapshot'lari force update eder
Problem: Nexus çok yavaş
JVM heap ayarını gözden geçirin. Varsayılan değerler küçük gelir:
# /opt/nexus/bin/nexus.vmoptions dosyasini duzenle
sudo tee /opt/nexus/bin/nexus.vmoptions > /dev/null <<EOF
-Xms1024m
-Xmx2048m
-XX:MaxDirectMemorySize=2048m
-XX:+UnlockDiagnosticVMOptions
-XX:+LogVMOutput
-XX:LogFile=../sonatype-work/nexus3/log/jvm.log
-XX:-OmitStackTraceInFastThrow
-Djava.net.preferIPv4Stack=true
-Dkaraf.home=.
-Dkaraf.base=.
-Dkaraf.etc=etc/karaf
-Djava.util.logging.config.file=etc/karaf/java.util.logging.properties
-Dkaraf.data=../sonatype-work/nexus3
-Dkaraf.log=../sonatype-work/nexus3/log
-Djava.io.tmpdir=../sonatype-work/nexus3/tmp
EOF
sudo systemctl restart nexus
Problem: Proxy repository dış internete erişemiyor
Eğer ortamınızda HTTP proxy kullanıyorsanız Nexus’un sistem özelliklerine eklemeniz gerekir. Nexus arayüzünden “System > HTTP” bölümünden proxy ayarlarını yapın ya da başlangıç parametrelerine ekleyin.
Sonuç
Jenkins ve Nexus entegrasyonu başta karmaşık görünse de bir kez doğru kurulup yapılandırıldığında ekibin günlük hayatını ciddi oranda kolaylaştırıyor. Artifact’lar artık kaybolmuyor, hangi versiyonun nerede çalıştığı izlenebiliyor, bağımlılıklar merkezi bir noktadan yönetiliyor ve dış dünyaya bağımlılık azalıyor.
Bu mimarinin gerçek değerini en çok “production’da bir bug çıktı, 3 hafta önceki build’i deploy etmemiz lazım” senaryosunda görürsünüz. Artifact deposu yoksa bu operasyon günler alır ya da imkansız hale gelir. Nexus ile sadece versiyonu belirtip deployment pipeline’ı tetiklemeniz yeterli olur.
Bir sonraki adım olarak Nexus’ta IQ Server entegrasyonu ile güvenlik açığı taramasına veya artifact signing ile release artifact’larınızın bütünlüğünü garanti altına almaya bakabilirsiniz. Ama temeli sağlam atmadan bu adımlara geçmek işe yaramaz, bu yüzden önce burada anlattığımız yapıyı sağlamlaştırmanızı tavsiye ederim.
