가시다님의 주관하시는 쿠버네티스 네트워크 스터디(KANS,Kubernetes Advanced Network Study) 의 내용 정리입니다.
컨테이너 격리
컨테이너를 사용하기에 앞서, 컨테이너의 핵심 기반 기술인 리눅스 프로세스 격리 기술의 변천사에 대해 알아봅니다.
현재는 쿼리파이의 CTO이시고, 전 카카오에서 근무하셨던 삼영(Sam.0)님께서 컨테이너 격리에 대해 실습과 내용을 아주 잘 정리해주셔서, 해당 내용을 참고하여 작성하였습니다.
- https://speakerdeck.com/kakao/ige-dwaeyo-dokeo-eobsi-keonteineo-mandeulgi?slide=102
- https://www.youtube.com/watch?v=mSD88FuST80
1. Chroot + 탈옥
chroot 명령어는 change root의 약자로, 지정한 디렉터리를 해당 프로세스의 새로운 루트 디렉토리로 변경하는 명령어 입니다.
일반적으로 `chroot`는 다음과 같이 사용됩니다.
sudo chroot /path/to/new/root /bin/bash
/path/to/new/root 디렉터리를 루트 디렉터리로 설정하고, 그안에서 새로운 bash 쉘을 실행합니다. 여기부터 실행되는 모든 명령어는 해당 디렉토리를 루트로 인식합니다. 이를 통해 기본적인 격리된 환경을 실행할 수 있습니다.
먼저, chroot 명령어로 생성한 디렉토리를 루트 디렉토리로 변경하여 /bin/sh을 진입합니다.
sudo su -
whoami
#
cd /tmp
mkdir myroot
# chroot 사용법 : [옵션] NEWROOT [커맨드]
chroot myroot /bin/sh
chroot: failed to run command ‘/bin/sh’: No such file or directory
해당 디렉터리에는 /bin/sh 파일이 없기 때문에 해당 명령어를 사용할 수 없습니다.
ldd 명령어를 이용해서 /bin/sh 명령어에 의존된 파일들을 모두 포함하여 복사해 주면 진입이 잘 동작합니다.
# sh 명령어 복사
which sh
ldd /bin/sh
# 바이러리 파일과 라이브러리 파일 복사
mkdir -p myroot/bin
cp /usr/bin/sh myroot/bin/
mkdir -p myroot/{lib64,lib/x86_64-linux-gnu}
tree myroot
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/
# 재진입
chroot myroot /bin/sh
chroot에서 아래 명령어들도 사용할 수 있도록 합니다.
- ls
- mount
- ps : 해당 경로에 /proc 파일이 존재하여야 사용 가능합니다.
# ls 명령어 복사
which ls
ldd /usr/bin/ls
#
cp /usr/bin/ls myroot/bin/
mkdir -p myroot/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
# copy ps
ldd /usr/bin/ps;
cp /usr/bin/ps /tmp/myroot/bin/;
cp /lib/x86_64-linux-gnu/{libprocps.so.8,libc.so.6,libsystemd.so.0,liblzma.so.5,libgcrypt.so.20,libgpg-error.so.0,libzstd.so.1,libcap.so.2} /tmp/myroot/lib/x86_64-linux-gnu/;
mkdir -p /tmp/myroot/usr/lib/x86_64-linux-gnu;
cp /usr/lib/x86_64-linux-gnu/liblz4.so.1 /tmp/myroot/usr/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/;
# copy mount
ldd /usr/bin/mount;
cp /usr/bin/mount /tmp/myroot/bin/;
cp /lib/x86_64-linux-gnu/{libmount.so.1,libc.so.6,libblkid.so.1,libselinux.so.1,libpcre2-8.so.0} /tmp/myroot/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/;
# copy mkdir
ldd /usr/bin/mkdir;
cp /usr/bin/mkdir /tmp/myroot/bin/;
cp /lib/x86_64-linux-gnu/{libselinux.so.1,libc.so.6,libpcre2-8.so.0} /tmp/myroot/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/myroot/lib64/;
chroot myroot /bin/sh
---------------------
# 왜 ps가 안될까요?
ps
Error, do this: mount -t proc proc /proc
#
mount -t proc proc /proc
mount: /proc: mount point does not exist.
#
mkdir /proc
mount -t proc proc /proc
mount -t proc
ps
PID TTY TIME CMD
2418 pts/1 00:00:00 sudo
2419 pts/1 00:00:00 bash
2664 pts/1 00:00:00 ps
루트디렉터리 확인
루트 디렉터리를 확인해보면 기존 루트 디렉토리와 파일이 다르며, 루트 디렉토리가 격리됨을 확인할 수 있습니다.
또한, cd 명령어를 통해서는 호스트의 루트를 접근하기 위해 탈출이 가능하지 않습니다.
## 탈출 가능한지 시도
cd ../../../
ls /
bin lib lib64
# [터미널2]
# chroot 실행한 터미널1과 호스트 디렉터리 비교
ls /
bin boot dev etc home lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv sys tmp usr var
실습 : 컨테이너 이미지 chroot 해보기
새로운 루트 디렉토리를 생성하고 nginx 컨테이너 압축 이미지를 받아서 압축을 풀어줍니다.
mkdir nginx-root
tree nginx-root
# nginx 컨테이너 압축 이미지를 받아서 압축 풀기
docker export $(docker create nginx) | tar -C nginx-root -xvf -;
docker images
ls nginx-root/
bin dev docker-entrypoint.sh home lib64 mnt proc run srv tmp var
boot docker-entrypoint.d etc lib media opt root sbin sys usr
chroot nginx-root /bin/sh
---------------------
ls /
chroot를 실행하면 마치 컨테이너 쉘 안에 진입한 것과 똑같은 환경을 보여줍니다.
nginx -g "daemon off;"
# 터미널1에서 아래 확인 후 종료
CTRL +C # nginx 실행 종료
exit
---------------------
# [터미널2]
## 루트 디렉터리 비교 및 확인
ls /
ps -ef |grep nginx
curl localhost:80
sudo ss -tnlp
따라서, Docker나 Kubernetes 컨테이너에서 사용되는 이미지는 단지, 실행되는 프로세스의 동작에 필요한 관련 모든 파일들이 패키징 된 것임을 알 수 있습니다.
Chroot의 한계
chroot 명령어를 이용해서 격리된 루트 디렉터리에서는 cd 명령어로 탈출이 되지 않아 문제가 되지 않지만, 탈옥 코드를 이용하면 탈출이 가능하다는 치명적인 문제가 발생합니다.
chroot 탈옥하기
탈옥 코드를 작성합니다.
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);
}
탈옥 코드를 컴파일하고 new-root에 복사합니다.
# 컴파일
gcc -o myroot/escape_chroot escape_chroot.c
tree -L 1 myroot
file myroot/escape_chroot
# chroot 실행
chroot myroot /bin/sh
이제 컴파일된 실행 파일을 가지고 탈출을 시도해 보면, 실제 루트 디렉터리로 접근이 가능해집니다.
# 탈출!
./escape_chroot
ls /
bin boot dev etc home lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv sys tmp usr var
# [터미널2]
## 루트 디렉터리 비교 및 확인
ls /
bin boot dev etc home lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv sys tmp usr var
2. Mount Namespace + Pivot_root
chroot의 문제점을 차단하기 위해서, pivot_root와 mount namespace라는 기술을 활용하여 격리를 강화합니다.
실제로 쿠버네티스는 chroot가 아닌 pviot_root와 namespace 기술을 활용하여 컨테이너를 격리한다고 합니다.
Pivot_root
chroot는 프로세스의 루트 디렉터리만 변경하지만, 실제 커널 루트 파일 시스템에는 영향을 미치지 않지만,
pviot_root는 chroot 명령보다 낮은 레벨에서 작동하며, 시스템 전체의 루트 파일 시스템을 변경합니다. 이로 인해서 모든 프로세스가 새로운 루트 파일 시스템을 루트로 인식합니다.
Mount Namespace
mount namespace는 리눅스 커널에서 제공하는 네임스페이스 기능의 하나로 프로세스 그룹의 파일시스템의 마운트 포인트를 격리합니다.
실습 : pivot_root & mount namespace를 활용한 격리
호스트와 격리 환경 비교를 위해 터미널 2개로 실습을 진행합니다.
- [ 터미널 1 ] : 격리 환경
- [ 터미널 2 ] : 호스트
먼저, unshare 명령어를 통해 mount namespace를 만들고 Shell에 접속합니다.
- pivot_root : 루트 파일시스템 피봇
- unshare : 새로운 네임스페이스를 만들고 나서 프로그램을 실행
host와 격리된 네임스페이스에서 마운트 정보를 비교해 보면 다른 것을 확인할 수 있습니다.
# [터미널1]
unshare --mount /bin/sh
-----------------------
# 아래 터미널2 호스트 df -h 비교 : mount unshare 시 부모 프로세스의 마운트 정보를 복사해서 자식 네임스페이스를 생성하여 처음은 동일
df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 29G 2.9G 27G 10% /
tmpfs 475M 0 475M 0% /dev/shm
tmpfs 190M 888K 189M 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 95M 4.0K 95M 1% /run/user/1000
/dev/xvda15 105M 6.1M 99M 6% /boot/efi
overlay 29G 2.9G 27G 10% /var/lib/docker/overlay2/49e6b9e3b3157e98c03cac8a907659f8d1dc6e5757d14b895e33e3fab656db1f/merged
-----------------------
# [터미널2]
df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 29G 2.9G 27G 10% /
tmpfs 475M 0 475M 0% /dev/shm
tmpfs 190M 888K 189M 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
/dev/xvda15 105M 6.1M 99M 6% /boot/efi
tmpfs 95M 4.0K 95M 1% /run/user/1000
overlay 29G 2.9G 27G 10% /var/lib/docker/overlay2/49e6b9e3b3157e98c03cac8a907659f8d1dc6e5757d14b895e33e3fab656db1f/merged
새로운 new_root 디렉터리를 생성하고 이전 실습에서 사용한 myroot 폴더를 마운트 하여 파일을 복사합니다.
이제, 호스트에서는 new_root 디렉터리가 보이지 않는 것을 확인할 수 있습니다.
# [터미널1]
-----------------------
#
mkdir new_root
mount -t tmpfs none new_root
ls -l
tree new_root
## 마운트 정보 비교 : 마운트 네임스페이스를 unshare
df -h
mount | grep new_root
findmnt -A
## 파일 복사 후 터미널2 호스트와 비교
cp -r myroot/* new_root/
tree new_root/
-----------------------
# [터미널2]
cd /tmp
ls -l
tree new_root
df -h
mount | grep new_root
findmnt -A
## 안보이는 이유 : 마운트 네임스페이스를 unshare 된 상태
tree new_root/
이제, mount namespace를 통해 격리는 확인하였으니, pivot_root를 실행하여 탈옥을 잘 방어하는지 확인해 봅니다.
pivot_root 명령어로 new_root 디렉터리를 신규 루트로 지정합니다.
이전 실습에서 사용한 탈옥 코드를 실행해도 탈옥이 되지 않고 신규 루트 디렉터리로 잘 격리됨을 확인할 수 있습니다.
# 터미널1
-----------------------
mkdir new_root/put_old
## pivot_root 실행
cd new_root # pivot_root 는 실행 시, 변경될 root 파일시스템 경로로 진입
pivot_root . put_old # [신규 루트] [기존 루트]
##
cd /
ls / # 터미널2와 비교
ls put_old
# 터미널2
ls /
탈옥 시도
# 터미널1
-----------------------
./escape_chroot
cd ../../../
ls /
exit
exit
-----------------------
3. Namespace : 격리
pviot_root의 한계(보완)
격리된 공간(컨테이너)에서 호스트의 다른 프로세스, 포트 공유하기 때문에, 호스트의 영향을 끼침
네임스페이스란?
프로세스가 서로 격리된 환경에서 실행될 수 있도록 해주는 리눅스 커널의 기능입니다. 네임스페이스를 사용하면 프로세스가 시스템 리소스를 독립적으로 사용할 수 있어 가상화 및 컨테이너 기술의 기반이 됩니다.
unshare 명령어로 손쉽게 사용할 수 있습니다.
1. 마운트 네임스페이스 : 2002년 , 마운트 포인트 격리, 최초의 네임스페이스
# PID 1과 현재 Shell 속한 프로세스의 MNT NS 정보 확인
lsns -t mnt -p 1
lsns -t mnt -p $$
# [터미널1] /tmp 디렉터리
# unshare -m [명령어] : -m 옵션을 주면 [명령어]를 mount namespace 를 isolation 하여 실행합니다
unshare -m # *[명령어]를 지정하지 않으면 환경변수 $SHELL 실행
-----------------------------------
# NPROCS 값과 PID 값의 의미 확인
lsns -p $$
# PID 1과 비교
lsns -p 1
# 빠져나오기
NPROCS는 네임스페이스에서 실행 중인 프로세스의 개수를 의미하고, 격리된 네임스페이스마다 고유한 번호를 가지고 있습니다.
2. UTC 네임스페이스 : 2006년, Unix Time Sharing (여러 사용자 작업 환경 제공하고자 서버 시분할 나눠 쓰기), 호스트명, 도메인명 격리
# unshare -u [명령어]
# -u 옵션을 주면 [명령어]를 UTS namespace 를 isolation 하여 실행
# [터미널1] /tmp 디렉터리
unshare -u
-----------------------------------
lsns -p $$
lsns -p 1
## 기본은 부모 네임스페스의 호스트 네임을 상속
hostname
## 호스트 네임 변경
hostname KANS
## 아래 터미널2에서 hostname 비교
hostname
exit
-----------------------------------
# [터미널2] /tmp 디렉터리
hostname
3. IPC 네임스페이스 : 2006년, Inter-Process Communication 격리, 프로세스 간 통신 자원 분리 관리 - Shared Memory, Pipe, Message Queue 등
4. PID 네임스페이스 : 2008년, Process ID 격리
- 부모와 자식 네임스페이스 중첩 구조로 부모 네임스페이스에서는 자식 네임스페이스를 볼 수 있음
- 자식 네임스페이스는 parent tree의 id와 subtree의 id 두 개를 가짐
unshare 명령어로 PID namespace를 격리하여 실행합니다. 격리된 PID의 네임스페이스의 번호를 비교합니다.
- -p : PID namespace
- -f : child를 fork 하여 새로운 네임스페이스로 격리
- --mount-proc : namespace 안에서 ps 명령어 사용을 위해 /proc를 mount
# [터미널1] /proc 파일시스템 마운트
echo $$
unshare -fp --mount-proc /bin/sh
--------------------------------
# 터미널2 호스트와 비교
echo $$
ps -ef
ps aux
# 내부에서 PID NS 확인 : 아래 터미널2에서 lsns -t pid -p <위 출력된 PID>와 비교
lsns -t pid -p 1
--------------------------------
# [터미널2]
ps -ef
ps aux
ps aux | grep '/bin/sh'
root 5887 0.0 0.1 5772 1792 pts/1 S 09:32 0:00 unshare -fp --mount-proc /bin/sh
root 5888 0.0 0.1 2892 1664 pts/1 S+ 09:32 0:00 /bin/sh
root 5892 0.0 0.2 6480 2432 pts/3 S+ 09:32 0:00 grep --color=auto /bin/sh
# 터미널1 PID NS와 비교
lsns -t pid -p <위 출력된 PID>
lsns -t pid -p 5888
부모의 네임스페이스에서는 자식 네임스페이스를 바라볼 수 있기 때문에 호스트에서 컨테이너 프로세스를 종료할 수 있습니다.
# [터미널1]
--------------------------------
# fork
sleep 10000
# 아래 종료로 자동으로 sleep 가 exit 됨
echo $$
# [터미널2]
ps aux | grep sleep
## 호스트에서 sleep 종료 시켜보기 : 어떻게 되는가?
kill -l
kill -SIGKILL $(pgrep sleep)
5. User 네임스페이스 : 2012년, 2012년, UID/GID 넘버스페이스 격리(Remap_, 컨테이너의 루트권한 문제를 해결함, 부모-자식 네임스페이스의 중첩 구조
도커 컨테이너 실행 시, 같은 User 네임스페이스를 사용하는 것을 확인할 수 있습니다. 호스트의 User를 그대로 사용한다는 의미입니다. 만약 컨테이너가 탈취당한다면, root 계정 권한 실행이 가능하기 때문에 보안상 매우 취약할 것입니다.
# 터미널1
docker run -it ubuntu /bin/sh
-----------------------------
# 아래 터미널2와 비교
whoami
id
# 아래 터미널2와 비교
ps -ef
# User 네임스페이스는 도커 컨테이너 실행 시, 호스트 User 를 그대로 사용
readlink /proc/$$/ns/user
lsns -p $$ -t user
NS TYPE NPROCS PID USER COMMAND
4026531837 user 2 1 root /bin/sh
# 터미널2
whoami
id
## root 로 실행됨
ps -ef |grep "/bin/sh"
root 6034 5587 0 09:40 pts/1 00:00:00 docker run -it ubuntu /bin/sh
root 6090 6068 0 09:40 pts/0 00:00:00 /bin/sh
root 6123 5671 0 09:43 pts/3 00:00:00 grep --color=auto /bin/sh
lsns -p $$ -t user
NS TYPE NPROCS PID USER COMMAND
4026531837 user 113 1 root /sbin/init
--map-root-user 옵션을 사용하여 User 네임스페이스를 격리하여 실행하면 호스트와 컨테이너에서도 격리되어 실행됨을 확인할 수 있습니다. docker v1.10 이상부터는 User Namespace 기능을 지원한다고 합니다.
# 터미널1
unshare -U --map-root-user /bin/sh
-----------------------------
# 내부에서는 여전히 root로 보임
whoami
id
# User 네임스페이스를 호스터(터미널2)와 비교
readlink /proc/$$/ns/user
lsns -p $$
# 아래 동작 확인 후 종료
exit
-----------------------------
# 터미널2
readlink /proc/$$/ns/user
lsns -p $$
## ubuntu 로 실행됨
root@MyServer:~# ps -ef | grep '/bin/sh'
root 6168 5587 0 09:49 pts/1 00:00:00 /bin/sh
root 6300 5671 0 09:50 pts/3 00:00:00 grep --color=auto /bin/sh
4. Cgroups
Cgroups는 ControlGroups의 약자로 각 프로세스의 리소스 자원(Cpu, memory) 등을 제어합니다. Cgroup은 커널을 통해 프로세스마다 격리된 리소스를 제어하고 관리합니다.
/sys/fs/cgroup/ 아래 경로에 파일들로 리소스들을 관리하고 있으며, Cgroup 네임스페이스로 격리할 수 있습니다.
# OS 정보 확인
name -a
Linux MyServer 6.5.0-1024-aws #24~22.04.1-Ubuntu SMP Thu Jul 18 10:43:12 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
# cgroup2 마운트 정보 확인
mount -t cgroup
mount -t cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
#
findmnt -t cgroup2
TARGET SOURCE FSTYPE OPTIONS
/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot
# cgroupv1 만 지원 시, cgroup2 출력되지 않음
grep cgroup /proc/filesystems
nodev cgroup
nodev cgroup2
stat -fc %T /sys/fs/cgroup/
cgroup2fs
# cgroup 목록 확인
ls /sys/fs/cgroup
cat /sys/fs/cgroup/cgroup.controllers
#
tree /sys/fs/cgroup/ -L 1
tree /sys/fs/cgroup/ -L 2
ubuntu 22.04에서부터는 cgroup2가 기본 버전인데, 환경에 따라서 디렉터리 하위의 파일들이 조금씩 다를 수 있습니다.
실습 : Cgroup 파일로 리소스 제어하기
부하를 발생시키고, Cgroup 파일들을 새로 추가하여 실제로 리소스의 용량을 늘어나는지 확인합니다.
부하 테스트를 위해 stress 툴을 설치합니다.
apt install cgroup-tools stress -y
먼저, cpu에 부하를 발생시키고, htop을 통해서 확인합니다. 1 core의 만큼 부하를 주었더니, CPU의 사용량이 100%까지 치솟는 것을 확인할 수 있습니다.
이제, Cgroup 파일을 새로 추가하여 CPU의 최대 사용량 설정을 변경합니다. /sys/fs/cgroup 아래에 서브 디렉터리를 생성하면 cgroup 파일들이 자동생성됩니다.
# 디렉터리 이동
cd /sys/fs/cgroup
# 서브 디렉터리 생성 후 확인 확인
mkdir test_cgroup_parent && cd test_cgroup_parent
ls
cgroup.controllers cgroup.threads cpuset.mems.effective io.weight memory.peak memory.zswap.max
cgroup.events cgroup.type cpu.stat memory.current memory.pressure pids.current
cgroup.freeze cpu.idle cpu.uclamp.max memory.events memory.reclaim pids.events
cgroup.kill cpu.max cpu.uclamp.min memory.events.local memory.stat pids.max
cgroup.max.depth cpu.max.burst cpu.weight memory.high memory.swap.current pids.peak
cgroup.max.descendants cpu.pressure cpu.weight.nice memory.low memory.swap.events
cgroup.pressure cpuset.cpus io.max memory.max memory.swap.high
cgroup.procs cpuset.cpus.effective io.pressure memory.min memory.swap.max
cgroup.stat cpuset.cpus.partition io.prio.class memory.numa_stat memory.swap.peak
cgroup.subtree_control cpuset.mems io.stat memory.oom.group memory.zswap.current
# cpu를 subtree이 추가하여 컨트롤 할 수 있도록 설정 : +/-(추가/삭제)
cat cgroup.subtree_control
echo "+cpu" >> /sys/fs/cgroup/test_cgroup_parent/cgroup.subtree_control
# cpu.max 제한 설정 : 첫 번쨰 값은 허용된 시간(마이크로초) 두 번째 값은 총 기간 길이 > 1/10 실행 설정
echo 100000 1000000 > /sys/fs/cgroup/test_cgroup_parent/cpu.max
# test용 자식 디렉토리를 생성하고, pid를 추가하여 제한을 걸어
mkdir test_cgroup_child && cd test_cgroup_child
echo $$ > /sys/fs/cgroup/test_cgroup_parent/test_cgroup_child/cgroup.procs
cat /sys/fs/cgroup/test_cgroup_parent/test_cgroup_child/cgroup.procs
cat /proc/$$/cgroup
이제 다시 동일하게 부하를 발생시키면 사용 중인 CPU 사용량이 달라짐을 확인할 수 있습니다. ( 100% -> 12.6%)
쿠버네티스 환경 cgroup 파일 탐색해 보기
간단하게 컨테이너에서 사용하는 Cgroup 파일을 탐색해 봅니다.
1. nginx 파드 생성
테스트 nginx 파드를 생성합니다.
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
namespace: default
spec:
containers:
- name: nginx-container
image: nginx:latest
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
# 파드 확인
k get pod -n default -o wide| grep nginx
nginx-pod 1/1 Running 0 7m2s 10.233.113.200 worker4 <none> <none>
2. 파드의 컨테이너 정보 확인
파드가 동작하는 워커노드에 접속하여 컨테이너 id를 확인합니다.
ctr -n k8s.io containers list | grep <container_id>
3. cgroup 파일 확인
/sys/fs/cgroup 경로 아래에 리소스 + 컨테이너 ID 패턴 이름의 리소스 파일들이 존재하는 것을 확인할 수 있습니다.
실행 중인 컨테이너의 PID를 통해서도 cgroup 정보를 확인할 수 있습니다.
4. 결론
쿠버네티스에서 런타임 환경에서도 컨테이너의 리소스를 손쉽게 제어할 수 있는 것은 결국, 이러한 Cgroup 기술을 이용해서 리소스를 제어한다는 것을 알 수 있었습니다.
또한, 이전에는 잘 알지 못하였지만, 이제는 쿠버네티스의 설정 파일에서 cgroup이 무엇을 의미하는지도 알 수 있었습니다.
Overlay filesystem
이전 실습에서 만든 myroot와 같이, 격리를 위해 패키징하여 컨테이너 이미지로 사용하게 됩니다. 하지만 nginx, apache 등 애플리케이션이 늘어날 때마다 중복되는 파일이 많아지고 불필요한 비용과 중복 문제를 야기합니다. 따라서 Overay 파일 시스템을 이용하면 이미지 중복 문제를 해결할 수 있습니다.
- Lower Layer (읽기 전용 레이어): 이 레이어는 변경할 수 없는 읽기 전용 파일 시스템입니다. 여러 이미지에서 공유되는 공통 파일들이 여기에 저장됩니다. 이로 인해 동일한 파일을 여러 번 저장할 필요가 없어 공간을 절약할 수 있습니다.
- Upper Layer (읽기-쓰기 레이어): 이 레이어는 변경 가능한 읽기-쓰기 파일 시스템입니다. 컨테이너에서 변경된 내용(파일 생성, 수정, 삭제 등)은 이 레이어에 저장됩니다. OverlayFS는 파일이 수정되거나 추가될 때만 Upper Layer에 기록하며, Lower Layer는 영향을 받지 않습니다.
- Merged View (통합된 보기): OverlayFS는 Lower Layer와 Upper Layer를 겹쳐서 사용자에게 하나의 통합된 파일 시스템 뷰를 제공합니다. 사용자가 파일을 읽으려고 할 때, OverlayFS는 Upper Layer에서 파일을 먼저 찾고, 없으면 Lower Layer에서 찾습니다. 쓰기 작업은 항상 Upper Layer에만 반영됩니다.
실습 : Overlay 파일 시스템 사용
mkdir tools
# which 명령어 복사
which which
ldd /usr/bin/which
mkdir -p tools/usr/bin
cp /usr/bin/which tools/usr/bin/;
# rm 명령어 복사
which rm
ldd /usr/bin/rm
mkdir -p tools/{bin,lib64,lib/x86_64-linux-gnu}
cp /bin/rm tools/bin/
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/
# upper 디렉토리 생성
mkdir -p rootfs/{container,work,merge}
mount -t overlay overlay -o lowerdir=tools:myroot,upperdir=rootfs/container,workdir=rootfs/work rootfs/merge
tree -L 2 myroot/{bin,usr}
tree -L 2 rootfs/merge/{bin,usr};
# 삭제
rm rootfs/merge/escape_chroot
tree -L 2 rootfs
새로운 레이어가 추가될 때, 파일을 삭제하거나 추가하여도 LowerDir를 통해서 원본 데이터는 유지하고 중복을 피할 수 있습니다.
결론
Overlay 파일 시스템은 현대 컨테이너 기술의 핵심 요소로, 효율적인 리소스 사용과 파일 시스템 격리를 가능하게 합니다. 이를 통해 컨테이너화된 애플리케이션의 배포 및 관리를 단순화하고, 중복 저장을 줄이며, 시스템 전반의 성능을 향상할 수 있습니다. Docker와 Kubernetes는 이러한 장점을 활용하여 컨테이너 이미지를 효과적으로 관리하고 있습니다. OverlayFS는 컨테이너 환경에서 중요한 파일 시스템 중 하나로, 컨테이너 기술의 지속적인 발전에 중요한 역할을 하고 있습니다.
컨테이너 네트워크
Iptables
iptables는 리눅스 운영체제에서 네트워크 트래픽을 필터링하고 제어하기 위해 사용되는 도구입니다. netfilter라는 기술을 이용하여 네트워크 패킷이 시스템에 도착하거나 나갈 때, 혹은 시스템을 통해 전달될 때 해당 패킷을 처리하는 규칙을 정의할 수 있습니다.
테이블(Table) : iptables는 여러 테이블로 구성되어 있으며, 각 테이블은 특정한 작업을 처리하는 규칙을 포함합니다.
- filter: 기본 테이블로, 패킷 필터링 작업을 담당합니다.
- nat: 네트워크 주소 변환(NAT) 작업을 처리합니다.
- mangle: 패킷의 헤더를 수정합니다.
- raw: 패킷의 연결 추적을 설정하기 전에 처리를 허용합니다.
체인(Chain) : 각 테이블은 여러 체인을 포함하며, 체인은 패킷이 이동할 수 있는 경로를 정의합니다.
- INPUT: 로컬 시스템으로 들어오는 패킷을 처리합니다.
- OUTPUT: 로컬 시스템에서 나가는 패킷을 처리합니다.
- FORWARD: 로컬 시스템을 통과하여 다른 목적지로 전달되는 패킷을 처리합니다.
- PREROUTING: 패킷이 라우팅 결정 전에 처리됩니다.
- POSTROUTING: 패킷이 라우팅 결정 후 처리됩니다.
1. 네트워크 네임스페이스 간 통신
0. 개요
-BLUE, RED라는 네트워크 네임스페이스 생성하고 veth pair하여 1) 네임스페이스 간의 통신이 잘되는지, 2) 호스트와 구별되는지 확인 해봅니다.
1. 네트워크 생성
veth(virtual) 가상 이더넷을 생성합니다.
# veth (가상 이더넷 디바이스) 생성, man ip-link
ip link add veth0 type veth peer name veth1
# veth 생성 확인(상태 DOWN), ifconfig 에는 peer 정보 확인 안됨
# very pair 정보 확인 : ({iface}@if{pair#N})
ip -c link
ip -c addr # 축약 ip -c a
ifconfig -a
2. 네트워크 네임스페이스 생성
BLUE, RED 네트워크 네임스페이스를 생성합니다.
# 네트워크 네임스페이스 생성 , man ip-netns
ip netns add RED
ip netns add BLUE
# 네트워크 네임스페이스 확인
ip netns list
3. 네트워크 이동 및 IP 설정
veth을 각각 네트워크 네임스페이스로 각각 이동시키고 IP를 설정합니다.
- RED
- veth0 : 11.11.11.2/24
- BLUE
- veth1 : 11.11.11.3/24
# veth0 을 RED 네트워크 네임스페이스로 옮김
ip link set veth0 netns RED
ip netns list
## 호스트의 ip a 목록에서 보이지 않음, veth1의 peer 정보가 변경됨
ip -c link
## RED 네임스페이스에서 ip a 확인됨(상태 DOWN), peer 정보 확인, link-netns RED, man ip-netns
ip netns exec RED ip -c a
# veth1 을 BLUE 네트워크 네임스페이스로 옮김
ip link set veth1 netns BLUE
ip -c link
ip netns exec BLUE ip -c a
# veth0, veth1 상태 활성화(state UP)
ip netns exec RED ip link set veth0 up
ip netns exec RED ip -c a
ip netns exec BLUE ip link set veth1 up
ip netns exec BLUE ip -c a
# veth0, veth1 에 IP 설정
ip netns exec RED ip addr add 11.11.11.2/24 dev veth0
ip netns exec RED ip -c a
ip netns exec BLUE ip addr add 11.11.11.3/24 dev veth1
ip netns exec BLUE ip -c a
4. 네트워크 정보 확인
각각 네임스페이스의 네트워크 정보를 확인합니다.
- netenter : 네임스페이스에 attach 하여 지정한 프로그램을 실행합니다.
# 터미널1 (RED 11.11.11.2)
tree /var/run/netns
nsenter --net=/var/run/netns/RED
ip -c a
## neighbour/arp tables management , man ip-neighbour
ip -c neigh
## 라우팅 정보, iptables 정보
ip -c route
iptables -t filter -S
iptables -t nat -S
# 터미널2 (호스트)
lsns -t net # nsenter 실행 후 TYPE(net) CMD(-bash) 생성 확인
ip -c a
ip -c neigh
ip -c route
iptables -t filter -S
iptables -t nat -S
# 터미널3 (BLUE 11.11.11.3)
nsenter --net=/var/run/netns/BLUE
ip -c a
ip -c neigh
ip -c route
iptables -t filter -S
iptables -t nat -S
5. ping 통신 확인
ping 명령어로 네트워크 간 통신을 확인합니다.
# ping 통신 확인
# 터미널3 (BLUE 11.11.11.3)
tcpdump -i veth1
ip -c neigh
exit
# 터미널1 (RED 11.11.11.2)
ping 11.11.11.3 -c 1
ip -c neigh
exit
# 터미널2 (Host)
ping 11.11.11.2 -c 1
ping 11.11.11.3 -c 1
# 삭제
ip netns delete RED
ip netns delete BLUE
결론
네트워크 네임스페이스 생성 시 호스트 네트워크와 구별되는 것을 확인할 수 있습니다.
2. Bridge 통신
0. 개요
BLUE, RED라는 네트워크 네임스페이스 생성하고 bridge 네트워크를 통해 통신되도록 합니다.
1. 네트워크 네임스페이스 및 veth를 생성
- RED
- reth0 -> reth1
- BLUE
- beth0 -> beth1
# 네트워크 네임스페이스 및 veth 생성
ip netns add RED
ip link add reth0 type veth peer name reth1
ip link set reth0 netns RED
ip netns add BLUE
ip link add beth0 type veth peer name beth1
ip link set beth0 netns BLUE
# 확인
ip netns list
ip -c link
ip netns exec RED ip -c a
ip netns exec BLUE ip -c a
2. 브리지 생성하고 veth를 연결
- bridge : br0
- reth1
- beth1
# 브리지 정보 확인
brctl show
# br0 브리지 생성
ip link add br0 type bridge
# br0 브리지 정보 확인
brctl show br0
brctl showmacs br0
brctl showstp br0
# reth1 beth1 을 br0 연결
ip link set reth1 master br0
ip link set beth1 master br0
brctl show br0
brctl showmacs br0
ip -br -c link
3. veth 및 bridge IP 설정 및 활성화
# reth0 beth0 에 IP 설정 및 활성화, br0 활성화
ip netns exec RED ip addr add 11.11.11.2/24 dev reth0
ip netns exec BLUE ip addr add 11.11.11.3/24 dev beth0
ip netns exec RED ip link set reth0 up; ip link set reth1 up
ip netns exec BLUE ip link set beth0 up; ip link set beth1 up
ip link set br0 up
ip -br -c addr
# 터미널1 (RED 11.11.11.2)
nsenter --net=/var/run/netns/RED
ip -c a;echo; ip -c route;echo; ip -c neigh
## 현재 네트워크 네임스페이스 정보 확인
ip netns identify $$
RED
# 터미널2 (호스트)
brctl showmacs br0
bridge fdb show
bridge fdb show dev br0
iptables -t filter -S
iptables -t filter -L -n -v
# 터미널3 (BLUE 11.11.11.3)
nsenter --net=/var/run/netns/BLUE
ip -c a;echo; ip -c route;echo; ip -c neigh
## 현재 네트워크 네임스페이스 정보 확인
ip netns identify $$
BLUE
4. Ping 통신 테스트
ping 통신 테스트 전 사전 설정을 확인합니다.( 도커 설치 시, 아래 설정은 기본적으로 세팅이 됩니다.)
- iptables
- Fowrad : DROP
- /proc/sys/net/ipv4/ip_forward: 1
# ping 통신 전 사전 설정
## iptables 정보 확인
iptables -t filter -S | grep '\-P'
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
iptables -nvL -t filter
## Ubuntu 호스트에서 패킷 라우팅 설정 확인 : 커널의 IP Forwarding (routing) 기능 확인 - 0(off), 1(on)
## echo 1 > /proc/sys/net/ipv4/ip_forward
cat /proc/sys/net/ipv4/ip_forward
1
11.11.11.2(RED) -> 11.11.11.3(BLUE)로 ping 통신을 해보면 실패가 일어납니다.
Iptables에는 요청한 패킷(pkts)만 늘어나고 tcpdump에는 ARP 포로토콜만 잡히는 것을 볼 수 있습니다.
5. RED -> BLUE ping failed 분석
RED(외부, src)-> 호스트 -> BLUE(외부, dest)
호스트를 Bridge 네트워크를 통해 통신하기 때문에 마치 프락시와 같이 forward를 해주는 역할을 하게 됩니다.
따라서, Foward Chain에 Filter 테이블 룰을 확인하고 허용하는 정책을 추가해주어야 합니다.
허용정책 추가
DOCKER-USER라는 체인에서 규칙과 일치하는 패킷이 통과할 수 있도록 합니다.
# 터미널2 (호스트)
# iptables 설정 정보 확인
iptables -t filter -S
iptables -t nat -S | grep '\-P'
# iptables 설정 추가 -t(table), -I(insert chain), -j(jump to - ACCEPT 허용)
iptables -t filter -I DOCKER-USER -j ACCEPT
iptables -nvL -t filter
iptables -t nat -S | grep '\-P'
iptables -t filter -S
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
-N DOCKER
-N DOCKER-ISOLATION-STAGE-1
-N DOCKER-ISOLATION-STAGE-2
-N DOCKER-USER
-A FORWARD -j DOCKER-USER "MyServer" 17:40 31-Aug-24
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j ACCEPT
-A DOCKER-USER -j RETURN
이제 다시 Ping 테스트를 해보면 정상적으로 통신이 되는 것을 확인할 수 있습니다.
결론
arp table, route table, bridge fdb를 통하여 통신하며 격리된 네트워크 간 통신에서 호스트의 Iptables이 영향을 끼친다는 것을 알 수 있습니다.
3. 호스트 & 외부(인터넷) 통신
Case 1 : 호스트 -> RED로 통신
호스트 Bridge 인터페이스에 같은 대역대의 IP를 추가해주어야 합니다.
# 터미널2 (호스트) >> 호스트에서 RED 로 통신이 안되는 이유가 무엇일까요?
ping -c 1 11.11.11.2
ip -c route
ip -c addr
==============================================================
# 터미널2 (호스트) >> br0 에 IP 추가(라우팅 정보)
ip addr add 11.11.11.1/24 dev br0
ping -c 1 11.11.11.2
ping -c 1 11.11.11.3
Case 2 : RED(11.11.11.2) -> 호스트(192.168.50.10) 통신
RED의 네트워크에 같은 대역의 호스트 IP를 default route로 추가합니다.
# 터미널1 (RED 11.11.11.2) >> 192.168.50.10 와 통신이 안되는 이유는 무엇일까요?
ping -c 1 11.11.11.1
ping -c 1 192.168.50.10
ip -c route
ip -c addr
==============================================================
# 터미널3 (호스트)
tcpdump -i any icmp -n
# 터미널1 (RED 11.11.11.2)
ip route add default via 11.11.11.1
ping -c 1 192.168.50.10
ip -c route
Case 3 : RED(11.11.11.2) -> 외부 대역(인터넷) 통신
외부와 통신을 위해 NAT 테이블의 POSTROUTING 체인에 11.11.11.0/24 대역이 들어오면 MASQUERADE 해주는 룰을 적용합니다.
# 터미널1 (RED 11.11.11.2)
ip -c route
ping -c 1 8.8.8.8 >> 외부 대역(인터넷)과 통신이 안되는 이유가 무엇일까요?
# 터미널2 (호스트)
iptables -S -t nat
iptables -nvL -t nat
## POSTROUTING : 라우팅 Outbound or 포워딩 트래픽에 의해 트리거되는 netfilter hook
## POSTROUTING 에서는 SNAT(Source NAT) 설정
iptables -t nat -A POSTROUTING -s 11.11.11.0/24 -j MASQUERADE
watch -d 'iptables -v --numeric --table nat --list POSTROUTING'
iptables -nvL -t nat
conntrack -L --src-nat
# 터미널1 (RED 11.11.11.2)
ping -c 1 8.8.8.8
exit
MASQUERADE 동적 IP 주소를 가진 네트워크 인터페이스에 적합한 NAT 설정으로, 소스 IP 주소를 NAT 장치의 외부 인터페이스 IP주소로 변환하여 외부 네트워크와 통신할 수 있게 합니다.
이미 적용된 도커의 룰을 살펴보면! -o docker 옵션이 추가되어 있습니다. 해당 의미는 Docker의 네트워크 인터페이스를 제외한 다른 인터페이스를 통해 나가는 트래픽에만 적용되는 규칙입니다.
도커 네트워크
도커 네트워크 모델
도커의 기본 네트워크 모드에는 bridge, host, None가 있습니다.
docker network ls
NETWORK ID NAME DRIVER SCOPE
1d1eb76b8413 bridge bridge local
6e01566eb268 host host local
a9ca9ef671ae none null local
docker info | grep Network
Network: bridge host ipvlan macvlan null overlay
추가적으로, macvlan, ipvlan, overlay네트워크 플러그인도 지원합니다.
1. Bridged Mode
- 설명: 컨테이너는 브리지 네트워크를 통해 서로 통신할 수 있습니다. 이 브리지는 호스트 시스템의 이더넷 인터페이스를 통해 외부 네트워크와 연결됩니다.
- 특징:
- 같은 브로드캐스트 도메인 내에서 서로 통신이 가능.
- NAT(네트워크 주소 변환)를 비활성화하면, 부모 인터페이스 네트워크에서 반환 통신을 위해 별도의 호스트 라우트가 필요.
- 호스트 시스템은 네트워크 접근을 통해 컨테이너와 통신할 수 있음.
- iptables를 사용하여 네트워크 통신을 제어.
- 마이크로세그멘테이션(microsegmentation)을 지원하여 보안 강화 가능.
2. Host Mode
- 설명: 컨테이너는 호스트 시스템과 동일한 네트워크 인터페이스를 사용합니다.
- 특징:
- 부모 네트워크 인터페이스를 컨테이너와 동일하게 사용.
- 컨테이너에 대해 자동 iptables 규칙이 설정되지 않음.
- 호스트와 모든 컨테이너가 동일한 인터페이스와 관련된 주소 및 포트를 공유함.
- 명시적인 인터페이스 및 포트 할당이 필요.
3. MacVLAN Bridge Mode L2
- 설명: 각 컨테이너는 독립된 시스템처럼 네트워크에 연결되며, MacVLAN 브리지 인터페이스를 사용합니다.
- 특징:
- 컨테이너는 독립 시스템처럼 네트워크와 상호작용함.
- 호스트 시스템은 브리지를 통해 컨테이너에 접근할 수 없음.
- 다른 대체 인터페이스를 통해 네트워크 연결 가능.
- iptables를 사용하지 않음.
4. IPVLAN L3 Mode
- 설명: 컨테이너는 같은 부모 인터페이스를 사용하는 한 서로 다른 서브넷에서도 통신할 수 있으며, 호스트 인터페이스는 라우터 역할을 합니다.
- 특징:
- 같은 부모 인터페이스를 사용하면 다른 서브넷에서도 컨테이너 간 통신이 가능.
- 호스트 인터페이스는 라우터로 동작하지만, 경로 광고는 하지 않음.
- iptables를 사용할 수 있지만, 이는 실험적인 지원에 해당함.
실습 : 컨테이너 Bridge 모드 통신
Docker에서 가장 많이 사용하는 Bridge 모드로 컨테이너 네트워크 통신을 실습합니다.
0. Bridge 모드 기본 정보 확인
Bridge 모드의 기본정보를 화인 합니다.
# 도커 네트워크 모드 확인
docker network ls
NETWORK ID NAME DRIVER SCOPE
1d1eb76b8413 bridge bridge local
6e01566eb268 host host local
a9ca9ef671ae none null local
# 도커 네트워크(플러그인) 정보 확인
docker info
docker info | grep Network
brctl show
bridge name bridge id STP enabled interfaces
br0 8000.52c4369b2c59 no beth1
reth1
docker0 8000.02425f0795d8 no
1. 컨테이너(Busybox) 2대 생성
ORENGE, PINK라는 이름의 busybox 컨테이너를 2개 생성하여 네트워크를 확인합니다.
# 터미널1 (PINK) : PINK 이름의 busybox 컨테이너 생성
docker run -it --name=PINK --rm busybox
ip a
ip neigh
# 터미널3 (ORANGE) : ORANGE 이름의 busybox 컨테이너 생성
docker run -it --name=ORANGE --rm busybox
ip a
ip neigh
Host의 bridge link 정보를 조회하면 veth에 각각 서로 연결되는 veth peer가 추가되었음을 확인할 수 있습니다.
( `인터페이스명 @ peer 대상` )
2. 컨테이너 간 통신 확인
컨테이너 간 통신을 확인합니다.
# 터미널2 (호스트)
tcpdump -i docker0 -n
iptables -t filter -S
-A FORWARD -i docker0 -o docker0 -j ACCEPT # 컨테이너 끼리는 FORWARD 가 ACCEPT(허용)
# 터미널1 (PINK)
## ORANGE 로 ping 테스트(IP는 다를 수 있습니다)
ping -c 1 172.17.0.3
ip neigh
route -n
# 터미널3 (ORNAGE)
## ORANGE 로 ping 테스트(IP는 다를 수 있습니다)
ping -c 1 172.17.0.2
ip neigh
route -n
tcpdump 결과 bridge 모드에서 잘 통신됨을 확인할 수 있습니다.
3. 컨테이너에서 외부로 통신
외부와의 통신을 위해 172.17.0.0/16 대역도 MASQUERADE 되도록 룰을 설정합니다.
# 터미널2 (호스트)
tcpdump -i any icmp
iptables -t nat -S
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
# 터미널1 (PINK)
ping 8.8.8.8
exit # 실습 완료 후
# 터미널3 (ORNAGE)
ping 8.8.8.8
exit # 실습 완료 후
결론
실습을 통해 네트워크 통신을 위해 필요한 작업들을 수작업으로 해보니, 도커가 이러한 작업들을 많은 부분 자동화해주면서 네트워크의 많은 비용을 줄일 수 있다는 것을 느낄 수 있었습니다.
'Kubernetes > Network' 카테고리의 다른 글
Ingress와 Kubernetes Gateway Api - KANS 6주차 (0) | 2024.10.13 |
---|---|
kube-proxy IPVS 모드의 동작 원리 이해 - KANS 5주차 2 (1) | 2024.10.06 |
MetalLB를 활용한 LoadBalancer 서비스의 동작 원리 이해 - KANS 5주차 1 (2) | 2024.10.06 |
Calico CNI 기본 네트워크 이해 & 네트워크 모드 - KANS 3주차 (4) | 2024.09.22 |
Pause 컨테이너 & Flannel CNI - KANS 2주차 (2) | 2024.09.08 |