Docker의 컨셉, 내부 구조, 활용 세미나 정리
사람의 욕심은 끝이 없고
사람들은 컴퓨터를 사용하면서 여러가지 욕구를 가지고 있습니다. 동시에 여러가지를 실행시키고 싶다는 욕구(맥 위에서 윈도우를 돌린다든가, 윈도우 위에서 리눅스를 돌린다든가…)도 있고 반대로 여러 PC위에서 정확히 똑같이 실행시키고 싶다는 욕구도 있죠. (제 자리에선 되는데요?) 사실 동시에 여러가지를 실행시키는 가장 단순 무식한 방법은 HW를 여러개 사서 하나에 하나씩 돌리는 방법이라는걸 생각해보면, 결국 HW에 상관 없이 프로그램을 돌리는 방법을 알 수 있다면 다 해결할 수 있지 않을까 하는 생각을 하게 됩니다.
먼저 등장한건 Hypervisor
를 이용한 가상 머신이었습니다. 처음 리눅스를 실습해볼 때 많이 사용하게 되는 Virtual Box
가 이런 가상 머신의 대표적인 예시지요. hypervisor는 guest OS들에게 Host가 가진 컴퓨팅 자원을 분배함과 동시에 guest OS의 App에서부터 내려온 커널의 작업을 번역해서 Host OS에 전달합니다.
이 가상 머신을 사진으로도 느낄 수 있듯 매우 크고 무겁습니다. 커널 디버깅을 해야한다든가등 OS자체가 필요하지 않은 경우에도 이렇게 큰 Guest OS를 돌려야 할 필요가 있을까요? OS와는 관계 없이 ‘어느 컴퓨터에서나 완벽하게 동일하게 실행되면 좋겠다’의 욕구는 어떤가요? 격리된 환경에서 지정된 라이브러리와 실행파일로 실행만 할 수 있으면 되지 않을까요? 이런 배경에서 컨테이너 기술이 등장합니다.
컨테이너 기술
컨테이너는 가상 머신에서 Guest OS에 해당하는 부분을 들어내고 Process 단위로 격리합니다. 각 격리된 공간(컨테이너)의 내부에는 실행시킬 애플리케이션을 구동하는데 필요한 라이브러리와 실행 파일만이 존재하게되지요. Host와 격리되어 있기 때문에 어느 환경에서든 동일하게 동작하게 됩니다. 컨테이너를 도커의 요소로 기억하는 경우도 많은데, 사실 docker는 이 컨테이너 기술을 구현한 것중 하나로 가장 널리 쓰이는 오픈소스입니다. 컨테이너라는 단어 자체도 좀 더 널리 쓰이곤 하지요. 저의 경우에는 Spring에서 IoC 컨테이너가 등록된 Bean들을 생성하고 관리했던 것처럼 이 Docker 컨테이너도 이미지의 목적에 맞게 프로세스를 생성/관리한다고 생각하니 같은 맥락으로 느껴졌습니다.
가상화를 목표한 다는점은 같지만, 하이퍼바이저가 OS 및 커널이 통째로 가상화되는 반면, Container는 FileSystem의 가상화만 이루어진다. Container는 Host PC의 커널을 공유하고, 따라서 init(1) 등의 프로세스가 떠있을 필요가 없으며, 가상화 프로그램과는 다르게 적은 메모리 사용량, 적은 Overhead를 보인다. 많은 벤치마크 결과가 입증하듯 Container는 Host PC의 자원을 격리(Isolation)된 상태 그대로 활용하기 때문에 VM에 비해 성능 저하가 눈에 띄게 적다.
docker hub : 레지스트리 서비스
사람들이 자주 사용하는 환경(우분투같은 OS, MySQL같은 DB등등…)은 도커 허브에 이미지가 올라와 있어서 이를 다운받아 손쉽게 사용할 수 있습니다. 마치 스마트폰에서 앱스토어(iOS), 플레이스토어(Android)에서 필요한 앱을 다운받아 실행시키는 것처럼 도커 허브에서 원하는 image를 받아 실행시켜 컨테이너를 만들면 됩니다. 다만, 저희 회사는 내부에 별도 허브를 가지고 모든 이미지는 이 내부 허브에서 가져와 쓰는 것을 ground rule로 가지고 있습니다.
기초 커맨드 정리
커맨드는 공식 Documentation을 참고하는게 좋을 것 같아 별도로 정리하지는 않겠습니다. 다만 어제 사용했던 커맨드 내용을 복기해가며 어떻게 사용하면 될지만 파악해보도록 할까요?
- pull : docker image 다운로드
- run : 이미지로부터 container 생성 & 실행
-
start stop : 기존에 실행되었던 컨테이너를 실행 / 중지
docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
docker run --name orders-service -e MYSQL_ROOT_PASSWORD=root -e MYSQL_USER=myname -e MYSQL_PASSWORD=mypassword -e MYSQL_DATABASE=orders-service -p 3308:3306 -d mysql:8
- 컨테이너의 이름
--name
: orders-service - 환경변수 설정
-e
- MYSQL_ROOT_PASSWORD=root
- MYSQL_USER=myname
- MYSQL_PASSWORD=mypassword
- MYSQL_DATABASE=orders-service
- 호스트의 3308포트와 컨테이너의 3306포트를 연결
-p
- detached 모드로 실행 (백그라운드 실행)
-d
- mysql이라는 이름 중 tag로 8이 붙은 이미지 사용
위의 커맨드를 통해 tag가 9이 붙은 mysql 이미지를 활용해서 orders-service라는 container를 만들고 실행시키게 됩니다. 이후에는 docker start orders-service
, docker stop orders-service
등을 사용해서 실행/중지를 하게 됩니다. 이름을 정해놓았더니 container ID대신 쓸 수 있어서 편리하네요.
Docker Engine
Docker를 사용하면서 다음과 같은 에러 메세지를 자주 보지 않으셨나요? 저는 이전 회사에서부터 이 메세지를 한번씩 보곤 하는데, 이번에 Docker를 세미나 주제로 선택하면서 이 메세지의 의미를 밝혀내야겠다고 생각했습니다. 그리고 이를 위해 컨테이너를 구축하고 실행하는 핵심 SW인 Docker Engine
에 대해 알아야합니다.
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
docker client
는 우리가 docker cli등에 입력한 명령어(eg.docker run
)를 적절한 API payload로 변환해서 dockerd에 post 요청하게 됩니다. 이 때/var/run/docker.sock
에 있는 유닉스 소켓을 통해 도커 데몬의 API를 호출하게 되지요. 즉, 제가 위에서 자주 본다고 했던 에러 메세지는 이 도커 데몬과 통신하기 위한 소켓에 연결할 수 없으니 데몬이 살아있는지 확인해보라는 메세지였던 것입니다.dockerd (docker deamon)
은 통신의 상대방인 도커 데몬은 도커 이미지의 관리, 이미지 빌드, REST API, 인증, 보안, 코어 네트워킹, 오케스트레이션 등 다양한 작업을 수행합니다. volumn이나 network등 컨테이너 사용에 필요 설정을 데몬이 합니다. 도커 프로젝트가 커지면서 이 dockerd는 더이상 직접 컨테이너를 실행시키지 않습니다. 대신에 만약 dockerd가 client로부터 ‘새로운 container를 생성하라’는 명령을 수신하면, containerd를 호출합니다.containered
은 컨테이너 데몬이라고 불러야겠죠? 현재는 docker에서 분리되어 오픈소스로 운영되고 있는 컨테이너 런타임입니다. 이게 실제로 컨테이너를 관장하게 되는데, 쿠버네티스에서 만든Container Runtime Interface(CRI)
를 구현합니다. 여기서 컨테이너의 생명 주기를 관장하는데, 이 때runC
를 사용합니다.runC
는 실제로 컨테이너를 만드는 기술들의 집합입니다. docker에서 컨테이너 기술을 개발하게 되면서 리눅스의 namespace, control group들과 같은 기술들을 많이 사용하게 되었는데, 이들을 하나의 low-level component로 묶고 runC라고 이름붙였다고 합니다. 오픈소스로 기부되어 하나의 standalone 프로젝트로서 플랫폼에 관계 없이 container를 구현하는 기술들을 가지고 있습니다. 실질적인 컨테이너도 여기서 생성됩니다.containerd-shim
은 생성된 container에 관여하게 되는 레이어입니다. runC는 컨테이너를 생성한 후 종료되는데, 이 시점부터 containered-shim 이 컨테이너의 부모 프로세스가 됩니다. 여기서 container의 STDIN/OUT을 유지하거나, deamon에게 container의 종료를 보고하는 등의 작업을 담당하게 됩니다.
여기까지 살펴보고 나니 우리가 현재 크게 docker라고 부르고 있지만 실제로 컨테이너 자체를 구현하는 기술은 Docker, Inc 빠져나와 대부분 오픈소스화 되었다는 것을 알 수 있네요. 현재의 도커 주식회사는 이런 오픈소스들을 더 편하게 쓸 수 있는 Interface를 제공하는 것을 업무로 하고 있다고 볼 수 있겠습니다.
배포 환경에 유용한 docker
사용법도 알았고 원리도 알았으니 이제 활용법을 알아보겠습니다. 현실에서 개발하실 때 docker는 주로 새로운 PC에서 개발 환경을 꾸릴 때와 배포할 때 자주 사용하실 것 같은데요, 여기서는 배포를 예로 들어보겠습니다.
새로운 AWS 인스턴스 하나를 만들고 우리가 만든 서비스 하나를 배포하려고 합니다.
- AWS에 접속
- 실행에 필요한 환경 준비 1
- 환경 준비 2
- 환경 준비 3
- 실행
우리의 서비스가 인기가 너무 많아서 scale out 해야 하는 상황이 온다면 매 인스턴스에 이 작업을 반복해야 하는데요, 많은 환경 설정 중 하나라도 잘못된다면, 배포 환경 버젼의 제 자리에선 되는데요?
가 발생하겠죠. 이 때 docker가 등장합니다. 필요한 환경을 만들어주는 image를 만든 후, 어디서든 해당 이미지로 컨테이너를 생성하면 동일한 환경을 쓸 수 있으니까요. 마치 내 캠핑카를 가고 산을 가든 바다를 가든 제가 항상 쓰는 침낭과 냄비가 있는 것처럼요.
여행에 필요한건 여권, 카드, 핸드폰
docker가 너무 좋은건 알겠어요. 근데 이미지를 매번 들고다니기엔 무겁지 않나요? 변경이 자주 일어나면 그 버젼관리도 너무 힘들 것 같구요. 그래서 사람들은 이미지를 통째로 들고 다니지 말고, 이미지의 레시피를 들고 다니면 어떨까? 하는 생각을 하게 됩니다. dockerfile
의 탄생입니다. 이미지를 들고 다니는 대신, 코드의 형태로 된 dockerfile을 들고다니고 이를 build해서 docker 이미지를 만들 수 있습니다. 여행을 갈 때도 짐을 캐리어에 바리바리 싸들고 가는 방법도 있지만, 여권과 카드 그리고 핸드폰만 있다면 나머지는 온라인으로 주문해서 숙소로 받을 수 있잖아요? 그렇게 편리한 시대가 되었습니다. 그리고 결정적으로 이 dockerfile은 Jenkinsfile을 생성하는데 사용 가능한 등 배포의 자동화를 더 편리하게 해줍니다.
이 이미지가 내용을 잘 설명해주는 것 같아서 가져왔는데요, code와 OS, 다른 의존성들을 보고 Dockerfile을 만들고 이 file을 이용해 docker build를 하면 docker engine이 docker image를 생성해줍니다. 그리고 이 이미지를 run해서 container를 생성할 수 있습니다. 그리고 이런 작업은 다른 사람의 컴퓨터에서도 그대로 수행할 수 있습니다.
Reference