하지만 기본적인 컨테이너의 경우 리눅스 명령어의 조합만으로도 구현이 가능합니다.
이 글에서는 리눅스 명령을 통해 컨테이너가 어떻게 격리되고 작동되는지 알아보겠습니다.
컨테이너를 공부하다 보면 무조건 들어보는 명령이 하나 있습니다 바로 chroot
입니다.
chroot
가 어떤 명령인지 매뉴얼을 통해 확인하면 다음과 같이 기술되어 있습니다.
NAME
chroot - run command or interactive shell with special root directorySYNOPSIS
chroot [OPTION] NEWROOT [COMMAND [ARG]...]
chroot OPTIONDESCRIPTION
Run COMMAND with root directory set to NEWROOT.
즉 특수한 루트 디렉토리에서 커맨드를 실행하거나 대화형 쉘을 실행시킬 수 있는 명령이며 chroot <디렉토리> <명령어> 구조로 새로운 루트 디렉토리에서 명령어를 실행하는 커맨드입니다.
chroot
가 어떤 명령인지 알았으니 실제 어떻게 작동하는지 새로운 폴더를 만들고 chroot
명령을 실행해 볼 시간입니다.Ω
test@jungnas:~/container$ mkdir new_root
test@jungnas:~/container$ sudo chroot new_root ls
chroot: failed to run command ‘ls’: No such file or directory
ls 커맨드가 없다고 하면서 생각했던 대로 동작하지 않습니다.
원인은 해당 폴더가 빈 폴더이기 때문에 새로 만든 루트에서 명령을 실행할 실행파일 또한 없어 발생하는 일입니다.
이를 위해 가장 가볍고 컨테이너 환경에서 많이 쓰이는 alpine 리눅스를 사용해 보겠습니다.
alpine 리눅스의 경우 tar.gz 파일로도 제공되어 이번 예제로 사용하기 적합한 배포판입니다.
test@jungnas:~/container$ mkdir alpinelinux
test@jungnas:~/container$ cd alpinelinux
test@jungnas:~/container/alpinelinux$ ls
test@jungnas:~/container/alpinelinux$ wget https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-minirootfs-3.17.3-x86_64.tar.gz
이제 압축을 해제하였으니 alpinelinux 디렉토리로 chroot 명령을 실행해 보겠습니다.
test@jungnas:~/container/alpinelinux$ sudo chroot alpinelinux ls /
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
ls / 명령을 실행하였더니 압축 해제된 폴더의 루트 디렉토리 모습을 확인할 수 있습니다.
그렇다면 실제로 shell 을 실행해 보면 어떨까요?
alpine 리눅스의 경우 bash 쉘이 없기 때문에 sh로 실행합니다.
test@jungnas:~/container/alpinelinux$ sudo chroot alpinelinux sh
/ # ls
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
/ # cat /etc/hostname
localhost
/ # cat /etc/passwd
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
/ # whoami
root
/ # exit
test@jungnas:~/container/alpinelinux$
보는 내용과 같이 실제 호스트 운영체제와는 별도의 배포판처럼 동작하는 것을 확인할 수 있습니다
다만 chroot 는 프로세스가 상위 루트를 볼수있는 현상이 있어 실제 컨테이너 구현에서는 chroot 보다는 pivot_root 를 더 선호합니다.
리눅스에서는 특정 프로세스를 네임스페이스에 넣게되면 그 네임스페이스에서 허용하는 것만 볼 수 있는 기능인 namespace 기능을 지원합니다.
리눅스에서 현재 동작중인 네임스페이스를 보고싶으면 다음 명령어를 통해 확인 가능합니다.
test@jungnas:~$ lsns
NS TYPE NPROCS PID USER COMMAND
4026531834 time 2 2949039 test bash
4026531835 cgroup 2 2949039 test bash
4026531836 pid 2 2949039 test bash
4026531837 user 2 2949039 test bash
4026531838 uts 2 2949039 test bash
4026531839 ipc 2 2949039 test bash
4026531840 net 2 2949039 test bash
4026531841 mnt 2 2949039 test bash
이 기능을 통해 리눅스의 여러 기능을 격리시켜 이용할 수 있습니다.
이때 네임스페이스를 변경하기 위해서는 unshare
라는 명령어를 이용합니다.
man unshare
명령을 통해 어떻게 생긴 명령어인지 확인해봅시다
NAMEunshare - run program in new namespaces
SYNOPSIS
unshare [options] [program [arguments]]DESCRIPTION
The unshare command creates new namespaces (as specified by the command-line options described
below) and then executes the specified program. If program is not given, then "${SHELL}" is run (default: /bin/sh).
설명을 보게 되면 unshare 명령은 아래에 설명된 명령줄 옵션에 지정된 대로 새 네임스페이스를 만듭니다.
새 네임스페이스를 만든 다음 지정된 프로그램을 실행합니다. 라는 설명을 확인할 수 있습니다.
격리 대상은 pid 부터 네트워크 까지 거의 모든 대상이지만 이번 예시에서는 pid, cgroup 를 격리시켜 컨테이너로 만들어보겠습니다.
컨테이너 내부에서 ps 명령을 실행해보신 경우 다음과 같이 pid 가 항상 1 인것을 확인 할 수 있습니다.
/ # ps -ef
PID USER TIME COMMAND
1 root 0:00 nginx: master process nginx -g daemon off;
30 nginx 0:08 nginx: worker process
31 nginx 0:08 nginx: worker process
32 root 0:00 sh
38 root 0:00 ps -ef
이는 pid 를 호스트와 격리시켜 개별적인 네임스페이스를 적용했기 때문입니다.
unshare 명령을 이용해 pid 에 새로운 네임스페이스를 만들어 실행시키는 명령어는 다음과 같습니다.
sudo unshare --pid <command>
여태가지 설명에 따르면 위 명령을 이용해 sh 커맨드를 새로운 네임스페이스에서 실행시키면 바로 pid 1 로 sh 명령이 실행될것으로 보입니다.
실제로도 그런지 한번 테스트 해봅시다
ubuntu@ip-10-1-1-227:~/container$ sudo unshare --pid sh
# ls
alpinelinux
# ls
sh: 2: Cannot fork
# ls
sh: 3: Cannot fork
뭔가 이상합니다
pid 검사를 하지도 않았는데 이상하게 에러가 발생합니다.
해당 내용의 원인은 sh 프로세스에 있습니다.
sudo unshare --pid sh
명령에서 sh 프로세스의 부모 프로세스는 unshare 가 되어야하지만
sudo 가 부모로 된것이 원인입니다. 이를 해결하기 위해서는 —fork 옵션을 추가해주면 됩니다.
ubuntu@ip-10-1-1-227:~/container$ sudo unshare --pid --fork sh
# ps
PID TTY TIME CMD
1339 pts/1 00:00:00 sudo
1340 pts/1 00:00:00 unshare
1341 pts/1 00:00:00 sh
1342 pts/1 00:00:00 ps
# ps
PID TTY TIME CMD
1339 pts/1 00:00:00 sudo
1340 pts/1 00:00:00 unshare
1341 pts/1 00:00:00 sh
1343 pts/1 00:00:00 ps
fork 문제는 해결되었지만 아직 이상한점이 남아있습니다.
우리는 컨테이너와 같이 pid 가 1로 표현되는 부분을 보고싶으나 1339 와 같이 이상하게 보이고 있습니다.
이 현상의 원인은 ps 명령어를 살펴보면 알수잇습니다.
This ps works by reading the virtual files in /proc. This ps does not need to be setuid kmem
or have any privileges to run. Do not give this ps any special permissions.
ps 명령어는 /proc 디렉토리를 읽어 표현해주는것이 원인이였습니다.
우리는 chroot 명령을 통해 루트 디렉토리를 변경하는 방법을 알고있습니다.
chroot 를 통해 루트를 변경하고 /proc 디렉토리를 추가 마운트 하여 ps 명령을 쳐보도록 하겠습니다.
ubuntu@ip-10-1-1-227:~/container$ sudo unshare --pid --fork chroot alpinelinux sh
/ # mount -t proc proc proc
/ # ps
PID USER TIME COMMAND
1 root 0:00 sh
3 root 0:00 ps
성공적으로 pid 가 격리된것을 보실 수 있습니다.
cgroup이란 Control groups 를 줄인 말로 주어진 그룹에 속한 프로세스들이 사용할수있는 자원을 제한하는 기능입니다.
해당 기능을 통하여 위에서 만든 컨테이너가
ubuntu 22.04 에서는 cgroup v2 가 사용됩니다. 따라서 다음 명령어를 따라 가면 cpu 사용량을 제한할 수 있습니다
sudo apt-get install cgroup-tools
cat /sys/fs/cgroup/cgroup.controllers
해당 명령어 실행시 다음과 같은 내용 출력시 정상적으로 사용 가능한 상황입니다.
cpuset cpu io memory hugetlb pids rdma
echo "+cpu" >> /sys/fs/cgroup/cgroup.subtree_control
echo "+cpuset" >> /sys/fs/cgroup/cgroup.subtree_control
해당 명령어는 /sys/fs/cgroup 의 하위 그룹에 대해 cpu, cpuset 컨트롤러를 사용할수 있습니다.
mkdir /sys/fs/cgroup/Example/
해당 폴더를 만들면 하위에 자동적으로 많은 파일들이 생성된것을 확인하실 수 있습니다.
ubuntu@ip-10-1-1-227:~/container$ ls /sys/fs/cgroup/Example/
cgroup.controllers cpu.max.burst io.prio.class memory.reclaim
cgroup.events cpu.pressure io.stat memory.stat
cgroup.freeze cpu.stat io.weight memory.swap.current
cgroup.kill cpu.uclamp.max memory.current memory.swap.events
cgroup.max.depth cpu.uclamp.min memory.events memory.swap.high
cgroup.max.descendants cpu.weight memory.events.local memory.swap.max
cgroup.pressure cpu.weight.nice memory.high memory.zswap.current
cgroup.procs cpuset.cpus memory.low memory.zswap.max
cgroup.stat cpuset.cpus.effective memory.max pids.current
cgroup.subtree_control cpuset.cpus.partition memory.min pids.events
cgroup.threads cpuset.mems memory.numa_stat pids.max
cgroup.type cpuset.mems.effective memory.oom.group pids.peak
cpu.idle io.max memory.peak
cpu.max io.pressure memory.pressure
이 파일들은 활성화된 컨트롤러로 기본적으로 새로 생성된 하위 그룹은 제한 없이 모든 시스템의 CPU 및 메모리 리소스에 대한 액세스를 상속합니다.
echo "+cpu" >> /sys/fs/cgroup/Example/cgroup.subtree_control
echo "+cpuset" >> /sys/fs/cgroup/Example/cgroup.subtree_control
해당 커맨드를 통해 cpu 타임을 컨트롤 하는 컨트롤러만 사용할수있습니다.
mkdir /sys/fs/cgroup/Example/tasks/
echo "1" > /sys/fs/cgroup/Example/tasks/cpuset.cpus
해당 디렉토리는 밑에 실제 cpu를 제한할 작업들을 넣는데 사용하게 됩니다.
echo "200000 1000000" > /sys/fs/cgroup/Example/tasks/cpu.max
sudo unshare --pid --fork chroot alpinelinux `for i in 1; do while : ; do : ; done & done`
위 명령어는 chroot 로 루트 격리, unshare —pid 로 pid 까지 격리시킨 후 for i in 1; do while : ; do : ; done & done
명령을 실행하는 명령어입니다.
위 명령어는 cpu 1코어를 사용하게 됩니다.
echo <pid> > /sys/fs/cgroup/Example/tasks/cgroup.procs
컨테이너 보안 - 리즈 라이스 지음