if(kakao)2022 이게 돼요? 도커 없이 컨테이너 만들기를 정리해봅니다. 저는 ubuntu server 22.04 환경에서 실습을 진행했습니다.
컨테이너 왜 쓸까?
- 다양한 서버 환경. 서버의 사양, OS 종류, 설치 환경
- 운영 비용 증가. 애플리케이션이 동작하는 환경 별로 대응이 어려움
서버 환경에 구애 받지 않을 수 있을까?
앱 전용 환경 만들기
- 패키징
- 격리
- 자원 할당
애플리케이션의 전용 환경을 제공합니다.
컨테이너의 사용 요건
- 리눅스 기술
- 런타임(컨테이너 관리 도구)
- 쿠버네티스
실습 환경 구축
root 사용자로 /tmp 디렉토리 밑에서 실습을 진행합니다.
sudo -Es
cd /tmp
# 필요 패키지 설치
apt-get update \
&& apt-get -y install gcc \
&& apt-get -y install make \
&& apt-get -y install pkg-config \
&& apt-get -y install libseccomp-dev \
&& apt-get -y install tree \
&& apt-get -y install jq \
&& apt-get -y install bridge-utils
도커 설치: https://docs.docker.com/engine/install/ubuntu/
VM vs Container
docker run -it busybox
VM, Container 비교하기
- 루트 디렉토리
- 파일 시스템
- 프로세스
- 네트워크
- 호스트네임
- uid, gid
ls /
df -h
ps aux
ip l
hostname
id
다 다르다는 것을 알 수 있습니다. 컨테이너 기술이 어떻게 시작 됐는지 첫 시작부터 알아봅시다.
컨테이너 파일시스템
프로세스를 가두자
chroot
- 사용자 프로세스를 가둘 수 없을까?
- 루트 디렉토리 밖으로는 프로세스가 나갈 수가 없다라는 점을 착안
- 유저 프로세스한테 실제 루트 디렉토리인 것 처럼 속임
chroot로 명령어 실행하기
실행 디렉토리 하위에 명령어 파일들이 있어야 합니다. myroot 하위 디렉토리로 sh, ls 명령어 관련 파일들을 복사하겠습니다. 공유 라이브러리들이 환경에 따라 다를 수 있어서 ldd로 조회하여 각 환경에 맞게끔 작업해야 합니다.
# sh 명령어 파일들을 myroot 밑으로 복사합니다.
which sh
ldd /usr/bin/sh
mkdir -p myroot/usr/bin
cp /usr/bin/sh myroot/usr/bin/
mkdir -p myroot/{lib64,lib/x86_64-linux-gnu}
cp /lib/x86_64-linux-gnu/libc.so.6 myroot/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libc.so.6 myroot/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 myroot/lib64/
tree myroot/
# ls 명령어 파일들을 myroot 밑으로 복사합니다.
which ls
ldd /usr/bin/ls
cp /usr/bin/ls myroot/usr/bin/
cp /lib/x86_64-linux-gnu/{libselinux.so.1,libc.so.6,libpcre2-8.so.0} myroot/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 myroot/lib64/
tree myroot/
# chroot를 실행합니다.
chroot myroot usr/bin/sh
# myroot 안에 넣은 usr/bin/sh을 가지고 접속합니다.
ls # sh, ls 명령어 사용 가능
cd ../../../ # 밖으로 탈출 안됨
exit
경로에 모아서 -> 패키징
경로에 가둬서 실행 -> 격리
chroot로 패키징 및 격리를 해봤습니다.
남이 만든 이미지를 가지고 chroot하기
# nginx-root 디렉토리에 nginx 이미지의 파일들을 가지고 옵니다.
mkdir nginx-root
docker export $(docker create nginx) | tar -C nginx-root -xvf -
tree -L 1 nginx-root/
# chroot를 실행
chroot nginx-root/ /bin/sh
# nginx 실행
nginx -g "daemon off;"
# 호스트에서 curl 명령어 실행
curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
네트워크를 공유하고 포트를 같이쓰고 있음을 알 수 있습니다.
chroot는 해킹이 가능하다
// vi escape_chroot.c
// 해킹 코드
#include <sys/stat.h>
#include <unistd.h>
int main(void)
{
mkdir(".out", 0755);
chroot(".out");
chdir("../../../../../");
chroot(".");
return execl("/bin/sh", "-i", NULL);
}
# 컴파일하여 실행파일을 myroot안에 생성합니다.
gcc -o myroot/escape_chroot escape_chroot.c
tree -L 1 myroot/
chroot myroot /usr/bin/sh
cd ../../../ # 밖으로 빠져나가지지 않습니다.
./escape_chroot.c
ls /
exit
exit
해킹 코드를 돌리니 호스트의 루트 디렉토리에 접근함을 알 수 있습니다.
해킹을 막아보자
pivot_root
- 루트파일시스템의 마운트 포인트를 변경함으로써 특정 디렉토리를 새로운 루트 디렉토리로 만드는 명령어
앞서 chroot로 만든 격리공간은 루트파일시스템이 동일하였기 때문에 탈옥이 가능했던 한편, pivot_root를 이용하면 루트파일시스템의 마운트 포인트 자체가 변경되기 때문에 탈옥 자체가 불가능합니다.
루트파일시스템
- 최상위 파일시스템
- 루트디렉토리를 포함
- 하위의 모든 파일시스템들이 마운트
루트파일시스템과 루트디렉토리는 다릅니다.
루트 파일시스템을 피봇하면 호스트에 영향이 갑니다. 호스트에 영향을 주지 않기 위해서 프로세스의 환경을 격리하는 네임스페이스를 개발했습니다. 네임스페이스의 여러 타입(마운트, PID, 네트워크)들 중 먼저 마운트의 환경 격리만 고려합니다.
마운트
- 파일시스템을 루트파일시스템의 하위 디렉토리로 부착하는 시스템 콜
- 마운트 포인트: 부착 지점, 접근 지점
- USB, CDROM 마운트
마운트 네임스페이스와 pivot_root를 같이 쓰면 호스트에 영향 없이 격리가 가능합니다.
unshare -m /bin/sh # mount 네임스페이스 격리
# 격리 공간 및 호스트에서 각각 명령어를 실행해서 비교
df -h
현재는 파일 시스템이 똑같습니다. 부모 프로세스 마운트 네임스페이스 정보를 복사해서 자식 네임스페이스를 만듭니다.
# 격리 공간
mkdir new_root # 새로운 루트 디렉토리가 될 곳
mount -t tmpfs none new_root # 메모리기반 파일시스템으로 테스트 용도로 마운트
mount | grep new_root # 격리 공간에만 mount되어 있음. 호스트에서 조회하면 안보임
cp -r myroot/* new_root
# 격리 공간 및 호스트에서 각각 명령어를 실행해서 비교
tree new_root
마운트 네임스페이스를 격리해서 격리 공간에만 복사됩니다. 호스트에는 복사한 내용이 안보입니다.
mkdir new_root/put_old
cd new_root
pivot_root . put_old # pivot_root {새로운 루트 디렉토리} {기존 루트 파일 시스템이 부착될 위치}
cd /
# 격리 공간 및 호스트에서 각각 명령어를 실행해서 비교
ls /
루트 디렉토리를 ls
로 조회하면 루트파일시스템의 마운트 포인트가 바꼈기 때문에 내용이 다름을 볼 수 있습니다. 격리 공간은 new_root 디렉토리를 루트 디렉토리로 인식합니다.
ls put_old # 루트 파일 시스템이 부착됨을 확인
./escape_chroot # 해킹(탈옥)이 되지 않습니다.
패키징도 잘했고, 완전히 격리 시켰지만 패키지들이 중복되는 문제가 있게 됩니다.
중복을 해결하자
오버레이 파일시스템
이미지의 중복 문제를 해결해봅시다.
유니온 파일시스템 = 상속 파일시스템 = 오버레이 파일시스템
- Lower 레이어는 ReadOnly
- Upper 레이어는 Writable
- CoW, copy-on-write(원본유지)
가장 위에 Mergied View(통합뷰)로 모든 것이 겹쳐진 형태의 레이어가 있게됩니다(셀로판지에 색이 투과되어 보이는 것에 비유).
오버레이 마운트 실습
Lower Dir1은 myroot 디렉토리를 사용합니다. Lower Dir2는 tools 디렉토리를 만들겠습니다.
mkdir tools
# which 명령어 파일들을 tools 밑으로 복사
which which
ldd /usr/bin/which
mkdir -p tools/usr/bin
cp /usr/bin/which tools/usr/bin/
# rm 명령어 파일들을 tools 밑으로 복사
which rm
ldd /usr/bin/rm
cp /usr/bin/rm tools/usr/bin/
mkdir -p tools/{lib64,lib/x86_64-linux-gnu}
cp /lib/x86_64-linux-gnu/libc.so.6 tools/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 tools/lib64/
오버레이 마운트를 할 디렉토리를 만들겠습니다.
# 오버레이 마운트 디렉토리 만들기
mkdir -p rootfs/{container,work,merge}
tree rootfs/
rootfs/
├── container # upper
├── merge # 통합뷰 Merged View
└── work # upper를 보장하기 위한 디렉토리
3 directories, 0 files
오버레이 마운트를 진행합니다.
# mount
mount -t overlay overlay -o lowerdir=tools:myroot,upperdir=rootfs/container,workdir=rootfs/work rootfs/merge/
tree -L 2 myroot/usr
tree -L 2 rootfs/merge/usr
tools 디렉토리로 인해 통합뷰에 rm, which 명령어가 추가되어 있습니다.
rm /tmp/rootfs/merge/escape_chroot
ls myroot
통합뷰의 escape_chroot를 삭제해도 myroot에 escape_chroot는 삭제되지 않고 존재합니다.
ls /tmp/rootfs/container
escape_chroot가 노란색으로 표시되어 있습니다. white out이라고 하는데 삭제 마킹을 한 것입니다. 삭제된 정보가 upper dir 밑에 쓰여지는 것입니다. 이렇게 하면 myroot에 있는 내용을 변경하지 않으면서 upper dir에만 그 변경된 정보를 관리할 수 있습니다. 이런식으로 원본이 보장됩니다.
컨테이너 격리와 자원
컨테이너 격리
전용 루트파일시스템으로 충분하다고 생각했었습니다. 하지만,
- 컨테이너에서 호스트의 다른 프로세스들이 다 보임
- 컨테이너에서 호스트의 포트를 사용
- 컨테이너에 루트 권한이 있음
이러한 문제들이 있습니다. 그래서
네임스페이스
- 프로세스에 격리된 환경을 제공
- 리눅스 커널의 기능
- 모든 프로세스는 타입(mount, net 등)별로 네임스페이스에 속함
- 자식 프로세스는 부모의 네임스페이스를 상속
mount, uts, ipc, net, pid, user, cgroup, time 네임스페이스 타입이 있습니다.
# 네임스페이스 조회 방법들
# 네임스페이스 확인
ls -al /proc/$$/ns
# symbolic link 읽기
readlink /proc/$$/ns/mnt
lsns -p 1 # pid 옵션
lsns -t mnt -p 1
마운트 네임스페이스를 격리해보겠습니다.
unshare -m
lsns -p $$
NS TYPE NPROCS PID USER COMMAND
4026531834 time 230 1 root /sbin/init
4026531835 cgroup 230 1 root /sbin/init
4026531836 pid 230 1 root /sbin/init
4026531837 user 230 1 root /sbin/init
4026531838 uts 227 1 root /sbin/init
4026531839 ipc 230 1 root /sbin/init
4026531840 net 230 1 root /sbin/init
4026532707 mnt 2 3596 root -bash
exit
mount 네임스페이스만 프로세스가 2개입니다. mount 네임스페이스만 isolation되어 있고, 나머지 네임스페이스는 1번 프로세스의 네임스페이스를 쓰고 있음을 볼 수 있습니다.
unshare -u # uts 네임스페이스 격리
exit
unshare -i # ipc 네임스페이스 격리
exit
나머지 컨테이너에 있어서 중요한 네임스페이스들은 좀 더 자세히 알아봅시다.
PID 네임스페이스
- PID(Process ID) 넘버스페이스를 격리
- 부모-자식 네임스페이스 중첩 구조
- 부모 네임스페이스에서 자식 네임스페이스가 보임
- 자식 네임스페이스에는 부모 네임스페이스 ,자식 네임스페이스 PID 2개를 가지고 있음
PID 1
- init 프로세스(커널이 생성)
- 시그널 처리
- 좀비, 고아 프로세스 처리
- 죽으면 시스템 패닉(reboot)
운영체제는 커널 레이어와 유저 레이어로 나뉘어집니다. 커널 레이어에는 커널 프로세스가 있는데 이건 PID가 0번인 프로세스입니다. 그리고 그 커널이 만든 PID 1번 프로세스가 유저 프로세스에서 최상위 프로세스입니다. 나머지 프로세스들은 1번 프로세스들의 자식 프로세스입니다.
다른 네임스페이스들은 unshare(격리)를 할 때 바로 네임스페이스를 격리를 하고 해당 프로세스가 그 네임스페이스에 속하게 됩니다.
근데 PID 네임스페이스 같은 경우는 unshare를 할 때 fork를 하게 됩니다. fork를 해서 실행하고자 하는 프로세스(커맨드)를 해당 PID 네임스페이스의 1번으로 만듭니다. 즉, unshare하는 프로세스에 자식 프로세스로 fork해서 만들게되는 특징이 있습니다. 이렇게 만들어지는 것이 컨테이너의 PID 1번입니다.
컨테이너 PID 1
- unshare 할 때 fork 하여 자식 PID 네임스페이스의 PID 1로 실행
- 시그널 처리
- 좀비, 고아 프로세스 처리
- 죽으면 컨테이너 종료
- 컨테이너의 라이프 사이클을 책임지고 있음
unshare -fp --mount-proc /bin/sh
# f: fork
# p: pid
# --mount-proc: proc 파일시스템 마운트
/proc
- 메모리 기반의 가상파일시스템
- 커널이 만들고, 관리하는 시스템 정보 제공
- 시스템 모니터링과 분석에 활용
컨테이너 및 호스트에서 각각 프로세스 확인 명령어를 실행해서 비교해보겠습니다.
# 컨테이너
ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 09:16 pts/1 00:00:00 /bin/sh
root 3 1 0 09:18 pts/1 00:00:00 ps -ef
ps -ef | grep /bin/sh # 호스트
root 17110 17087 0 06:53 pts/0 00:00:00 unshare -fp --mount-proc /bin/sh
root 17111 17110 0 06:53 pts/0 00:00:00 /bin/sh
root 17795 17277 0 07:17 pts/3 00:00:00 grep --color=auto /bin/sh
호스트에서 unshare의 자식 프로세스로 /bin/sh인 것을 볼 수 있습니다. 네임스페이스를 조회해봅시다.
# 컨테이너
lsns -t pid -p 1
NS TYPE NPROCS PID USER COMMAND
4026532708 pid 2 1 root /bin/sh
# 호스트
lsns -t pid -p 17111
NS TYPE NPROCS PID USER COMMAND
4026532708 pid 1 17111 root /bin/sh
동일한 pid namespace를 가짐을 알 수 있습니다. 같은 프로세스입니다.
이번에는 kill 명령어로 컨테이너를 죽이겠습니다.
# 호스트
kill -SIGKILL 17111 # 컨테이너 1번 프로세스 /bin/sh 종료
# 격리공간
# Killed
PID 1번을 죽이니 컨테이너가 종료됩니다.
네트워크 네임스페이스
- 네트워크 스택을 격리
- 네트워크 가상화, 가상 인터페이스(장치) 사용
네트워크 인터페이스
- 여러 네트워크 네임스페이스에 걸쳐 있을 수 있음
- 다른 네트워크 네임스페이스로 이동할 수 있음
- veth, bridge, vxian, …
- 네트워크 네임스페이스가 삭제될 때 가상 인터페이스도 삭제됨, 물리 장치들은 기존 네임스페이스로 복원됨
1:1 통신 실습을 해봅시다. 먼저 네트워크 세팅을 하겠습니다.
ip link add veth0 type veth peer name veth1 # 양쪽 끝이 veth0, veth1인 랜선 만들기
ip link # 로 확인
ip netns add RED
ip netns add BLUE # RED, BLUE 네트워크 네임스페이스 만들기
ip link set veth0 netns RED
ip link set veth1 netns BLUE # RED, BLUE에 각각 veth0, veth1 연결
ip netns exec RED ip link set veth0 up
ip netns exec BLUE ip link set veth1 up # 각 장치 활성화
ip netns exec RED ip addr add 11.11.11.2/24 dev veth0
ip netns exec BLUE ip addr add 11.11.11.3/24 dev veth1 # IP 부여
네트워크 네임스페이스를 생성하면 /var/run/netns 경로 밑에 파일을 만들게 됩니다. 2개의 터미널로 통신 테스트를 해봅시다.
# 네트워크 네임스페이스에 접속
nsenter --net=/var/run/netns/RED
# 다른 터미널
nsenter --net=/var/run/netns/BLUE
# 각 터미널에서 ip 확인
ip a
ip route
# 각각 통신을 확인
ping 11.11.11.2
ping 11.11.11.3
# 네트워크 네임스페이스 삭제
ip netns del RED
ip netns del BLUE
ls /var/run/netns
ip netns
USER 네임스페이스
- UID/GID 넘버스페이스 격리
- 컨테이너의 루트권한 문제를 해결
- 부모-자식 네임스페이스의 중첩구조
- UID/GID Remap
일반 계정으로 전환해서 실습을 진행합니다.
docker run -it ubuntu /bin/sh
id
도커 컨테이너 안은 루트 유저입니다.
# 호스트
id
호스트는 일반 유저입니다. 프로세스 상태를 비교해봅시다.
# 호스트
ps -ef | grep "bin/sh"
ihp001 20382 1779 0 08:12 pts/2 00:00:00 docker run -it ubuntu /bin/sh
root 20484 20465 0 08:12 pts/0 00:00:00 /bin/sh
ihp001 20591 20359 0 08:15 pts/0 00:00:00 grep --color=auto bin/sh
호스트에서 /bin/sh이 실제로 root로 실행되고 있습니다. 컨테이너와 호스트의 유저 네임스페이스를 확인해야 진짜 루트인지 아닌지 확인할 수 있습니다.
# 컨테이너 및 호스트
readlink /proc/$$/ns/user
user:[4026531837]
컨테이너, 호스트의 유저아이디, 그룹아이디 넘버스페이스가 동일하다라는 의미 즉, 같은 계정입니다. 컨테이너의 루트가 실제 호스트의 유저임을 알 수 있습니다.
USER 네임스페이스 격리해보겠습니다.
unshare -U --map-root-user /bin/sh
user 아이디를 remap해서 user 네임스페이스를 생성하게 됩니다. 아이디를 확인해보겠습니다.
id # 호스트
uid=1000(ihp001) gid=1000(ihp001) groups=1000(ihp001),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),999(docker)
id # 컨테이너
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
프로세스 상태를 조회합니다.
# 호스트
ps -ef | grep "/bin/sh"
ihp001 21281 20359 0 08:31 pts/0 00:00:00 /bin/sh
ihp001 22911 1779 0 09:24 pts/2 00:00:00 grep --color=auto /bin/sh
도커의 경우와 다르게 이번에는 컨테이너의 프로세스가 일반 계정으로 떴습니다.
# 컨테이너
# ps -ef | grep "/bin/sh"
root 21281 20359 0 08:31 pts/0 00:00:00 /bin/sh
root 22909 21281 0 09:24 pts/0 00:00:00 grep /bin/sh
컨테이너 내부는 root입니다. 앞서 도커로 실행한 경우와 다릅니다. 도커는 호스트에서 컨테이너 프로세스가 root로 실행 됐었습니다.
# 호스트
readlink /proc/$$/ns/user
user:[4026531837]
# 컨테이너
readlink /proc/$$/ns/user
user:[4026532586]
도커에서는 inode가 동일 했었는데 지금은 inode 값이 서로 다릅니다.
USER 네임스페이스 결론
- 컨테이너 안에서만 root
- USER 네임스페이스 간 UID/GID Remap
도커의 USER 네임스페이스 지원
- 기본 설정이 아님. 따로 설정해야함
- 호스트 UID/GID Remap
컨테이너 자원
Cgroups
- Cgroups, Control groups
- 컨테이너 별로 자원을 분배하고 limit 내에서 운용
- 하나 또는 복수의 장치를 묶어서 그룹화
- 프로세스가 사용하는 리소스 통제
Cgroup 파일시스템
- 자원 할당과 제어를 파일시스템으로 제공
- Cgroup 네임스페이스로 격리
tree -L 1 /sys/fs/cgroup/
cgroups 실습
# 필요 패키지 설치
sudo su -Es
apt install -y cgroup-tools
apt install -y stress
cgroup을 생성합니다.
cgcreate -a root -g cpu:mycgroup
tree /sys/fs/cgroup/mycgroup/
cpu 사용률을 제어해보겠습니다. 제 리눅스 환경은 cgroup v2를 사용하고 있습니다. cgroup v2에서는 cpu.cfs_quota_us, cpu.cfs_period_us가 cpu.max로 대체되었다고 합니다. cgroup v1이랑 디렉토리 구조도 다릅니다.
참고: https://lore.kernel.org/lkml/20160812221742.GA24736@cmpxchg.org/T/
cpu.max 값을 수정하고, cgroup으로 stress를 실행합니다.
cgset -r cpu.max=30000 mycgroup # cpu 사용률 30% 제한
cgexec -g cpu:mycgroup stress -c 1 # 해당 cgroup을 사용해서 stress를 실행
다른 터미널에서 top로 cpu 상태를 조회합니다.
top - 09:58:29 up 4:32, 3 users, load average: 0.04, 0.07, 0.08
Tasks: 237 total, 2 running, 235 sleeping, 0 stopped, 0 zombie
%Cpu(s): 7.9 us, 0.3 sy, 0.0 ni, 91.7 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
MiB Mem : 7915.9 total, 6009.0 free, 412.4 used, 1494.5 buff/cache
MiB Swap: 4096.0 total, 4096.0 free, 0.0 used. 7225.6 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
13364 root 20 0 3704 108 0 R 29.9 0.0 0:09.86 stress
30%가 넘지않게 커널이 쓰로틀링을 걸게 됩니다.
- Cgroup 파일시스템으로 리소스 관리
- 제어그룹(mygroup) 생성
- 제어그룹 리소스 설정
- 제어그룹 프로세스 할당
도커 없이 컨테이너 만들기
지금까지 실습한 내용들을 바탕으로 도커 없이 컨테이너를 만들어봅시다.
Goal
- RED, BLUE 컨테이너를 만듭니다.
- 2개의 컨테이너는 cgroup으로 자원이 할당됩니다.
- 네트워크 연결로 서로 통신이 가능합니다.
이미지 준비
위 실습에서 사용한 myroot, tools 이미지에 명령어를 추가해서 Lower Layer로 사용합니다. myroot에는 ps
, mount
, mkdir
명령어를 추가하고, tools에는 ping
, stress
, hostname
, umount
명령어를 추가하겠습니다.
- ping: 컨테이너 통신 테스트
- stress: 컨테이너 부하 테스트
- hostname: 호스트네임 변경
- umount: put_old 제거
myroot 이미지
# ps
cp /usr/bin/ps /tmp/myroot/usr/bin/
cp /lib/x86_64-linux-gnu/libprocps.so.8 \
/lib/x86_64-linux-gnu/libc.so.6 \
/lib/x86_64-linux-gnu/libsystemd.so.0 \
/lib/x86_64-linux-gnu/liblzma.so.5 \
/lib/x86_64-linux-gnu/libzstd.so.1 \
/lib/x86_64-linux-gnu/liblz4.so.1 \
/lib/x86_64-linux-gnu/libcap.so.2 \
/lib/x86_64-linux-gnu/libgcrypt.so.20 \
/lib/x86_64-linux-gnu/libgpg-error.so.0 \
/tmp/myroot/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/
# mount
cp /usr/bin/mount /tmp/myroot/usr/bin/
cp /lib/x86_64-linux-gnu/libmount.so.1 \
/lib/x86_64-linux-gnu/libselinux.so.1 \
/lib/x86_64-linux-gnu/libc.so.6 \
/lib/x86_64-linux-gnu/libblkid.so.1 \
/lib/x86_64-linux-gnu/libpcre2-8.so.0 \
/tmp/myroot/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/
# mkdir
cp /usr/bin/mkdir /tmp/myroot/usr/bin/
cp /lib/x86_64-linux-gnu/libselinux.so.1 \
/lib/x86_64-linux-gnu/libc.so.6 \
/lib/x86_64-linux-gnu/libpcre2-8.so.0 \
/tmp/myroot/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 \
/tmp/myroot/lib64/
tools 이미지
# ping
cp /usr/bin/ping /tmp/tools/usr/bin/
cp /lib/x86_64-linux-gnu/libcap.so.2 \
/lib/x86_64-linux-gnu/libidn2.so.0 \
/lib/x86_64-linux-gnu/libc.so.6 \
/lib/x86_64-linux-gnu/libunistring.so.2 \
/tmp/tools/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 \
/tmp/tools/lib64/
# stress
cp /usr/bin/stress /tmp/tools/usr/bin/
cp /lib/x86_64-linux-gnu/libm.so.6 \
/lib/x86_64-linux-gnu/libc.so.6 \
/tmp/tools/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 \
/tmp/tools/lib64/
# hostname
cp /usr/bin/hostname /tmp/tools/usr/bin/
cp /lib/x86_64-linux-gnu/libc.so.6 \
/tmp/tools/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 \
/tmp/tools/lib64/
cp /usr/bin/umount /tmp/tools/usr/bin/
cp /lib/x86_64-linux-gnu/libmount.so.1 \
/lib/x86_64-linux-gnu/libc.so.6 \
/lib/x86_64-linux-gnu/libblkid.so.1 \
/lib/x86_64-linux-gnu/libselinux.so.1 \
/lib/x86_64-linux-gnu/libpcre2-8.so.0 \
/tmp/tools/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 \
/tmp/tools/lib64/
네트워크 네임스페이스
위의 네트워크 통신 실습에서 진행했던 네트워크 네임스페이스 설정을 그대로 RED, BLUE 네트워크 네임스페이스를 만듭니다.
ip netns add RED
ip netns add BLUE
# veth0, veth1를 연결
# RED, BLUE에 veth0, veth1 연결
ip link add veth0 netns RED type veth peer name veth1 netns BLUE
ip netns exec RED ip addr add dev veth0 11.11.11.2/24
ip netns exec RED ip link set veth0 up
ip netns exec BLUE ip addr add dev veth1 11.11.11.3/24
ip netns exec BLUE ip link set veth1 up
ip netns exec RED ip l
ip netns exec BLUE ip l
RED Cgroups 생성
위의 실습이랑 원리가 같습니다.
cgcreate -a root -g cpu:red
cgcreate -a root -g memory:red
ls /sys/fs/cgroup/red/
RED Cgroups 설정
cpu 40%, 200MB 메모리를 설정합니다.
# 파일에 직접 쓰기
echo 40000 > /sys/fs/cgroup/red/cpu.max
echo 209715200 > /sys/fs/cgroup/red/memory.max
RED 격리
RED 컨테이너를 띄우겠습니다.
# mount, uts, itc, fork한 pid, RED 네트워크 네임스페이스
unshare -m -u -i -fp nsenter --net=/var/run/netns/RED /bin/sh
현재 cgroup 제한은 되지 않은 상태의 컨테이너입니다.
RED Cgroups 할당
unshare할 때 pid 네임스페이스를 isolation해서 컨테이너 안의 프로세스가 1번이기 때문에 RED cgroup 프로세스에 1번을 넣어주면 cgroup에 할당이 되게 됩니다.
echo "1" > /sys/fs/cgroup/red/cgroup.procs
오버레이 마운트
파일 시스템을 만들어봅시다. 위의 실습과 비슷하게 진행됩니다.
# 컨테이너
mkdir /redfs
mkdir /redfs/container
mkdir /redfs/work
mkdir /redfs/merge
tree /redfs
/redfs
├── container
├── merge
└── work
오버레이 마운트를 합시다.
mount -t overlay overlay -o lowerdir=/tmp/tools:/tmp/myroot,upperdir=/redfs/container,workdir=/redfs/work /redfs/merge
tree /redfs/merge
redfs/merge 통합뷰 밑으로 2개의 이미지에 있던 명령어들이 잘 merge되어 들어가 있음을 알 수 있습니다.
pivot_root
새로운 루트 파일 시스템을 만들기 위해 pivot_root를 해봅시다. 새로운 루트 파일 시스템의 위치는 오버레이 마운트를 통해서 만든 /redfs/merge입니다. put_old 디렉토리만 만들면 됩니다. pivot_root를 할 때는 꼭 마운트 포인트로 들어가서 해야합니다.
mkdir -p /redfs/merge/put_old
cd /redfs/merge
pivot_root . put_old
cd /
ls /
ls put_old
pivot_root를 실행을 위해선 만든 put_old는 제거해야 합니다. put_old에는 현재 컨테이너가 속해 있는 호스트의 루트 파일 시스템이 put_old 밑에 부착되어 있기 때문입니다.
이 경로를 제거할려면 put_old를 umout해야 합니다. umount할려면 proc 파일 시스템을 마운트 해야지 umount를 사용할 수 있습니다.
mkdir /proc
mount -t proc proc /proc
umount -l put_old
rm -rf put_old
깔끔하게 호스트의 루트 파일 시스템하고 끊겨지게 됩니다.
ps -ef
hostname
RED 컨테이너가 다 만들어졌습니다. 모든 네임스페이스 격리, 자원 격리까지 해서 완전한 컨테이너를 만들었습니다. 위와 같이 BLUE 컨테이너도 작업합니다.
RED, BLUE 컨테이너에 ping
명령어를 통해 통신이 잘 되는 것을 확인할 수 있습니다.
RED CPU 리소스 확인
RED 컨테이너에서 stress -c 1
을 실행합니다. 호스트에서 top
명령어로 cpu 확인시에 cgroup에서 제한을 설정한 40% 밑으로 쓰로틀링이 걸림을 확인할 수 있습니다.
top - 11:18:35 up 5:51, 7 users, load average: 0.27, 0.17, 0.10
Tasks: 255 total, 2 running, 247 sleeping, 5 stopped, 1 zombie
%Cpu(s): 10.4 us, 0.2 sy, 0.0 ni, 89.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7915.9 total, 4215.7 free, 437.0 used, 3263.2 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 7174.9 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
61703 root 20 0 3704 108 0 R 40.0 0.0 0:02.04 stress
RED 메모리 리소스 확인
cgroup을 통해 설정한 메모리 200MB 메모리 제한을 확인해보겠습니다. stress
명령어로 200MB 근처까지 메모리를 주면서 테스트합니다.
stress --vm 1 --vm-bytes 197M
stress --vm 1 --vm-bytes 198M
stress --vm 1 --vm-bytes 199M
stress: info: [35] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [35] (416) <-- worker 36 got signal 9
stress: WARN: [35] (418) now reaping child worker processes
stress: FAIL: [35] (452) failed run completed in 0s
호스트에서 dmesg
로 커널 메세지를 확인하면 cgroup 제한을 초과했기 때문에 해당 프로세스를 죽였음을 알 수 있습니다.
[20144.892638] Memory cgroup out of memory: Killed process 61372 (stress) total-vm:207484kB, anon-rss:201756kB, file-rss:204kB, shmem-rss:0kB, UID:0 pgtables:444kB oom_score_adj:0