Communitygithub.com

K8s Debug Bridge

실행 중인 Kubernetes 서비스의 트래픽을 로컬 머신으로 터널링하여 디버깅할 수 있게 해줍니다. 다섯 개의 Bash 스크립트와 에이전트 스킬만으로 작동하며, 별도의 에이전트나 Operator 설치가 필요 없습니다.

K8s Debug Bridge란 무엇인가요?

K8s Debug Bridge는 Kubernetes/OpenShift 서비스의 라이브 트래픽을 로컬 머신으로 안전하게 터널링하여 디버깅할 수 있게 해주는 에이전트 스킬입니다. 표준 `kubectl`/`oc exec`, Bash 내장 `/dev/tcp`, 브리지 파드 내 `nc`만 사용하며, 추가 CLI나 에이전트 설치가 전혀 필요하지 않습니다. 임시 '브리지 파드'를 생성하고 대상 서비스의 `spec.selector`를 일시적으로 변경한 후, 역방향 터널을 통해 파드와 로컬 포트를 연결합니다. 모든 Kubernetes 배포판(vanilla K8s, OpenShift, EKS/GKE/AKS)에서 작동하며, VS Code + Air + Delve(Go)는 하나의 예시 통합일 뿐입니다. 보안에 주의하여 서비스 셀렉터 복원을 자동으로 처리하며, 세션 종료 시 정리를 확인합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add bilgihankose/k8s-debug-bridge

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

K8s Debug Bridge은(는) 무엇을 하나요?

실행 중인 Kubernetes/OpenShift 서비스의 트래픽을 로컬 머신으로 터널링하여 디버깅할 수 있게 해줍니다. 표준 kubectl/oc exec, Bash의 내장 /dev/tcp, 브리지 파드 내 nc만 사용하며, 추가 CLI나 에이전트 설치가 필요하지 않습니다. 작동 방식: 임시 "브리지 파드"를 생성하고, 대상 서비스의 spec.selector를 일시적으로 이 파드의 레이블로 전환한 후, 역방향 터널이 파드와 로컬 포트를 연결합니다. 모든 Kubernetes 배포판(vanilla K8s, OpenShift, EKS/GKE/AKS)에서 작동하며, oc가 가능하면 사용하고 그렇지 않으면 kubectl을 사용합니다(scripts/lib-kube-cli.sh 참조). README.md에서 이 방식의 근거와 Telepresence, mirrord 같은 도구와의 장단점을 다룹니다.

특정 언어/프레임워크에 종속되지 않습니다. 브리지는 트래픽만 전송합니다. VS Code + Air + Delve(Go)는 예시 통합일 뿐입니다. 다른 언어로 작업하는 경우, 자체 감시자/디버거(Node: nodemon+inspector, Python: watchdog+debugpy 등)를 동일한 터널 위에 연결하면 됩니다. 사용자의 로컬 측이 아직 설정되지 않은 경우(리로더 없음, 디버거 미연결, 또는 자신의 프로젝트에 맞게 조정이 필요한 경우)에는 그 시점에 docs/local-debug-setup.md를 읽고 따르세요. 내용을 기억해서 재현하지 말고, 로컬 앱이 이미 실행 중이고 청취 중일 때는 읽지 마세요.

이 스킬은 실제 클러스터 리소스를 생성하고 수정합니다. 파드를 생성하고 서비스의 spec.selector를 일시적으로 변경합니다. 이는 시뮬레이션이 아닌 실제 클러스터 상태 변경입니다. 모든 단계에서 롤백 계획이 있어야 하며, 셀렉터를 변경하고 잊어버리는 일이 없도록 해야 합니다.

전제 조건 — 사용자에게 묻기 전에 확인

사용자에게 네임스페이스/서비스 이름을 물어보고 bridge-up.sh에 그대로 전달하지 마세요. 철자가 틀리면 스크립트 중간에서 실패합니다. 먼저 scripts/check-env.sh [namespace] 를 실행하세요(읽기 전용, 아무것도 변경하지 않음):

  • 인자 없이 실행: 로그인 상태와 접근 가능한 네임스페이스를 나열합니다. 로그인되지 않은 경우 여기서 멈추고 사용자에게 oc login/kubectl config use-context를 실행하라고 알리세요.
  • 네임스페이스와 함께 실행: 네임스페이스가 존재하는지 확인하고 내부의 서비스(이름 + 포트 + targetPort, 이미 브리지된 것 포함)를 나열합니다.

출력을 사용자에게 보여주고 목록에서 대상 서비스를 고르게 하세요. 추측하게 하지 마세요. 그런 다음:

  • 브리지 파드 이미지 — 항상 묻고, 절대 추정하지 마세요. 기본값을 정하고 그냥 실행하지 말고, 사용자에게 "브리지 파드가 사용할 이미지는 무엇인가요?"라고 명확히 묻고 답변을 기다리세요. 이미지는 해당 특정 클러스터에서 승인되고 접근 가능해야 하며, 사용자만 어떤 이미지인지 알 수 있습니다. 많은 클러스터가 승인된 내부 레지스트리에서만 가져오기 때문에, 추정한 이미지는 서비스가 이미 리디렉션된 상태에서 풀 타임에 실패할 수 있습니다. nicolaka/netshoot는 제안할 예시일 뿐, 조용히 사용할 폴백이 아닙니다. 사용자가 선택한 이미지에는 nc가 포함되어야 합니다. 파드 측은 socat보다 nc를 사용하는데, 강화된 이미지는 종종 socat을 제거하지만 Nmap의 nc는 유지하기 때문입니다. tunnel.sh는 터널을 열기 전에 nc를 확인하고, 이미지에 없으면 명확한 오류와 함께 중단됩니다.
  • 파드 매니페스트: 브리지 파드는 assets/bridge-pod.yaml에서 렌더링됩니다. 클러스터에 imagePullSecrets, securityContext, nodeSelector/tolerations 또는 다른 리소스 제한이 필요한 경우, 스크립트에서 해결하지 말고 해당 파일을 편집하세요. debug: "__BRIDGE_NAME__" 레이블과 debug-bridge 컨테이너 이름은 유지하세요. 다른 스크립트가 이들을 통해 파드에 접근합니다.
  • 포트: check-env.sh 출력의 서비스 숫자 targetPort를 사용하고, 임의의 빈 포트를 사용하지 마세요. 서비스가 명명된 targetPort(예: http)를 사용하는 경우, 해당하는 숫자 컨테이너 포트로 변환하세요. kube-proxy는 셀렉터가 리디렉션되면 트래픽을 파드의 targetPort로 전달합니다. 다른 숫자를 선택하면 트래픽이 브리지 파드에 도달하지 않습니다.
  • 아웃바운드 종속성: 이 브리지는 인바운드 트래픽(클러스터 → 로컬)만 처리합니다. 로컬 코드가 다른 클러스터 서비스(.svc.cluster.local)를 호출해야 하는 경우, 외부 Route/Ingress 또는 로컬 포트-포워드를 사용하세요.
  • 시크릿 및 환경 변수: 애플리케이션 파드에 대해 oc exec ... env, printenv, set 또는 유사한 명령을 절대 실행하지 마세요. 값이 여러 줄의 자격 증명일 수 있으며, 명령이 이름만 선택하는 것처럼 보여도 터미널/도구 출력에 유출됩니다. 파드 환경 값을 로컬 명령, 셸 히스토리 또는 .env에 복사하지 마세요. 기존 로컬 시크릿 관리 워크플로를 요구하거나, 사용자가 필요한 로컬 환경 파일을 대역 외에서 제공하도록 하세요. 로컬 애플리케이션이 시작되는지만 확인하고, 환경을 출력하지 마세요.

로컬 애플리케이션은 서비스를 건드리기 전에 이미 실행 중이어야 합니다. 이는 나중 단계가 아니라 전제 조건입니다. 터널은 트래픽을 127.0.0.1:<port>로 전달하므로, 셀렉터가 전환될 때 그곳에서 리스닝하는 것이 없으면 모든 가로챈 요청이 connection-refused/502로 실패하고 실제 파드는 이미 순환에서 제외됩니다. 사용자가 앱을 시작하게 하고(안전하게 프로비저닝된 로컬 env 파일 사용 — 클러스터 파드에서 읽어온 파일 절대 사용 금지), 2단계 직전에 리스너를 확인하세요:

lsof -nP -iTCP:<port> -sTCP:LISTEN

흐름

  1. 브리지 설정: 아무것도 실행하기 전에 사용자에게 명확히 알리세요: "네임스페이스 <namespace>에 실제 파드 <bridge-pod>를 생성합니다(이미지: <image>) — 이는 클러스터 리소스를 소비하며 작업이 끝나면 삭제됩니다." 승인을 받은 후 scripts/bridge-up.sh <namespace> <service> <bridge-pod> <image> [port]를 실행하세요. 이는 브리지 파드를 생성하고 원래 셀렉터를 로컬 상태와 서비스의 k8s-debug-bridge/owner 어노테이션에 저장합니다(python3/jq 필요 없음, 순수 bash). 이 단계를 건너뛰지 마세요. 원래 셀렉터를 저장하지 않고 리디렉션하면 되돌릴 방법이 없습니다. 서비스가 다른 사람에 의해 이미 브리지된 경우, 이 스크립트는 실패하고 중단됩니다(어노테이션 확인) — 이 경우 오류 메시지에서 소유자를 사용자에게 보여주고, 직접 재시도하지 마세요.

  2. 서비스 리디렉션: 대상 서비스의 셀렉터를 브리지 파드의 레이블로 전환하기 전에 사용자에게 명확히 알리고 실제 승인을 받으세요: "이 순간부터 <service>에 대한 모든 트래픽이 브리지 파드로 전달됩니다. 실제 파드는 트래픽 수신을 중단합니다." 승인 후 scripts/redirect-service.sh <namespace> <service> --yes를 실행하세요. --yes는 스크립트 자체의 대화형 read 프롬프트를 건너뜁니다(이미 채팅에서 승인을 받았으므로). --yes 없이 실행하면 스크립트가 stdin에서 대기하여 멈출 수 있습니다. 이 스크립트는 전체 셀렉터 맵을 debug=<bridge-pod>만으로 교체합니다. 원래 셀렉터에 이 레이블을 병합해서는 안 됩니다. 브리지 파드는 애플리케이션의 원래 레이블을 가지고 있지 않기 때문입니다.

  3. 터널 시작: scripts/tunnel.sh <namespace> <service> <bridge-pod> <port> — 이 스크립트는 블록킹되므로, 별도의 터미널에서 실행하거나 백그라운드 프로세스로 실행해야 합니다. 자동 정리: trap이 EXIT/INT/TERM을 잡아서, Ctrl+C나 다른 종료 시 자동으로 cleanup.sh를 호출합니다. 직접 백그라운드에서 시작한 경우 Ctrl+C를 누를 수 없습니다 — kill -TERM <pid>로 중단하세요. 트랩이 이를 처리합니다. 시작할 때 PID를 기록하고 사용자에게 알려주세요. cleanup.sh를 수동으로 실행해야 하는 경우는 프로세스가 잡을 수 없이 종료된 경우(kill -9)뿐입니다. 터널은 한 번에 하나의 연결을 전달하며, 각 연결 종료 후 자동으로 새 파드 측 리스너를 생성하므로, 연속적인 디버그 요청 사이에 동일한 터널을 계속 사용할 수 있습니다. 터널이 반복적으로 WARNING: could not carry a connection을 로깅하면 127.0.0.1:<port>에서 리스닝하는 것이 없다는 뜻입니다. 로컬 앱이 다운되었거나, 아직 빌드 중이거나, 핫 리로드가 포트를 해제한 것입니다. 터널을 다시 시작하지 말고 그 문제를 해결하세요.

  4. 로컬 디버그: 브리지하는 포트는 애플리케이션이 서빙하는 포트(서비스의 targetPort)이지, 디버거가 리스닝하는 포트가 아닙니다. 사용자가 "내 디버거가 2345에 있으니, 그 포트로 브리지해 주세요"라고 말하면, 이 둘을 혼동하고 있는 것입니다. 정정하고, 애플리케이션 자체가 서빙하는 포트를 물어본 후 해당 포트로 브리지하세요. 디버거 포트로 보내진 트래픽은 터널 문제처럼 보이는 502 응답을 반환합니다. 로컬 앱은 이미 실행 중이고 확인된 상태입니다(위 전제 조건 참조). 이 앱이 터널링된 트래픽을 수신합니다. 해당 언어의 디버거(예: Delve, Node inspector)는 별도로 실행 중인 애플리케이션에 연결되며, 보통 다른 포트를 사용합니다. 예를 들어 Go의 경우, Air + Delve가 포트 2345에서 실행되고 앱 자체는 터널링된 <port>에서 리스닝합니다. 핫 리로드는 일시적으로 리스너를 제거할 수 있습니다. 리스너가 없는 동안 브리지는 연결 실패/502를 반환합니다. 로컬 앱이 신속히 복구되지 않으면, 터널을 중단하고 서비스를 정리하여 모든 트래픽이 실패하는 브리지로 향하지 않도록 하세요. 브리지는 HTTP 경로를 변경 없이 전송합니다. 로컬 앱의 기본 접두사를 서비스로 전달되는 경로와 일치하도록 구성하세요. Ingress의 재작성 규칙을 재현하지 않습니다. 트래픽은 클러스터 내 서비스 호출자(예: 대시보드 Nginx 프록시)에서 올 수 있습니다. 공개 Route/Ingress가 필요하지 않습니다. 실행이 호출자의 타임아웃을 초과하여 일시 중지된 경우, 실행을 재개하고 새 요청을 트리거하세요. 핫 리로드 후 Delve가 다시 시작되면 IDE가 다시 연결해야 할 수 있습니다. Delve가 --accept-multiclient로 시작된 경우, 에이전트가 VS Code와 함께 터미널 Delve 클라이언트를 포트 2345에 연결하여 현재 소스 라인, 스택 및 변수를 검사할 수 있습니다. 관찰 결과를 사용자에게 보고하세요. 실행을 계속하거나 디버거 상태를 변경하기 전에 명시적 승인을 받으세요.

  5. 작업 완료 시: 터널 터미널에서 Ctrl+C를 누르거나, 백그라운드에서 시작한 경우 kill -TERM <pid>를 실행하세요. 서비스 셀렉터와 브리지 파드가 자동으로 복원되며, 완전한 원래 셀렉터 맵이 복원됩니다. 트랩이 실행되지 않은 경우(예: kill -9), scripts/cleanup.sh <namespace> <service>를 수동으로 실행해야 합니다. 세션을 종료하기 전에 사용자에게 이를 상기시키세요.

보안 참고 사항

이 방법은 클러스터에 영구적인 구성 요소를 설치하지 않습니다(변경 웹훅, 사이드카 인젝터 또는 클러스터 전체 RBAC 없음). 이름/레이블(debug=<bridge-pod-name>)로 명확히 표시된 임시 파드와 표준 exec 권한만 사용합니다. 그럼에도 두 가지 실제 위험이 있으므로 사용자에게 경고하세요:

  1. 서비스 셀렉터 복원을 잊는 경우 — 프로덕션/스테이징에서 영구 장애 발생.
  2. exec 권한이 RBAC을 통해 남용될 가능성 — 이 스킬은 자신의 네임스페이스, 자신의 브리지 파드에서만 실행해야 하며, cluster-admin이 필요하지 않습니다.

자세한 단계와 문제 해결은 README.md를 참조하세요.

세션을 종료하기 전에 정리되지 않은 브리지가 남아 있는지 확인하세요. 상태는 scripts/ 옆, 이 스킬의 자체 폴더에 저장되며, 사용자의 프로젝트 디렉터리에 저장되지 않습니다. 실행 중인 스크립트를 기준으로 해결하세요. 예:

find "$(dirname <path-to>/scripts)/.state" -name '*.env'

사용자 프로젝트에서 find .state를 실행하면 조용히 아무것도 찾지 못하고, 브리지가 여전히 활성 상태인데도 정리된 것으로 보고합니다. .env 파일이 존재하면, 해당 파일이 가리키는 네임스페이스/서비스를 사용자에게 알리고 scripts/cleanup.sh <namespace> <service>를 제안하세요. 이는 알림일 뿐, 결정적인 게이트가 아닙니다. 잊지 않도록 주의하세요.

관련 스킬