본문 바로가기
스터디 이야기/Kubernetes Advanced Networking Study

[KANS] Container와 Docker

by lakescript 2024. 8. 28.
더보기

이 스터디는 CloudNet@에서 진행하는 KANS 스터디를 참여하면서 공부하는 내용을 기록하는 블로그 포스팅입니다.

CloudNet@에서 제공해주는 자료들을 바탕으로 작성되었습니다.

 

도커

도커란?

 

 

도커(Docker)는 소프트웨어를 구동시키기 위한 가상 실행 환경을 제공해주는 오픈 소스 플랫폼입니다. 도커에서는 이 가상실행 환경을 '컨테이너(Container)'라고 부릅니다. 조금 더 정확히 표현하는 용어는 '컨테이너화된 프로세스(Containerized Process)' 입니다.


이 컨테이너는 애플리케이션과 애플리케이션에 필요한 모든 종속성을 포함하며, 호스트 운영체제와 독립적으로 실행될 수 있습니다. 이를 통해 개발자는 코드가 어떤 환경에서도 일관되게 동작할 수 있도록 보장할 수 있습니다. 즉, Docker 플랫폼이 설치된 곳이라면 컨테이너로 묶인 애플리케이션는 어디서든 실행할 수 있습니다.

 

Docker Container VS VM

그렇다면 컨테이너와 비슷하게 가상 실행 환경을 구축하는 VM과는 무엇이 다를까요? 

https://www.docker.com/resources/what-container/

VM(Virtual Machine)

하드웨어 애뮬레이션

가상 머신은 호스트 운영체제 위에 하이퍼바이저(hypervisor)를 통해 하드웨어 레벨의 가상화를 지원하며, 그 위에 별도의 운영체제(Guest OS)를 설치하여 독립적으로 동작하며, 애플리케이션을 실행할 수 있습니다. 

 

독립적인 커널

가상 머신은 각자 독립된 운영체제 커널을 가지고 있으며, 이로 인해 리소스가 더 많이 소모될 수 있습니다.

 

-> 개별 VM은 독립된 OS를 사용하여 도커에 비해 고립성(보안)은 더 좋지만, 오버헤드가 크고 무겁고 느리다는 단점을 갖게 됩니다.

 

Docker Container

리눅스 커널 공유

Docker 컨테이너는 VM처럼 하드웨어를 애뮬레이션하지 않고, 호스트 운영체제의 커널을 공유하여 동작합니다. 즉, 각 컨테이너는 독립적인 프로세스로 실행되지만, 모두 같은 운영체제 커널을 사용합니다. (OS 레벨의 가상화 지원 및 개별적인 사용자 공간(user space) 할당) 이때, 가상화된 공간을 생성하기 위해 리눅스의 기능인 pivot-root, 네임스페이스(namespace), cgroup 를 사용함으로써 프로세스 단위의 격리 환경과 리소스을 제공합니다

 

경량화된 환경

Docker 컨테이너는 운영체제를 포함하지 않고, 애플리케이션과 그에 필요한 라이브러리 및 설정만 포함하기 때문에 가상 머신에 비해 훨씬 가볍고 빠릅니다. 즉, 컨테이너는 Guest OS와 하이버파이저가 없기 때문에 이로 인한 오버헤드를 줄임으로써 훨씬 더 가볍게 프로세스를 실행할 수 있고 컨테이너에 대한 복제와 배포가 더 용이합니다.

 

즉, 가상 머신은 하드웨어 애뮬레이션을 통해 독립적인 운영체제를 실행하는 반면, Docker 컨테이너는 운영체제의 커널을 공유하여 애플리케이션을 격리된 환경에서 가볍게 실행할 수 있게 됩니다.

 

Linux Process

프로세스는 실행 중인 프로그램의 인스턴스를 말하며, 각 프로세스는 고유한 PID(Process ID)를 가지며, 메모리 공간, 파일 디스크립터, 사용자 및 그룹 ID, 현재 작업 디렉토리, 환경 변수 등의 속성을 포함합니다. 또한, Linux는 프로세스를 트리 구조로 관리하기 때문에 부모 프로세스와 자식 프로세스의 구조로 동작합니다. 프로세스는 사용자 모드커널 모드에서 실행되며, 커널의 스케줄러에 의해 CPU와 Memory를 배정받아 실행됩니다. ,  프로세스는 CPU와 메모리를 사용하는 기본 단위로, OS 커널(Cgroup)에서 각 프로세스의 자원을 관리합니다.

 

amazing directory - /proc

리눅스의 /proc 디렉터리는 커널이 동적으로 생성하는 정보(시스템 상태, 프로세스(/proc/[PID), HW 정보 등)를 실시간 제공합니다.  

 

ls /proc

 

ls /proc 명령어는 Linux 시스템에서 /proc 디렉토리의 내용을 나열합니다. 이 디렉토리에는 각 실행 중인 프로세스에 대한 하위 디렉토리가 있으며, CPU 정보, 메모리 상태, 커널 설정 등 시스템 운영과 관련된 다양한 정보를 포함하는 파일들이 있습니다.

 

 

/proc/cpuinfo - 커널이 동적으로 생성하는 정보

CPU에 대한 정보가 포함되어있으며, CPU 모델, core 수, clock 등의 정보를 확인할 수 있습니다

/proc/meminfo - 커널이 동적으로 생성하는 정보

메모리 사용 현황을 보여주며, 전체 메모리, 사용중인 메모리, 사용 가능한 메모리, 캐시 메모리 등 다양한 메모리 관련 정보를 확인할 수 있습니다.

 

/proc/uptime - 실시간(갱신) 정보

시스템이 부팅된 후 경과된 시간을 초 단위로 보여줍니다.

첫 번째 숫자는 총 가동 시간, 두번째 숫자는 시스템의 유효시간 입니다.

 

/proc/loadavg - 커널이 동적으로 생성하는 정보

시스템의 현재 부하 상태를 나타냅니다.

첫 번째 세 개의 숫자는 1, 5, 15분간의 시스템 부하 평균을 의미하며, 네 번째 숫자는 현재 실행 중인 프로세스와 총 프로세스 수, 마지막 숫

자는 마지막으로 실행된 프로세스의 PID를 나타냅니다.

 

/proc/version - 커널이 동적으로 생성하는 정보

커널 버전, GCC 버전 및 컴파일된 날짜와 같은 커널의 빌드 정보를 포함합니다.

 

/proc/filesystems - 커널이 동적으로 생성하는 정보

커널이 인식하고 있는 파일 시스템의 목록을 보여줍니다.

 

/proc/partitions - 커널이 동적으로 생성하는 정보

시스템에서 인식된 파티션 정보를 제공합니다. 디스크 장치와 해당 파티션 크기 등을 확인할 수 있습니다.

 

프로세스(/proc/[PID]) 별 정보

더보기

/proc/[PID]/cmdline

해당 프로세스를 실행할 때 사용된 명령어와 인자를 포함합니다.
/proc/[PID]/cwd

프로세스의 현재 작업 디렉터리에 대한 심볼릭 링크입니다. `ls -l`로 확인하면 해당 프로세스가 현재 작업 중인 디렉터리를 알 수 있습니다.
/proc/[PID]/environ

프로세스의 환경 변수를 나타냅니다. 각 변수는 NULL 문자로 구분됩니다.
/proc/[PID]/exe

프로세스가 실행 중인 실행 파일에 대한 심볼릭 링크입니다.
/proc/[PID]/fd

프로세스가 열어놓은 모든 파일 디스크립터에 대한 심볼릭 링크를 포함하는 디렉터리입니다. 이 파일들은 해당 파일 디스크립터가 가리키는 실제 파일이나 소켓 등을 참조합니다.
/proc/[PID]/maps

프로세스의 메모리 맵을 나타냅니다. 메모리 영역의 시작과 끝 주소, 접근 권한, 매핑된 파일 등을 확인할 수 있습니다.
/proc/[PID]/stat

프로세스의 상태 정보를 포함한 파일입니다. 이 파일에는 프로세스의 상태, CPU 사용량, 메모리 사용량, 부모 프로세스 ID, 우선순위 등의 다양한 정보가 담겨 있습니다.
/proc/[PID]/status

프로세스의 상태 정보를 사람이 읽기 쉽게 정리한 파일입니다. PID, PPID(부모 PID), 메모리 사용량, CPU 사용률, 스레드 수 등을 확인할 수 있습니다.

 

Docker 아키텍처

https://docs.docker.com/get-started/docker-overview/

 

Docker Client (docker)

사용자가 주로 상호작용하는 인터페이스로, docker run, docker build등과 같은 명령을 dockerd에 전달하여 실행합니다.

 

Docker Daemon (dockerd)

Docker API 요청을 수신하고, 이미지, 컨테이너, 네트워크 및 볼륨을 관리합니다. 또한, 다른 데몬과 통신해 Docker 서비스를 관리할 수도 있습니다.

 

Docker Registries

Docker 이미지를 저장하고 관리하는 공간입니다. 공용 레지스트리(Docker Hub) 또는 사설 레지스트리를 사용할 수 있습니다.


Docker 실습

위의 Docker Architecture의 core라고 볼 수 있는  docker client와 docker daemon에 대해 조금 더 깊히 이해하기 위해 실습을 진행해보겠습니다.

실습 환경 구성

VPC 1개(퍼블릭 서브넷 2개), EC 인스턴스 1대(Ubuntu 22.04 LTS, t3.small)의 실습환경을 구성합니다.

 

Docker 설치

관리자 권한 전환

sudo su -
whoami
id

 

 

Docker 설치

curl -fsSL https://get.docker.com | sh

 

Docker 정보 확인 

Docker가 설치되면 위에 Docker Architecture에서 살펴본 Client 와 Server, Storage Driver(overlay2), Cgroup Version(2), Default Runtime(runc)에 대해 확인해보겠습니다.

docker info

docker version

 

도커 서비스 상태 확인

systemctl status docker -l --no-pager

 

 도커 루트 디렉터리 확인

Docker Root 경로인 /var/lib/docker를 tree 명령어를 사용해서 살펴보겠습니다.

 

 

 

Docker를 사용자로 관리하기

공식 문서를 참고하면 root 권한으로 Docker를 관리할 시에 여러 보안 이슈가 발생하여 권장하지 않는다고 합니다. 


Docker Clinet가 Docker Daemon으로 통신을 할 때에 이루어지는 통신은 TCP가 아니라 Unix Domain Socker을 사용합니다. 그렇기에 기본적으로 Unix Socket을 소유한 사용자는 Root 사용자미여 다른 사용자는 sudo 명령어를 통해서만 접근할 수 있습니다. 즉, Docker Daemon은 항상 Root 사용자로 실행됩니다

Unix Socker이란?
컴퓨터 운영 체제의 프로세스 간 통신(IPC, Inter-Process Communication)을 위해 사용되는 메커니즘입니다. 즉, OS 커널에 구현되어 있는 프로토콜 요소에 대한 추상화된 인터페이스, 장치 파일의 일종입니다.

 

일반 유저 ubuntu 로 실습 진행

일반 유저로 ubuntu 실행 환경에 접근합니다.

 

도커 서버 정보 획득 실패

docker info

 

위에서 관리자 권한으로 docker info 명령어를 이용하여 docker의 정보를 조회했을 때와 다르게 server 쪽에서 Error가 발생합니다.

 

이것은 일반 사용자는 Unix socket을 사용하지 못하기 때문에 Docker client에서 Docker Daemon으로 접근하려 했을 때 Error가 발생하는 상황입니다.

 

ls -l /run/docker.sock /var/run/docker.sock

리눅스는 모든 것이 파일로 관리되기 때문에 해당 파일을 살펴보겠습니다.

실행권한을 살펴보면 맨앞에 s가 붙은 것을 확인하실 수 있는데요. 이건 socket이라는 의미입니다.

 

file /var/run/docker.sock

 

file 명령어를 통해 보면 socket이라고 알려줍니다. 즉, 해당 파일(/var/run/docker.sock)을 이용해서 Docker Daemon을 이용할 수 있다는 것을 확인할 수 있습니다.

 

관리자 권한을 접속해서 아래의 명령어를 통해 Unix domain socket으로 실행되고 있는 프로세스들 중 docker를 grep으로 필터하여 확인해보겠습니다.

위의 결과와 같이 /run/docker.sock이 확인됩니다. 

 

그렇다면 unix domain socket이 무엇일까요?

 

Unix domain socket

Unix Domain은 동일한 시스템 내에서 실행되는 프로세스들 간의 통신을 의미하며, "Domain"은 로컬 시스템 내의 통신 영역을 나타냅니다. 즉, Unix Domain Socket이라는 이름은 네트워크 소켓과 유사한 API를 사용하면서도, 로컬 시스템 내에서만 동작하는 소켓이라는 의미입니다.

https://www.verycosy.net/posts/2023/09/unix-domain-socket

 

왼쪽의 상황을 살펴보면 Process 1이 Process 2와 TCP 통신을 하게 되면 layer를 여러개 거치게 됩니다. 하지만 오른쪽의 Unix를 살펴보면 Process 1에서 Process 2와 통신하기 위해선 로컬 환경의 프로세스간 통신이기에 하나의 socket만 거치면 통신이 가능합니다.

즉, Unix Domain Socket은 네트워크 소켓과 달리 로컬 환경에서의 프로세스 간 통신이기 때문에  속도와 메모리 측면에서 더 효율적입니다. 

 

docker 명령어 수행

일반 사용자가 docker 명령어를 사용하면 아래와 같이 permission denied가 발생하며 실행이 되지 않습니다.

 

 

docker group에 일반 유저 포함

그러면 어떻게해야 일반 유저 권한으로 docker를 사용할 수 있을까요? 생각보다 간단합니다. docker group에 일반 유저를 포함시키면 됩니다!

 

getent group | tail -n 3

getent 명령어로 현재 어떤 group이 있는지 확인해보겠습니다.

 

sudo usermod -aG docker $USER

usermod -aG 명령어를 통해 docker group에 현재 접속해있는 유저를 넣겠습니다. 이때 바로 적용이 되지 않고 한번 쉘을 닫았다가 켜야 적용됩니다.

 

 

ssh 재접속 후 확인

docker info

 

재접속 한뒤 docker info 명령어를 통해 client와 server를 확인해보겠습니다.

 

아까와는 다르게 server의 정보까지 잘 확인되는 것을 보실 수 있습니다.

 

 컨테이너가 host의 docker socket file 공유로 도커 실행

그렇다면 host에서 띄어진 container에서 docker에 접근하려면 어떻게 해야 할까요?

 

실제로 업무 하다보면 CI/CD Pipeline의 대명사라고 불리는 Jenkins를 통해 CI Pipeline에서 Docker build를 실행해야 하는 경우가 있습니다. 그럴 때 container 내부에서 docker를 또 설치할 수는 없는데요. (Docker-in-Docker (DinD)는 속도와 복잡성과 보안적인 이슈때문에 권장하지 않습니다.) 그럴 때 jenkins를 docker로 run할 때에 host의 /run/docker.sock 파일과 /usr/bin/docker 파일을 볼륨 마운트 시켜주면 됩니다. 

 

docker run --rm -it -v /run/docker.sock:/run/docker.sock -v /usr/bin/docker:/usr/bin/docker ubuntu:latest bash

위의 명령어를 통해 jenkins를 docker로 띄어줍니다.

docker info
docker run -d --rm --name webserver nginx:alpine
docker ps
docker rm -f webserver
docker ps -a

그 후 jenkins container에 접근해서 위의 docker 명령어를 실행하면 정상적으로 실행되는데요.  위의 환경을 자세히 살펴보면 docker가 host에서 실행되는 것이 아닌 격리된 container 환경에서 실행됩니다. 그렇기 때문에 container에서 호출에 필요한 /run/docker.sock 파일과/usr/bin/docker를 볼륨 마운트 시켜주었기 때문에 host의 docker deamon을 호출한다고 보면 됩니다. 

 

 

Docker의 기본 정보 확인

네트워크 정보 확인

ip -br -c addr

docker0라는 네트워크 인터페이스가 추가된 것을 확인하실 수 있습니다.

route 정보 확인

ip -c route

 

 

bridge 정보 확인

brctl show

 

iptables 정책 확인

iptables -t filter -S

 

 

iptables -t nat -S