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

Jenkins Agent'ları: SSH Agent, Inbound Agent, Label'lar ve Docker Container'da Build

Mert Özen 8 Kas 2026 10 dk 5 görüntülenme
Jenkins Agent'ları: SSH Agent, Inbound Agent, Label'lar ve Docker Container'da Build

SSH agent'ta bağlantıyı controller, inbound agent'ta agent açar; build'i label expression'larla seçtiğin agent'ta ya da agent { docker } ile SDK container'ında koşturursun. JCasC ile agent tanımı, gerçek SSH ve WebSocket bağlantı log'ları, Docker Pipeline'ın workspace mount'u ve uid 1000 ayrıntısı, cleanWs ile disk, Kubernetes pod template ve Windows agent'a kısa bakış.

Sürüm notu: Bu yazıdaki örnekler Jenkins 2.580.2 LTS (Long-Term Support), jenkins/ssh-agent ve jenkins/inbound-agent (JDK 21) image'ları, Docker Pipeline plugin'i 653, Docker 29 ve .NET 10 SDK (10.0.403) ile denenmiştir.

Önce Tahmin Et

docker-agent-1 adlı agent'ın kendisi bir container ve içinde jenkins kullanıcısı (uid 1000) çalışıyor. Pipeline'da bir stage'i agent { docker { image 'mcr.microsoft.com/dotnet/sdk:10.0' } } ile .NET SDK (Software Development Kit) container'ında koşturuyorsun ve stage'in içinde echo $HOME yazdırıyorsun. Ne görürsün: /root, /home/jenkins, yoksa bambaşka bir şey mi? Cevap, Docker agent'larda karşılaşacağın izin sorunlarının yarısını açıklıyor.

İki Tür Kalıcı Agent: Kim Kimi Arar?

Kalıcı (permanent) bir agent'ı controller'a bağlamanın iki yolu var ve aralarındaki fark bağlantıyı kimin açtığı. SSH (Secure Shell) agent'ta controller agent makinesine SSH ile bağlanır, remoting.jar'ı kopyalar ve başlatır; agent'ın üzerinde SSH sunucusu ve Java olması yeter. Inbound agent'ta (eski adıyla JNLP (Java Network Launch Protocol) agent) yön terstir: agent makinesindeki process controller'a bağlanır. Bu bağlantı eskiden ayrı bir TCP (Transmission Control Protocol) portundan (50000) yapılırdı; bugün WebSocket ile controller'ın normal HTTP (Hypertext Transfer Protocol) portundan yapılabiliyor.

04-agent-connections-tr.png

Hangisini seçeceğin çoğu zaman ağa bağlı. Agent controller'ın erişebildiği bir ağdaysa SSH basit ve agent tarafında servis yönetimi gerektirmez. Agent bir firewall'un arkasındaysa, başka bir veri merkezindeyse ya da controller ona hiç erişemiyorsa inbound agent dışarı doğru tek bir HTTPS (HTTP Secure) bağlantısıyla işi çözer. Bu serideki iki agent'ı JCasC (Jenkins Configuration as Code) ile şöyle tanımladım:

jenkins:
  nodes:
    - permanent:
        name: docker-agent-1
        labelString: "linux docker"
        remoteFS: /home/jenkins/agent
        numExecutors: 2
        mode: EXCLUSIVE
        launcher:
          inbound:
            webSocket: true
    - permanent:
        name: ssh-agent-1
        labelString: "linux ssh"
        remoteFS: /home/jenkins/agent
        numExecutors: 1
        mode: EXCLUSIVE
        launcher:
          ssh:
            host: jenkwork-ssh-agent
            port: 22
            credentialsId: agent-ssh-key
            sshHostKeyVerificationStrategy:
              manuallyTrustedKeyVerificationStrategy:
                requireInitialManualTrust: false

mode: EXCLUSIVE, bu agent'a yalnız label'ıyla açıkça onu isteyen işlerin gideceği anlamına gelir; NORMAL modda label'sız işler de oraya düşebilir. remoteFS agent'taki kök klasör: workspace'ler, remoting jar cache'i ve log'lar burada.

SSH Agent

SSH agent için jenkins/ssh-agent:latest-jdk21 image'ını kullandım. Image, JENKINS_AGENT_SSH_PUBKEY environment variable'ındaki public key'i jenkins kullanıcısının authorized_keys'ine yazıyor. Private key controller'da bir credential olarak duruyor (agent-ssh-key). Controller'ın bağlantı log'undan seçtiğim satırlar:

[11/08/26 03:27:48] [SSH] Opening SSH connection to jenkwork-ssh-agent:22.
[11/08/26 03:27:48] [SSH] SSH host key matches key seen previously for this host. Connection will be allowed.
[11/08/26 03:27:48] [SSH] Authentication successful.
[11/08/26 03:27:48] [SSH] Starting sftp client.
[11/08/26 03:27:48] [SSH] Copying latest remoting.jar...
Source agent hash is E3CCA24233EF979BCEFF9192C9549D2F. Installed agent hash is E3CCA24233EF979BCEFF9192C9549D2F
Verified agent jar. No update is necessary.
[11/08/26 03:27:48] [SSH] Starting agent process: cd "/home/jenkins/agent" && java  -jar remoting.jar -workDir /home/jenkins/agent -jar-cache /home/jenkins/agent/remoting/jarCache
Remoting version: 3386.v353e57a_1b_ea_0
Launcher: SSHLauncher
Communication Protocol: Standard in/out
This is a Unix agent
Agent successfully connected and online

Akış tam figürdeki gibi: controller bağlanıyor, remoting.jar'ın güncel olduğunu hash ile doğruluyor, agent process'ini SSH oturumunun içinde başlatıyor ve iletişim o oturumun standart girdi/çıktısı üzerinden yürüyor. "Host key matches key seen previously" satırı host key doğrulamasından geliyor. manuallyTrustedKeyVerificationStrategy ile requireInitialManualTrust: false ilk görülen anahtara güvenip sonra değişirse bağlantıyı reddeder. Test ortamı için makul; production'da knownHostsFileKeyVerificationStrategy ile anahtarı önceden bilinen bir known_hosts dosyasından doğrulamak daha sağlam.

Inbound Agent ve WebSocket

Inbound agent için resmi jenkins/inbound-agent:latest-jdk21 image'ına iki şey ekledim: Docker CLI'ı (Command Line Interface), çünkü bu agent Docker içinde build alacak, ve dokuzuncu yazıdaki deploy için rsync:

FROM jenkins/inbound-agent:latest-jdk21
USER root
RUN apt-get update \
    && apt-get install -y --no-install-recommends rsync \
    && rm -rf /var/lib/apt/lists/*
USER jenkins
COPY --from=docker:29.9.3-cli /usr/local/bin/docker /usr/local/bin/docker

Compose tanımı:

  agent:
    build: ./agent
    image: jenkwork/inbound-agent-docker:jdk21
    container_name: jenkwork-agent
    environment:
      JENKINS_URL: http://jenkwork-jenkins:8080/
      JENKINS_AGENT_NAME: docker-agent-1
      JENKINS_SECRET: ${DOCKER_AGENT_SECRET}
      JENKINS_WEB_SOCKET: "true"
      DOCKER_HOST: tcp://docker:2376
      DOCKER_CERT_PATH: /certs/client
      DOCKER_TLS_VERIFY: "1"
    volumes:
      - agent-workspace:/home/jenkins/agent
      - docker-certs:/certs/client:ro
    networks: [ci]

JENKINS_SECRET agent'ın kimliği: controller, agent adından ve kendi anahtarından türettiği bu değeri bilmeyen bir process'in o agent adına bağlanmasına izin vermez. Değeri agent'ın sayfasından ya da yetkili bir kullanıcıyla script console'dan alırsın; ben kendi .env dosyamda tuttum. Agent'ın açılış log'u:

INFO: Setting up agent: docker-agent-1
INFO: Using Remoting version: 3395.v6e5377b_0fb_3c
WARNING: No Working Directory. Using the legacy JAR Cache location: /home/jenkins/.jenkins/cache/jars
INFO: WebSocket connection open
INFO: Connected

WebSocket connection open satırı bağlantının 8080 üzerinden kurulduğunu gösteriyor; 50000 portunu hiç açmadık. Aradaki WARNING satırı zararsız ama ilginç: remoting'e bir -workDir verilmediği için jar cache'ini kullanıcının home klasöründe tutuyor. SSH agent'ta Jenkins bu parametreyi kendisi verdiği için aynı uyarı yok.

Label ve Label Expression

Label'lar agent'lara yetenek etiketi vermenin yolu: linux, docker, windows, dotnet10, gpu. Pipeline tarafında tek bir label yerine bir ifade yazabilirsin. agent none ile üst seviyede agent almadan her stage'e ayrı agent verdiğim bir pipeline:

pipeline {
    agent none
    stages {
        stage('On SSH agent') {
            agent { label 'linux && ssh' }
            steps {
                sh 'echo "$NODE_NAME: $(java -version 2>&1 | head -1)"'
            }
        }
        stage('In .NET SDK container') {
            agent {
                docker {
                    image 'mcr.microsoft.com/dotnet/sdk:10.0'
                    label 'docker'
                }
            }
            steps {
                sh 'echo "user: $(id -u):$(id -g) home: $HOME"'
                sh 'dotnet --version'
            }
        }
    }
}

&&, ||, ! ve parantezle ifade kurabilirsin: 'linux && !gpu', 'windows || (linux && mono)'. İlk stage'in çıktısı:

Running on ssh-agent-1 in /home/jenkins/agent/workspace/agents
+ echo ssh-agent-1: openjdk version "21.0.13" 2026-10-20 LTS
ssh-agent-1: openjdk version "21.0.13" 2026-10-20 LTS

Build'i Docker Container'ında Koşturmak

İkinci stage'in çıktısı daha öğretici. Docker Pipeline plugin'i ne yaptığını log'a açıkça yazıyor (ortam değişkenlerini ******** ile maskeliyor, kısalttım):

Running on docker-agent-1 in /home/jenkins/agent/workspace/agents
+ docker inspect -f . mcr.microsoft.com/dotnet/sdk:10.0
.
docker-agent-1 seems to be running inside container 4248414140b8c6e0ce7c843559a07ad95f8756e62b276734ceafe67695e66cd5
but /home/jenkins/agent/workspace/agents could not be found among []
but /home/jenkins/agent/workspace/agents@tmp could not be found among []
$ docker run -t -d -u 1000:1000 -w /home/jenkins/agent/workspace/agents -v /home/jenkins/agent/workspace/agents:/home/jenkins/agent/workspace/agents:rw,z -v /home/jenkins/agent/workspace/agents@tmp:/home/jenkins/agent/workspace/agents@tmp:rw,z -e ******** -e ******** ... mcr.microsoft.com/dotnet/sdk:10.0 cat
+ id -u
+ id -g
+ echo user: 1000:1000 home: /home/ubuntu
user: 1000:1000 home: /home/ubuntu
+ dotnet --version
10.0.403
$ docker stop --time=1 e8aaaa67a508913e91d344bf5379a98358b77d8915befeca3ee9ec661d25d958
$ docker rm -f --volumes e8aaaa67a508913e91d344bf5379a98358b77d8915befeca3ee9ec661d25d958

Plugin önce agent'a bir executor alıyor, image'ın yerelde olup olmadığına bakıyor, sonra container'ı cat komutuyla ayakta tutup her sh adımını docker exec ile içinde çalıştırıyor. Build bitince container durduruluyor ve siliniyor. Dikkat etmen gereken üç ayrıntı var.

Birincisi, workspace container'a -v ile mount ediliyor ve bu path'i Docker daemon'ı çözüyor. Agent'ın kendisi container'da çalıştığı için plugin önce "workspace'i agent container'ının volume'lerinde bulabilir miyim" diye bakıyor; bulamayınca (o iki could not be found satırı) path'i olduğu gibi veriyor. Bizim ortamda bu çalışıyor, çünkü Docker-in-Docker container'ına aynı workspace volume'ünü aynı path'le bağladım. Agent'ın içinde host'un Docker socket'ini kullanıp workspace volume'ünü paylaşmazsan container'ın içinde boş bir klasör görürsün ve dotnet build "proje dosyası yok" diye düşer.

İkincisi, container -u 1000:1000 ile, yani agent'ın kullanıcısıyla çalışıyor ki workspace'e yazılan dosyalar agent'a ait olsun. Baştaki sorunun cevabı da burada: .NET SDK image'ı Ubuntu tabanlı ve uid 1000 orada ubuntu kullanıcısı, bu yüzden HOME /home/ubuntu. Başka bir image'da uid 1000'in hiç kullanıcısı yoksa HOME / olur ve dotnet ya da npm cache'ine yazmaya çalışırken permission hatası alırsın. Çözüm cache path'lerini environment variable ile workspace'e ya da yazılabilir bir volume'e yönlendirmek; yedinci ve sekizinci yazıda yapacağız.

Üçüncüsü, label 'docker' şart. Docker CLI'ı ve bir daemon'a erişimi olmayan bir agent'a düşerse stage docker: not found ile kırılır.

Docker agent'ın asıl değeri araç zincirinin agent'tan ayrılması. Agent makinesine .NET SDK, Node.js ya da Playwright tarayıcıları kurmazsın; her pipeline hangi sürümü istediğini image tag'iyle söyler. .NET 10'dan .NET 11'e geçen bir proje yalnız kendi Jenkinsfile'ındaki tag'i değiştirir, aynı agent'ta .NET 8 kullanan eski proje etkilenmez.

Kubernetes ve Windows Agent'lara Kısa Bir Bakış

Kubernetes plugin'i agent'ları talep anında pod olarak açar: build kuyruğa girince plugin Kubernetes API'sine (Application Programming Interface) bir pod oluşturtur, pod'daki jnlp container'ı controller'a inbound agent olarak bağlanır, build bitince pod silinir. Pipeline tarafında pod tanımı Jenkinsfile'da durur. Bu blok bu yazı için çalıştırılmadı (ortamda Kubernetes cluster'ı yok), syntax'ı plugin dokümantasyonundan:

agent {
    kubernetes {
        yaml '''
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: dotnet
    image: mcr.microsoft.com/dotnet/sdk:10.0
    command: ['sleep']
    args: ['infinity']
'''
        defaultContainer 'dotnet'
    }
}

Cluster'ın kendisini, namespace'leri ve kaynak limitlerini ilerideki Kubernetes serisinde konuşacağız. Windows agent ise hâlâ gerekli bir şey: .NET Framework projeleri, IIS (Internet Information Services) deploy'u ya da Windows'a özgü testler için. Windows'ta en pratik yol inbound agent'ı WebSocket ile çalıştırıp bir Windows service olarak kaydetmek ve agent'a windows label'ı vermek. Bu makinede Windows olmadığı için Windows agent'ı burada çalıştırmadım; IIS deploy'unu dokuzuncu yazıda pipeline tarafından anlatacağım.

Üretimde Dikkat

Workspace'ler agent diskinde birikir. Bir build bittikten sonra workspace silinmez; bir sonraki build aynı klasörü yeniden kullanır. Bu hız kazandırır ama yüzlerce branch'li multibranch projelerde agent diskini doldurur. Workspace Cleanup plugin'inin cleanWs() adımını post { cleanup { ... } } içinde çağırmak en basit çözüm:

pipeline {
    agent { label 'docker' }
    stages {
        stage('Build') {
            steps {
                sh 'mkdir -p out && head -c 52428800 /dev/urandom > out/Bookstore.Api.zip'
                sh 'du -sh "$WORKSPACE"'
            }
        }
    }
    post {
        cleanup {
            cleanWs()
        }
    }
}
+ du -sh /home/jenkins/agent/workspace/cleanws
51M	/home/jenkins/agent/workspace/cleanws
[WS-CLEANUP] Deleting project workspace...
[WS-CLEANUP] Deferred wipeout is used...
[WS-CLEANUP] done

Build'den sonra agent'ta du ile baktım: cleanws klasörü yoktu, ama cleanws@tmp (4 KB) ve temizlenmeyen diğer job'ların klasörleri duruyordu. @tmp klasörleri Jenkins'in adımlar için kullandığı geçici alan ve kendi kendine silinmez. Agent disklerini izleyen bir alarm ve dönemsel bir temizlik yine de gerekli; Jenkins'in her node için bir "Free Disk Space" eşiği var ve eşiğin altına düşen node'u offline yapar.

Executor sayısını agent'ın kaynaklarına göre ayarla ve Docker agent'larda container'ların da aynı CPU'yu (Central Processing Unit) ve belleği paylaştığını unutma. Agent'ları "evcil hayvan" gibi değil, gerektiğinde silinip yeniden kurulabilen sürü hayvanı gibi düşün: image'ları kodla tanımlı olsun, üzerlerinde elle kurulmuş araç olmasın.

Sık Düşülen Tuzaklar

Build'leri controller'da koşmak. İlk yazıdan beri söylüyoruz ama bir kez daha: Built-In Node'un executor sayısı sıfır olmalı.

Docker socket'ini agent'a mount edip workspace'i paylaşmamak. Yukarıda anlattığım boş klasör sorunu ve buna ek olarak, host socket'ine erişen her pipeline o makinedeki bütün container'ları yönetebilir. Ayrı bir Docker daemon'ı (dind) veya Kubernetes daha izole.

uid uyuşmazlığı. Docker container'ında root olarak çalışan bir adım workspace'e root'a ait dosyalar bırakır, sonraki build bu dosyaları silemez. Plugin'in -u 1000:1000 vermesi bunu önlemek için; args '-u root' ile ezmeden önce iki kez düşün.

Agent'larda kalıcı state'e güvenmek. "Bu agent'ta Node 18 kurulu" bilgisi bir gün yanlış olur. Araç sürümünü image tag'inde ya da label'da açıkça iste.

Disk dolmasını beklemek. Workspace, @tmp, Docker image cache'i ve NuGet/npm cache'leri aynı diski paylaşır; izlemiyorsan ilk haberi kırmızı build'lerden alırsın.

Küçük Bir Deneme

★☆☆ Kendi agent'larına iki label daha ekle (ör. dotnet10 ve fast-disk) ve bir stage'i 'linux && fast-disk' ifadesiyle seç. Eşleşmeyen bir ifade yazdığında build'in queue'da beklediğini gör.

★★☆ node:24-trixie image'ıyla bir Docker agent stage'i yaz ve echo $HOME ile npm config get cache çıktısına bak. Cache'i workspace içine yönlendirmek için hangi environment variable'ı vermen gerekiyor?

★★★ SSH agent'ın host key doğrulamasını knownHostsFileKeyVerificationStrategy'ye çevir. Agent container'ını yeniden oluşturup host key'i değiştir ve controller'ın bağlantıyı reddettiğini log'da gör.

Agent'lar hazır. Bir sonraki yazıda pipeline'ın en hassas kısmına geçiyoruz: credential'lar, withCredentials, masking'in sınırları ve Groovy string interpolation tuzağının gerçek çıktısı.