인텔리제이(IntelliJ)로 K8s 파드(Pod) 실시간 Java Remote Debugging 방법

쿠버네티스 환경의 서비스에 애플리케이션 배포를 하기 전에 QA 검증 과정을 거칠 것이다. 검증 과정 중에 발생한 이슈의 원인을 확인 하기 위해서 때로는 추가적인 로그를 코드에 추가해서 다시 테스트 환경에 애플리케이션에 배포 후에 로그 내용을 확인하는 매우 번거로운 과정을 반복하는 경우가 있다. 추가한 로그를 통해서도 명확히 원인 파악이 안된다면? 또 다시 의심되는 부분을 찾아서 로그를 추가하고 또 배포해서 확인하고… 시간적으로 그리고 일의 능률 측면에서 매우 비효율적이 아닐 수 없다. 이번 포스팅에서는 테스트 환경에서 이러한 비효율을 단번해 해결할 수 있는 Remote Debugging 을 적용하여 Pod 내의 애플리케이션을 로컬에서 디버깅 할 수 있는 방법을 소개하고자 한다. (ft. IntelliJ)

사전조건

원격 디버깅 (Remote Debugging)을 위해서는 로컬에서 현재 빌드한 애플리케이션이 서버에 적용되어야 한다. 만약 로컬 코드와 서버에 적용된 애플리케이션 버전이 다르다면 우선 로컬 코드를 빌드하여 테스트 환경에 배포를 해야 하는 작업이 한번은 필요하다.

Remote Debugging 설정 옵션

원격 디버깅을 위해서는 클라이언트의 프로젝트와 디버거가 되는 서버 애플리케이션에 원격 디버깅을 위한 주문을 걸어야 하는데 다음과 같다.

-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005
Plaintext

위 옵션은 java 애플리케이션의 실행 옵션으로 붙여주면 된다.

  • -agentlib: JVM에게 네이티브 에이전트 라이브러리를 로드하라는 JVM 표준 플래그다. 쉽게 말해 자바 실행시 특정 기능을 보조 프로그램으로 같이 띄우라는 뜻이다.
  • jdwp: 로드할 라이브러리의 이름이다. Java Debug Wire Protocol의 약자다. IDE(IntelliJ 등)와 실행 중인 자바 애플리케이션 사이에서 통신 규약(프로토콜)을 담당한다.
  • transport=dt_socket: 디버거와 애플리케이션이 소켓 통신으로 데이터를 주고 받겠다는 것이다.
  • server=y: 애플리케이션이 서버 역할을 하여 디버거(IDE)의 연결을 기다리겠다는 뜻이다.
  • suspend=n: 디버거가 연결될 때까지 애플리케이션 실행을 일시 정지할지 정하는 옵션이다. y는 일시 정지를 하고 n은 멈추지 않고 바로 애플리케이션을 시작한다. 부팅 시점의 에러를 잡을 때 y를 지정하는 것이 필수적이다. 그 외의 경우에는 n으로 지정하는 것이 좋다.
    (suspend=y 옵션을 사용하는 경우 livenessProbe나 readinessProbe가 실패하여 파드가 계속 재시작 할 수 있다. 이 경우에는 livenessProbe, readinessProbe를 잠시 비활성화 해야 한다.)
  • address=*:5005: 5005포트를 사용하며 ‘*’는 모든 IP에서 접속을 허용한다는 의미다. (보안을 위해서 내부망에서만 여는 것이 좋다)

Remote Debugging IDE(IntelliJ) 설정

Remote Debugging (원격 디버깅) 위한 IntelliJ 설정을 알아보자. IntelliJ 설정은 매우 간단하다. 디버깅 하고자 하는 프로젝트를 열어서 원격 디버깅 설정(Remote JVM Debug)을 추가 하면 된다.

Run/Debug Configurations(Edit Configurations) >> '+' 선택 >> Remote JVM Debug
Plaintext

위 순서로 Remote JVM Debug 설정을 추가할 수 있다.

IntelliJ Remote Debugging 설정 추가 화면
IntelliJ Remote JVM Debug 추가 화면

Remote JVM Debug Configuration을 추가하면 ‘Command line arguments for remote JVM’ 항목은 자동으로 입력된다. 특별한 일 없으면 그냥 써도 무방하다.
Host 정보는 만약 Pod의 접속 IP가 외부로 오픈된 공인 아이피라면 해당 아이피를 설정해 주면 되겠지만 K8s 내에 가려져 있는 경우 localhost로 지정하고 port forwarding으로 처리하면 된다.

Remote Debugging 서버 (Pod) 설정

우리는 K8s에서 실행되고 있는 Pod 내의 애플리케이션을 디버깅 할 것이므로 Pod에도 Remote Debugging을 위한 옵션을 추가해 줘야 한다.
Spring Boot Image나 JIB를 통해서 이미지로 빌드하는 경우 이미지 빌드 시점에 JvmFlag 옵션으로
‘-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005’ 를 추가해도 되지만 이렇게 되면 운영 환경에 배포되는 이미지와 테스트 환경에 배포되는 이미지를 분리해야 하거나 빌드 스크립트 내에서 테스트와 프로덕션으로 분기를 해야하므로 추천하지 않는다.

보통은 테스트 환경과 운영 환경이 분리되어 있고 각각 deployment manifest를 가지고 있을 것이므로 테스트용 deployment manifest에 환경 변수를 등록하는 것을 권장한다. 물론 디버깅을 적용하면 애플리케이션 성능에 영향을 주므로 테스트 환경이라고 하더라도 항상 Remote Debugging 설정을 환경변수에 넣고 사용하는 것보다 필요시에만 해당 설정을 넣고 삭제하는 것이 좋겠다.

설정 방법은 다음과 같다.
spec > template > spec > containers 항목을 찾아 아래 내용을 추가한다.

containers:
  # 디버깅 환경 변수 추가
  - env:
    - name: JAVA_TOOL_OPTIONS
      value: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
    ports:
      - containerPort: <애플리케이션 서비스포트>
      - containerPort: 5005
YAML

주의할 점

-agentlib 옵션으로 디버깅을 활성화 하면 성능의 저하가 발생할 수 있으므로 livenessProbe와 readinessProbe의 initialDelaySeconds, timeoutSecond, periodSecond, failureThreshold 값을 늘려주자. 수치가 작게 설정되어 있으면 livenessProbe, readinessProbe 실패로 애플리케이션이 재시작 될 수 있다.
원격 디버깅을 다 했으면 deployment manifest를 기존 설정으로 되돌리는 것도 잊지말자!

Deployment 적용

deployment의 manifest를 수정할 때 로컬에 YAML 파일이 있다면 해당 YAML 파일을 수정 후 ‘kubectl apply -f <파일명>‘을 실행하거나 실시간으로 리소스를 수정하려면 ‘kubectl edit deployment <deployment 이름>‘ 으로 수정하면 된다.

# 실시간 리소스 수정
kubectl edit deployment <deployment 름>

# 로컬의 yaml을 수정한 경우
kubectl apply -f <deployment 파일명>
Bash

kubectl edit 명령으로 deployment manifest를 수정한 경우 저장하면 바로 deployment가 적용된다.

로컬 -> Debugger Port Forwarding

앞서 언급했듯이 디버깅 하고자 하는 파드(Pod)가 K8s 내에 숨겨져 있을 때는 바로 접속하기 어려우므로 로컬에서 Debugger 대상이 되는 Pod로 직접 Port Forwarding 하여 접속하도록 하는 방법을 쓴다.

kubectl port-forward deployment/my-app 5005:5005
Bash

쿠버네티스가 알아서 실행 중인 파드 하나를 골라서 연결해 준다. 하지만 여러개의 Pod가 deployment된 경우 service를 통해서 트래픽이 로드밸런싱 되는 경우 port forwording된 파드에 트래픽이 들어갈 때까지 시도해야 한다는 단점이 있다.
만약 Postman으로 직접 요청을 날릴 수 있는 상황이라면 특정 파드를 하나 골라서 직접 port forwarding을 걸어주고 서비스 포트도 port forwarding을 걸어주고 해당 pod로 직접 트래픽이 전달되도록 하는 것이 좋을 것 같다.

kubectl port-forward pod/my-app-xxxxxxx-xxxxx 5005:5005 8080:8080
Bash

서비스 포트가 8080이라고 했을 때 5005 디버거 포트와 8080 서비스 포트 모두 port forwarding 하는 명령이다.
Postman에서 http://localhost:8080/xxx/xxx와 같이 직접 요청을 날려 port forwarding 되고 있는 Pod로 직접 요청을 날린다.

나의 생각

원격 디버깅은 항상 활성화 시키는 것이 아닌 특수한 상황에서 사용하는 것인 만큼 원격 디버깅을 수행하는 경우에는 그냥 deployment 되는 pod 수를 1개로 설정하고 테스트 하는 것이 깔끔할 것 같다.

디버깅 렛츠 고

K8s deployment가 실행된 이후에 IDE(IntelliJ) 에서 추가했던 Remote JVM Debug를 실행하면 된다.

IntelliJ Remote Debugging 실행
IntelliJ 원격 디버깅 실행

원격 디버깅 연결이 잘 되었다면

Connected to the target VM, address: 'localhost:5005', transport: 'socket'
Plaintext

와 같은 메시지가 출력되면 연결은 성공된 것이다. 이제 내 로컬 IDE 환경에서 마음껏 브레이크 포인트 잡고 확인해 보자!!


지금까지 원격 디버깅 방법에 대해서 정리해 보았다. K8s 환경에서 설정하는 방법을 다루었지만 이는 온프레미스 환경에서도 동일하게 적용할 수 있다. 서버에서 애플리케이션 기동 스크립트에 마법같은
‘-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005’
옵션을 추가해 주면 된다. 물론 이 경우에는 방화벽에 5005 포트가 막혀있지 않은지 확인이 필요할 것이다.
원격 디버깅은 원인을 찾기 어려운 이슈가 발생했을 때 우리의 시간을 상당히 절약해 주는 좋은 도구가 될 것이다.