KANS3) 컨테이너 동작에 대하여
올 초 이후로, 오랜만에 다시 스터디를 시작했다. 첫 주차의 내용은 컨테이너에 대해 공부하고, 그에 대해 정리하였다. 🔗
도커와 컨테이너
Docker는 가상 환경인 컨테이너를 실행하고 관리하는 오픈 소스 프로젝트이다. 컨테이너는 가상화와 다르게, os를 호스트의 os로 공유하여 사용한다. 그래서 각 컨테이너는 os가 필요없이 어플리케이션 코드와 그에 필요한 라이브러리만 존재하게된다. 이렇기에 장점으로는 가볍지만, 단점으로는 커널단이 공유되기 때문에 비교적 보안에 취약하다. 하이퍼바이저 가상화의 경우, 하드웨어 레벨에서 가상화하여 여러 guest os가 올라갈 수 있도록 한다. 이와 다르게 컨테이너의 경우, 하드웨어가 아닌 호스트의 리눅스 커널을 공유하며 여러 어플리케이션이 동작할 수 있다.
컨테이너와 리눅스 커널
컨테이너는 기본적으로 호스트 커널을 공유하지만, 각각 user space를 가지게 된다. 각각의 user space를 가지기 위해 pivot-root, namespace, cgroup의 기능을 사용하여 프로세스 단위로 격리 환경을 제공한다.🔗 프로세스 단위로 격리 환경이 제공되기 때문에, 하나의 격리된 프로세스가 컨테이너라고 할 수 있다. 호스트 os에서 프로세스 정보를 확인해보면, 올려 둔 컨테이너 프로세스도 확인 할 수 있다. 물론, 컨테이너 내부에서의 pid 와는 다르다. 이렇게 호스트 os를 공유하기 때문에, 각 컨테이너의 정보도 호스트 os에서 확인 할 수가 있다. 리눅스 /proc 파일 시스템에는 현재 커널이 메모리에서 사용하고 있는 모든 자원들에 대한 정보를 볼 수 있다. 그래서 proc하위의 현재 올라와있는 프로세스의 pid의 폴더들이 있는데, 그 하위에는 각각의 정보로 구성되어있다. 예를 들어, /proc/PID/cmdline 는 해당 프로세스가 실행하고 있는 명령이나 프로그램을 확인 할 수 있다. 이런 식으로 각각의 컨테이너에서 자원 사용 현황 등을 확인 할 수 잇다.
/proc 하위에 각 프로세스의 PID번호의 폴더들이 구성되어있고, 그 하위에는 프로세스에 대한 정보를 확인 할 수 있다.
실행한 nginx container PID를 찾아서 하위의 정보를 확인해 본 결과
도커 설치
root@MyServer:~# curl -fsSL https://get.docker.com | sh
# Executing docker install script, commit: 0d6f72e671ba87f7aa4c6991646a1a5b9f9dae84
+ sh -c apt-get update -qq >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ca-certificates curl >/dev/null
+ sh -c install -m 0755 -d /etc/apt/keyrings
+ sh -c curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" -o /etc/apt/keyrings/docker.asc
+ sh -c chmod a+r /etc/apt/keyrings/docker.asc
+ sh -c echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu jammy stable" > /etc/apt/sources.list.d/docker.list
+ sh -c apt-get update -qq >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-ce-rootless-extras docker-buildx-plugin >/dev/null
+ sh -c docker version
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
위와 같이 도커는 리눅스 커널의 여러 기능을 사용해야하기 때문에 root 권한으로 실행된다. 컨테이너에서 root라 하더라도 격리된 공간으로 권한이 제한되지만, 호스트에서는 root 권한을 사용 할 수 있기에 보안상의 위협이 있을 수 있다. 그래서 도커 공신문서에서도 rootless mode로 실행하는 방안에 대해 안내를 하고 있다. 🔗
도커 동작 방식을 보면 docker cli를 날리면 docker daemon 을 호출하기 위해 rest api endpoint로 접근한다. 이때, 127.0.0.1에 바인딩된 tcp 소켓이 아닌 unix 소켓을 사용한다. 왜그런지 이유는.. . tcp 소켓을 사용하는 경우 csrf(cross site request forgery) 공격 받기 쉽기 때문이라고, 독스에 나와있다. 🔗 원격 접속 가능해지면, 외부에서 docker daemon에 접근해 악의적인 명령을 실행 할 수 있기 때문 unix 소켓을 사용하여 로컬로 접근 제한을 하는 것으로 보인다.
unix socket
uds(unix domain socket)은 동일한 시스템 내에서 실행되는 프로세스 간 통신을 위해 사용되는 소켓이다. 로컬에서만 동작해서 local socket이나 IPC(Inter-Process Communications)소켓이라 하기도 한다.
tcp sockek와 다른 점은 단일 프로세스 간 통신을 위해 만들어졌기 때문에 네트워크 스택을 거치지 않고 커널 내에서 직접 통신하므로 네트워크 오버헤드가 없이 빠르다. tcp socket과 인터페이스가 유사해서 tcp 통신에서 사용하던 함수에 ip:port가 아닌 name.sock으로 넣어도 tcp처럼 작동된다. (그렇다고 ip:port 통신한다는 것이 아니라… 소켓파일로 통신)
이러한 유닉스 소켓을 사용해서 컨테이너에서 다른 컨테이너를 관리 할 때 사용 할 수도 있다. 예를 들어 cicd 작업의 경우, cicd 툴이 올라가 있는 컨테이너는 호스트에서 다른 컨테이너를 생성 등의 권한이 필요하다. 이 때, 볼륨을 유닉스 소켓을 붙여 사용하는 것이다.
DooD(Docker Out Of Docker) 와 DinD 🔗
컨테이너에 docker.socket을 볼륨으로 붙여서 실행하는 것을 DooD라고 한다. 떠있는 컨테이너에서 docker cli 를 날리게 되면 호스트 시스템의 docker daemon이 호출되어 호스트에서 컨테이너가 생성된다.
root@MyServer:~# docker run --rm -it -v /run/docker.sock:/run/docker.sock -v /usr/bin/docker:/usr/bin/docker ubuntu:latest bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
31e907dcc94a: Pull complete
Digest: sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab63ee
Status: Downloaded newer image for ubuntu:latest
root@bd82d8318a4b:/# docker info
Client: Docker Engine - Community
Version: 27.2.0
Context: default
Debug Mode: false
Server:
Containers: 1
...
Total Memory: 1.857GiB
Name: MyServer
ID: 9810e509-651c-4b23-9b86-4e99fec2bc42
Docker Root Dir: /var/lib/docker
Debug Mode: false
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false
root@bd82d8318a4b:/# docker run -d --rm --name webserver nginx:alpine
Unable to find image 'nginx:alpine' locally
...
Status: Downloaded newer image for nginx:alpine
edd74d3aeb73b64b9e61895e73d4201e20ff754bd2e15112d4eec47cdc49eea0
root@bd82d8318a4b:/#
# 컨테이너 내에서 nginx 컨테이너를 만들은 것을 확인
root@bd82d8318a4b:/# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
edd74d3aeb73 nginx:alpine "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 80/tcp webserver
bd82d8318a4b ubuntu:latest "bash" 2 minutes ago Up 2 minutes cranky_lewin
root@bd82d8318a4b:/# exit
exit
# 호스트에서 nginx 컨테이너를 만들은 것을 확인
root@MyServer:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
edd74d3aeb73 nginx:alpine "/docker-entrypoint.…" 31 seconds ago Up 30 seconds 80/tcp webserver
bd82d8318a4b ubuntu:latest "bash" 55 seconds ago Up 54 seconds cranky_lewin
비슷한 방식으로는 DinD(Docker in Docker)가 있는데, 이 경우는 호스트 시스템의 docker daemon과는 별개로 컨테이너 내에 docker daemon을 실행 시키는 것이다. 이 때, 호스트 시스템의 권한을 얻을수 있게 –privileged옵션을 사용해야한다. privileged를 가진 컨테이너가 실행되면 컨테이너와 호스트 간의 격리가 덜 되기도하고, 위에서 말한 것처럼 호스트와 같이 root 사용자와 동일한 권한으로 실행되기 때문에 보안적으로 문제가 생길 수 있다. 이런 이유들로 DinD보다는 DooD 방안을 더 권장하는 분위기다.
root@MyServer:~# docker run --privileged --name dind-container -d docker:dind
70c421b51eae9dd33443dfd181dcd99a6ef33e45f8d68dcb2aca05e470956f98
root@MyServer:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
70c421b51eae docker:dind "dockerd-entrypoint.…" 6 seconds ago Up 5 seconds 2375-2376/tcp dind-container
root@MyServer:~# docker exec -it 70 sh
/ # docker run -d --rm --name webserver nginx:alpine
Unable to find image 'nginx:alpine' locally
alpine: Pulling from library/nginx
...
Status: Downloaded newer image for nginx:alpine
3106590f6649bc8fbec7675eacdc70ba62672d74337705ead81d274c4193e115
/ # docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3106590f6649 nginx:alpine "/docker-entrypoint.…" 55 seconds ago Up 54 seconds 80/tcp webserver
# 호스트에서는 nginx 컨테이너가 안보이는 것을 확인
root@MyServer:~# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
70c421b51eae docker:dind "dockerd-entrypoint.…" 2 minutes ago Up 2 minutes 2375-2376/tcp dind-container