Github 주소
https://github.com/ekdh600/kubernetes_playground

프로젝트는 AI 에이전트를 적극 활용하였습니다.
숙련된 개발 경험이 많지 않아 설명이 명확하지 않을 수 있습니다.
참조
광고 클릭은 큰 힘이 됩니다!
FastAPI
FastAPI framework, high performance, easy to learn, fast to code, ready for production
fastapi.tiangolo.com
Xterm.js
Terminal front-end component written in JavaScript that works in the browser.
xtermjs.org
GitHub - kubernetes-client/python: Official Python client library for kubernetes
Official Python client library for kubernetes. Contribute to kubernetes-client/python development by creating an account on GitHub.
github.com
Kubernetes를 처음 접근 할 때 가장 번거로운 건 환경 세팅이다.
또, 여러 유저에게 환경 전달이 필요할때 일일히 환경 셋팅해서 배포하는것 또한 꽤나 긴 시간이 소요된다.
위 같은 이유로 브라우저 터미널에서 바로 kubectl을 쓸 수 있는 간단한 플랫폼을 구성 해보았다.
어떤 고민들이 있었나.
*"파드 하나 띄우고 웹 터미널 연결하면 되지 않나?"*
그런데 막상 만들다 보니 생각보다 고려할 게 많았다.
- 사용자마다 격리된 환경이 필요하다.
- 브라우저 터미널과 Kubernetes Pod를 어떻게 연결할지.
- 세션은 어떻게 관리하지? DB를 따로 써야 할지
- SSH 접속도 지원하고 싶다.
- 클러스터가 여러 개라면?
전체 구조
브라우저 (xterm.js)
│
▼
Nginx UI 서버 (NodePort)
│ 역방향 프록시
▼
FastAPI API 서버 (ClusterIP)
│
├── REST API → K8s API 서버 (플레이그라운드 생성/삭제)
└── WebSocket → K8s Pod exec 스트림 (터미널 연결)
구조 자체는 단순하다.
API 서버가 Kubernetes의 Pod를 직접 만들고, 그 Pod에 WebSocket으로 연결한다.
사용자 입장에서는 버튼 하나만 누르면 된다.
WebSocket + kubernetes.stream
xterm.js는 WebSocket 기반이다. 그런데 Kubernetes Pod에 붙는 kubernetes.stream은 동기(blocking) 방식이다.
이 둘을 어떻게 연결하느냐가 문제였다. 그래서 나온 해결책이 asyncio.to_thread()이다.
async def k8s_to_ws(resp, ws):
def _blocking_read():
while resp.is_open():
resp.update(timeout=0.1)
if resp.peek_stdout():
data = resp.read_stdout()
# 이벤트 루프에 전송 요청
asyncio.run_coroutine_threadsafe(
ws.send_text(data), loop
)
await asyncio.to_thread(_blocking_read)
kubernetes.stream의 블로킹 루프를 별도 스레드에서 돌리고, 데이터가 오면 run_coroutine_threadsafe()로 이벤트 루프에 콜백을 던지는 방식이다.
이 패턴으로 FastAPI 이벤트 루프를 막지 않으면서도 실시간 터미널 스트리밍이 가능해졌다.
DB 없이 세션 관리
세션 데이터를 어디에 저장할지 고민했다.
처음에는 SQLite를 썼다.
그런데 생각해보니 이미 Kubernetes가 있는데 굳이 DB를 추가해야 하나 싶었다.
Kubernetes ConfigMap을 세션 저장소로 쓴다.
# playground-session-{uuid} ConfigMap
data:
created_at: "2026-01-01T00:00:00"
expires_at: "2026-01-02T00:00:00"
playground_id: "ab3c7d91"
cluster_id: "f2e8a1b3"
- API 서버를 재시작해도 세션이 살아있다 (stateless 서버)
kubectl get configmap -l type=session으로 실시간 현황 확인 가능- 외부 DB가 없으니 운영 복잡도가 줄어든다
마찬가지로 클러스터 kubeconfig도 Kubernetes Secret에 저장한다.
별도의 암호화 저장소 없이 K8s etcd 암호화에 의존하는 방식이다.
ConfigMap은 기본적으로 etcd 암호화 대상이 아니라는 점은 알고 있어야 한다.
세션 데이터 자체는 민감하지 않지만, 운영 환경이라면 etcd 암호화 설정을 권장한다.
세션별 SSH 키 쌍
SSH 접속을 지원하고 싶었다.
브라우저 터미널은 편리하지만 결국 로컬 터미널이 더 쾌적하다.
그래서 NodePort 서비스로 SSH 직접 접속도 가능하게 만들었다.
정적인 비밀번호는 공유될 위험이 있다.
그래서 플레이그라운드마다 새 RSA 4096-bit 키 쌍을 동적으로 생성한다.
from cryptography.hazmat.primitives.asymmetric import rsa
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
)
공개키는 파드 내부의 authorized_keys에 들어가고, 개인키는 플레이그라운드 생성 직후 딱 한 번만 사용자에게 노출된다.
생성 완료 → 개인키 화면에 표시 (download/copy 가능)
→ 이후 페이지 이탈 시 서버에서는 다시 볼 수 없음
개인키를 저장하지 않으면? sessionStorage에 탭 세션 동안 보관해두어 새로고침 후에도 복원된다.
탭을 닫으면 자동 삭제된다.
RSA 4096이 약간 무겁긴 하지만 플레이그라운드 생성이 어차피 15~30초 걸리는 작업이라 체감상 차이가 없었다.
멀티 클러스터 지원
"하나의 클러스터면 충분하지 않나?"라고 생각할 수 있다.
그런데 현실에서는 개발 클러스터, 실습 클러스터, 테스트용 클러스터 등이 분리되어 있는 경우가 많다.
그래서 kubeconfig를 Secret에 등록하면 여러 클러스터에 플레이그라운드를 배포할 수 있는 레지스트리 구조를 만들었다.
관리자 → kubeconfig YAML 붙여넣기 → K8s Secret 저장
사용자 → 클러스터 선택 → 해당 클러스터에 플레이그라운드 배포
클러스터마다 독립적인 K8sManager 인스턴스를 생성한다.
인메모리 캐싱을 적용하여 매 요청마다 kubeconfig를 재파싱하지 않도록 했다.
여기서 한 가지 주의점이 있었다. kubeconfig의 server 필드에 https://kubernetes.default.svc가 들어있으면 외부 클러스터에서는 통하지 않는다. 등록된 kubeconfig에서 실제 API 서버 주소를 추출해서 각 클러스터 연결에 사용해야 한다.
RBAC 커스터마이징
관리자는 플레이그라운드 생성 시 사용자의 Kubernetes 접근 범위를 직접 설정할 수 있다.
| namespaces | verbs | 생성되는 리소스 | 실습 범위 |
|---|---|---|---|
["sandbox"] |
["get","list","watch"] |
Role + RoleBinding | sandbox 읽기 전용 |
["ns1","ns2"] |
["*"] |
Role × 2 + RoleBinding × 2 | 지정 네임스페이스 전체 |
["*"] |
["get","list"] |
ClusterRole + ClusterRoleBinding | 클러스터 전체 읽기 |
기본 사용자는 자신의 sandbox-{id} 네임스페이스에 ClusterRole admin이 바인딩된다.
다른 사용자의 sandbox에 접근할 수 없다.
K8s RBAC이 네임스페이스 단위로 격리를 보장하기 때문이다.
RBAC은 파드 재시작 없이 실시간으로 변경할 수 있다. K8s RBAC이 API 요청 시점에 평가되기 때문이다.
Helm Chart
배포는 Helm으로 통일했다.
관리자 비밀번호 해시를 배포 시점에 동적으로 생성하는 스크립트를 만들었다.
ADMIN_PASS=secure_password ./helm-deploy.sh
내부적으로는 python3 -c "bcrypt.hashpw(...)" 를 실행해서 bcrypt 해시를 즉석에서 생성한 뒤 --set-string으로 Helm에 전달한다.
덕분에 values.yaml에 평문 비밀번호가 남지 않는다.
helm upgrade --install playground-platform ./chart/...
--set-string adminPasswordHash="$ADMIN_HASH"
사용 흐름
1. 관리자: 클러스터 kubeconfig 등록
2. 사용자: 브라우저로 접속
3. 클러스터 선택 → Start Playground 클릭
4. 15~30초 후 브라우저 터미널 자동 연결
5. kubectl get pods -n sandbox-{id} 실행 가능
6. 원하면 SSH로 로컬 터미널에서도 접속
7. 24시간 후 자동 삭제 (또는 직접 Terminate)
개선
보안 측면
/clusters 엔드포인트가 인증 없이 클러스터 목록을 반환한다. 클러스터 이름과 ID만 반환하도록 제한했지만, 인증을 추가하는 것이 더 깔끔하다.
관리자 대시보드에서 자격증명을 sessionStorage에 base64로 저장하는 방식은 XSS에 취약하다.
서버 사이드 세션으로 전환하는 것이 이상적이다.
운영 측면
API 서버가 단일 레플리카라면 재시작 시 진행 중인 WebSocket이 끊긴다.
세션 복원이 되긴 하지만 개선 여지가 있다.
ClusterRole admin 권한이 sandbox 네임스페이스에 바인딩되어 있어 PVC 같은 클러스터 수준 리소스에는 접근할 수 없다. 이는 의도된 설계이지만 이후 이 플랫폼의 활용 용도에 따라 권한에 대해 다시 고려 할 생각이다.
중요
잘못된 정보나, 문의등은 댓글로 메일과 함께 적어주시면 감사하겠습니다.
'Kubernetes' 카테고리의 다른 글
| Kubestronaut 여정과 그 마지막 (0) | 2026.01.13 |
|---|---|
| GET 그리고 WATCH는 뭐가 다를까? - OpenTelemetry 장애 케이스 (0) | 2026.01.08 |
| Disk Thin Provisioning 방식과 Node Deadlock 상관관계 (1) | 2026.01.01 |
| I/O 병목과 ETCD 그리고 API 서버 (0) | 2025.12.09 |
| Ingress NGINX의 공식 은퇴 (0) | 2025.11.27 |