도커를 응용하기 위한 작동 원리 및 베이스들
How Docker works and the foundations for applying it
써보면 느끼겠지만 도커는 정말 편리하다. 하지만, 구성환경이 복잡해질수록 커스텀 하기 복잡해지며 다루기 어려워진다. 그래서 단순히 사용하는 방법을 넘어 동작 원리를 이해할 필요가 있어 정리한다. 해당 내용을 옛날에 처음 봤을 때는 진짜 이게 뭔 소리지 했던 내용이다. 근데 계속해서 부딪히며 체감하다 보니 아 이게 그때 그거구나 하고 조금씩 쌓여 막상 지금 다시 이론을 보니 잘 이해가 되니 신기하기도 하다. 확실히 야생형이 습득하는데 최고인 거 같기도 하고 나한테 잘 맞는 것 같다. 단점은 시간도 좀 걸릴 수 있고 많은 시행착오가 필요한 거? 그래도 나중에 깨달았을 때의 희열이란… 말로 표현할 수 없지 않나
도커(Dcoekr)란?
Accelerate how you build, share, and run applications
애플리케이션을 더 쉽게 배포, 공유, 실행 할 수 있도록 설계된 컨테이너 기반의 오픈소스 가상화 플랫폼이다. 여기서 컨테이너라는 개념이 핵심이다.
일반적으로 컨테이너를 떠올리면 여러 개의 운송 상품들을 컨테이너에 넣고 쉽게 운송하는게 떠오를 것이다.
마찬가지로 도커에서도 여러 개의 프로그램, 실행환경을 컨테이너로 추상화하고 동일한 인터페이스를 제공하여 프로그램의 배포 및 관리를 단순하게 해준다.
즉, 도커 컨테이너는 코드와 모든 종속성을 패키지화하여 응용 프로그램이 한 컴퓨팅 환경에서 다른 컴퓨팅 환경으로 빠르고 안정적으로 실행되게 된다. 이렇게 되면 환경으로부터 격리되어 인프라에 관계없이 동일하게 실행할 수 있다는 장점이 있다.
이 도커 컨테이너를 생성하기 위해서는 도커 이미지라는 것이 필요한데 도커 이미지는 코드, 런타임, 시스템 도구, 시스템 라이브러리 및 설정과 같은 응용 프로그램을 실행하는 데 필요한 모든 것을 포함하는 가볍고 독립적이며 실행 가능한 소프트웨어 패키지이다.
도커 이미지가 도커 엔진에 의해 실행되면 도커 컨테이너가 생성되고 해당 도커 컨테이너를 이용하여 프로그램이 실행된다.(스냅샷과 실행 인스턴스라고 생각하면 될 것 같다.)
도커 사용 흐름
도커의 기본적인 사용 흐름은 다음과 같다.
- 도커 클라이언트(CLI)에서 명령어 입력( ex)docker run moomin )
- 클라이언트에서 도커 서버(Daemon)로 요청
- 서버에서 moomin이라는 이미지가 로컬에 cache 되어 있는지 확인
- 있으면 그 이미지를 이용해서 컨테이너를 생성하고 없으면 Docker Hub라는 곳에 가서 이미지를 가져와 로컬에 Cache로 보관.
- 생성된 컨테이너가 이미지에서 받은 설정이나 조건에 따라 프로그램 실행
도커와 기존 가상화 기술과의 차이
가상화 기술이 나오기 전에는 한대의 서버에 하나의 운영체제 하나의 프로그램만을 운영하며 남는 공간은 그대로 방치되었다. 안정적이지만 비효율적이라 할 수 있다. 하이퍼 바이저 기반의 가상화가 출현하면서 논리적으로 공간을 분할하여 VM(Virtual Machine)이라는 독립적인 가상 환경의 서버를 이용 가능하게 되었다. 여기서 하이퍼 바이저란 호스트 시스템에서 다수의 게스트 OS를 구동할 수 있게 하는 소프트웨어이다. 그리고 하드웨어를 가상화하면서 하드웨어와 각각의 VM을 모니터링하는 중간 관리자라고 할 수 있다.
하이퍼 바이저에는 네이티브 하이퍼 바이저와 호스트형 하이퍼 바이저가 있다.
네이티브 하이퍼 바이저 같은 경우 하이퍼 바이저가 하드웨어를 직접 제어하기 때문에 자원을 효율적으로 사용 가능하며, 별도의 호스트 OS가 없으므로 오버헤드가 적다. 하지만, 여러 하드웨어 드라이버를 세팅해야 하므로 설치가 어렵다는 단점이 있다.
호스트형 하이퍼 바이저는 일반적인 소프트웨어처럼 호스트 OS 위에서 실행되며, 하드웨어 자원을 VM 내부의 게스트 OS에 에뮬레이트 하는 방식으로 오버헤드가 크다. 하지만, 게스트 OS 종류(window, linux 등)에 대한 제약이 없고 구현이 다소 쉬워 일반적으로 많이 이용하는 방법이다.
하이퍼 바이저 기반의 VM 구조
하이퍼바이저에 구동되는 VM은 각 VM마다 독립된 가상 하드웨어 자원을 할당받는다. 논리적으로 분리되어 있어 한 VM에 오류가 발생해도 다른 VM에 영향이 가지 않고 각 VM마다 다른 OS를 설치할 수 있다는 장점이 있다. 도커도 이러한 가상화 기술에서 나온 컨테이너 가상화 기술이다.
컨테이너 가상화 기술 vs 기존 가상화 기술(VM)
VM과 비교했을 때 컨테이너는 하이퍼바이저와 게스트 OS가 필요하지 않기 때문에 더 가볍다. 또한, 애플리케이션을 실행할 때 컨테이너 방식에서는 호스트 OS 위에 애플리케이션의 실행 패키지인 이미지를 배포하기만 하면 되는데, VM 같은 경우 애플리케이션을 실행하기 위해 VM을 띄우고 자원을 할당한 다음, 게스트 OS를 부팅하여 애플리케이션을 실행해야 해서 훨씬 무겁게 실행된다.
도커 컨테이너와 가상 머신의 공통점으로는 기본 하드웨어를 격리된 환경내에서 애플리케이션을 배치한다는 것이고 가장 큰 차이점은 격리된 환경을 얼마나 격리 시키는지이다.
도커 컨테이너에서 돌아가는 애플리케이션은 컨테이너로 격리되어 있지만 여전히 같은 호스트의 다른 컨테이너와 동일한 커널을 공유한다. 결과적으로, 컨테이너 내부에서 실행되는 프로세스는 호스트 시스템에서 확인할 수 있다. 또한, 컨테이너가 전체 OS를 내장할 필요가 없는 결과 매우 가볍다.
가상 머신 같은 경우 호스트 운영 체제 또는 하피어바이저와 독립되어 있다. 그래서 이 특정 VM만을 위한 커널을 부팅하고 운영체제 프로세스를 시작해야 하기 때문에 응용 프로그램만 포함하는 컨테이너보다 VM의 크기를 훨씬 크게 만든다. 해당 방법은 비교적 사용법이 간단하지만 매우 느리다. (가상 머신을 써본 사람은 알겠지만 진짜 너무 느려서 답답해 미친다..)
어떻게 컨테이너를 격리 시키는걸까?
컨테이너를 과연 어떻게 격리 시키는 걸까? 먼저 Linux에서 사용되는 cgroup과 namespace에 대해 알아봐야 된다. cgroup은 CPU 메모리, Network BandWidth, HD io 등 프로세스 그룹의 시스템 리소스 사용량을 관리하는 기술이다. 즉, 어떤 애플리케이션의 사용량이 너무 많다면 cgroup을 이용해 사용량을 제한할 수 있다. namespace는 하나의 시스템에서 프로세스를 격리시켜 별개의 독립된 공간을 사용하는 것처럼 격리된 환경을 제공하는 경량 프로세스 가상화 기술이다. 이러한 기술들을 이용하여 컨테이너를 격리시킨다.
여기서 궁금증이 하나 생길 수 있다. 우리가 사용하고 있는 Host OS는 보통 Mac OS나 window 일텐데 어떻게 리눅스 기술인 cgroup과 namespace가 사용될 수 있는가? 사실 도커 내부는 다음과 같이 되어있다.
도커가 돌아갈 때 우리의 환경은 Mac OS/windows지만 도커는 결국 리눅스 환경에서 돌아가고 있다. 그래서 리눅스 커널을 기반으로 cgroup과 namespace도 사용할 수 있기 때문에 컨테이너를 분리시킬 수 있게 된다. CLI에 docker version을 한번 쳐보자. 도커 엔진이 linux/arm64 기반으로 돌아가고 있는 걸 확인할 수 있다.
이미지로 컨테이너 만드는 과정
지금까지 도커 정의와 동작 방식에 대해서 알아봤다. 이미지를 이용하여 컨테이너를 생성한다고 했다. 어떻게 이미지로 컨테이너를 만드는지에 대해 알아보자. 도커 명령어 같은 경우는 검색하면 바로바로 나오니깐 자세하게 설명은 하지 않고 나올 때마다 잠깐잠깐씩 설명하겠습니다.
이미지는 프로그램을 실행하는데 필요한 모든 것(설정이나 종속성 등)을 가지고 있다. 여기서 말하는 필요한 모든 것이란 뭘까?
- 컨테이너가 시작될 때 실행되는 명령어
- 파일 스냅샷(파일 스냅샷은 디렉토리나 파일을 카피한 것으로 예를들어, 컨테이너에서 스프링 부트 서버를 실행하고 싶다면 jar 파일 스냅샷)
이미지로 컨테이너 만드는 순서
- docker run <이미지> 명령어를 이용해 도커 이미지를 실행하게 되면이미지>
- 먼저 컨테이너가 생성되고 도커 이미지 파일 스냅샷에 있는 파일을 컨테이너 하드 디스크에 넣어준다.
- 그 후, 이미지에서 가지고 있는 명령어를 이용해서 해당 파일을 실행시키게된다.
도커 이미지 생성
그렇다면 과연 컨테이너에 기반이 되는 이미지는 어떻게 만드는 걸까?
- Dockerfile 작성
- Dockerfile이란 도커 이미지를 만들기 위한 설정 파일로 컨테이너가 어떻게 행동해야 되는지에 대한 설정들을 정의한 파일이다.
- Dockerfile에 입력된 것들을 도커 서버로 전달하기 위해 도커 클라이언트에서 docker build . 명령어를 입력하게 되면
- 이미지가 생성된다.
1. Dockerfile을 만들어보자
도커 파일은 도커 이미지를 만들기 위해 작성하는 것이므로 도커 이미지가 필요한 것이 무엇인지를 생각해 봐야 된다.
- 베이스 이미지를 명시
- 도커 이미지는 여러 개의 레이어로 되어 있는데 그중 베이스 이미지는 이 이미지의 기반이 되는 부분이다.
- 예를 들어, 프론트 도커 이미지를 만들기 위해서는 node, 서버 도커 이미지를 만들기 위해서는 jdk를 기반으로 해볼 수 있다.
- 추가적으로 필요한 파일을 다운 받기 위한 몇 가지 명령어를 명시
- 컨테이너 시작 시 실행될 명령어를 명시
기본적인 형식은 다음과 같다.
# 베이스 이미지를 명시
FROM baseImage
# 추가적으로 필요한 파일들을 다운로드
RUN command
...
# 컨테이너 시작시 실행 될 명령어를 명시
CMD [ "executable" ]
2. 도커파일로 도커 이미지 만들기
위에서 만든 도커파일과 build 명령어( ex)docker build . )를 이용해 이미지를 생성해준다. build 명령어는 해당 디렉토리 내에서 dockerfile이라는 파일을 찾아서 도커 클라이언트에 전달시켜준다. 간단한 도커파일을 한번 빌드해보자. 빌드하는 과정을 보면 이미지를 생성하기 위해 중간중간 임시 컨테이너를 사용하고 삭제하는 걸 확인할 수 있다.
FROM alpine
CMD [ "echo", "moomin2" ]
- Step1에서 첫 번째 임시 컨테이너(ace17…)
- Step2에서 두 번째 임시 컨테이너(46884..)
를 사용한 후에 임시 컨테이너를 삭제 후 최종 컨테이너(d1f5c4…)를 이용해 이미지를 생성한다.
즉, 베이스 이미지로부터 다른 종속성이나 새로운 커맨드를 추가할 때 임시 컨테이너를 만든 후 그 컨테이너를 토대로 새로운 이미지를 만들고 그 임시 컨테이너는 삭제된다.
위에서 배운 걸로 간단히 node.js 애플리케이션 띄워보자
nodejs 앱은 이미 구현되어 있다고 가정하고 Dockerfile을 먼저 작성해 보자.
FROM node:10
# 해당 애플리케이션과 관련된 별도의 폴더를 사용하기 위해 전환
WORKDIR /usr/src/app
# package.json, 다른 파일들(server.js) 같은 파일들울 컨테이너안 /usr/src/app에 복사
COPY ./ ./
RUN npm install
CMD ["node", "server.js"]
위처럼 작성 후 docker build -t app ./ 명령어를 이용해 이미지를 생성한다. 그리고 만들어진 이미지를 실행하기 위해서 docker run -p 49160:8080 app 명령어를 실행한다. 해당 옵션어로 명령어를 실행하게되면 다음 구조와 같이 컨테이너가 실행된다.
그 후 http://localhost:49160에서 잘 실행이 되면 끝.
재빌드 효율성을 높이는 방법이나 volume 같은 건 이번 포스트에서 다루지 않겠다. 일단 간단한 예제를 통해 알아봤고 조만간 복잡한 docker 환경(docker compose) + CI, CD 파이프라인을 구축해야 될 일이 있어서 시간이 나면 해당 포스트를 다뤄보려고 한다. 어쨌든, 이 뒤부터는 위의 내용에서 응용 레벨이기 때문에 위의 내용을 잘 이해했으면 충분히 잘할 수 있을 것이다!
참고:
*틀린 부분이 있으면 언제든지 말씀해 주시면 공부해서 수정하겠습니다.