컨테이너는 왜 "가벼운 격리"인가
컨테이너를 VM처럼 생각하면 디버깅이 어렵다. VM은 하이퍼바이저가 하드웨어를 가상화하지만, 컨테이너는 커널을 호스트와 공유한 채 프로세스에 격리된 뷰만 제공한다. 이 뷰를 만드는 핵심이 네임스페이스다. PID, 네트워크, 마운트 등 리소스별로 별도의 이름 공간을 부여해, 같은 커널 위에서도 프로세스가 서로를 보지 못하게 한다. "격리가 새는" 대부분의 사고는 이 경계를 오해한 데서 나온다.
네임스페이스 종류와 격리 대상
| 종류 | 격리 대상 | 대표 증상 |
|---|---|---|
| pid | 프로세스 ID 트리 | 컨테이너 안 PID 1이 시그널을 못 받음 |
| net | 인터페이스·라우팅·포트 | 포트 충돌, DNS 미동작 |
| mnt | 마운트 포인트 | 볼륨이 안 보임 |
| uts | hostname·domainname | hostname 변경이 호스트에 영향 |
| ipc | 공유메모리·세마포어 | SysV IPC 누수 |
| user | UID/GID 매핑 | root처럼 보이나 실제 무권한 |
직접 만들어 확인하기
런타임 없이 unshare로 네임스페이스를 만들어보면 개념이 명확해진다. 아래는 PID·마운트·UTS를 새로 열고, /proc를 다시 마운트해 프로세스 뷰를 격리하는 예다.
sudo unshare --pid --mount --uts --fork --mount-proc bash
# 새 셸 안에서
hostname container-demo # 호스트에는 영향 없음
ps -ef # PID 1이 이 bash. 호스트 프로세스는 안 보임
echo $$ # 1
--mount-proc 없이 ps를 실행하면 호스트의 /proc가 그대로 보여 격리가 깨진 것처럼 보인다. 이 한 줄이 빠져 "격리가 안 된다"고 오해하는 경우가 많다.
왜 PID 1이 문제가 되는가
컨테이너 안 첫 프로세스는 PID 1이 되고, 커널은 PID 1에 특별 규칙을 적용한다. 핸들러가 등록되지 않은 시그널은 무시되며, 자식 프로세스의 좀비를 거두는 책임도 PID 1에 있다. 애플리케이션을 직접 PID 1로 띄우면 SIGTERM을 못 받아 graceful shutdown이 실패하고, 종료가 SIGKILL로 강제되며, 좀비가 쌓인다.
# 나쁜 예: SIGTERM 무시 위험
CMD ["python", "app.py"]
# 좋은 예: init이 시그널 전달·reaping 담당
# docker run --init 또는
CMD ["tini", "--", "python", "app.py"]
user 네임스페이스로 root 권한 줄이기
컨테이너 안 root가 호스트 root와 같으면, 컨테이너 탈출 시 피해가 크다. user 네임스페이스는 컨테이너의 UID 0을 호스트의 비특권 UID로 매핑해 이 위험을 낮춘다. 아래는 매핑 확인 예다.
# rootless 컨테이너 안에서
id # uid=0(root)
cat /proc/self/uid_map
# 0 100000 65536
# 컨테이너 UID 0 → 호스트 UID 100000. 실제 호스트 권한은 없음
다만 user 네임스페이스는 파일 소유권 매핑, 일부 파일시스템 미지원 등 부수 효과가 있어, 볼륨 권한 문제가 새로 생길 수 있다.
실무 주의점
- 네임스페이스는 격리이지 자원 제한이 아니다. CPU·메모리 한도는 cgroups가 담당하므로 둘을 혼동하지 말 것.
--net=host,--pid=host는 해당 네임스페이스 격리를 통째로 포기한다. 편의로 켜두면 포트 충돌과 권한 노출을 부른다.- PID 1에는 반드시 init(tini,
--init)을 두어 시그널과 좀비를 처리한다. - 보안이 중요하면 user 네임스페이스와 seccomp를 함께 적용하되, 볼륨 소유권 변화를 사전에 검증한다.
정리
컨테이너는 마법이 아니라 네임스페이스(격리)와 cgroups(제한)의 조합이다. 어떤 리소스가 어느 네임스페이스로 나뉘는지 알면, 포트 충돌·시그널 미수신·볼륨 권한 문제의 원인을 런타임 로그가 아니라 커널 관점에서 짚어낼 수 있다.