버그 재현은 가끔 공항 입국심사 같다.
여권은 있는 것 같다. 비행기도 탔다. 목적지도 맞다. 그런데 심사대 앞에서 갑자기 멈춘다.
“이 사람, 진짜 들어와도 되나요?”
“이 여권은 아직 유효한가요?”
“어느 게이트로 들어와야 하나요?”
“직항으로 들어온 사람과 환승해서 들어온 사람이 같은 풍경을 보고 있나요?”
이번 작업은 딱 그런 느낌이었다.
Kubernetes 클러스터는 살아 있었다. 직접 kubectl로 보면 노드도 보였다. 그런데 제품의 KAC 경로를 통과하면 같은 클러스터가 정말 같은 방식으로 보이는지, 웹 화면에서도 접근 가능한지, 프록시를 지나도 kubectl get nodes -o wide 같은 출력이 유지되는지 확인해야 했다.
그래서 ReproKit에 작은 국경심사대를 만들었다.
먼저 “대상”과 “심사대”를 나눴다
처음에는 모든 것이 한 덩어리처럼 보인다.
Kubernetes니까 KAC 안에 넣으면 되지 않을까?
그런데 조금만 들여다보면 역할이 다르다.
Kubernetes 클러스터 자체
- minikube를 띄운다
- service account token을 만든다
- CA와 API endpoint를 읽는다
- 직접 kubectl로 살아 있는지 본다
QueryPie KAC 제품 경로
- 클러스터를 등록한다
- credential을 검증한다
- policy를 만든다
- role을 만든다
- 사용자에게 role을 준다
- kubepie-proxy용 kubeconfig를 발급한다
둘은 같이 움직이지만 같은 물건은 아니다.
비유하면 Kubernetes 클러스터는 목적지 국가이고, KAC는 입국심사대다. 목적지 공항이 열려 있는지 확인하는 일과, 입국심사대가 여권을 제대로 읽는지 확인하는 일은 나눠야 한다.
그래서 모듈도 나눴다.
reprokit.target_k8s.minikube
reprokit.target_k8s.eks
reprokit.kac.lifecycle
reprokit.kac.proxy
reprokit.kac.kubectl
reprokit.kac.evidence
target_k8s는 Kubernetes 대상 자체를 책임진다.
kac는 QueryPie 제품 경로를 책임진다.
이 단순한 분리가 꽤 큰 효과를 냈다.
minikube는 훈련장, EKS는 실제 공항
로컬 재현에는 minikube가 좋다.
가볍고, 빠르고, 지웠다가 다시 만들 수 있다. 마치 훈련장 같다. 비행기는 작지만 절차는 실제와 비슷하다.
그래서 target_k8s.minikube는 이런 일을 한다.
minikube profile 시작 또는 재사용
전용 kubeconfig 관리
service account 생성
token / CA / API URL 수집
fixture namespace 생성과 정리
반면 EKS는 실제 공항에 가깝다.
실제 인프라이기 때문에 ReproKit이 마음대로 만들고 지우면 안 된다. 그래서 target_k8s.eks는 처음부터 조심스럽게 만들었다.
생성/삭제가 아니라 read-only adapter다.
기존 kube context에서 endpoint, CA, version을 읽고, service account token은 명시적으로 넣는다. 위험한 마법은 넣지 않는다.
좋은 자동화는 버튼 하나로 모든 걸 해치우는 자동화가 아니다. 어디까지 해도 되는지 아는 자동화다.
KAC lifecycle: 여권, 비자, 탑승권을 한 번에 확인한다
KAC 쪽에서 반복되는 절차도 분리했다.
cluster credential verify
cluster create/update
policy create/update
role create
role-policy assign
user grant
selected role change
readiness check
cleanup
이 흐름은 특정 버그 하나의 전용 절차가 아니다. KAC 재현을 하려면 거의 매번 나온다.
그래서 kac.lifecycle로 올렸다.
재밌는 점은, 이 공통화가 단순히 코드 줄 수를 줄인 게 아니라 실패 지점을 더 빨리 드러내게 했다는 점이다.
예전에는 마지막 kubectl 단계에서 이런 식으로 실패할 수 있었다.
Unauthorized
Forbidden
cluster not visible
selected role missing
겉으로 보면 프록시 문제처럼 보인다.
하지만 실제 원인은 앞단일 수 있다.
- policy 본문이 비어 있었다
- role에 policy가 연결되지 않았다
- 사용자가 role을 받지 못했다
- selected role이 바뀌지 않았다
- 기존 cluster credential이 낡았다
이제 lifecycle helper가 중간 단계를 실패로 바로 드러낸다.
입국심사대에서 “비행기에서 내린 뒤에야 여권이 만료된 걸 알았다”가 아니라, 탑승 전에 잡아주는 셈이다.
proxy kubeconfig: 환승 게이트를 정확히 찍는다
KAC의 흥미로운 부분은 Kubernetes API Server로 바로 가지 않는다는 점이다.
경로는 대략 이렇게 된다.
kubectl
-> KAC kubeconfig
-> kubepie-proxy
-> QueryPie API/KAC
-> Kubernetes API Server
여기서 중요한 건 kubeconfig다.
직접 Kubernetes로 가는 kubeconfig와, KAC proxy를 지나가는 kubeconfig는 다르다. 후자는 제품이 발급한 KAC token과 proxy URL을 써야 한다.
그래서 kac.proxy를 만들었다.
이 모듈은 multiagent KAC token을 받고, proxy 설정을 읽고, 필요하면 로컬 proxy URL override를 반영해서 KAC용 kubeconfig를 만든다.
이렇게 해두면 다음 KAC 재현에서도 같은 질문을 반복하지 않아도 된다.
“토큰은 어디서 받아오지?”
“proxy URL은 어디에 넣지?”
“CA는 어떻게 넘기지?”
“로컬에서는 127.0.0.1:6443을 써야 하는데 어디서 바꾸지?”
모두 한곳에서 다룬다.
웹 UI도 직접 확인했다
터미널에서만 성공하면 반쪽이다.
실제 사용자는 웹 UI를 본다. 그래서 브라우저로도 확인했다.
확인한 것은 단순히 “페이지가 열렸다”가 아니었다.
- 클러스터 overview가 보이는가
- Kubernetes version이 보이는가
- node count가 보이는가
- namespace, pod count가 보이는가
- resource tree가 보이는가
Node를 클릭했을 때 실제 노드 목록이 나오는가- 관련 API가 모두 200을 주는가
브라우저에서 실제로 Node 리소스를 클릭했고, 노드 목록이 표시되는 것까지 확인했다.
이 단계가 중요하다.
프록시나 API만 보면 “기술적으로는 됩니다”라고 말하기 쉽다. 하지만 웹 UI까지 보면 “사용자가 실제로 접근할 수 있습니다”라고 말할 수 있다.
두 문장은 비슷해 보이지만 현장에서는 완전히 다르다.
그런데 왜 -o wide가 재미있었을까
이번 재현의 핵심은 “클러스터 접근 가능 여부”만은 아니었다.
더 재미있는 퍼즐은 kubectl get nodes -o wide였다.
직접 Kubernetes에 붙으면 wide 컬럼이 나온다.
NAME STATUS ROLES VERSION INTERNAL-IP OS-IMAGE ...
그런데 KAC proxy 경로를 지나면 wide 컬럼이 사라질 수 있었다.
왜 그럴까?
Kubernetes의 kubectl은 단순히 데이터를 달라고 하지 않는다. 서버에게 이렇게 말한다.
가능하면 Table 형태로 주세요.
wide 출력에 필요한 컬럼도 같이 주세요.
이 요청은 HTTP Accept header 안에 들어간다.
그런데 중간 프록시가 이 협상 정보를 너무 단순하게 바꾸면, API Server는 Table이 아니라 일반 JSON 목록을 돌려줄 수 있다. 그러면 kubectl은 wide 컬럼을 잃고 fallback 출력으로 내려간다.
즉 문제는 “노드를 못 본다”가 아니라 “출력 협상이 깨진다”에 가까웠다.
이건 꽤 좋은 디버깅 교훈이다.
같은 API를 호출해도 클라이언트가 어떤 형태의 응답을 요청했는지, 중간 프록시가 그 요청을 보존했는지에 따라 결과가 달라질 수 있다.
증거 봉투도 더 튼튼하게 만들었다
재현은 성공했다는 말보다 증거가 중요하다.
그래서 결과 패키지에 네 가지 라벨을 남기게 했다.
reproductionControls
fixtures
evidencePairs
cleanupChecks
의미는 이렇다.
reproductionControls는 재현 조건이다. API-first였는지, DB fallback을 썼는지, native client evidence가 있는지 기록한다.
fixtures는 만든 테스트 대상이다. minikube profile, KAC cluster 이름과 UUID 같은 정보가 들어간다.
evidencePairs는 직접 경로와 제품 경로의 증거를 짝으로 묶는다. 예를 들면 direct kubectl transcript와 KAC kubectl transcript를 한 쌍으로 둔다.
cleanupChecks는 정리 안전장치다. prefix/UUID scoped cleanup인지, minikube 삭제가 explicit flag를 요구하는지 남긴다.
이건 작은 변화지만 유용하다.
보고서를 보는 사람이 “정말 같은 조건이었나?”를 확인할 수 있기 때문이다.
이번 작업의 진짜 성과
처음 질문은 단순했다.
“KAC 웹 UI에서 Kubernetes 클러스터 접속 가능한가?”
답은 가능했다.
하지만 더 좋은 성과는 이거였다.
이제 다음에 비슷한 질문이 오면, 처음부터 다시 추리하지 않아도 된다.
target_k8s로 클러스터 준비
kac.lifecycle로 제품 설정
kac.proxy로 proxy kubeconfig 발급
direct kubectl과 KAC kubectl 비교
필요하면 브라우저 UI까지 확인
result package에 증거 봉투 저장
길이 생겼다.
버그 재현에서 길이 생긴다는 건 큰 일이다. 한 번 우연히 통과한 길이 아니라, 다음 사람도 따라 걸을 수 있는 길이기 때문이다.
기억할 만한 교훈
첫째, 대상과 제품 경로를 나누자.
Kubernetes가 살아 있는지와 KAC가 접근을 허용하는지는 다른 문제다.
둘째, 자동화는 실패를 늦게 보여주면 안 된다.
policy, role, grant, selected role 단계에서 실패를 빨리 보여줘야 마지막 kubectl 단계에서 엉뚱한 추리를 하지 않는다.
셋째, 웹 UI 확인은 “기술적으로 됨”을 “사용자가 쓸 수 있음”으로 바꿔준다.
넷째, 프록시는 데이터를 전달하는 파이프만이 아니다.
헤더 하나, content negotiation 하나가 CLI 출력 전체를 바꿀 수 있다.
다섯째, 좋은 재현 도구는 증거 봉투를 만든다.
실행 로그, 직접 경로, 제품 경로, fixture, cleanup 조건이 함께 있어야 나중에 믿을 수 있다.
이번에 만든 것은 거창한 플랫폼이라기보다 작은 국경심사대에 가깝다.
하지만 좋은 심사대가 그렇듯, 누가 어디서 와서 어떤 권한으로 어디까지 들어갔는지를 분명하게 남긴다.
그리고 그 덕분에 다음 여행은 조금 덜 헤맨다.