Docker?
- 도커(Docker)는 가상실행 환경을 제공해주는 오픈소스 플랫폼입니다. 도커에서는 이 가상실행 환경을 '컨테이너(Container)'라고 합니다.
- 도커는 컨테이너라는 단위로 패키징되고, 가상화보다 가볍고 효율적이기도 합니다.
- 또 MSA 환경에서 빼놓을 수 없는 개념입니다.
Container?
- Container는 어플리케이션과 그 실행에 필요한 의존성을 포함하는 경량화된 독립 실행 환경입니다.
- 호스트 OS와 커널을 공유하면서도 프로세스, 파일 시스템, 네트워크를 다른 컨테이너와 격리하여 실행합니다.
- 일관된 환경을 제공하여 개발, 테스트, 배포 과정에서 개발자들의 단골 멘트인 "내 컴퓨터에서는 작동했는데" 문제를 해결하고 이식성을 높입니다.
Docker와 Container
컨테이너적 관점에서 Docker를 간단하게 설명해보면 이렇습니다.
- Docker 컨테이너는 어플리케이션 코드, 런타임, 시스템 도구, 라이브러리 등을 포함하는 독립 실행 환경입니다.
- 각 컨테이너는 호스트 OS의 커널을 공유하지만 프로세스, 네트워크, 파일 시스템 측면에서 격리되어 있습니다.
- 컨테이너는 이미지를 기반으로 생성되며, 빠르게 시작/중지 할 . 수있고 이식성이 뛰어나 다양한 환경에서 일관되게 실행합니다.
[Docker Architecture]
/proc?
Docker를 살펴보기 전에 /proc 에 대해 알아볼 필요가 있습니다.
/proc에는 현재 Linux에서 실행 중인 프로세스와 시스템 정보를 파일 형태로 저장되어 있습니다.
/proc은 디스크에 저장되지 않고, in-memory에 존재합니다.
특징으로는 /proc 하위에는 현재 실행하고 있는 PID를 이름으로하는 디렉토리가 생성되어 있습니다.
또, 시스템 설정, 하드웨어 정보, 메모리 사용량 같은 다양한 시스템 정보를 포함합니다.
/proc 하위에는 위에 pid 디렉토리 하위에 담긴 정보 외에도 많은 정보가 있습니다.
ex) /proc/cpuinfo(cpu정보), /proc/meminfo(메모리 정보), /proc/uptime(부팅 후 경과 시간), /proc/version(커널 버전 및 빌드 날짜), /proc/filesystem(파일 시스템 리스트)
#
mount -t proc
findmnt /proc
TARGET SOURCE FSTYPE OPTIONS
/proc proc proc rw,nosuid,nodev,noexec,relatime
#
ls /proc
tree /proc -L 1
tree /proc -L 1 | more
# 커널이 동적으로 생성하는 정보
cat /proc/cpuinfo
cat /proc/meminfo
cat /proc/uptime
cat /proc/loadavg
cat /proc/version
cat /proc/filesystems
cat /proc/partitions
# 실시간(갱신) 정보
cat /proc/uptime
Docker 설치
- OS : ubuntu:22.04
$ sudo su -
$ curl -fsSL https://get.docker.com | sh
# 설치 확인
$ docker info; docker version
####################################################################
root@MyServer:~# docker info; docker version
Client: Docker Engine - Community
Version: 27.2.0
Context: default
Debug Mode: false
Plugins:
buildx: Docker Buildx (Docker Inc.)
Version: v0.16.2
Path: /usr/libexec/docker/cli-plugins/docker-buildx
compose: Docker Compose (Docker Inc.)
Version: v2.29.2
Path: /usr/libexec/docker/cli-plugins/docker-compose
Server:
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 27.2.0
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Using metacopy: false
Native Overlay Diff: true
userxattr: false
Logging Driver: json-file
Cgroup Driver: systemd
Cgroup Version: 2
Plugins:
Volume: local
Network: bridge host ipvlan macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
Swarm: inactive
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 472731909fa34bd7bc9c087e4c27943f9835f111
runc version: v1.1.13-0-g58aa920
init version: de40ad0
Security Options:
apparmor
seccomp
Profile: builtin
cgroupns
Kernel Version: 6.5.0-1024-aws
Operating System: Ubuntu 22.04.4 LTS
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 1.857GiB
Name: MyServer
ID: 71993505-f919-413b-bbbf-013f8f9583c9
Docker Root Dir: /var/lib/docker
Debug Mode: false
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false
Client: Docker Engine - Community
Version: 27.2.0
API version: 1.47
Go version: go1.21.13
Git commit: 3ab4256
Built: Tue Aug 27 14:15:13 2024
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 27.2.0
API version: 1.47 (minimum version 1.24)
Go version: go1.21.13
Git commit: 3ab5c7d
Built: Tue Aug 27 14:15:13 2024
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.7.21
GitCommit: 472731909fa34bd7bc9c087e4c27943f9835f111
runc:
Version: 1.1.13
GitCommit: v1.1.13-0-g58aa920
docker-init:
Version: 0.19.0
GitCommit: de40ad0
####################################################################
$ systemctl status docker
The Docker daemon binds to a Unix socket, not a TCP port. By default it's the root user that owns the Unix socket, and other users can only access it using sudo.
Docker는 TCP Port를 사용하지 않고, Unix Socket에 바인딩 되어 있으며,
Socket의 소유자는 root이며, 다른 유저가 Access하고자 할 경우 sudo를 통해 접근할 수 있습니다.
ubuntu 계정에서 docker info 명령어를 실행하면 결과값 하단과 같이 Permission 관련 오류가 발생하는 것을 확인할 수 있습니다.
tcp listen socket을 조회하면 Docker 관련 정보가 확인되지 않는다.
unix listen socket을 조회하면 아래와 같이 Docker 관련 정보가 확인되는 것을 볼 수 있다.
Unix Domain Socket 은?
같은 시스템 내에서 실행되고 있는 프로세스 간의 통신 메커니즘입니다. 기존에는 유닉스 환경에서만 제공되었으나 현재는 Windows환경에서도 사용 가능합니다.
링크의 블로그에서 설명한 것과 같이 계층을 TCP보다 덜 통과하기 때문에 성능에 이점이 있습니다.
또 일반적으로 .sock 이라는 이름의 파일이 생성되고 로컬 시스템에서만 접근할 수 있기 때문에 네트워크 기반의 공격에도 안전하며,
.sock 파일의 권한을 조정할 수 있어 접근 제어에도 용이합니다.
때문에 UDS는 DB, WEB, Container runtime 등 여러 SW에서 효율적으로 사용하고 있습니다.
일반 User를 docker group에 추가한다면 권한 상승 없이 docker 사용이 가능한지 확인해 보겠습니다.
$ whoami
ubuntu
# docker 설치 시 Group 자동 생성
$ getent group | grep docker
ubuntu@MyServer:~$ getent group | grep docker
docker:x:999
# docker group 에 ubuntu 계정 추가
sudo usermod -aG docker $USER # $USER 변수를 선언하지 않아도 현재 User가 자동 선언되어있습니다.
# docker 정보 확인 시도
$ docker info
컨테이너 실행을 포함한 여러 명령어를 통해 동작도 확인해 보겠습니다.
$ docker run hello world
$ docker ps
$ docker ps -a
$ docker images
DinD? DooD!
예제 1) 컨테이너가 host의 docker socket file 공유 설정을 통해 컨테이너에서 Docker를 사용해 보겠습니다.
$ docker run --rm -it -v /run/docker.sock:/run/docker.sock -v /usr/bin/docker:/usr/bin/docker ubuntu:latest bash
$ docker run -d --rm --name webserver nginx:alpine
# [터미널 2번]
docker ps
host Docker의 sock 파일과 binary 파일을 볼륨 형태로 공유받고, 컨테이너 내부에서 docker run 명령어를 수행하면,
아래와 같이 정상적으로 Container가 기동되는 것을 확인할 수 있습니다.
또 host 에서 docker ps 명령어를 통해 실행 중인 컨테이너를 조회하면 컨테이너 내부에서 실행한 컨테이너 역시 정상적으로 조회되는 것을 확인할 수 있습니다.
DinD(Docker in Docker)와 비슷하게 보여 헷갈릴 수 있지만,
DinD는 도커 내에서 도커 데몬을 실행하는 방식인 반면 위 그림과 같이 host의 도커 데몬을 사용하는 것이므로 정확히는 개념이 다릅니다.
위와 같은 방식을 DooD(Docker out of Docker) 방식이라고 이야기 합니다.
예제 2) DooD 방식으로 Jenkins 컨테이너를 생성하고 콘솔에 접속해보겠습니다.
$ docker run -d -p 8080:8080 -p 50000:50000 --name jenkins-server --restart=on-failure -v jenkins_home:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker jenkins/jenkins
[명령어 설명]
1. -d 를 통해 데몬 형태로 실행하여 컨테이너가 백그라운드에서 실행되도록 합니다.
2. -p 8080:8080 -p 50000:50000 호스트의 8080, 50000 포트를 컨테이너의 8080, 50000 포트로 접속되도록 포트포워딩 설정을 합니다.
3. --name jenkins-server 컨테이너의 이름을 설정합니다.
4. --restart=on-faulure 컨테이너가 오류로 종료될 경우 자동으로 재시작되는 규칙을 부여합니다.
5. -v jenkins_home:/var/jenkins_home jenkins_home 볼륨을 컨테이너 /var/jenkins_home 디렉토리에 마운트합니다.
6. -v /var/run/docker.sock:/var/run/docker.sock 호스트의 docker 데몬을 사용할 수 있도록 socket 파일을 컨테이너 내부에 마운트합니다.
7. -v /usr/bin/docker:/usr/bin/docker 호스트의 docker binary 명령어를 사용하도록 컨테이너 내부에 마운트합니다.
8. jenkins/jenkins 컨테이너를 실행할 jenkins 이미지입니다.
Jenkins 초기 패스워드를 확인하고 콘솔에 접속합니다.
💡 테더링 환경에서 접속하는 경우 AWS는 Public ip가 아닌 Public dns를 통해 접속이 가능합니다.
$ docker exec -it jenkins-server cat /var/jenkins_home/secrets/initialAdminPassword
$ echo "http://$(curl -s ipinfo.io/ip):8080"
DooD 처음 기재했던 컨테이너 내부에서 실행하는 명령어 역시 정상 동작하는 것을 확인할 수 있습니다.
Docker 설치 후 Host에 바뀌는 것들
Docker 가 host에 설치되면 기본적으로 변경되는 것들이 있습니다.
1) pstree -p
docker 관련 데몬이 표시됩니다.
2) Network 관련
# 네트워크 정보 확인 >> docker0 네트워크 인터페이스가 추가됨, 현재는 DOWN 상태
$ ip -br -c addr | grep docker
docker0 DOWN 172.17.0.1/16 fe80::42:94ff:fe8d:9a64/64
$ ip -c route | grep docker
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
# Ethernet bridge 확인
# interfaces 항목은 실행 중인 컨테이너가 없으면 빈 값으로 표시되지만
# 실행 중인 컨테이너가 있으면 interfaces가 추가 됩니다.
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242948d9a64 no
# 컨테이너 실행 시
docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
veth65f5a4e@if18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
bridge name bridge id STP enabled interfaces
docker0 8000.0242948d9a64 no veth65f5a4e
# iptables 정책 확인
$ iptables -t filter -S
-P FORWARD DROP # 기본 ACCEPT 정책이 DROP으로 변경됩니다.
# docker0 > docker0 으로 docker > outside 정책이 추가 됩니다.
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT #docker to outside
-A FORWARD -i docker0 -o docker0 -j ACCEPT #docker to docker
$ iptables -t nat -S
# Postrouting 체인을 통해 출발지가 172.17.0.0/16(docker default bridge) cidr 출발지를 대상으로
# 호스트 IP로 마스커레이딩하여 outside와 통신할 수 있도록 합니다.
# 해당 정책은 Docker > outside에만 적용되며 Docker > Docker에 영향을 주지 않습니다.
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
컨테이너 격리
컨테이너의 개념은 Docker를 통해 처음 정의된 것은 아닙니다.
이미 오래전부터 다양한 방법을 통해 격리의 방법들이 존재했으며,
사용자가 쉽게 사용할 수 있도록 패키지 형태로 출시된 것이 현재의 Docker입니다.
이전에 존재했던 격리 방법 몇개를 다뤄보겠습니다.
Chroot + 탈옥
chroot는 change root 프로세스와 자식 프로세스들의 루트 디렉토리를 변경하는 방법입니다.
Step 1) 필요한 명령어 복사하고 chroot 시도합니다.
## root 계정으로 수행합니다.
cd /tmp
mkdir myroot
# myroot 디렉토리 내부에 아무런 데이터가 없으므로 에러가 발생합니다.
chroot myroot /bin/sh
chroot: failed to run command ‘/bin/sh’: No such file or directory
# shell을 실행할 수 있도록 설정 파일의 위치를 확인하고 복사합니다.
which sh
ldd /bin/sh
# shell 관련 설정 복사
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/
# 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
Step 2) 탈옥 시도
# change root 및 탈옥 시도
$ chroot myroot /bin/sh
$ cd /
$ ls /
탈옥 코드 : 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);
}
탈옥 코드를 gcc로 컴파일하고 myroot로 복사합니다.
$ gcc -o myroot/escape_chroot escape_chroot.c
$ tree -L 1 myroot
chroot를 통해 sh로 접속하고 컴파일한 코드를 실행해 봅니다.
$ chroot myroot /bin/sh
# 탈옥 코드 실행
$ ./escape_chroot
$ ls /
chroot에서 ps 명령어 실행해보기
기본 상태
이전 ls 명령어 작업 방식과 동일하게 명령어 및 라이브러리 복사
$ which 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/
ps 명령어 실행 - 에러 발생
Error, do this: mount -t proc proc /proc
에러문 조치를 위해 Proc mount 진행
step 1) 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/
step 2) proc 디렉터리를 chroot 내부에서 생성하기 위해 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/;
myroot tree 확인
myroot
├── bin
│ ├── ls
│ ├── mkdir
│ ├── mount
│ ├── ps
│ └── sh
├── lib
│ └── x86_64-linux-gnu
│ ├── libblkid.so.1
│ ├── libc.so.6
│ ├── libcap.so.2
│ ├── libgcrypt.so.20
│ ├── libgpg-error.so.0
│ ├── liblzma.so.5
│ ├── libmount.so.1
│ ├── libpcre2-8.so.0
│ ├── libprocps.so.8
│ ├── libselinux.so.1
│ ├── libsystemd.so.0
│ └── libzstd.so.1
├── lib64
│ └── ld-linux-x86-64.so.2
└── usr
└── lib
└── x86_64-linux-gnu
└── liblz4.so.1
Step 3) /proc 경로 마운트
$ mkdir /proc
$ mount -t proc proc /proc
$ mount -t proc
$ mount -t proc proc /proc
$ mount
proc on /proc type proc (rw,relatime)
Step 4) ps 명령어 실행 확인
# ps
PID TTY TIME CMD
7795 ? 00:00:00 sudo
7796 ? 00:00:00 su
7797 ? 00:00:00 bash
7847 ? 00:00:00 sh
7854 ? 00:00:00 ps
다른 사람이 만든 image chroot 해보기
Step 1) nginx-root 디렉토리 생성하고 docker image 내부 파일을 nginx-root에 압축 해제합니다.
$ mkdir nginx-root
$ tree nginx-root
# nginx 이미지 다운로드 받아 tar로 export해서 생성한 디렉토리에 압축해제
$ docker export $(docker creat nginx) | tar -C nginx-root -xvf -;
$ tree -L 1 nginx-root
nginx-root
├── bin -> usr/bin
├── boot
├── dev
├── docker-entrypoint.d
├── docker-entrypoint.sh
├── etc
├── home
├── lib -> usr/lib
├── lib64 -> usr/lib64
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin -> usr/sbin
├── srv
├── sys
├── tmp
├── usr
└── var
Step 2) nginx-root로 chroot 시도합니다.
Step 3) nginx 실행하고 host에서 확인하기
$ nginx -g "daemon off;"
[터미널 2번]
$ netstat -nlp | grep nginx
Mount namespace + pivot_root
위의 탈옥 코드와 같이 chroot는 상단 경로로 접근이 가능한 여지가 있습니다.
마운트 네임스페이스를 통해 파일 시스템을 변경하고, 프로세스 환경을 격리해 보겠습니다.
💡 pivot_root
Linux 시스템에서 프로세스의 루트 파일 시스템을 변경하는데 사용합니다.
chroot와 유사하지만 기존 루트 파일 시스템을 완전히 언마운트 할 수 없습니다.
주로 Docker와 같은 컨테이너 런타임에서 루트 파일 시스템 격리에 사용됩니다.
Step 1) unshare 명령어를 통해 네임스페이스를 생성하고, 격리 환경에 진입합니다.
$ unshare --mount /bin/sh
# 기본 로컬 환경에서의 df -h와 비교해봅니다.
# mount unshare 시 부모 프로세스의 마운트 정보를 복사하여 자식 네임스페이스를 생성하므로
# 별도 설정이 없는 초기에는 동일 합니다.
$ df -h
# 디렉토리를 생성하고, 마운트를 합니다.
$ mkdir new_root
$ mount -t tmpfs none new_root
$ ls -l
$ tree new_root
$ df -h
df 명령어를 격리 공간에서 실행하면 방금 마운트한 new_root를 확인할 수 있지만,
로컬 환경에서 df 명령어 수행 시에는 new_root가 확인되지 않습니다.
Step 2) chroot 단계에서 사용했던 myroot 디렉토리 파일을 new_root로 복사하고,
격리 환경과 로컬 환경을 비교합니다.
[격리 환경]
$ cp -r myroot/* new_root/
$ tree new_root/
[로컬 환경]
$ cd /tmp
$ tree new_root/
Step 3) pivot_root를 통해 루트 파일 시스템을 변경해 봅니다.
$ mkdir new_root/put_old
$ cd new_old
$ pivot_root . put_old
pivot_root를 실행하고 로컬 환경의 /(root) 경로를 비교해 봅니다.
정상적으로 pivot_root가 수행된 것을 확인할 수 있고, escape_chroot를 통해 탈옥을 시도해 봅니다.
아래에서 확인할 수 있듯 excape_chroot를 통해 탈옥을 시도했으나, 탈옥에 실패하는 것을 확인할 수 있습니다.
namespace 격리
- 네임스페이스와 관련된 프로세스의 특징
- 모든 프로세스들은 네임스페이스 타입별로 특정 네임스페이스에 속합니다.
- Child 는 Parent 의 네임스페이스를 상속받습니다.
- 프로세스는 네임스페이스 타입별로 일부는 호스트(root) 네임스페이스를 사용하고 일부는 컨테이너의 네임스페이스를 사용할 수 있습니다.
- mount 네임스페이스는 컨테이너의 것으로 격리하고, network 네임스페이스는 호스트 것을 사용할 수 있다.
- Mount(파일시스템), Network(네트워크), PID(프로세스 id), User(계정), ipc(프로세스간 통신), Uts(Unix time sharing, 호스트네임), cgroup
$ sudo su -
$ cd /tmp
# 프로세스 별 네임스페이스 확인
$ ll /proc/$$/ns
lrwxrwxrwx 1 root root 0 Aug 31 22:25 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Aug 31 22:25 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Aug 31 22:25 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx 1 root root 0 Aug 31 22:25 net -> 'net:[4026531840]'
lrwxrwxrwx 1 root root 0 Aug 31 22:25 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Aug 31 22:25 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Aug 31 22:25 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Aug 31 22:25 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Aug 31 22:25 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Aug 31 22:25 uts -> 'uts:[4026531838]'
# 특정 네임스페이스의 inode 값 확인
$ readlink /proc/$$/ns/mnt
mnt:[4026531841]
$ readlink /proc/$$/ns/net
net:[4026531840]
# 네임스페이스 확인 방법
$ lsns -p 1
NS TYPE NPROCS PID USER COMMAND
4026531834 time 115 1 root /sbin/init
4026531835 cgroup 115 1 root /sbin/init
4026531836 pid 115 1 root /sbin/init
4026531837 user 115 1 root /sbin/init
4026531838 uts 111 1 root /sbin/init
4026531839 ipc 115 1 root /sbin/init
4026531840 net 115 1 root /sbin/init
4026531841 mnt 107 1 root /sbin/init
# NPROCS : 해당 네임스페이스에 속해있는 프로세스 갯수
$ lsns -t mnt -p 1
NS TYPE NPROCS PID USER COMMAND
4026531841 mnt 106 1 root /sbin/init
Case 1) 마운트 네임스페이스 MOUNT (mnt) Namespace : 2002년, 마운트 포인트 격리, 최초의 네임스페이스
# unshare -m [명령어] : -m 옵션을 주면 [명령어]를 mount namespace 를 isolation 하여 실행합니다
$ unshare -m # *[명령어]를 지정하지 않으면 환경변수 $SHELL 실행
lsns -p $$
NS TYPE NPROCS PID USER COMMAND
4026531834 time 112 1 root /sbin/init
4026531835 cgroup 112 1 root /sbin/init
4026531836 pid 112 1 root /sbin/init
4026531837 user 112 1 root /sbin/init
4026531838 uts 108 1 root /sbin/init
4026531839 ipc 112 1 root /sbin/init
4026531840 net 112 1 root /sbin/init
4026532206 mnt 2 5834 root -bash ## lsns -p 1에 없음
Case 2) UTS 네임스페이스 Namespace : 2006년, Unix Time Sharing (여러 사용자 작업 환경 제공하고자 서버 시분할 나눠쓰기), 호스트명, 도메인명 격리
# unshare -u [명령어]
# -u 옵션을 주면 [명령어]를 UTS namespace 를 isolation 하여 실행
$ unshare -u
$ lsns -p $$
NS TYPE NPROCS PID USER COMMAND
4026531834 time 115 1 root /sbin/init
4026531835 cgroup 115 1 root /sbin/init
4026531836 pid 115 1 root /sbin/init
4026531837 user 115 1 root /sbin/init
4026531839 ipc 115 1 root /sbin/init
4026531840 net 115 1 root /sbin/init
4026531841 mnt 107 1 root /sbin/init
4026532206 uts 2 8478 root -bash
----------
$ lsns -p 1
NS TYPE NPROCS PID USER COMMAND
4026531834 time 115 1 root /sbin/init
4026531835 cgroup 115 1 root /sbin/init
4026531836 pid 115 1 root /sbin/init
4026531837 user 115 1 root /sbin/init
4026531838 uts 109 1 root /sbin/init
4026531839 ipc 115 1 root /sbin/init
4026531840 net 115 1 root /sbin/init
4026531841 mnt 107 1 root /sbin/init
$ hostname ## 부모 네임스페이스에서 상속 받음
$ hostname SSUNGZ ## hostname 변경
$ exit
$ hostname ## 로컬 환경의 hostname 확인 시 변경되지 않음
Case 3) IPC 네임스페이스 : 2006년, Inter-Process Communication 격리, 프로세스 간 통신 자원 분리 관리 - Shared Memory, Pipe, Message Queue 등
# unshare -i
# -i 옵션을 주면 [명령어]를 IPC namespace 를 isolation 하여 실행
$ unshare -i
$ lsns -p $$
NS TYPE NPROCS PID USER COMMAND
4026531834 time 116 1 root /sbin/init
4026531835 cgroup 116 1 root /sbin/init
4026531836 pid 116 1 root /sbin/init
4026531837 user 116 1 root /sbin/init
4026531838 uts 112 1 root /sbin/init
4026531840 net 116 1 root /sbin/init
4026531841 mnt 108 1 root /sbin/init
4026532206 ipc 2 8497 root -bash
---------------
$ lsns -p 1
NS TYPE NPROCS PID USER COMMAND
4026531834 time 116 1 root /sbin/init
4026531835 cgroup 116 1 root /sbin/init
4026531836 pid 116 1 root /sbin/init
4026531837 user 116 1 root /sbin/init
4026531838 uts 112 1 root /sbin/init
4026531839 ipc 114 1 root /sbin/init
4026531840 net 116 1 root /sbin/init
4026531841 mnt 108 1 root /sbin/init
ex) 서로 다른 컨테이너 2개가 IPC 네임스페이스를 공유하는 방법
아래 예제에서 확인할 수 있 듯이 IPC라는 이름과 같이 프로세스간 자원을 분리하고, 공유 통신할 수 있는 것과 같이
네임스페이스를 분리하고, 컨테이너 간의 자원을 공유하여 사용할 수 있습니다.
데이터 공유가 효율적이라는 것과, 시스템 리소스의 효율이 늘어나는 장점이 있으나, 복잡성이 증가하고, 동기화 문제가 발생할 수 있는 단점이 존재합니다.
# 컨테이너 생성 시 ipc 옵션을 통해 네임스페이스 설정하여 자체 네임스페이스를 소유하도록 합니다.
[터미널 1번]
$ docker run --rm --name test1 --ipc=shareable -it ubuntu bash
# ipcmk를 통해 공유 메모리를 생성합니다.
$ ipcmk -M 2000
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x1a43809e 0 root 644 2000 0
# lsns를 통해 로컬 환경과 innde 값을 비교하면 서로 다른 것을 확인할 수 있고,
# 비교 값을 통해 자체 네임스페이스가 생성된 것을 알 수 있습니다.
$ lsns -p $$ | grep ipc
4026532210 ipc 3 1 root bash
$ lsns -p 1 | grep ipc
4026531839 ipc 119 1 root /sbin/init
-----------------------
[터미널 2번]
# 2번 컨테이너 실행
$ docker run --rm --name test2 --ipc=container:test1 -it ubuntu bash
$ ipcs -m
# 2000 byte memory 확인
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x1a43809e 0 root 644 2000 0
$ lsns -p $$
$ 4026532210 ipc 3 1 root bash
Case 4) PID 네임스페이스 : 2008년, Process ID 격리
- 부모-자식 네임스페이스 중첩 구조, 부모 네임스페이스에서는 자식 네임스페이스를 볼 수 있습니다.
- 자식 네임스페이스는 parent tree의 id와 subtree의 id 두개를 갖습니다.
보모 네임스페이스에서는 6번이지만 자식 네임스페이스 자체의 pid는 1번이기 때문에
pid 1번이 종료되면 namespace 자체가 종료된다.
# unshare -p [명령어]
## -p 옵션을 주면 [명령어]를 PID namespace 를 isolation 하여 실행합니다
## -f(fork) : PID namespace 는 child 를 fork 하여 새로운 네임스페이스로 격리함
## --mount-proc : namespace 안에서 ps 명령어를 사용하려면 /proc 를 mount 하기위함
# [터미널 1번] /proc 파일시스템 마운트
$ echo $$
8444
# 위 설명과 같이 unshare가 되면 서 pid 1번으로 생성되었습니다.
$ unshare -fp --mount-proc /bin/sh
$ echo $$
# 부모 네임스페이스와 자식 네임스페이스 비교를 해보면 innode 값과 커맨드가 다른 것을 확인할 수 있습니다.
## 부모
$ lsns -t pid -p 1
NS TYPE NPROCS PID USER COMMAND
4026531836 pid 115 1 root /sbin/init
## 자식
$ lsns -t pid -p 1
NS TYPE NPROCS PID USER COMMAND
4026532207 pid 2 1 root /bin/sh
위에서 설명한 것과 같이 부모 네임스페이스와 자식 네임스페이스는 중첩되어 있으므로 부모 네임스페이스가 자식 네임스페이스 내부를 볼 수 있기도 하고, 프로세스를 종료할 수 있고 또 컨테이너 자체도 종료할 수 있습니다.
ex) 자식 네임스페이스 shell 확인
# 부모 네임스페이스에서 실행
$ ps auxf | grep '/bin/sh'
root 8793 0.0 0.0 6192 1792 pts/3 S 23:08 0:00 | \_ unshare -fp --mount-proc /bin/sh
root 8794 0.0 0.0 2892 1664 pts/3 S+ 23:08 0:00 | \_ /bin/sh
ex) 자식 네임스페이스의 프로세스 kill과 container kill
# 자식 프로세스에서 실행
$ sleep 10000
# 부모 프로세스에서 실행
$ ps -ef | grep sleep
root 8803 8794 0 23:15 pts/3 00:00:00 sleep 10000
# 프로세스 종료 시켜보기
$ kill -9 8803 # pid
# 자식 네임스페이스
## forground로 실행 중에 부모에서 kill 명령과 동시에 종료됨을 확인할 수 있습니다.
$ sleep 10000
Killed
## 동일한 방법으로 네임스페이스도 종료할 수 있습니다.
Case 6) 자원 관리, cgroup [DOCS]
- cgroup은 cpu, Disk I/O, Memory, Network등 자원 사용을 제한/격리 시키는 커널 기능
- 프로세스는 실행 중인 프로그램의 인스턴스를 의미 OS에서 프로세스를 관리하며, 각 프로세스는 고유한 ID(PID)를 갖습니다.
- 프로세스는 CPU와 MEM을 사용하는 기본 단위로, OS 커널(cgroup)에서 각 프로세스의 자원을 관리합니다.
- cgroup v1, cgroup v2의 가장 큰 변화는 서브 시스템을 통해 시스템 리소스를 특정 프로세스에 할당하는 형태로 제한했으나,
v2로 넘어오면서 단일 계층 구조로 리소스 컨트롤러가 cgroup과 연결되어 여러 리소스 컨트롤러와 동시에 연결되었습니다.
그로 인해 v2에서는 모든 자원 제어를 한곳에서 설정하고 관리할 수 있게 되었습니다. - cgroup v2에서는 memoryQoS라는 기능을 추가하여 컨테이너 등에서 쉽사리 OOM이 나지않게 하는 기능 지원 → Memory High : 메모리 사용량 조절 제한. cgroup의 사용량이 높은 경계를 초과하면 cgroup의 프로세스가 제한되고 회수 압력이 커짐
- control groups : 프로세스들의 자원의 사용(CPU, 메모리, 디스크 입출력, 네트워크 등)을 제한, 격리시키는 리눅스 커널 기능
$ mount -t cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
$ findmnt -A
├─/sys sysfs sysfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/security securityfs securityfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot
│ ├─/sys/fs/pstore pstore pstore rw,nosuid,nodev,noexec,relatime
│ ├─/sys/firmware/efi/efivars efivarfs efivarfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/fs/bpf bpf bpf rw,nosuid,nodev,noexec,relatime,mode=700
│ ├─/sys/kernel/debug debugfs debugfs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/kernel/tracing tracefs tracefs rw,nosuid,nodev,noexec,relatime
│ ├─/sys/fs/fuse/connections fusectl fusectl rw,nosuid,nodev,noexec,relatime
│ └─/sys/kernel/config configfs configfs rw,nosuid,nodev,noexec,relatime
$ grep cgroup /proc/filesystems
# cgroup만 지원할 경우 cgroup2는 출력되지 않습니다.
nodev cgroup
nodev cgroup2
# 툴 설치
$ apt install cgroup-tools stress -y
$ stress -c 1 # 부하 발생 확인
# 디렉토리 이동
$ cd /sys/fs/cgroup
# 서브 디렉터리 생성 후 확인 확인
mkdir test_cgroup_parent && cd test_cgroup_parent
tree
#디렉토리를 생성하면 아래와 같이 cgroup에서 제어 가능한 항목이 자동으로 생성된다
├── cgroup.controllers
├── cgroup.events
├── cgroup.freeze
├── cgroup.kill
├── cgroup.max.depth
├── cgroup.max.descendants
...
├── memory.zswap.max
├── misc.current
├── misc.events
├── misc.max
├── pids.current
├── pids.events
├── pids.max
├── pids.peak
├── rdma.current
└── rdma.max
# cgroup에서 제어 가능한 항목 확인
$ cat cgroup.controller
cpuset cpu io memory hugetlb pids rdma misc
# 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
# 실행 결과
8444
9078
0::/test_cgroup_parent/test_cgroup_child
# 부하 발생
$ stress -c 1
# 모니터링 확인 부하가 적은 것을 확인할 수 있습니다.
$ htop
'Linux' 카테고리의 다른 글
[KANS] 컨테이너 네트워크 & IPTables (0) | 2024.09.01 |
---|---|
[Linux] Symbolic link 안전하게 제거하기 (0) | 2024.07.18 |
[Linux] nginx로 blue green 배포 흉내내기 (0) | 2024.07.11 |