티스토리 뷰

728x90
반응형

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(통합뷰)로 모든 것이 겹쳐진 형태의 레이어가 있게됩니다(셀로판지에 색이 투과되어 보이는 것에 비유).

container_image_layer

오버레이 마운트 실습

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
728x90
반응형
댓글
반응형
250x250
글 보관함
최근에 달린 댓글
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Total
Today
Yesterday
링크