도커 없이 컨테이너를 만든다
이 실습은 VM 에서 돕니다
우분투 24.04 VM 한 대입니다. 새 네임스페이스를 만들고, 마운트를 바꾸고, veth 를 다른 네임스페이스로 옮기는 일은 전부 커널 권한이 필요합니다. 실습 파드는 그 권한이 없어서(그리고 없어야 해서) 이 실습은 VM 에서만 돕니다.
목표
컨테이너 런타임이 해 주는 일을 unshare·nsenter·cgroup 파일만으로 하나씩
합니다. busybox 로 루트 파일시스템을 짓고, 네임스페이스 다섯 개를 파일에 고정해
상자를 띄우고, cgroup 으로 자원을 묶고, veth 로 바깥과 잇고, nsenter 로 안에
들어가 봅니다.
왜 중요한가
"컨테이너" 라는 커널 객체는 없습니다. 있는 것은 네임스페이스(무엇이 보이는가)와
cgroup(얼마나 쓰는가)과 루트 파일시스템(어디가 / 인가)이고, 런타임은 이것들을 한 번에
묶어 줄 뿐입니다. 이것을 손으로 한 번 해 보면 장애 때 보는 눈이 바뀝니다. 파드 안에서
ps 가 이상하면 PID 네임스페이스를, 이름 해석이 이상하면 네트워크 네임스페이스를,
OOM 이면 cgroup 을 보게 됩니다. kubectl debug 와 nsenter 가 하는 일도 그대로 이해됩니다.
단계
- 호스트의 네임스페이스 식별자 여섯 개를
/root/kx/box/01-host.txt에 적으세요. - busybox 로 루트 파일시스템
/root/kx/box/rootfs를 지으세요. - 네임스페이스를 고정할 자리
/run/kx-ns를 private 바인드 마운트로 만드세요. - 상자를 띄우세요. 호스트 이름은
kx-box, 안에서는sleep이 PID 1 입니다. - cgroup
kx-box에 PID 와 메모리 한도를 걸고 상자를 넣으세요. - veth 로 호스트(
10.77.0.1/24)와 상자(10.77.0.2/24)를 이으세요. nsenter로 상자 안에서ps와hostname을 실행한 결과를/root/kx/box/07-inside.txt에 저장하세요.- 런타임이 대신 해 주는 일을
/root/kx/box/08-report.md에 정리하세요.
참고
- 네임스페이스 식별자는
readlink /proc/<pid>/ns/<종류>로 봅니다. 숫자가 같으면 같은 네임스페이스입니다. unshare --uts=<파일>처럼 종류 뒤에 파일을 주면 새 네임스페이스를 그 파일에 바인드 마운트로 고정합니다. 프로세스가 죽어도 네임스페이스가 남습니다.unshare의--root=<디렉터리>는 새 루트로 들어가고,--mount-proc는 새 루트 안의/proc에 proc 을 올립니다.- 흔한 실수:
--pid만 주고--fork를 빠뜨리는 것. 새 PID 네임스페이스의 첫 프로세스는 unshare 의 자식이어야 합니다.
호스트의 번호부터 적는다
호스트 셸에서 ipc·mnt·net·pid·uts·cgroup 여섯 네임스페이스의 식별자를 이 순서대로 한 줄에 하나씩 /root/kx/box/01-host.txt 에 적으세요. 각 줄은 종류:[숫자] 꼴입니다.
/proc/self/ns/ 아래 항목은 심볼릭 링크이고, 링크가 가리키는 문자열이 곧 식별자입니다. 링크를 따라가 내용을 읽는 것이 아니라 링크 자체를 읽는 명령을 쓰세요.
루트 파일시스템을 짓는다
/root/kx/box/rootfs 아래에 bin·proc·sys·tmp·etc 디렉터리를 만들고, /bin/busybox 를 rootfs/bin/busybox 로 복사한 뒤 sh·ls·ps·hostname·ip·sleep·cat 을 busybox 로 가는 심볼릭 링크로 만드세요. chroot /root/kx/box/rootfs /bin/sh -c 'echo ok' 가 ok 를 내야 합니다.
busybox 는 정적 바이너리라 혼자서 돕니다. /bin/bash 같은 동적 바이너리를 그냥 복사하면 chroot 안에서 공유 라이브러리와 로더를 못 찾아 No such file or directory 로 죽습니다. 링크는 ln -s busybox rootfs/bin/sh 처럼 상대 경로로 겁니다.
네임스페이스를 고정할 자리
/run/kx-ns 디렉터리를 자기 자신에 바인드 마운트하고 전파 방식을 private 로 바꾼 뒤, 그 안에 빈 파일 uts·ipc·net·pid·mnt 를 만드세요.
unshare --mount=<파일> 은 마운트 네임스페이스를 파일에 고정하는데, 그 파일이 shared 전파 마운트 위에 있으면 거부됩니다. 자기 자신에 바인드 마운트(mount --bind 디렉터리 디렉터리)하면 독립된 마운트가 생기고, 그 마운트만 private 로 바꿀 수 있습니다. findmnt -o TARGET,PROPAGATION 으로 확인하세요.
상자를 띄운다
unshare 하나로 uts·ipc·net·pid·mnt 다섯 네임스페이스를 새로 만들어 각각 /run/kx-ns/<종류> 에 고정하고, 루트를 /root/kx/box/rootfs 로 바꾸고, 그 안의 /proc 에 proc 을 올리고, 호스트 이름을 kx-box 로 바꾼 뒤 sleep 100000 을 PID 1 로 남기세요. 셸을 닫아도 살아 있게 띄우세요.
PID 네임스페이스는 --fork 가 있어야 자식이 그 안의 PID 1 이 됩니다. 상자 안 명령은 /bin/sh -c 'hostname kx-box; exec sleep 100000' 처럼 쓰면 sh 가 sleep 으로 바뀌어 PID 1 이 sleep 이 됩니다. 터미널과 떼어 띄우려면 setsid … & 를 쓰고 입출력을 /dev/null 로 돌립니다.
상자를 cgroup 에 넣는다
/sys/fs/cgroup/kx-box 를 만들고 pids.max 를 32, memory.max 를 128M 으로 건 뒤, 상자의 PID 1(호스트 쪽 PID)을 그 그룹에 넣으세요.
상자 안의 PID 1 은 호스트에서는 다른 번호입니다. /proc/<pid>/status 의 NSpid 줄이 두 번호를 함께 보여 줍니다. 그룹을 만들고 값만 쓰면 아무 일도 일어나지 않습니다. 그룹의 cgroup.procs 에 그 PID 를 써야 합니다.
veth 로 바깥과 잇는다
veth 한 쌍을 만들어 호스트 쪽 이름은 kx-b0, 주소는 10.77.0.1/24 로, 다른 끝은 상자의 네트워크 네임스페이스로 옮겨 이름을 eth0, 주소를 10.77.0.2/24 로 두고 둘 다(상자의 lo 포함) 올리세요. 호스트에서 ping -c1 10.77.0.2 가 성공해야 합니다.
새 네트워크 네임스페이스에는 lo 하나만 있고 그것도 내려가 있습니다. ip link set <장치> netns <대상> 의 대상에는 PID 를 줄 수 있습니다. 옮긴 뒤 설정은 nsenter --net=/run/kx-ns/net ip … 으로 안에서 합니다.
nsenter 로 들어가 본다
nsenter 로 상자의 pid·mnt·uts·net·ipc 네임스페이스와 루트에 들어가 /bin/hostname 과 /bin/ps 를 실행하고, 두 출력을 /root/kx/box/07-inside.txt 에 이어서 저장하세요.
nsenter -t <pid> 에 -p -m -u -n -i -r 을 함께 주면 그 프로세스의 네임스페이스와 루트로 들어갑니다. 루트가 바뀌므로 명령은 상자 안의 경로(/bin/ps)로 부릅니다. 상자 안의 ps 에는 프로세스가 몇 개만 보여야 합니다.
런타임이 대신 해 주는 것
/root/kx/box/08-report.md 에 ## 무엇을 손으로 했나·## 런타임이 더 해 주는 것·## 네임스페이스는 언제 사라지나 세 절을 쓰세요. 본문에 unshare·nsenter·pivot_root 가 나와야 하고, 상자의 uts 네임스페이스 식별자(uts:[숫자])를 그대로 인용해야 합니다.
식별자는 readlink /proc/<상자 pid>/ns/uts 로 읽습니다. 런타임(runc)은 chroot 대신 pivot_root 를 쓰고, capability 를 줄이고, seccomp 와 AppArmor 프로필을 걸고, 사용자 네임스페이스를 다룹니다. 네임스페이스가 언제 사라지는지는 3단계에서 파일에 고정한 이유와 이어집니다.