Development Log
6편. Argo CD Web Terminal — 검은 화면의 원인을 RBAC까지 추적하기
Argo CD Web Terminal의 검은 화면을 브라우저 이벤트부터 ServiceAccount와 RBAC 권한까지 추적한 기록입니다.
- #Argo CD
- #RBAC
- #K3s
문제 Argo CD Web Terminal 탭은 열렸지만 프롬프트가 없고 입력도 되지 않았다. 선택 브라우저 focus → WebSocket → 컨테이너 shell → 일반
kubectl exec→ Argo CD ServiceAccount 권한 순서로 계층을 분리해 확인했다. 결과argocd-server에pods/exec create권한이 없던 RBAC 문제를 찾아 필요한 권한을 추가한 뒤 Web Terminal을 정상화했다. 배운 점 UI 장애처럼 보여도 실제 실패 지점은 Kubernetes authorization 계층일 수 있고, 가설을 하나씩 증거로 제외하는 방식이 가장 빠른 진단 경로였다.
5편까지 배포 자동화와 Deployment 운영 설정을 다듬은 뒤, 다음 목표는 배포된 Pod의 런타임 상태까지 Argo CD 안에서 확인할 수 있는 운영 경로를 만드는 것이었다. 그래서 Web Terminal을 활성화했지만, Terminal 탭은 열리는데 프롬프트가 없고 입력도 되지 않았다. 이 문제를 브라우저 → WebSocket → Argo CD 서버 → Kubernetes
pods/exec→ 컨테이너 셸 순서로 나눠 검증했고, 최종 원인은argocd-server** ServiceAccount에pods/exec create권한이 없었던 RBAC 문제**로 확인했다.
왜 Argo CD Web Terminal까지 필요했나
앞 단계에서 이미 git push → GitHub Actions → GHCR → Argo CD → K3s 자동 배포가 동작하고 있었고, Argo CD UI에서는 Sync/Health 상태와 리소스 트리, Pod 로그까지 확인할 수 있었다.
하지만 메모리 문제처럼 실행 중인 컨테이너 내부에서 직접 확인해야 하는 값도 있다.
예를 들어 이번에 확인하고 싶었던 값은 Node.js의 V8 heap limit이었다.
GitOps 배포 상태
↓
Argo CD에서 Healthy / Synced 확인
↓
로그 확인
↓
그래도 런타임 내부 값이 필요하면?
↓
Pod Web Terminal
↓
Node.js / 프로세스 / 파일시스템 진단
물론 SSH로 홈서버에 접속해서 kubectl exec를 실행해도 된다. 그래도 Web Terminal을 붙인 이유는 Argo CD에서 보고 있는 Application과 Pod 문맥을 유지한 채 바로 진단할 수 있는 운영 흐름을 만들어보고 싶었기 때문이다.
이 기능을 외부에 공개하려는 목적은 아니었다. Argo CD UI 자체는 기존처럼 SSH Tunnel 뒤에서만 접근하고, 그 안에서 선택한 Pod에 한해 exec를 수행하는 구조를 유지했다.
Web Terminal은 모니터링이나 로그 수집을 대체하는 도구가 아니다. 평상시 관측은 metrics/logs가 담당하고, Web Terminal은 문제가 생겼을 때 컨테이너 내부를 직접 확인하는 진단 수단으로 두는 것이 목적이었다.
먼저 요청 경로부터 그렸다
처음에는 단순히 "터미널 입력이 안 된다"는 UI 문제처럼 보였다. 하지만 Web Terminal은 브라우저 하나만으로 동작하는 기능이 아니다.
따라서 어느 한 계층을 추측으로 고치는 대신, 요청이 실제로 어디까지 도달하는지를 한 단계씩 확인하기로 했다. 이 문제의 조사 기준은 다음과 같이 잡았다.
- Terminal 기능 자체가 활성화됐는가
- 브라우저가 키보드 입력을 받고 있는가
- WebSocket 연결이 유지되는가
- 컨테이너 셸이 실제로 존재하는가
- 일반 Kubernetes exec는 정상인가
- Argo CD 서버가 Kubernetes exec를 만들 권한이 있는가
1. Web Terminal 기능은 정상적으로 활성화돼 있었다
argocd-cm에서 Web Terminal을 활성화했다.
exec.enabled: "true"
exec.shells: "bash,sh"
설정 반영 후 Pod 상세 화면에 TERMINAL 탭이 나타났다.
즉 첫 번째 단계인 기능 활성화 자체는 성공했다.
그런데 Terminal을 열면 검은 화면과 커서만 보였고 프롬프트가 나타나지 않았다. 키를 눌러도 화면에 아무것도 표시되지 않았다.
2. 브라우저 입력 문제인지 먼저 제외했다
처음에는 focus나 macOS 한글 IME 문제를 의심했다. Chrome DevTools에서 xterm의 hidden textarea를 확인했고 강제로 focus도 줬다.
const t = document.querySelector('.xterm-helper-textarea');
t.focus();
document.activeElement === t;
결과는 true였다.
키 이벤트도 직접 확인했다.
t.addEventListener('keydown', e => {
console.log({
key: e.key,
code: e.code,
keyCode: e.keyCode,
isComposing: e.isComposing
});
}, true);
l을 입력했을 때 다음처럼 정상적인 이벤트가 발생했다.
key: l
code: KeyL
keyCode: 76
isComposing: false
따라서 키보드 자체가 죽은 것도 아니고, IME composition 때문에 입력이 막힌 것도 아니었다. 이 시점에서 UI focus를 계속 만지는 것은 의미가 없다고 판단했다.
3. WebSocket은 연결됐지만 바로 끊기고 있었다
Network 탭에서 terminal WebSocket을 확인했다.
처음 연결은 정상적으로 101 Switching Protocols가 되었고, 터미널 크기 변경을 알리는 resize 프레임도 전송됐다.
그런데 입력에 해당하는 stdin 프레임은 보이지 않았다.
처음에는 xterm의 onData() 문제처럼 보였다. 하지만 WebSocket 객체 상태를 직접 추적해 보니 더 중요한 사실을 발견했다.
WS OPEN
→ readyState 1
→ 잠시 뒤 CLOSE
→ code 1006
→ wasClean false
즉 브라우저가 terminal 연결을 계속 유지하고 있던 것이 아니라, 세션이 열린 직후 비정상 종료되고 있었다.
이 결과를 기준으로 조사 방향을 브라우저에서 서버 쪽으로 옮겼다.
4. Alpine과 bash를 의심한 이유와, 왜 원인에서 제외했나
ddongmy-os 이미지는 node:20-alpine 기반이다. Alpine은 기본 셸이 bash가 아니라 BusyBox ash(/bin/sh)이기 때문에, Web Terminal과 셸 호환 문제가 있을 가능성을 확인할 필요가 있었다.
그래서 테스트 목적으로 runtime 이미지에 bash를 추가했다.
RUN apk add --no-cache bash
여기서 또 하나의 문제가 생겼다. Dockerfile은 수정됐지만 첫 GitHub Actions가 Buildx 부팅 단계에서 실패해 새 이미지가 실제 Pod까지 올라가지 않았다. 즉 Git에 코드가 존재한다는 것과 실제 실행 중인 컨테이너에 반영됐다는 것은 별개였다. Actions를 재실행한 뒤 두 Pod가 새 이미지 SHA를 사용하고 있는지 확인했고, bash도 직접 검증했다.
sudo kubectl exec deploy/web-app -- bash -lc 'echo "BASH_OK / $BASH_VERSION"'
결과:
BASH_OK / 5.3.3(1)-release
일반 kubectl exec도 정상 동작했다.
따라서 다음 두 가지는 제외할 수 있었다.
컨테이너에 셸이 없음 ❌
Kubernetes exec 자체가 고장 ❌
이 단계가 중요했던 이유는 가능성이 있어 보이는 가설을 실제 증거로 제거했기 때문이다. bash를 넣었다고 문제가 해결될 것이라고 가정하지 않고, 새 이미지 반영과 실제 exec 동작까지 확인했다.
5. Argo CD 서버 로그에서 실패 지점을 찾았다
이제 argocd-server 로그를 Terminal을 여는 시점부터 확인했다.
sudo kubectl logs -n argocd deployment/argocd-server \
--since=2m -f | grep -Ei 'terminal|websocket|exec|error|EOF'
Terminal을 열면 다음 로그가 나타났다.
terminal session starting
http: response.WriteHeader on hijacked connection ... terminal.go
브라우저와 Argo CD 사이의 WebSocket은 이미 열렸지만, 그 뒤 컨테이너 exec를 시작하는 과정에서 실패한 것으로 볼 수 있었다. 여기서 다음 질문으로 넘어갔다.
argocd-server는 실제로 Kubernetespods/exec를 생성할 권한이 있는가?
6. RBAC 구조를 먼저 확인했다
무작정 권한을 추가하기 전에 Argo CD 서버가 어떤 ServiceAccount와 Role을 사용하는지 확인했다.
sudo kubectl get deploy argocd-server -n argocd \
-o jsonpath='{.spec.template.spec.serviceAccountName}{"\n"}'
결과:
argocd-server
ClusterRoleBinding도 확인했다.
ClusterRoleBinding/argocd-server
↓
ClusterRole/argocd-server
↓
ServiceAccount argocd/argocd-server
즉 이미 존재하는 RBAC 경로는 명확했다.
처음에는 default namespace에 별도의 Role / RoleBinding을 만들어 최소 권한으로 제한하는 방법도 시도했다. 운영 관점에서는 namespace 단위 권한이 더 좁고 안전하기 때문이다.
하지만 현재 홈서버 환경에서는 이미 argocd-server가 ClusterRoleBinding을 통해 권한을 받고 있었고, 우선 문제의 원인을 빠르게 검증하는 것이 목적이었다. 그래서 기존 ClusterRole/argocd-server에 필요한 verb 하나만 추가하는 방식으로 원인을 확인하기로 했다.
7. 결정적인 원인 — pods/exec create가 없었다
pods/exec는 일반 Pod 조회와 다른 subresource다.
Pod 정보를 get할 수 있다고 해서 컨테이너에 exec 세션을 생성할 수 있는 것은 아니다.
확인 결과 argocd-server의 ClusterRole에는 Pod 조회 권한은 있었지만 pods/exec create가 없었다.
그래서 기존 ClusterRole에 다음 rule을 추가했다.
sudo kubectl patch clusterrole argocd-server --type='json' -p='[
{
"op": "add",
"path": "/rules/-",
"value": {
"apiGroups": [""],
"resources": ["pods/exec"],
"verbs": ["create"]
}
}
]'
적용된 rule을 다시 확인했다.
["pods/exec"] -> ["create"]
여기서 검증 명령도 한 번 잘못 사용했다. 처음에는 다음처럼 확인했다.
kubectl auth can-i create pods/exec ...
이 명령은 내 환경에서 기대와 다르게 no를 반환했고, 한동안 RoleBinding이 잘못된 것으로 판단하게 만들었다.
subresource는 다음처럼 명시해서 확인하는 것이 정확했다.
sudo kubectl auth can-i create pods --subresource=exec \
--as=system:serviceaccount:argocd:argocd-server \
-n default
결과:
yes
최종 원인: Terminal UI, xterm, Alpine, bash, SSH Tunnel이 아니라
argocd-server****가 Kubernetespods/execsubresource를 생성할 RBAC 권한이 없었던 것이었다. WebSocket은 열렸지만 실제 exec 세션 생성이 실패하면서 브라우저에서는1006비정상 종료로 보였다.
왜 ClusterRole patch를 선택했나
최종 해결에서는 ClusterRole/argocd-server에 pods/exec create를 추가했다.
이 선택은 현재 홈서버 단일 클러스터에서 원인을 검증하고 Web Terminal을 동작시키기 위한 결정이었다.
장점은 기존 Argo CD RBAC 흐름을 그대로 사용하면서 필요한 권한 하나만 추가할 수 있다는 점이다.
반대로 ClusterRole은 namespace 범위를 넘기 때문에 권한 범위가 넓다. 따라서 실제 조직 환경이라면 다음처럼 더 좁은 권한 모델을 먼저 검토하는 것이 맞다.
홈서버 실습
→ 기존 ClusterRole에 pods/exec create 추가
→ 구조 단순 / 원인 검증 빠름
다중 팀·운영 클러스터
→ namespace별 Role + RoleBinding
→ 필요한 Application/namespace만 exec 허용
→ 최소 권한 원칙에 더 적합
이번에는 기술적으로 동작시키는 것과 권한을 어디까지 허용할 것인가는 별도의 설계 문제라는 점도 같이 확인했다.
권한을 붙인 뒤 Web Terminal이 실제로 열렸다
RBAC 확인 결과가 yes로 바뀐 뒤 Argo CD Terminal을 다시 열었다.
이번에는 처음으로 실제 프롬프트가 나타났다.
web-app-86f4c7fbcd-g7mw9:/app$
이제 목표했던 운영 흐름이 완성됐다.
원래 하려던 것 — V8 heap limit 확인
Web Terminal 자체가 목적은 아니었다. 원래 확인하려던 것은 512Mi 메모리 제한을 가진 컨테이너 안에서 Node.js V8 heap 상한이 어떻게 잡히는가였다. 다음 명령을 실행했다.
node -e 'const v8=require("v8"); console.log(Math.round(v8.getHeapStatistics().heap_size_limit/1024/1024)+" MiB")'
결과:
259 MiB
Deployment의 컨테이너 memory limit은 다음과 같다.
resources:
limits:
memory: 512Mi
즉 현재 cgroup 조건에서 새로 실행한 Node 프로세스의 V8 heap limit은 약 259MiB였다.
추가로 process.memoryUsage()도 확인했다.
rss: 42 MiB
heapUsed: 3 MiB
heapTotal: 4 MiB
external: 1 MiB
heapLimit: 259 MiB
다만 이 값은 중요한 한계가 있다.
node -e로 새로 실행한 진단용 Node 프로세스의 메모리 값이지, 현재 요청을 처리하고 있는 Next.js 서버 프로세스의 실시간 RSS/heap 값은 아니다.
따라서 이번 단계에서 확실하게 검증한 것은 다음이다.
컨테이너 memory limit : 512Mi
V8 heap limit : 약 259MiB
Web Terminal exec : 정상
실제 Next.js 서버의 메모리 증가 패턴은 다음 단계에서 서버 프로세스 내부 계측으로 확인해야 한다.
실제 사례와 공식 문서를 참고한 지점
문제를 좁히는 과정에서 Argo CD 공식 문서와 GitHub Issue도 함께 참고했다. 다만 비슷한 증상이 있다고 해서 같은 원인이라고 단정하지 않고, 내 환경의 관측 결과와 맞는지 비교하는 용도로만 사용했다.
공식 문서 — Web Terminal은 결국 kubectl exec와 같은 권한 경로를 탄다
Argo CD 공식 Web-based Terminal 문서는 이 기능을 브라우저에서 실행하는 ****kubectl exec****와 유사한 기능으로 설명하고 있다. 또한 Web Terminal은 arbitrary command 실행이 가능한 강한 권한이므로 기본 비활성화되어 있고, RBAC의 exec/create 권한을 명시적으로 관리해야 한다고 설명한다.
- Argo CD 공식 문서 — Web-based Terminal
- Argo CD 공식 문서 — RBAC의 exec resource
이 문서를 보고 단순히
exec.enabled만 볼 것이 아니라 Argo CD 사용자 권한과 ****argocd-server****가 Kubernetes API에 요청할 수 있는 권한을 분리해서 봐야 한다고 판단했다.
사례 1 — Alpine 컨테이너에서 검은 화면과 입력 불가
Argo CD GitHub Issue #9641에는 node:lts-alpine이나 BusyBox 기반 컨테이너에서 Terminal이 검은 화면과 깜빡이는 커서만 보이고 입력이 되지 않는 사례가 보고되어 있었다.
- GitHub Issue #9641 — Problem with Terminal and linux shell different from bash
내 증상과 화면이 매우 비슷했기 때문에 Alpine
ash와 bash 부재를 먼저 의심했다. 그래서 실제 runtime 이미지에 bash를 추가하고 Pod 안에서BASH_OK까지 확인했다. 하지만 **bash가 존재하고 ****kubectl exec**도 정상인데 Argo CD Web Terminal만 계속 실패했기 때문에 이 사례와 최종 원인은 달랐다. 이 Issue는 원인을 맞힌 자료라기보다, 셸 계층을 하나의 가설로 세우게 만든 참고 사례였다.
사례 2 — Terminal은 열리지만 입력할 수 없고 WebSocket 오류 발생
Issue #9643에는 Web Terminal 화면은 열리지만 입력할 수 없고 websocket: unexpected reserved bits 오류가 발생하는 사례가 있다.
- GitHub Issue #9643 — Terminal read message err: websocket: unexpected reserved bits
이 사례 때문에 WebSocket 프레임과
argocd-server로그를 직접 확인했다. 다만 내 환경에서는 해당reserved bits오류가 나타나지 않았고, 대신101** 연결 후 ****1006****으로 세션이 종료되는 현상**이 관찰됐다. 따라서 WebSocket 문제라는 큰 범주는 참고했지만, 이 Issue의 원인을 그대로 적용하지는 않았다.
사례 3 — 프록시 환경에서 Web Terminal WebSocket 실패
Issue #11954에는 nginx/proxy 뒤에 있는 Argo CD에서 Terminal WebSocket이 실패하고, 서버 로그에 terminal session starting과 response header 관련 로그가 나타나는 사례가 있었다.
- GitHub Issue #11954 — Web Terminal WebSocket connection failed
내 환경도 처음에는
kubectl port-forward + SSH Tunnel을 통해 Argo CD에 접속하고 있었기 때문에 이 사례를 보고 proxy/port-forward 경로도 의심했다. 하지만 이후kubectl port-forward를 빼고 Argo CD Service의 ClusterIP로 직접 SSH Local Forwarding을 해도 동일하게 Web Terminal이 끊겼다. 따라서 프록시 계층도 최종 원인에서는 제외할 수 있었다.
외부 사례는 "정답을 찾는 자료"라기보다 다음에 어디를 검증할지 정하는 가설 생성 도구로 사용했다. 비슷한 화면을 보고 그대로 따라가기보다, 내 환경에서
bash 존재,kubectl exec 정상,WS 1006,pods/exec create 부재를 직접 확인하면서 후보를 하나씩 제거했다.
이번 문제를 해결한 방식
이번 문제의 핵심은 "Argo CD Terminal을 켜는 방법"보다 문제를 어떻게 잘라서 봤는가에 있었다.
증상
터미널에서 입력이 안 됨
↓
1. xterm focus / keydown 확인
↓
브라우저 입력 정상
↓
2. WebSocket 확인
↓
101 성공 후 1006 종료 발견
↓
3. bash / kubectl exec 확인
↓
컨테이너와 Kubernetes exec 정상
↓
4. argocd-server 로그 확인
↓
exec 단계 실패로 범위 축소
↓
5. ServiceAccount / RBAC 확인
↓
pods/exec create 없음
↓
6. 권한 추가 후 can-i 재검증
↓
yes
↓
7. Web Terminal 프롬프트 확인
↓
해결
처음 보이는 증상은 프론트엔드 UI였지만 원인은 Kubernetes 권한이었다. 증상이 발생한 위치와 실제 원인이 있는 위치는 다를 수 있다. 그래서 앞으로 비슷한 장애를 볼 때는 특정 기술을 먼저 의심하기보다 요청 경로를 먼저 그리고, 각 경계에서 관측 가능한 증거를 확인하는 방식으로 접근하려고 한다.
이번 단계에서 배운 것
1. Git에 반영됐다고 실행 환경에 반영된 것은 아니다
bash를 Dockerfile에 추가했어도 Actions가 실패하면 실제 Pod에는 존재하지 않는다. 이미지 SHA와 실행 중 Pod를 다시 확인해야 했다.
2. kubectl exec와 Argo CD Web Terminal은 같은 것이 아니다
kubectl exec가 정상이어도 Argo CD는 자기 ServiceAccount 권한으로 별도의 pods/exec 요청을 만든다. 따라서 각 주체의 RBAC을 따로 봐야 한다.
3. Kubernetes subresource는 별도 권한이다
pods와 pods/exec는 RBAC 관점에서 같은 resource가 아니다. get pods가 가능해도 create pods/exec가 없으면 Terminal은 열 수 없다.
4. 검증 명령 자체도 검증해야 한다
can-i create pods/exec 결과에 끌려다녔지만, 실제로는 --subresource=exec를 사용한 확인이 필요했다. 관측 도구나 명령을 잘못 사용하면 정상적인 구조도 장애처럼 보일 수 있다.
5. 권한은 기능과 함께 설계해야 한다
Web Terminal은 편리하지만 사실상 컨테이너 내부 명령 실행 권한이다. 단순히 기능을 켜는 것이 아니라 누가 어느 namespace의 어떤 Pod에 exec할 수 있는가까지 운영 설계에 포함해야 한다.
다음 단계 — 실제 Next.js 프로세스 메모리를 관측한다
이번 단계에서 Argo CD를 통해 Pod 내부까지 접근하는 경로는 만들었다. 다음에는 새 Node 프로세스가 아니라 실제로 서비스 중인 Next.js 서버 프로세스를 대상으로 다음 값을 관측하려고 한다.
- RSS
- heapUsed / heapTotal
- external / ArrayBuffer
- cgroup/container memory
- 요청량 증가에 따른 baseline 변화
- memory limit 근처에서의 OOMKill / Pod restart 이를 통해 예전에 실무에서 경험했던 **"V8 heap만 보면 정상처럼 보이지만 Pod RSS가 먼저 증가하는 문제"**를 홈서버에서 재현하고, Node.js 메모리와 Kubernetes 컨테이너 메모리가 어떻게 다른지 직접 검증하는 것이 다음 목표다.
이번 6편의 결론은 Web Terminal을 켰다는 것이 아니다. 배포 상태를 보는 단계에서 실행 중인 런타임을 직접 진단하는 단계로 운영 범위를 확장했고, 그 과정에서 브라우저부터 Kubernetes RBAC까지 요청 경로를 따라가며 장애 지점을 좁혔다는 데 의미가 있다.