컨테이너 격리는 왜 부족한가
기본 컨테이너는 호스트 커널을 공유한다. 네임스페이스와 cgroup으로 프로세스를 나누지만, 시스템 콜은 결국 하나의 리눅스 커널에 도달한다. 커널에 취약점이 하나라도 있으면 컨테이너 탈출(container escape)로 이어져 노드 전체가 위협받는다. 특히 신뢰할 수 없는 코드(멀티테넌트 SaaS, 사용자 제출 코드, CI 러너)를 실행할 때 이 공유 커널이 가장 큰 공격면이 된다.
VM으로 완전히 격리하면 안전하지만 부팅 시간과 메모리 오버헤드가 크다. gVisor는 그 중간을 노린다. 사용자 공간에서 동작하는 애플리케이션 커널(Sentry)이 컨테이너의 시스템 콜을 가로채 대신 처리하므로, 호스트 커널에 직접 도달하는 시스템 콜 수를 크게 줄인다.
RuntimeClass가 하는 일
쿠버네티스는 노드마다 여러 런타임 핸들러를 둘 수 있다. RuntimeClass는 "이 파드는 어떤 런타임으로 실행할지"를 파드 스펙에서 선택하게 해주는 오브젝트다. 즉 gVisor(runsc)를 쓰는 파드와 기본 runc 파드를 같은 클러스터에서 워크로드별로 섞어 돌릴 수 있다.
노드에 gVisor 설치와 containerd 설정
먼저 노드에 runsc 바이너리를 설치하고 containerd에 런타임 핸들러를 등록한다.
#!/bin/bash
set -euo pipefail
# runsc 설치
URL=https://storage.googleapis.com/gvisor/releases/release/latest/$(uname -m)
wget "${URL}/runsc" "${URL}/containerd-shim-runsc-v1"
chmod +x runsc containerd-shim-runsc-v1
mv runsc containerd-shim-runsc-v1 /usr/local/bin/
# containerd에 runsc 핸들러 등록 (/etc/containerd/config.toml)
cat >> /etc/containerd/config.toml <<'EOF'
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"
EOF
systemctl restart containerd
RuntimeClass 오브젝트와 파드 지정
containerd의 핸들러 이름(runsc)을 RuntimeClass의 handler에 연결한다. 파드는 runtimeClassName으로 이를 참조한다.
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc # containerd 런타임 핸들러 이름과 일치
scheduling:
nodeSelector:
sandbox.gke.io/runtime: gvisor # runsc 설치된 노드로만 스케줄
---
apiVersion: v1
kind: Pod
metadata:
name: untrusted-job
spec:
runtimeClassName: gvisor
containers:
- name: worker
image: python:3.12-slim
command: ["python", "-c", "import os; print(os.uname())"]
gVisor 안에서 uname을 찍어 보면 커널 릴리스가 호스트와 다르게 나온다. Sentry가 자체 커널로 응답하기 때문이다.
runc와 gVisor 비교
| 항목 | runc(기본) | gVisor(runsc) |
|---|---|---|
| 커널 | 호스트 커널 공유 | 사용자 공간 Sentry |
| 공격면 | 전체 시스템 콜 | 제한된 호스트 시스템 콜 |
| 시작 지연 | 가장 빠름 | 수백 ms 추가 |
| 시스템 콜 성능 | 네이티브 | 가로채기 오버헤드 |
운영 시 주의점
- 모든 워크로드에 넣지 말 것. 시스템 콜이 잦은 I/O 집약 앱은 오버헤드가 눈에 띄게 커진다. 신뢰 경계가 필요한 워크로드에만 선택 적용한다.
- gVisor는 리눅스 시스템 콜의 부분집합만 구현한다. 특정
ioctl, 저수준 네트워킹, GPU 접근 등은 지원이 제한적이므로 실제 이미지로 호환성을 먼저 검증한다. - 스케줄링을 반드시 제한하라. runsc가 없는 노드에 gVisor 파드가 배치되면 파드가 기동되지 않는다. RuntimeClass의 scheduling.nodeSelector와 노드 라벨을 함께 쓴다.
- hostPath 마운트나 특권 컨테이너는 격리 이점을 무력화한다. 샌드박스를 쓴다면 특권 설정을 함께 제거해야 의미가 있다.
정리
RuntimeClass는 런타임 선택을 파드 단위로 분리하는 스위치이고, gVisor는 공유 커널 공격면을 줄이는 실질적 격리 계층이다. 완전한 VM 격리는 아니지만, 신뢰할 수 없는 코드를 돌려야 하는 지점에 정밀하게 적용하면 노드 침해 위험을 크게 낮출 수 있다. 성능 오버헤드와 시스템 콜 호환성을 실제 워크로드로 측정한 뒤, 필요한 곳에만 붙이는 것이 핵심이다.