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.

Bir yanıt yazın

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