Jenkins ile Maven ve Gradle Build Otomasyonu

Yazılım projelerinde build süreçlerini otomatikleştirmek, modern geliştirme pratiklerinin bel kemiğini oluşturuyor. Özellikle Java ekosisteminde Maven ve Gradle, bu işin fiilen standardı haline gelmiş durumda. Tek başlarına kullanıldıklarında bile ciddi kolaylıklar sağlıyorlar, ama Jenkins ile birleştirdiğinizde ortaya gerçekten güçlü bir CI/CD pipeline’ı çıkıyor. Bu yazıda sıfırdan başlayarak Maven ve Gradle projelerini Jenkins üzerinde nasıl build edeceğinizi, pipeline’larınızı nasıl yapılandıracağınızı ve production ortamında karşılaşacağınız gerçek sorunlarla nasıl başa çıkacağınızı ele alacağız.

Jenkins’e Maven ve Gradle Entegrasyonu: Temel Kavramlar

Jenkins, Maven ve Gradle’ı iki farklı şekilde kullanabilir. Birincisi, Jenkins’in kendi tool yönetim sistemi üzerinden bu araçları otomatik indirip yapılandırmasına izin vermek. İkincisi ise sistem üzerinde zaten kurulu olan araçları kullanmak. Production ortamında genellikle ikinci yöntemi tercih ediyorum çünkü build süreçlerinde hangi versiyonun kullanıldığı konusunda tam kontrol sahibi olmak istiyorum.

Jenkins’in tool yönetimi, küçük ekipler ve geliştirme ortamları için pratik olsa da büyük ölçekli sistemlerde beklenmedik versiyon değişiklikleri ciddi sorunlara yol açabiliyor.

Jenkins Kurulumu ve Ön Hazırlık

Öncelikle sisteminizde Java’nın kurulu olduğundan emin olalım. Jenkins 2.387 ve sonrası Java 11 veya Java 17 gerektiriyor.

# Ubuntu/Debian için Jenkins kurulumu
sudo apt update
sudo apt install -y openjdk-17-jdk

# Jenkins repo ekleme
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo tee 
  /usr/share/keyrings/jenkins-keyring.asc > /dev/null

echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] 
  https://pkg.jenkins.io/debian-stable binary/ | sudo tee 
  /etc/apt/sources.list.d/jenkins.list > /dev/null

sudo apt update
sudo apt install -y jenkins

# Servisi başlatma
sudo systemctl enable jenkins
sudo systemctl start jenkins

# Başlangıç şifresini alma
sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Jenkins kurulduktan sonra Maven ve Gradle’ı sistem genelinde kuruyoruz.

# Maven kurulumu
sudo apt install -y maven

# Maven versiyonunu kontrol et
mvn --version

# Gradle kurulumu (SDKMAN ile daha pratik)
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install gradle 8.5

# Jenkins kullanıcısı için de SDKMAN kurulumu
sudo su - jenkins
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install gradle 8.5
exit

Jenkins Global Tool Configuration

Jenkins web arayüzünden Manage Jenkins > Global Tool Configuration yolunu izleyerek Maven ve Gradle’ı tanımlamamız gerekiyor. Bu tanımlamayı CLI üzerinden de yapabilirsiniz.

# Jenkins CLI ile tool konfigürasyonu için groovy script
# Jenkins Script Console'da çalıştırın

def mavenDesc = Jenkins.instance.getDescriptor(hudson.tasks.Maven.DescriptorImpl.class)
def mavenInstallation = new hudson.tasks.Maven.MavenInstallation(
    "Maven-3.9",
    "/usr/share/maven",
    []
)
mavenDesc.setInstallations(mavenInstallation)
mavenDesc.save()

def gradleDesc = Jenkins.instance.getDescriptor(hudson.plugins.gradle.GradleInstallation.DescriptorImpl.class)
def gradleInstallation = new hudson.plugins.gradle.GradleInstallation(
    "Gradle-8.5",
    "/home/jenkins/.sdkman/candidates/gradle/8.5",
    []
)
gradleDesc.setInstallations(gradleInstallation)
gradleDesc.save()

Maven Projesi için Temel Jenkinsfile

Şimdi gerçek işe geliyoruz. Bir Maven projesi için Declarative Pipeline yazalım. Bu örnekte tipik bir Spring Boot uygulamasını build edip test ettiğimizi varsayıyoruz.

// Jenkinsfile - Maven projesi için
pipeline {
    agent any
    
    tools {
        maven 'Maven-3.9'
        jdk 'JDK-17'
    }
    
    environment {
        APP_NAME = 'spring-boot-app'
        MAVEN_OPTS = '-Xmx1024m -XX:MaxPermSize=512m'
        DOCKER_REGISTRY = 'registry.sirketim.com'
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
                sh 'git log --oneline -5'
            }
        }
        
        stage('Build') {
            steps {
                sh 'mvn clean compile -B'
            }
        }
        
        stage('Unit Tests') {
            steps {
                sh 'mvn test -B'
            }
            post {
                always {
                    junit testResults: 'target/surefire-reports/*.xml',
                          allowEmptyResults: true
                    
                    jacoco(
                        execPattern: 'target/jacoco.exec',
                        classPattern: 'target/classes',
                        sourcePattern: 'src/main/java'
                    )
                }
            }
        }
        
        stage('Integration Tests') {
            steps {
                sh 'mvn verify -P integration-test -B'
            }
            post {
                always {
                    junit testResults: 'target/failsafe-reports/*.xml',
                          allowEmptyResults: true
                }
            }
        }
        
        stage('Package') {
            steps {
                sh 'mvn package -DskipTests -B'
                archiveArtifacts artifacts: 'target/*.jar',
                                fingerprint: true
            }
        }
        
        stage('Deploy to Nexus') {
            when {
                branch 'main'
            }
            steps {
                withCredentials([usernamePassword(
                    credentialsId: 'nexus-credentials',
                    usernameVariable: 'NEXUS_USER',
                    passwordVariable: 'NEXUS_PASS'
                )]) {
                    sh '''
                        mvn deploy -B 
                            -DskipTests 
                            -Dnexus.username=${NEXUS_USER} 
                            -Dnexus.password=${NEXUS_PASS}
                    '''
                }
            }
        }
    }
    
    post {
        failure {
            mail to: '[email protected]',
                 subject: "Build Hatasi: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                 body: "Detaylar: ${env.BUILD_URL}"
        }
        always {
            cleanWs()
        }
    }
}

Bu pipeline’da dikkat etmenizi istediğim birkaç nokta var. -B flag’i (batch mode) Maven’i interaktif olmayan modda çalıştırıyor ve log çıktısını temizliyor. MAVEN_OPTS environment variable’ı ile JVM heap ayarlarını yapabiliyorsunuz; özellikle büyük projelerde bu ayarı yapmadan OutOfMemoryError ile karşılaşabilirsiniz.

Gradle Projesi için Pipeline

Gradle’ın Maven’den farkı, build script’lerinin Groovy veya Kotlin DSL ile yazılmış olması ve daha esnek yapılandırma sunması. Jenkins pipeline’ında da buna göre farklı yaklaşım gerekiyor.

// Jenkinsfile - Gradle projesi için
pipeline {
    agent {
        label 'build-server'
    }
    
    tools {
        gradle 'Gradle-8.5'
        jdk 'JDK-17'
    }
    
    environment {
        GRADLE_OPTS = '-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true'
        GRADLE_USER_HOME = "${WORKSPACE}/.gradle"
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Gradle Wrapper Dogrulama') {
            steps {
                sh './gradlew --version'
                sh 'chmod +x gradlew'
            }
        }
        
        stage('Build ve Test') {
            steps {
                sh './gradlew clean build --no-daemon --stacktrace'
            }
            post {
                always {
                    junit testResults: '**/build/test-results/test/*.xml',
                          allowEmptyResults: true
                    
                    publishHTML(target: [
                        allowMissing: false,
                        alwaysLinkToLastBuild: true,
                        keepAll: true,
                        reportDir: 'build/reports/tests/test',
                        reportFiles: 'index.html',
                        reportName: 'Gradle Test Raporu'
                    ])
                }
            }
        }
        
        stage('Kod Kalitesi') {
            parallel {
                stage('Checkstyle') {
                    steps {
                        sh './gradlew checkstyleMain --no-daemon'
                    }
                }
                stage('SpotBugs') {
                    steps {
                        sh './gradlew spotbugsMain --no-daemon'
                    }
                }
            }
        }
        
        stage('Docker Image Olusturma') {
            when {
                anyOf {
                    branch 'main'
                    branch 'release/*'
                }
            }
            steps {
                sh './gradlew jib --no-daemon'
            }
        }
    }
    
    post {
        always {
            // Gradle cache'ini temizle ama .gradle/wrapper cache'ini koru
            sh 'find ${GRADLE_USER_HOME}/caches -name "*.lock" -delete 2>/dev/null || true'
        }
        cleanup {
            cleanWs(
                patterns: [[pattern: '.gradle/caches/**', type: 'INCLUDE']]
            )
        }
    }
}

Gradle pipeline’ında özellikle –no-daemon flag’ine dikkat edin. Jenkins agent’larında Gradle daemon’ı kapatmak, uzun süreli çalışan sistemlerde memory leak sorunlarını önlüyor. GRADLE_USER_HOME değişkenini workspace içine yönlendirmek de agent’lar arasında cache karışıklığını engelliyor.

Multi-Module Projeler için İleri Düzey Yapılandırma

Gerçek dünya senaryolarında çoğunlukla tek modüllü projelerle değil, onlarca alt modülden oluşan kurumsal projelerle uğraşıyorsunuz. Bu durumda build stratejinizi değiştirmeniz gerekiyor.

// Multi-module Maven projesi için pipeline
pipeline {
    agent any
    
    tools {
        maven 'Maven-3.9'
    }
    
    parameters {
        choice(
            name: 'BUILD_MODULE',
            choices: ['all', 'api-module', 'service-module', 'web-module'],
            description: 'Hangi modulu build etmek istiyorsunuz?'
        )
        booleanParam(
            name: 'SKIP_TESTS',
            defaultValue: false,
            description: 'Testleri atla'
        )
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Dependency Check') {
            steps {
                // Sadece degisen modulleri bul
                script {
                    def changedFiles = sh(
                        script: 'git diff --name-only HEAD~1 HEAD',
                        returnStdout: true
                    ).trim()
                    
                    echo "Degisen dosyalar:n${changedFiles}"
                    
                    env.CHANGED_MODULES = changedFiles.contains('api-module/') ? 'api-module ' : ''
                    env.CHANGED_MODULES += changedFiles.contains('service-module/') ? 'service-module ' : ''
                }
            }
        }
        
        stage('Build') {
            steps {
                script {
                    def mvnCmd = 'mvn clean install -B'
                    
                    if (params.SKIP_TESTS) {
                        mvnCmd += ' -DskipTests'
                    }
                    
                    if (params.BUILD_MODULE != 'all') {
                        mvnCmd += " -pl ${params.BUILD_MODULE} -am"
                    }
                    
                    sh mvnCmd
                }
            }
        }
        
        stage('Artifact Boyut Kontrolu') {
            steps {
                script {
                    def artifacts = findFiles(glob: '**/target/*.jar')
                    artifacts.each { artifact ->
                        def sizeMB = artifact.length / (1024 * 1024)
                        if (sizeMB > 100) {
                            echo "UYARI: ${artifact.name} boyutu ${sizeMB.round(2)} MB - beklenenden buyuk!"
                        }
                    }
                }
            }
        }
    }
}

Shared Library ile Pipeline Kodunu Yeniden Kullanma

Birden fazla Java projeniz varsa, pipeline kodunu tekrar tekrar yazmak yerine Jenkins Shared Library kullanmak çok daha mantıklı. Önce shared library yapısını kuralım.

# Shared library dizin yapısı
jenkins-shared-library/
├── vars/
│   ├── mavenBuild.groovy
│   └── gradleBuild.groovy
├── src/
│   └── org/
│       └── sirketim/
│           └── BuildHelper.groovy
└── resources/
    └── settings.xml
// vars/mavenBuild.groovy - Shared Library
def call(Map config = [:]) {
    def mavenVersion = config.mavenVersion ?: 'Maven-3.9'
    def jdkVersion = config.jdkVersion ?: 'JDK-17'
    def deployBranch = config.deployBranch ?: 'main'
    def sonarEnabled = config.sonarEnabled ?: true
    
    pipeline {
        agent any
        
        tools {
            maven mavenVersion
            jdk jdkVersion
        }
        
        stages {
            stage('Build ve Test') {
                steps {
                    sh 'mvn clean verify -B'
                }
                post {
                    always {
                        junit testResults: '**/surefire-reports/*.xml',
                              allowEmptyResults: true
                    }
                }
            }
            
            stage('SonarQube Analizi') {
                when {
                    expression { sonarEnabled }
                }
                steps {
                    withSonarQubeEnv('SonarQube-Server') {
                        sh 'mvn sonar:sonar -B'
                    }
                    
                    timeout(time: 5, unit: 'MINUTES') {
                        waitForQualityGate abortPipeline: true
                    }
                }
            }
            
            stage('Deploy') {
                when {
                    branch deployBranch
                }
                steps {
                    sh 'mvn deploy -DskipTests -B'
                }
            }
        }
    }
}

Bu shared library’yi projenizde kullanmak çok basit hale geliyor.

// Projenin Jenkinsfile'ı
@Library('jenkins-shared-library@main') _

mavenBuild(
    mavenVersion: 'Maven-3.9',
    jdkVersion: 'JDK-17',
    deployBranch: 'main',
    sonarEnabled: true
)

Build Cache Optimizasyonu ve Performans

Production ortamında build sürelerini kısaltmak ciddi bir operasyonel ihtiyaç. Özellikle büyük projelerde Maven ve Gradle’ın local repository/cache yönetimi çok önemli.

# Jenkins agent'larında Maven local repo için NFS mount kullanımı
# /etc/fstab'a eklenecek satır
nas-server:/jenkins/maven-repo /var/lib/jenkins/.m2/repository nfs 
  defaults,rw,soft,intr,timeo=14,bg 0 0

# Maven settings.xml içinde local repo konumu
cat > /var/lib/jenkins/.m2/settings.xml << 'EOF'
<settings>
    <localRepository>/var/lib/jenkins/.m2/repository</localRepository>
    
    <mirrors>
        <mirror>
            <id>nexus</id>
            <mirrorOf>*</mirrorOf>
            <url>https://nexus.sirketim.com/repository/maven-public/</url>
        </mirror>
    </mirrors>
    
    <servers>
        <server>
            <id>nexus</id>
            <username>${env.NEXUS_USER}</username>
            <password>${env.NEXUS_PASS}</password>
        </server>
    </servers>
    
    <profiles>
        <profile>
            <id>nexus</id>
            <properties>
                <altDeploymentRepository>
                    nexus::default::https://nexus.sirketim.com/repository/maven-releases/
                </altDeploymentRepository>
            </properties>
        </profile>
    </profiles>
</settings>
EOF

Gradle için build cache server kurulumu da oldukça faydalı oluyor.

# Gradle Enterprise veya free Build Cache Node için Docker kurulumu
docker run -d 
  --name gradle-build-cache 
  -p 5071:5071 
  -v /data/gradle-cache:/data 
  --restart unless-stopped 
  gradle/build-cache-node:latest

# Projedeki gradle.properties dosyasına eklenmesi gereken ayarlar
cat >> gradle.properties << 'EOF'
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.workers.max=4
org.gradle.daemon=false
EOF

# settings.gradle dosyasında cache node tanımlaması
cat >> settings.gradle << 'EOF'
buildCache {
    remote(HttpBuildCache) {
        url = 'http://gradle-cache-server:5071/cache/'
        push = System.getenv("CI") == "true"
    }
}
EOF

Yaygın Sorunlar ve Çözümleri

Gerçek dünyada bu sistemleri kurarken mutlaka karşılaşacağınız bazı sorunlar ve çözümleri şunlar:

OutOfMemoryError during build sorunu için Jenkins agent JVM parametrelerini düzenleyin.

# /etc/default/jenkins dosyasını düzenle
sudo vim /etc/default/jenkins

# JAVA_ARGS satırını güncelle
JAVA_ARGS="-Djava.awt.headless=true -Xms512m -Xmx4g -XX:+UseG1GC"

# Servisi yeniden başlat
sudo systemctl restart jenkins

# Agent JVM argümanları için de ek ayar
# Jenkins > Node yapılandırmasında JVM Options alanına:
-Xmx2g -XX:+UseG1GC -Dfile.encoding=UTF-8

Flaky testler nedeniyle pipeline’ın sürekli kırılması için test retry mekanizması ekleyin.

// Maven Surefire plugin konfigürasyonu - pom.xml
// Bu ayar pom.xml'e gitmeli ama Jenkinsfile'dan da override edilebilir
stage('Test') {
    steps {
        sh '''
            mvn test -B 
                -Dsurefire.rerunFailingTestsCount=2 
                -Dmaven.test.failure.ignore=false
        '''
    }
}

Paralel build’lerde port çakışması sıkça yaşanan bir sorun. Özellikle entegrasyon testlerinde.

# Her build için dinamik port atama
export SERVER_PORT=$(shuf -i 8080-9000 -n 1)
export MANAGEMENT_PORT=$(shuf -i 9001-9999 -n 1)

mvn verify -B 
    -Dserver.port=${SERVER_PORT} 
    -Dmanagement.port=${MANAGEMENT_PORT}

Jenkins Blue Ocean ile Pipeline Görselleştirme

Blue Ocean, pipeline’larınızı görsel olarak takip etmenizi sağlıyor. Kurulumu plugin manager üzerinden yapabilirsiniz.

# Jenkins CLI ile Blue Ocean kurulumu
java -jar jenkins-cli.jar 
    -s http://localhost:8080 
    -auth admin:admin_password 
    install-plugin blueocean

# Jenkins'i yeniden başlat
sudo systemctl restart jenkins

# Pipeline status kontrolü için Jenkins API kullanımı
curl -u admin:token 
    "http://jenkins.sirketim.com/job/my-maven-project/lastBuild/api/json" 
    | python3 -m json.tool | grep -E '"result"|"duration"|"timestamp"'

Güvenlik En İyi Uygulamaları

Build pipeline’larında güvenlik çoğu zaman göz ardı edilen bir konu. Credentials yönetimi konusunda dikkat edilmesi gerekenler:

  • Plaintext şifre kullanmayın: Jenkinsfile içine hiçbir zaman şifre yazmayın, her zaman credentials store kullanın
  • withCredentials bloğu: Şifreler sadece gerekli stage’de açılsın, tüm pipeline boyunca erişilebilir olmasın
  • Artifact imzalama: Production’a giden artifact’ları GPG ile imzalayın
  • Dependency vulnerability scan: OWASP Dependency Check plugin’i her build’a ekleyin
  • Workspace temizleme: Her build sonrası cleanWs() çağrısı yapın, hassas dosyalar diskte kalmasın
  • Agent güvenliği: Build agent’larını least privilege prensibiyle çalıştırın, root yerine jenkins kullanıcısı kullanın
// Güvenli credentials kullanımı örneği
stage('Guvenli Deploy') {
    steps {
        withCredentials([
            usernamePassword(
                credentialsId: 'nexus-deploy-creds',
                usernameVariable: 'DEPLOY_USER',
                passwordVariable: 'DEPLOY_PASS'
            ),
            string(
                credentialsId: 'gpg-signing-key',
                variable: 'GPG_KEY'
            )
        ]) {
            sh '''
                mvn deploy -B 
                    -DskipTests 
                    -Dgpg.keyname=${GPG_KEY} 
                    -Prel
            '''
        }
    }
}

Sonuç

Maven ve Gradle’ı Jenkins ile entegre etmek, başlangıçta karmaşık görünse de temel kavramları oturduktan sonra oldukça sistematik bir hal alıyor. Bu yazıda anlattığım yaklaşımları sırasıyla uygulamanızı öneririm: önce basit bir pipeline ile başlayın, ardından test raporlama ekleyin, sonra shared library ile kodunuzu temizleyin ve son olarak cache optimizasyonlarıyla build sürelerinizi kısaltın.

Gerçek dünyada en çok zaman kaybettiren şeyler genellikle teknik sorunlar değil, tutarsız konfigürasyonlar oluyor. Her projenin kendi Jenkinsfile’ına sahip olması, her agent’ın aynı şekilde yapılandırılmış olması ve tüm credentials’ların merkezi olarak yönetilmesi, uzun vadede ciddi operasyonel kolaylık sağlıyor. Ekibinizle pipeline’ları code review sürecine dahil edin, Jenkinsfile’ı sıradan kod gibi inceleyin. Build otomasyonu ne kadar güvenilir olursa, geliştiriciler o kadar rahat çalışır ve siz de gece iki’de uyandırılmazsınız.

Bir yanıt yazın

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