버그 재현 자동화에는 달콤한 유혹이 하나 있다.
“어차피 같은 API를 부르는 거니까, 중간 단계를 좀 건너뛰어도 되지 않을까?”
대부분의 경우 이 유혹은 꽤 합리적으로 들린다. 토큰을 직접 넣고, kubeconfig를 손으로 만들고, 마지막 API만 호출하면 빠르다. 결과도 나온다. 로그도 남는다.
그런데 접근 제어 제품이나 에이전트 기반 제품에서는 이 지름길이 가끔 위험해진다.
사용자가 실제로 밟는 길은 훨씬 길기 때문이다.
kubectl
-> kubeconfig exec plugin
-> qpctl
-> desktop Multi Agent
-> product API
-> proxy
-> Kubernetes API Server
이 경로에서 문제가 생기는데, 테스트는 중간을 건너뛴다면 어떻게 될까?
테스트는 초록색인데 사용자는 계속 빨간불을 본다.
이번 작업은 그 간극을 줄이는 이야기였다.
먼저 용어를 사람 말로 풀어보자
이 글에는 낯선 이름이 많이 나온다. 처음 보는 사람에게는 qpctl, kubeconfig, exec plugin 같은 말이 거의 주문처럼 보일 수 있다.
그래서 먼저 아주 쉽게 풀어보자.
kubectl: 쿠버네티스에 말을 거는 리모컨
kubectl은 Kubernetes를 조작하는 명령어다.
TV 리모컨을 생각하면 쉽다. 리모컨으로 채널을 바꾸고, 볼륨을 조절하듯이 kubectl로 Kubernetes에게 묻는다.
kubectl get nodes
이 명령은 이런 뜻이다.
쿠버네티스야, 네가 관리하는 서버 목록을 보여줘.
kubectl get pods
이건 이런 뜻이다.
쿠버네티스야, 지금 떠 있는 앱 조각들을 보여줘.
여기서 node는 “쿠버네티스가 일하는 서버”, pod는 “그 서버 위에서 실행되는 앱 묶음” 정도로 보면 된다.
kubeconfig: 리모컨 사용 설명서와 출입증
kubectl은 혼자서는 어디로 가야 할지 모른다.
그래서 kubeconfig라는 설정 파일을 본다. 이 파일에는 대략 이런 정보가 들어 있다.
어느 Kubernetes 서버에 접속할지
누구 이름으로 접속할지
어떤 인증 정보를 쓸지
비유하면 회사 건물 출입증과 방문 안내문이 합쳐진 파일이다.
건물 주소: Kubernetes API Server
방문자 이름: 사용자 또는 서비스 계정
출입 방법: 토큰, 인증서, 또는 외부 명령
그래서 kubeconfig가 잘못되면 kubectl은 엉뚱한 건물로 가거나, 문 앞에서 거절당한다.
token: 임시 출입증
토큰은 “나 이 사람 맞아요”를 증명하는 문자열이다.
놀이공원 자유이용권이나 회사 임시 출입증을 떠올리면 된다. 보여주면 들어갈 수 있지만, 보통 만료 시간이 있고 아무에게나 보여주면 안 된다.
그래서 좋은 재현 도구는 토큰 값을 로그에 그대로 남기지 않는다.
중요한 것은 토큰의 생김새가 아니라, “정상적으로 발급받았고 사용할 수 있는가”다.
qpctl: QueryPie용 작은 안내 데스크
qpctl은 QueryPie Multi Agent와 대화하는 CLI 도구다.
CLI는 Command Line Interface의 줄임말이다. 쉽게 말하면 터미널에서 쓰는 리모컨이다.
kubectl이 Kubernetes에게 말 거는 리모컨이라면, qpctl은 QueryPie Multi Agent에게 말 거는 리모컨이다.
예를 들어 이런 명령이 있다.
qpctl auth status
뜻은 간단하다.
Multi Agent야, 지금 로그인되어 있어?
또 이런 명령도 있다.
qpctl kube get-token
이건 이런 뜻이다.
Multi Agent야, Kubernetes에 들어갈 임시 출입증을 받아와 줘.
즉 qpctl은 직접 Kubernetes에 들어가는 주인공이라기보다, QueryPie 제품 경로를 통해 필요한 정보를 받아오는 안내 데스크에 가깝다.
Multi Agent: 내 PC에 떠 있는 작은 중계 직원
Multi Agent는 사용자의 PC에서 실행되는 데스크톱 앱이다.
브라우저나 터미널이 모든 일을 직접 하는 대신, Multi Agent가 중간에서 이런 일을 도와준다.
로그인 세션 확인
현재 선택된 역할 확인
제품 서버와 통신
필요한 임시 토큰 발급 요청
로컬 접속 경로 중계
비유하면 회사 로비에 있는 직원이다.
방문자가 “저 회의실에 들어가도 되나요?”라고 물으면, 로비 직원은 회사 시스템을 확인하고 임시 출입증을 발급해준다.
exec plugin: 출입증을 파일에 저장하지 않고 그때그때 받아오는 방식
일반 kubeconfig는 토큰을 파일 안에 직접 넣을 수 있다.
kubeconfig 안에 token 저장
-> kubectl이 그 token을 그대로 사용
이 방식은 단순하지만, 실제 제품 경로와 다를 수 있다.
exec plugin 방식은 다르다. kubeconfig 안에 토큰을 넣지 않고, “필요할 때 이 명령을 실행해서 토큰을 받아와”라고 적어둔다.
kubectl 실행
-> kubeconfig 확인
-> /usr/local/bin/qpctl 실행
-> Multi Agent에게 토큰 요청
-> 받은 토큰으로 Kubernetes 접근
식당으로 비유하면 이렇다.
정적 토큰 방식은 도시락을 미리 가방에 넣어두는 것이다.
exec plugin 방식은 식당에 갈 때마다 카운터에서 방금 만든 식권을 받는 것이다.
실제 제품이 식권 방식으로 움직인다면, 테스트도 식권 방식으로 해야 한다. 그래야 카운터가 닫혀 있는지, 직원이 내 예약을 못 찾는지, 식권 발급기가 고장 났는지 알 수 있다.
proxy: 대신 문을 두드려주는 중간 창구
proxy는 중간 창구다.
사용자가 Kubernetes API Server에 바로 가지 않고, proxy를 통해 간다.
사용자 -> proxy -> Kubernetes API Server
왜 굳이 중간 창구를 둘까?
접근 제어 제품에서는 누가, 언제, 어떤 리소스에 접근했는지 확인해야 한다. proxy는 이 흐름을 통제하고 기록하는 문지기 역할을 한다.
단, 문지기가 말을 잘못 전달하면 문제가 생긴다. 이번 kubectl get nodes -o wide 문제도 “문 앞에서 막힌 것”이라기보다 “중간에서 요청의 뉘앙스가 바뀐 것”에 가까웠다.
KAC와 role: 어느 문까지 열 수 있는지 정하는 규칙
KAC는 Kubernetes Access Control, 즉 Kubernetes 접근 제어 영역이라고 보면 된다.
role은 역할이다.
예를 들어 회사에서 방문자, 직원, 관리자 출입증이 다르듯이 Kubernetes에서도 어떤 사람은 보기만 할 수 있고, 어떤 사람은 수정도 할 수 있다.
이번 문제에서 중요했던 것은 “웹에서는 role을 골랐는데, Multi Agent가 쓰는 세션에는 role이 안 골라져 있을 수 있다”는 점이었다.
사람으로 치면 이런 상황이다.
웹 로비에서는 방문증을 받았다.
그런데 터미널 로비 직원은 아직 내가 어떤 방문자인지 모른다.
그래서 재현 도구가 이 둘을 따로 확인해야 했다.
지름길은 빠르지만, 진짜 길은 다르다
Kubernetes 접근 문제를 재현할 때 가장 빠른 방법은 정적 토큰을 넣은 kubeconfig를 만드는 것이다.
kubectl
-> static token kubeconfig
-> proxy
-> Kubernetes API Server
이 방식은 좋다. 단순하고 빠르다. 프록시가 Kubernetes API와 어떻게 대화하는지 보기에는 충분하다.
하지만 실제 사용자가 데스크톱 에이전트를 통해 접근한다면 경로가 달라진다.
kubectl
-> exec kubeconfig
-> qpctl kube get-token
-> Multi Agent
-> product session / selected role
-> KAC token API
-> proxy
-> Kubernetes API Server
여기에는 정적 토큰 경로에는 없는 실패 지점이 많다.
- Multi Agent 앱이 설치되어 있는가
qpctl이 올바른 앱 번들을 가리키는가- 앱이 실행 중인가
- 로컬 제품 서버가 host로 등록되어 있는가
- 로그인 세션이 살아 있는가
- 현재 선택된 역할이 있는가
kubectl이 exec plugin을 통해 토큰을 받을 수 있는가
정적 토큰 테스트는 이 질문들을 대답하지 못한다.
그래서 이번에는 ReproKit에 “진짜 길”을 넣었다.
Multi Agent를 테스트의 승객이 아니라 등장인물로 만들기
처음에는 Multi Agent를 부가 조건처럼 생각하기 쉽다.
“앱이 설치되어 있으면 좋고, 아니면 수동으로 확인하면 되겠지.”
하지만 실제 제품 경로를 재현하려면 Multi Agent는 배경 소품이 아니다. 주인공 중 하나다.
그래서 ReproKit에 Multi Agent 관련 모듈을 나눠 넣었다.
multiagent.install
multiagent.runtime
multiagent.config
multiagent.auth
multiagent.kubeconfig
multiagent.verify
역할은 단순하다.
1. 설치 확인
앱이 있는지 본다.
/Users/.../Applications/QueryPieMultiAgent.app
/usr/local/bin/qpctl
여기서 중요한 건 “파일이 있다”만이 아니다. qpctl이 실제 앱 번들 안의 실행 파일을 가리키는지도 본다.
테스트 자동화에서 제일 억울한 실패는 이런 것이다.
“명령은 있는데, 내가 생각한 그 명령이 아니었다.”
2. 실행 상태 확인
데스크톱 앱은 라이브러리 함수가 아니다.
실행 중이어야 하고, 필요하면 다시 시작해야 한다. 그래서 runtime 모듈은 프로세스를 확인하고 앱을 열고, qpctl로 상태를 묻는다.
이 단계가 있으면 다음 사람이 “Multi Agent 켜져 있나요?”를 매번 물어보지 않아도 된다.
3. 로컬 host 설정
로컬 개발 서버는 보통 이런 주소로 떠 있다.
http://127.0.0.1:18080
그런데 Multi Agent가 이 서버를 모르면 qpctl은 엉뚱한 곳을 보거나 아예 실패한다.
여기서 재미있는 점은 설정 파일이 단순한 YAML이나 JSON이 아니었다는 것이다. 데스크톱 앱은 LiteDB에 host와 기본 서버 상태를 저장하고 있었다.
그래서 모듈은 먼저 DB를 백업한다. 그 다음 로컬 서버를 등록하고 현재 host로 선택한다.
좋은 자동화는 빠른 자동화가 아니라, 되돌릴 수 있는 자동화다.
4. 인증과 역할 선택
이번에 가장 흥미로웠던 함정은 “로그인했는데 역할이 없다”였다.
웹에서 역할을 선택했다고 해서, qpctl이 사용하는 에이전트 세션에도 같은 역할이 잡혀 있다고 보장할 수는 없었다.
겉으로는 이런 에러 하나로 보인다.
Cluster Role is not selected.
하지만 실제 의미는 더 구체적이다.
“웹 앱 세션 말고, Multi Agent가 쓰는 AGENT 세션에 선택된 KAC role이 없다.”
이 차이를 모듈로 만들었다.
qpctl auth status로 로그인 여부 확인- 필요하면 사용자가 직접 로그인할 명령 안내
- 로컬 재현 환경에서는
--allow-db-seed옵션으로 AGENT token의 selected role 보정 qpctl kube get-token이 실제 ExecCredential을 반환하는지 검증
토큰 값은 로그에 남기지 않는다. 필요한 것은 토큰의 내용이 아니라 “토큰을 정상적으로 받을 수 있는가”다.
5. exec-plugin kubeconfig 생성
마지막으로 진짜 kubeconfig를 만든다.
정적 토큰 kubeconfig가 아니라, kubectl이 실행 시점에 qpctl을 호출하는 kubeconfig다.
users:
- name: product-real-user
user:
exec:
command: /usr/local/bin/qpctl
args:
- kube
- get-token
- --server
- http://127.0.0.1:18080
- --cluster
- <cluster-id>
- --user-role
- <role-id>
- --output
- json
이 kubeconfig에는 bearer token이 저장되지 않는다.
kubectl이 실행될 때마다 qpctl이 Multi Agent에게 토큰을 받아온다. 즉 테스트가 사용자의 실제 길을 걷는다.
빠르게 만들었더니 더 정확해졌다
재미있는 점은 “진짜 경로”가 꼭 느리지만은 않았다는 것이다.
초기 setup은 할 일이 많다.
- minikube 준비
- KAC cluster 등록
- policy/role/grant 구성
- Multi Agent host 설정
- 앱 재시작
- 역할 동기화
- exec kubeconfig 생성
- ExecCredential 검증
그래도 로컬에서 약 40초 안팎이면 준비가 끝났다.
그 다음부터는 빨랐다.
verify --with-multiagent < 1초
kubectl direct probe 약 0.2초
kubectl via Multi Agent 약 0.2초
체감상 큰 차이는 이거다.
예전에는 사람이 체크리스트를 따라가며 물었다.
앱 설치되어 있나요?
qpctl 연결되어 있나요?
로그인되어 있나요?
role 선택되어 있나요?
kubeconfig 맞나요?
직접 kubectl과 제품 경유 kubectl 결과가 어떻게 다른가요?
이제는 명령 하나가 대답한다.
python3 dev-scripts/run-repro.py qcp-5498-kac-wide-output verify --with-multiagent
자동화의 가치는 단순히 “빠르다”가 아니다.
반복 질문을 없애고, 실패 지점을 이름 붙여주고, 증거를 같은 위치에 남기는 것이다.
kubectl get nodes -o wide가 알려준 것
이번 재현의 핵심 증상은 꽤 작아 보였다.
직접 Kubernetes에 붙으면 이렇게 나온다.
NAME STATUS ROLES VERSION INTERNAL-IP OS-IMAGE ...
그런데 제품 경로를 지나면 이렇게 줄어든다.
NAME AGE
처음 보면 권한 문제처럼 보일 수 있다.
하지만 여기서 중요한 단서는 kubectl의 출력 협상이다.
kubectl get nodes -o wide는 서버에게 “Table 형태로, wide 컬럼도 포함해서 주세요”라고 요청한다. 이 요청은 HTTP Accept header에 들어간다.
중간 프록시가 이 협상 정보를 너무 단순하게 바꾸면, Kubernetes API Server는 Table이 아니라 일반 JSON 목록을 돌려준다. 그러면 kubectl은 이렇게 말한다.
Unable to decode server response into a Table.
Falling back to hardcoded types.
즉 문제는 “클러스터에 못 들어간다”가 아니었다.
“들어가긴 들어갔는데, 대화 방식이 바뀌어서 wide 출력 정보를 잃었다”였다.
이 차이는 작지만 중요하다.
접근 제어 제품에서는 “통과했다”와 “원래 클라이언트 경험을 보존했다”가 다르기 때문이다.
좋은 재현 도구는 길을 짧게 만들지 않는다
이번 작업에서 얻은 가장 큰 교훈은 이것이다.
좋은 재현 도구는 실제 길을 지워버리지 않는다.
오히려 실제 길을 더 선명하게 만든다.
shortcut path
빠르다
단순하다
특정 레이어만 보기 좋다
real product path
조금 복잡하다
실제 사용자 경험과 가깝다
세션/역할/에이전트/프록시 문제까지 드러낸다
둘 중 하나만 있으면 부족하다.
정적 토큰 경로는 프록시와 Kubernetes 대화를 빠르게 본다. Multi Agent 경로는 실제 사용자가 밟는 길을 검증한다.
ReproKit의 역할은 이 둘을 싸우게 하는 것이 아니라, 나란히 세워 비교하게 하는 것이다.
direct kubectl
vs
product-real kubectl via Multi Agent
이렇게 보면 버그가 더 또렷해진다.
다음에 비슷한 일을 한다면
나는 이제 에이전트 기반 기능을 재현할 때 먼저 이 질문들을 던질 것 같다.
- 우리가 만든 테스트 경로가 실제 사용자 경로와 같은가?
- 중간에 토큰 발급, 역할 선택, 로컬 앱 상태 같은 숨은 문이 있는가?
- 실패했을 때 “마지막 에러”가 아니라 “어느 문에서 막혔는지” 말할 수 있는가?
- 결과가 스크린샷이나 기억이 아니라 transcript와 summary로 남는가?
- 반복 실행이 1분 안에 끝나는가?
이 다섯 질문에 답할 수 있으면, 재현 자동화는 단순한 스크립트가 아니라 작은 실험실이 된다.
그리고 좋은 실험실은 재미있다.
버그를 잡는 장소가 아니라, 시스템이 실제로 어떻게 움직이는지 보여주는 작은 무대가 되기 때문이다.
이번에는 그 무대 위에 Multi Agent, qpctl, kubectl, proxy, Kubernetes API Server가 차례로 올라왔다.
커튼이 열리자, 문제는 권한이 아니라 대화 방식이었다.
그걸 1초 안에 다시 볼 수 있게 된 것.
그게 이번 자동화의 가장 마음에 드는 부분이다.