[docker] part 5. docker architecture

Minyoung kim·2024년 12월 9일
0

devops

목록 보기
6/6

docker engine

docker engine의 구성

  • CLI
    : CLI는 command Line Interface로 지금까지 실행했던 명령 터미널입니다. 컨테이너의 실행, 중지 및 이미지 제거 등에 사용됩니다. 또한 rest api를 이용해 docker daemon과도 통신합니다.
  • Rest Api
    : docker api는 프로그램이 사용하는 api로 daemon과 통신하고 명령어를 제공할 때 사용됩니다.
  • Docker Daemon
    : docker daemon은 docker 객체인 이미지와 컨테이너, 볼륨, 네트워크 등을 관리하는 백그라운드 프로세스입니다.

그러나 도커 CLI는 반드시 같은 호스트에 존재하지 않아도 됩니다!
CLI는 다른 노트북 등 다른 시스템에서도 여전히 docker engine과 작업할 수 있습니다.

docker run -H=10.123.2.1:2375 nginx

위와 같이 -H 옵션을 사용하여 접근할 호스트의 주소와 port번호를 입력하고, 그 위에 실행하고자 하는 명령어를 입력해주면 외부 호스트의 도커 엔진에도 접근할 수 있습니다.

docker container의 구성

docker는 namespace로 공간을 구분합니다. 따라서 process ID, mount, network 등등이 독립된 namespace에 생성되기 때문에 컨테이너가 각각 분리될 수 있습니다.

namespace - pid

namespace의 분리 기술 중 namespace 프로세스 id(pid)에 대해 살펴보겠습니다.

리눅스 시스템이 부팅되면, pid가 1인 프로세스가 생성되고 시작됩니다. 그리고 이 pid가 1인 프로세스는 시스템 내부의 다른 모든 프로세스를 시작하는 루트 프로세스가 됩니다.

pid는 고유하며, 프로세스 두개가 같은 pid를 가질 수는 없습니다.

이러한 리눅스 시스템 내부에서 새로운 자식 시스템 컨테이너를 생성하면, 이 자식 시스템은 자체적으로 독립된 시스템으로 취급됩니다.

자식 시스템 컨테이너 내부에서 실행되는 프로세스들은 위 예시 사진에서 볼 수 있듯이 앞서 살펴본 리눅스 시스템과 마찬가지로 시스템이 부트되며 pid가 1인 루트 프로세스가 생성되고, 이 루트 프로세스에서 자식 프로세스가 파생되어 실행됩니다. 이 프로세스들은 모두 고유의 프로세스입니다.

그러나 자식 컨테이너와 기본 리눅스 시스템 호스트은 물리적으로 분리되지는 않습니다.
자식 컨테이너 시스템에서 pid가 1,2,,,인 프로세스가 실행되지만, 이는 기본 호스트와 분리된 별도의 프로세스가 아니라, 결국 기본 호스트에서 실행되는 프로세스입니다. 그렇다면 pid가 같은 두개의 프로세스는 존재할 수 없다고 했는데, 어떻게 이것이 가능한 것일까요?

이때 필요한 것이 NameSpcae입니다. 사실, 컨테이너 내부에서 프로세스가 생성되고 실행되면, 이것은 기본 리눅스 시스템의 또다른 프로세스이기 때문에 순차적으로 pid를 부여받게 되고, 위 예시 사진을 참고하면 5,6을 받게 됩니다.
하지만 컨테이너 namespace에는 컨테이너 내부에서만 볼 수 있도록 pid 5,6인 프로세스가 pid1,2로 구분되어 있습니다. 따라서 이 컨테이너 시스템을 루트 프로세스를 가진 독립 시스템이라고 간주할 수 있습니다.
즉, 모든 프로세스는 같은 호스트 시스템에서 실행되지만, 각각의 컨테이너 시스템이 가지는 namespace에 의해 분리되는 겁니다.

예를 들어 nginx를 컨테이너로 실행하면, nginx 컨테이너 내부에서는 이 프로세스가 pid 1로 생성되어 실행되지만, docker host 내부적으로 이 프로세스를 살펴보면 pid가 1이 아닌 다른 숫자로 생성되어 있습니다.

cgroups

도커 호스트 위에 올라간 컨테이너는 호스트 시스템의 cpu와 메모리 등의 시스템 리소스를 공유합니다. 기본적으로 컨테이너가 사용할 수 있는 리소스는 제한이 없습니다.
컨테이너는 결국 기본 호스트의 모든 리소스를 사용할 수 있습니다.

하지만 컨테이너가 사용할 cpu나 memory의 양을, 컨테이너를 실행할 때 제한해줄 수는 있습니다.
docker는 Cgroups, 즉 제어 그룹을 사용하여 각 컨테이너에 할당된 하드웨어 리소스의 양을 제한합니다.

docker run --cpus=.5 ubuntu

위와 같은 명령어를 입력하면, 해당 컨테이너가 cpu 리소스의 사용 시간 중 50%를 넘기지 못하도록 합니다.

docker run --memory=100m ubuntu

메모리의 양도 제한해줄 수 있습니다.
위와 같이 명령어를 입력하면 ubuntu 컨테이너가 도커 호스트의 memory를 100m를 넘게 사용하는 것을 제한합니다.

Demo

예를들어 tomcat 서버를 실행시켜 보겠습니다.

docker run -d --rm -p 9999:8080 tomcat:8.0

여기서 exec명령어를 사용하면 docker 컨테이너 안에서 명령어를 실행할 수 있습니다.

docker exex 5a5f ps -eaf

5a5f는 실행되고 있는 톰캣 컨테이너 id입니다. -eaf 옵션을 사용하면, 컨테이너에서 실행되는 모든 프로세스 목록을 표시합니다.

(이미지는 따로 없지만)명령어를 입력해보면 컨테이너 내부에서는 프로세스 id가 1로 실행되고 있는 것을 볼 수 있습니다.

이 컨테이너에서 실행되고 있는 프로세스를 docker host에서도 확인해보겠습니다.

ps -eaf | grep {프로세스}

이 때 도커 호스트에서 다른 프로세스 id로 실행되는 것을 확인할 수 있습니다.

Docker의 File System 관리

기본적으로 도커를 설치하면 root direcotry내에 /var/lib/docker 가 구성됩니다. 그 안에 aufs, containers,image, volmues 등등이 생성되고 이는 모두 도커의 컨테이너, 도커 이미지, 도커 컨테이너에서 생성한 볼륨 등을 저장하는데 사용됩니다. aufs는 스토리지 드라이버입니다.

Layered Architecture

예를 들어 다음과 같은 도커파일을 build했다고 가정해보겠습니다.

생성되는 레이어는 위와 같습니다.

이 때 application의 소스코드만 변경하여 다시 빌드하고자 합니다.

FORM Ubuntu

RUN apt-get update && apt-get -y install python

RUN pip install flask flask-mysql

COPY app2.py /opt/source-code

ENTRYPOINT FLAS_APP=/opt/source-code/app2.py flask run

이 때 docker에서는 첫 세 레이어를 다시 생성하지 않고 대신 캐시에서 첫 애플리케이션의 첫 세 레이어를 불러온 다음에 새로운 소스 코드와 진입점이 있는 마지막 두개의 레이어만 새로 생성합니다. 이를 통해 이미지를 빠르게 생성하고 디스크 공간을 효율적으로 사용할 수 있습니다.

이러한 레이어들을 도커 이미지를 구축하기 위한 레이어들로, 한번 구축이 완료되면 레이어 내용을 수정할 수 없고 읽기 전용(read-only)상태로 전환되기 때문에 수정하고자 하는 경우 새로운 빌드를 통해 수정할 수 있습니다.

쓰기 가능 layer

그리고 이러한 이미지 레이어를 기반으로 도커 컨테이너를 생성하며, read-only의 이미지 레이어 위에 쓰기 가능한 새로운 레이어를 생성합니다. 이 쓰기 레이어에는 각 컨테이너에서 생성한 데이터가 저장됩니다.

애플리케이션에서 생성된 임시 파일, 사용자가 수정한 파일, 로그 파일 등이 저장되는데, 이는 컨테이너가 활성화되어 있을때에만 유효하며, 컨테이너가 없어지면 레이어와 그 위에 저장된 모든 변경사항도 삭제됩니다.

즉, 특정 동일한 이미지로 생성한 컨테이너들은 모두 같은 레이어를 공유하게 되며, 다시 반복하자면 이는 read-only입니다. 여러 컨테이너가 같은 레이어를 공유하기 때문에, 레이어가 수정되거나 하면 다른 실행중인 컨테이너에 영향을 미칠 수 밖에 없을 것입니다. 그리고 각 컨테이너에서 변경하거나 수정, 생성된 데이터들은 이미지 레이어 위에 각 컨테이너 별로 쓰기 레이어가 생성되어 이곳에 저장됩니다.

그렇다면, 컨테이너를 실행한 뒤에 변경 사항 테스트를 위해 코드를 수정하고 싶다면 어떻게 할 수 있을까요? 이 경우, image layer에 있는 소스 코드 파일을 변경하는 것이 아니라, 해당 소스 코드 파일을 쓰기가 가능한 contianer layer에 사본으로 생성하여 소스 코드의 변경 사항을 적용됩니다. 이러한 매커니즘을 copy-on-wirte(COW)라고 합니다.
이미지 레이어는 읽기 전용이므로 레이어 내 파일은 이미지 자체에서는 수정할 수 없기 때문에, 이미지는 계속 같은 상태로 머물고, 이후에 이 변경된 소스 코드를 가지고 이미지를 재구축한 다음에 컨테이너를 실행하면 변경 사항을 컨테이너에 반영할 수 있게 됩니다.

Volume

그러나 쓰기 레이어에 저장되어 있는 수정된 임시 소스 코드 파일은 컨테이너가 종료되는 즉시 사라지게 됩니다. 이 데이터를 보존하기 위해서는 어떻게 해야할까요?
컨테이너에 영구 volume을 추가하면 됩니다.

docker volume create data_volume

해당 명령어를 입력하면, 위에서 설명한 /var/lib/docker/volumes 폴더 아래 data_volume이라는 볼륨 폴더가 생성됩니다.

docker run -v data_volume:/var/lib/mysql mysql

-v 옵션을 사용하면 컨테이너 내의 데이터들을 영구 볼륨 폴더에 마운트할 수 있습니다.
위 명령어는 mysql 컨테이너를 생성하고 실행할 때, 컨테이너 내에 있는 /var/lib/mysql 아래 data들을 data_voulme에 매핑하도록 합니다. 데이터베이스에 쓰인 모든 데이터는 도커 호스트의 영구 볼륨에 저장되며, 컨테이너가 종료되어도 여전히 남아있습니다.

docker run -v data_volume2:/var/lib/mysql mysql

만약 data_volume2라는 폴더를 따로 생성해주지 않고 위와 같은 명령어를 입력하면, 도커는 기본적으로 도커 호스트의 /var/lib/docker/volumes/ 아래에 data_volume2을 자동 생성하여 마운트 합니다.

docker run -v /data/mysql:/var/lib/mysql mysql

만약 데이터를 볼륨에 저장하는 것이 아니라 특정 경로에 저장하고 싶다면 위와 같이 명령어를 입력해줄 수도 있습니다. 마찬가지로 -V 옵션을 사용하고, 데이터를 저장하고자 하는 폴더의 전체경로를 입력하고, 컨테이너 내부 데이터 경로를 입력해줍니다.

볼륨 디렉토리에 볼륨을 마운트하는 것을 볼륨 마운팅 , docker host의 원하는 곳에 있는 디렉토리에 데이터를 마운트하는 것을 바인드 마운팅이라고 합니다.

-mount

-v 옵션 외에도 새로운 방식으로 --mount 옵션을 사용할 수 있습니다.

docker run --mount type=bind,source=/data/mysql, target = /var/lib/mysql mysql

위와 같이 type, source(호스트 상의 위치), target(컨테이너 상의 위치)을 지정해주어 데이터를 마운트해줄 수 있습니다. type에는 앞서 말했듯이 볼륨과 바인드 type이 있습니다.

Storage drivers

이 모든 것, 계층형 아키텍쳐 관리, 레이어간 파일 이동과 복사 등의 모든 작업은 docker의 storage driver를 통해 가능해집니다.

storage driver의 종류는 다음과 같습니다.

  • AUFS
  • ZFS
  • BTRFS
  • Device Mapper

스토리지 드라이버는 운영 체제에 따라 다르게 쓰입니다. AUFS 같은 스토리지 드라이버는 Fedora나 CentOs와 같은 운영체제에서는 사용이 불가능합니다. 이 경우 예를 들면, device mapper 같은 것을 사용해줄 수 있습니다.
docker는 운영 체제에 가장 적합한 스토리지 드라이버를 선택해주기도 하지만, 드라이버마다 성능과 안정성이 조금씩 다르기 때문에 애플리케이션과 조직에 적합한 드리이버를 선택해서 사용할 수 있습니다.

AUFS

storage driver 중에 aufs에 조금 더 알아보도록 하겠습니다.
var/lib/docker 폴더안에 aufs 폴더가 생성되어 있고, 그 아래 다음과 같은 폴더가 있습니다.

  • diff
    : 각 계층의 컨텐츠가 저장됩니다. 예를 들어 어떤 어플리케이션 스크립트 파일이 있는 이미지를 실행한다고 했을 때, 소스코드 파일은 diff 폴더에 저장됩니다. 그리고 컨테이너를 통해 실행하는 것 뿐만 아니라 도커 호스트에서 바로 해당 파일에 접근할 수도 있습니다.
  • layers
    : 이미지 레이어가 어떻게 쌓여있는지에 대한 meta data를 가지고 있습니다.
  • mnt
    : 마운트 포인트 정보를 저장합니다.

docker system df

docker system df명령어를 입력하면, 도커의 디스크 사용 용량을 보여줍니다.
이미지, 컨테이너, 로컬 볼륨 등의 디스크 용량을 알려줍니다.

docker system df -v 명령어를 입력하면, 각 이미지의 디스크 사용 용량을 알려줍니다.

Docker Network

docker를 설치하면 자동으로 브리지, none(null), host 총 3개의 네트워크를 생성합니다.
브리지는 컨테이너에서 사용하는 기본 네트워크입니다.

컨테이너를 다른 네트워크에 연결하고자 한다면, netwokr 명령어 매개변수로 네트워크 정보를 지정합니다.

docker run Ubuntu --network={네트워크}

Bridge network

브릿지 네트워크는 docker가 호스트에 생성한 프라이빗 내부 네트워크입니다.
모든 컨테이너는 기본적으로 이 네트워크에 연결되며, 보통 172.17로 시작하는 내부 ip를 할당 받으며, 이 내부 ip를 통해 컨테이너들끼리 네트워크 안에서 통신할 수 있습니다.

외부에서 이 네트워크에 접속하려면, host의 port를 컨테이너의 port와 mapping 시켜줄 수 있습니다.
또다른 방법으로 컨테이너를 host 네트워크에 연결하는 방법이 있습니다.
예를 들어 호스트 네트워크를 사용하는 웹 앱 컨테이너에서 웹 서버를 5000번 port에서 동작시키고외부에서 접근하는 경우 별도의 포트매핑 없이 docker host의 ip로 5000번 포트로 접속할 수 있습니다. 기존의 방법처럼 호스트 네트워크에서 내부 브리지 네트워크로 연결되는 것이 아니라, 호스트 네트워크를 사용하고 있는 웹 서버에 접근하는 것이기 때문입니다.

네트워크 분리

만약 이 컨테이너들을 호스트 내부에서 격리시키고 싶다면 어떻게 할 수 있을까요?
예를 들어 위 사진과 같이 두 컨테이너는 내부 네트워크 172에 있고, 다른 두 네트워크는 182에 있는것으로 격리시키고자 한다면 말이죠.

기본적으로 docker는 하나의 내부 브리지 네트워크만 생성합니다.
따라서 명령어를 사용해 내부 네트워크를 생성하려면 다음과 같이 입력할 수 있습니다.

docker network create -- driver bridge --subnet 182.18.0.0/16 custom-isolated-network`

드라이버를 bridge로 지정하고, 네트워크 서브넷을 입력하고, 네트워크명을 입력합니다.

EMbeded DNS

컨테이너는 서로 이름을 통해 접근할 수 있습니다.
예를 들어 web server container에서 mysql 컨테이너에 접근하고자할 때,
mysql.connect(172.17.0.3)으로 접근할 수 있습니다. (해당 ip 주소는 mysql 컨테이너의 ip주소라고 가정해보았습니다. )

그러나 시스템이 재부팅되었을 때, 컨테이너가 이전과 동일한 ip를 가지게 된다는 보장이 없기 때문에 좋은 방법이 아닙니다.

대신 컨테이너의 이름을 통해 접근하는 것이 좋습니다. 도커 호스트에 있는 모든 컨테이너는 이름을 통해 서로 액세스할 수 있습니다. 도커에는 컨테이너들이 이름으로 접근할 수 있도록 해주는 DNS 서버가 내장되어 있기 때문입니다.

따라서 위와 같이 컨테이너 이름에 따른 ip주소 정보를 dns 서버에서 가지고 있고, 시스템이 재부팅되어도, dns 서버의 mapping 테이블만 변경되면, 코드 내부를 고칠일은 없게 됩니다.

내장 dns 서버는 항상 127.0.0.11 주소로 접근할 수 있게 되어있습니다.

0개의 댓글