• Jenkins의 master-agent 통신은 Remoting이라는 라이브러리가 담당한다. TCP 기반 통신 프로토콜, 원격 프로시저 호출(RPC), 원격 클래스 로딩, 데이터 스트리밍을 구현한 통신 계층이다.
  • 용어: 예전의 master는 지금 controller, slaveagent로 이름이 바뀌었다(2020년경). 이 글에서는 controller/agent로 쓴다.
  • 핵심은 controller와 agent가 하나의 양방향 채널(channel) 을 열고, 그 위로 직렬화된 자바 객체를 주고받는다는 것이다. 프로토콜과 포트는 이 채널을 어떻게 뚫느냐의 문제일 뿐이고, 채널이 뚫린 뒤 그 위에서 무엇이 흐르느냐가 본질이다.

아키텍처 전제: 두뇌는 controller에만 있다

  • agent는 controller에 접속하는 작은 자바 프로세스(agent.jar, 단일 jar ~170KB)일 뿐이다. 빌드 오케스트레이션, 스케줄링, 상태 관리는 전부 controller가 한다.
  • agent는 Jenkins 코드를 미리 설치하고 있지 않아도 된다. 필요한 클래스는 controller가 채널로 실시간 전송한다(원격 클래스 로딩). agent에 필요한 최소 전제는 JVM과 controller로의 네트워크 경로뿐이다.

연결 방향: 누가 먼저 접속하나

  • 통신 방향(데이터 흐름)은 항상 양방향이지만, 누가 TCP 연결을 먼저 여느냐로 두 종류로 나뉜다. 방화벽 설계가 여기서 갈린다.

  • SSH agent (controller-initiated / outbound): controller가 필요할 때 agent로 SSH 접속을 열고 agent.jar를 복사·실행한다. 주로 Unix. controller가 agent의 22번 포트에 닿을 수 있어야 한다.

  • Inbound agent (agent-initiated, 옛 이름 JNLP agent): agent가 먼저 controller로 접속한 뒤, controller가 그 연결로 명령을 밀어넣는다. 주로 Windows나 agent가 방화벽 뒤에 있어 controller에서 직접 닿을 수 없는 환경. agent만 controller 쪽으로 아웃바운드 접속이 가능하면 된다.

  • 한 노드는 아래 둘 중 하나만 고른다(동시 양립 불가). 화살표 방향이 곧 “누가 먼저 접속하느냐"다.

전송 프로토콜

  • JNLP4-connect: 현재의 표준 inbound 프로토콜(Remoting 3.0~). 평문 연결을 맺은 뒤 자바 SSLEngine으로 TLS 업그레이드를 하고, 그 다음에야 접속 비밀(secret)을 교환한다. NIO(논블로킹 I/O) 기반이라 이전 JNLP3-connect의 성능 병목을 제거했다.
    • JNLP1/2/3-connect는 전부 deprecated. “JNLP"라는 이름은 남았지만 지금은 Java Web Start와 무관하고, 사실상 Remoting 프로토콜의 식별자일 뿐이다.
  • WebSocket: Remoting 4.0~(Jenkins 2.217, 2020년 도입). agent가 controller의 HTTP(S) 포트로 WebSocket 연결을 맺어 그 위에서 Remoting을 터널링한다. 별도 TCP agent 포트를 열 필요가 없다.
    • 리버스 프록시·로드밸런서·쿠버네티스처럼 80/443만 열려 있는 환경에서 이점이 크다. 핸드셰이크·암호화·프레이밍을 HTTP(S) 레이어에 위임한다.

포트

  • HTTP(S) 포트 (8080 / 443): 웹 UI 겸, WebSocket agent의 통신 경로 겸, inbound agent의 부트스트랩 창구.
  • TCP agent 포트 (기본 50000): JNLP4-connect inbound agent가 실제 채널을 맺는 포트. 관리자가 고정/랜덤/비활성으로 설정할 수 있다.
  • inbound agent가 TCP 포트를 쓸 때는 2단계다: 먼저 HTTP(S) 포트에 붙어 접속 정보(비밀·포트 번호)를 받고, 그 정보로 TCP agent 포트에 다시 붙는다. -webSocket 옵션을 쓰면 HTTP(S) 연결 하나로 끝난다.

채널의 수명: 빌드 단위가 아니라 agent 접속 단위

  • 채널은 job 빌드마다 새로 만들지 않는다. agent가 online 되는 순간 한 번 맺고, 그 agent가 붙어 있는 내내 하나의 세션을 여러 빌드가 재사용한다. 축이 다르다 — 채널 수명 = agent 접속 세션 수명이고, 빌드는 그 위를 지나가는 트래픽이다.
  • 생성 타이밍 = agent가 연결될 때. inbound면 agent 프로세스가 controller에 접속하는 순간, SSH면 controller가 agent.jar를 띄우는 순간(보통 부팅·재연결 시점). 빌드가 없어도 idle 상태로 열려 있다.
  • 빌드가 하는 일은 채널 생성이 아니다. 빌드가 dispatch되면 executor 슬롯을 점유하고 워크스페이스를 할당한 뒤, 빌드 스텝을 기존 채널 위로 RPC로 보낸다. 빌드 중의 파일 접근·프로세스 실행(FilePath, Launcher)도 전부 그 한 채널 위의 호출이다.
  • 예외처럼 보이는 것은 cloud/동적 agent(Kubernetes·Docker·EC2). 빌드가 큐에 들어올 때 agent를 즉석 생성하고 끝나면 폐기하므로 agent 수명 ≈ 빌드 1개라 채널도 그만큼만 산다. 하지만 이것도 “빌드가 채널을 연다"가 아니라 “agent가 뜨면서 채널이 열리는데 그 agent가 짧게 살 뿐"으로, 채널이 agent 수명에 묶인다는 원칙은 같다.
  • 정적 agent라면 한 채널이 수천 빌드를 나른다.

채널 위에서 실제로 오가는 것

  • 채널이 맺어지면 그 위로 흐르는 것은 대략 네 가지다.

  • RPC (Callable / Command 객체): controller가 “이 코드를 agent에서 실행해라"를 직렬화된 Callable/Command 객체로 보낸다. agent가 실행하고 결과 객체를 직렬화해 돌려준다. 빌드 스텝 실행이 이 형태다.
  • 원격 클래스 로딩: agent가 실행에 필요한 클래스를 갖고 있지 않으면, 그 클래스 바이트코드를 controller가 채널로 스트리밍한다. 그래서 agent에 Jenkins/플러그인을 사전 배포하지 않아도 동작한다.
  • 데이터 스트리밍: 빌드 콘솔 로그(agent→controller), 워크스페이스 파일·아티팩트 전송, 표준 입출력 등이 채널의 스트림으로 오간다.
  • 이 모든 것의 바탕은 자바 직렬화다. Remoting이 객체를 직렬화해 바이트로 보내고 상대가 역직렬화해 복원한다.

보안

  • 채널의 밑바탕이 자바 역직렬화라는 점 때문에, 신뢰할 수 없는 agent 연결을 그대로 받으면 역직렬화 취약점의 표적이 된다. 과거 Jenkins의 원격 코드 실행 CVE 상당수가 이 지점에서 나왔다.
  • 방어는 두 층이다. (1) TLS 암호화 — JNLP4-connect는 secret 교환 전에 TLS로 올린다. (2) agent secret — agent마다 발급된 토큰으로 controller가 접속 주체를 인증한다. secret이 없거나 틀리면 채널을 맺지 못한다.
  • controller가 agent를 신뢰하는 관계라는 점도 기억할 것. agent에서 실행되는 코드는 controller가 보낸 것이고, 반대로 악의적 agent는 controller에 영향을 줄 수 있다. agent-to-controller 접근 제어(Agent → Controller Access Control)가 이 경계를 지킨다.

참고 자료