Jenkins ile Docker Agent Kullanımı

CI/CD pipeline’larında en sık karşılaşılan sorunlardan biri build ortamlarının tutarsızlığıdır. “Bende çalışıyor” sendromu sadece uygulama geliştiricilerin değil, altyapıcıların da baş belasıdır. Jenkins agent’larını Docker container’ları olarak çalıştırmak bu sorunu kökten çözer ve her build’in temiz, izole bir ortamda başlamasını sağlar. Bu yazıda Jenkins ile Docker agent kullanımını, gerçek dünya senaryolarıyla birlikte ele alacağız.

Docker Agent Neden Kullanılır?

Klasik Jenkins kurulumlarında, agent’lar kalıcı sanal makineler ya da fiziksel sunuculardır. Bu yaklaşımın birkaç ciddi problemi vardır. Zamanla agent üzerine yüklenen araçlar birikir, versiyon çakışmaları yaşanır, bir job’ın bıraktığı artifact başka bir job’ı etkiler ve agent sayısını ölçeklendirmek zahmetli hale gelir.

Docker agent yaklaşımında ise her build için yeni bir container ayağa kalkar, build tamamlandıktan sonra container silinir. Bu sayede:

  • Her build tamamen izole ve tekrarlanabilir olur
  • Farklı projeler farklı tool versiyonları kullanabilir
  • Agent kapasitesi otomatik olarak ölçeklenir
  • Agent yönetimi için harcanan operasyonel yük azalır
  • Build ortamı kod ile birlikte versiyonlanabilir (Dockerfile)

Ön Gereksinimler

Başlamadan önce şunların hazır olması gerekiyor:

  • Çalışan bir Jenkins instance’ı (2.x ve üzeri)
  • Jenkins sunucusunda veya agent’larda Docker kurulu
  • Jenkins kullanıcısının Docker socket’ına erişim yetkisi
  • Docker Pipeline ve Docker plugin’lerinin kurulu olması

Jenkins kullanıcısını docker grubuna eklemek için:

sudo usermod -aG docker jenkins
sudo systemctl restart jenkins

Eğer Jenkins container içinde çalışıyorsa, Docker socket’ını mount etmek gerekir:

docker run -d 
  --name jenkins 
  -p 8080:8080 
  -p 50000:50000 
  -v jenkins_home:/var/jenkins_home 
  -v /var/run/docker.sock:/var/run/docker.sock 
  jenkins/jenkins:lts

Socket mount işleminden sonra container içindeki Jenkins kullanıcısının socket’a erişebildiğini doğrulayın:

docker exec -it jenkins ls -la /var/run/docker.sock
docker exec -u root -it jenkins chmod 666 /var/run/docker.sock

Plugin Kurulumu

Jenkins arayüzünden Manage Jenkins > Plugins > Available Plugins kısmına gidin ve şu plugin’leri kurun:

  • Docker Plugin: Jenkins’in Docker daemon ile konuşmasını sağlar
  • Docker Pipeline Plugin: Jenkinsfile içinde docker direktifini kullanmaya yarar
  • Docker Slaves Plugin (opsiyonel): Dinamik agent provisioning için

Plugin kurulumunu Jenkins CLI ile de yapabilirsiniz:

java -jar jenkins-cli.jar 
  -s http://localhost:8080 
  -auth admin:your_api_token 
  install-plugin docker-plugin docker-workflow

İlk Docker Agent Pipeline’ı

En basit kullanım şekliyle, mevcut bir Docker image’ı agent olarak belirtebilirsiniz:

pipeline {
    agent {
        docker {
            image 'maven:3.9.4-eclipse-temurin-17'
            args '-v $HOME/.m2:/root/.m2'
        }
    }
    stages {
        stage('Build') {
            steps {
                sh 'mvn --version'
                sh 'mvn clean package -DskipTests'
            }
        }
        stage('Test') {
            steps {
                sh 'mvn test'
            }
        }
    }
    post {
        always {
            junit '**/target/surefire-reports/*.xml'
            archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
        }
    }
}

Burada args '-v $HOME/.m2:/root/.m2' kısmına dikkat edin. Maven dependency’lerini her build’de tekrar indirmemek için .m2 dizinini host’tan mount ediyoruz. Bu küçük detay build sürenizi ciddi ölçüde kısaltır.

Farklı Stage’lerde Farklı Agent Kullanımı

Gerçek dünya projelerinde frontend ve backend farklı toolchain’ler gerektirir. Bu durumda her stage için ayrı Docker agent tanımlayabilirsiniz:

pipeline {
    agent none
    stages {
        stage('Backend Build') {
            agent {
                docker {
                    image 'maven:3.9.4-eclipse-temurin-17'
                    args '-v $HOME/.m2:/root/.m2'
                    reuseNode false
                }
            }
            steps {
                sh 'mvn clean package -DskipTests'
                stash includes: 'target/*.jar', name: 'backend-artifact'
            }
        }
        stage('Frontend Build') {
            agent {
                docker {
                    image 'node:20-alpine'
                    args '-v $HOME/.npm:/root/.npm'
                }
            }
            steps {
                dir('frontend') {
                    sh 'npm ci'
                    sh 'npm run build'
                    stash includes: 'dist/**', name: 'frontend-artifact'
                }
            }
        }
        stage('Integration Test') {
            agent {
                docker {
                    image 'python:3.11-slim'
                }
            }
            steps {
                unstash 'backend-artifact'
                unstash 'frontend-artifact'
                sh 'pip install -r tests/requirements.txt'
                sh 'pytest tests/integration/'
            }
        }
    }
}

agent none ile pipeline seviyesinde herhangi bir agent atanmıyor. Her stage kendi agent’ını kendisi tanımlıyor. stash ve unstash ile artifact’lar stage’ler arasında taşınıyor.

Özel Dockerfile ile Agent Oluşturma

Bazen hazır bir image yeterli olmaz. Projeye özel araçların kurulu olduğu bir image gerekebilir. Bu durumda Dockerfile’ı doğrudan Jenkinsfile içinde referans verebilirsiniz:

pipeline {
    agent {
        dockerfile {
            filename 'ci/Dockerfile.agent'
            dir '.'
            additionalBuildArgs '--build-arg VERSION=1.2.3'
            args '-v /tmp/gradle_cache:/root/.gradle'
        }
    }
    environment {
        GRADLE_OPTS = '-Dorg.gradle.daemon=false'
    }
    stages {
        stage('Build and Test') {
            steps {
                sh './gradlew clean build'
            }
        }
    }
}

ci/Dockerfile.agent dosyası şöyle görünebilir:

FROM eclipse-temurin:17-jdk-alpine

ARG VERSION=latest

RUN apk add --no-cache 
    git 
    curl 
    bash 
    openssh-client

# Özel araçların kurulumu
RUN curl -sL https://github.com/gruntwork-io/terragrunt/releases/download/v0.53.0/terragrunt_linux_amd64 
    -o /usr/local/bin/terragrunt && 
    chmod +x /usr/local/bin/terragrunt

WORKDIR /workspace

Bu yaklaşımın güzel yanı, Dockerfile’ın da kaynak koda dahil olması. CI ortamı değiştiğinde PR açılır, gözden geçirilir ve merge edilir. Kimse gidip agent sunucusuna manuel bir şeyler kurmaz.

Docker in Docker ve Alternatif Yaklaşımlar

Docker agent içinde Docker komutları çalıştırmanız gerekiyorsa (örneğin image build edip push etmek), iki seçeneğiniz var: gerçek DinD (Docker in Docker) ya da Docker socket’ı mount etmek.

Socket mount yöntemi genellikle daha güvenli ve performanslıdır:

pipeline {
    agent {
        docker {
            image 'docker:24-dind'
            args '-v /var/run/docker.sock:/var/run/docker.sock -v /tmp:/tmp'
        }
    }
    environment {
        DOCKER_BUILDKIT = '1'
        REGISTRY = 'registry.company.com'
        IMAGE_NAME = 'myapp'
    }
    stages {
        stage('Docker Build') {
            steps {
                script {
                    def gitCommit = sh(
                        script: 'git rev-parse --short HEAD',
                        returnStdout: true
                    ).trim()
                    
                    sh """
                        docker build 
                            --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') 
                            --build-arg GIT_COMMIT=${gitCommit} 
                            -t ${REGISTRY}/${IMAGE_NAME}:${gitCommit} 
                            -t ${REGISTRY}/${IMAGE_NAME}:latest 
                            .
                    """
                    
                    withCredentials([usernamePassword(
                        credentialsId: 'registry-credentials',
                        usernameVariable: 'REGISTRY_USER',
                        passwordVariable: 'REGISTRY_PASS'
                    )]) {
                        sh """
                            echo $REGISTRY_PASS | docker login ${REGISTRY} 
                                -u $REGISTRY_USER --password-stdin
                            docker push ${REGISTRY}/${IMAGE_NAME}:${gitCommit}
                            docker push ${REGISTRY}/${IMAGE_NAME}:latest
                        """
                    }
                }
            }
        }
    }
}

Docker Cloud ile Dinamik Agent Provisioning

Jenkins’in Docker Cloud özelliği, agent’ları talep üzerine oluşturup işi bitince kaldırır. Bu, kaynak kullanımını optimize eder.

Manage Jenkins > Clouds > New Cloud > Docker yolunu izleyin ve şu ayarları yapın:

  • Docker Host URI: unix:///var/run/docker.sock (local) ya da tcp://docker-host:2376 (remote)
  • Enabled: İşaretli olmalı
  • Container Cap: Eş zamanlı maksimum container sayısı (örneğin 5)

Docker Agent Template ayarları için:

  • Labels: docker-agent (Jenkinsfile’da bu label ile referans vereceksiniz)
  • Docker Image: jenkins/inbound-agent:latest ya da özel image’ınız
  • Remote File System Root: /home/jenkins/agent
  • Connect method: Attach Docker container

Bu yapılandırmadan sonra pipeline’da şöyle kullanabilirsiniz:

pipeline {
    agent {
        label 'docker-agent'
    }
    stages {
        stage('Dynamic Agent Test') {
            steps {
                sh 'echo "Bu container talep üzerine oluşturuldu"'
                sh 'hostname && uname -a'
            }
        }
    }
}

Kubernetes Üzerinde Docker Agent (Opsiyonel İleri Seviye)

Eğer altyapınızda Kubernetes varsa, Jenkins Kubernetes plugin’i ile agent’ları pod olarak çalıştırabilirsiniz. Bu yaklaşım daha esnek ve ölçeklenebilirdir:

pipeline {
    agent {
        kubernetes {
            yaml '''
                apiVersion: v1
                kind: Pod
                spec:
                  containers:
                  - name: maven
                    image: maven:3.9.4-eclipse-temurin-17
                    command:
                    - sleep
                    args:
                    - 99d
                    volumeMounts:
                    - name: m2-cache
                      mountPath: /root/.m2
                  - name: docker
                    image: docker:24-cli
                    command:
                    - sleep
                    args:
                    - 99d
                    volumeMounts:
                    - name: docker-sock
                      mountPath: /var/run/docker.sock
                  volumes:
                  - name: m2-cache
                    persistentVolumeClaim:
                      claimName: maven-cache-pvc
                  - name: docker-sock
                    hostPath:
                      path: /var/run/docker.sock
            '''
        }
    }
    stages {
        stage('Build') {
            steps {
                container('maven') {
                    sh 'mvn clean package'
                }
            }
        }
        stage('Docker Push') {
            steps {
                container('docker') {
                    sh 'docker build -t myapp:latest .'
                }
            }
        }
    }
}

Güvenlik ve Best Practice’ler

Docker agent kullanırken güvenliği ihmal etmemek gerekiyor. Birkaç kritik nokta:

Container izolasyonu için:

  • Production credential’larını sadece ihtiyaç duyan stage’lerde kullanın, pipeline geneline yaymayın
  • --privileged flag’ini mümkün olduğunca kullanmaktan kaçının
  • Network erişimini gerektiği kadar verin

Image yönetimi için:

  • latest tag’ini production pipeline’larında kullanmayın, sabit versiyonlar belirtin
  • Kendi registry’nizden image çekin, pull rate limit sorunlarından korunun
  • Image’ları düzenli olarak vulnerability taramasından geçirin (Trivy, Grype gibi araçlarla)

Performans için:

  • Dependency cache’lerini volume mount ile kalıcı hale getirin
  • Büyük image’lar yerine alpine tabanlı minimal image’lar tercih edin
  • reuseNode true ayarını aynı node’da birden fazla stage çalıştırmanız gerektiğinde kullanın

Örnek güvenli bir pipeline şablonu:

pipeline {
    agent {
        docker {
            image 'maven:3.9.4-eclipse-temurin-17-alpine'
            args '''
                -v $HOME/.m2:/root/.m2:rw
                --memory=2g
                --cpus=1.5
                --read-only
                --tmpfs /tmp:rw,exec,nosuid
            '''
            registryUrl 'https://registry.company.com'
            registryCredentialsId 'registry-pull-credentials'
        }
    }
    options {
        timeout(time: 30, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }
    stages {
        stage('Security Scan') {
            steps {
                sh 'mvn dependency-check:check'
            }
        }
        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }
        stage('Test') {
            steps {
                sh 'mvn verify'
            }
            post {
                always {
                    junit '**/target/surefire-reports/*.xml'
                    publishCoverage adapters: [jacocoAdapter('**/jacoco.xml')]
                }
            }
        }
    }
}

--memory ve --cpus kısıtlamaları sayesinde tek bir build tüm sistem kaynaklarını tüketemez. --read-only ile container filesystem’i salt okunur yapılmış, sadece /tmp yazılabilir bırakılmış.

Troubleshooting

Sık karşılaşılan sorunlar ve çözümleri:

“Permission denied” hatası Docker socket üzerinde:

# Jenkins kullanıcısının docker grubuna üyeliğini kontrol et
id jenkins
# Grup değişikliğinin aktif olması için servisi yeniden başlat
sudo systemctl restart jenkins

Container başlamıyor, “image not found” hatası:

Jenkins’in çalıştığı ortamdan registry’e erişimi olduğunu doğrulayın. Private registry kullanıyorsanız credential’ları Jenkinsfile’da belirtin veya Jenkins sunucusunda docker login yapılmış olmalı.

Build tamamlandıktan sonra container temizlenmiyor:

Bu genellikle build sırasında oluşan bir hata sonucu Jenkins’in cleanup yapamadığı durumlarda olur. Manuel temizlik için:

# Durmuş container'ları listele
docker ps -a --filter "status=exited" --filter "label=jenkins"

# Jenkins tarafından bırakılan container'ları temizle
docker container prune --filter "label=jenkins" -f

# Kullanılmayan image'ları temizle
docker image prune -a --filter "until=24h" -f

Workspace izin hataları:

Container içindeki kullanıcı ID’si ile Jenkins workspace’in sahibi uyuşmuyorsa dosya erişim sorunları yaşarsınız. Bunu çözmek için:

agent {
    docker {
        image 'maven:3.9.4-eclipse-temurin-17'
        args '-u root:root'  // Geliştirme ortamı için, production'da dikkatli kullanın
    }
}

Ya da daha doğru yaklaşım, Dockerfile’ınızda kullanıcı ID’sini Jenkins ile eşleştirmek.

Sonuç

Jenkins ile Docker agent kullanımı, modern CI/CD altyapılarının vazgeçilmez bir parçası haline geldi. Başlangıçta biraz kurulum gerektiriyor ama kazanımları oldukça büyük: tutarlı build ortamları, kolay ölçeklendirme, düşük operasyonel yük ve artifact izolasyonu.

En basit senaryodan başlayın. Tek bir pipeline’ı Docker agent ile çalıştırın, build ve test edin. Sonra kademeli olarak tüm pipeline’larınızı taşıyın. Cache stratejilerini ve güvenlik kısıtlamalarını da ekleyince oldukça sağlam bir yapıya kavuşursunuz.

Kubernetes üzerinde çalışıyorsanız, bir sonraki adım olarak Jenkins Kubernetes plugin’ini incelemenizi öneririm. Docker Cloud’a göre çok daha esnek ve üretim ortamlarında daha stabil çalışıyor. Ama mütevazi bir başlangıç için bu yazıda anlattıklarımız gayet yeterli.

Bir yanıt yazın

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