Docker Deepdive #2 - 컨테이너 이미지 레이어

· Infra, Docker

INFRA DEEPDIVE 시리즈 목차


ASK: 컨테이너 이미지란?

컨테이너를 실행하기 위한 서버 프로그램, 소스코드 및 라이브러리, 컴파일된 실행 파일 등을 묶어 놓은 파일
특정 프로세스(컨테이너)를 실행하기 위해 필요한 모든 파일 및 설정값을 지닌 파일

Untitled

위 이미지와 같이, 실행된 컨테이너는 여러 이미지 레이어들과 컨테이너 레이어로 구성되어 있음

ASK: 이미지 레이어란?

컨테이너 이미지는 내부적으로 여러 레이어들을 가진 여러 스냅샷으로 나누어 저장
중복되는 영역을 하나의 레이어를 통해 관리하여 중복을 줄이고, 공간 및 시간 효율을 얻기 위함

ASK: 컨테이너 레이어란?

컨테이너가 실행되면, 이미지 레이어 스냅샷들이 복원되고 신규 컨테이너 공간에 mount됨
이후, mount된 공간 위에 새로운 레이어가 하나 생성되고, 이 레이어에 해당 컨테이너 환경 내에서 발생하는 모든 변경사항이 저장됨. → 이 레이어를 컨테이너 레이어라고 부름
따라서 컨테이너에서의 변경 사항이 기존 이미지 레이어에 영향을 주지 않고, 해당 컨테이너의 격리된 공간 내에서만 동작할 수 있음을 보장함

레이어 별 특징

Untitled

컨테이너를 생성하면 이미지 레이어들은 Read Only로 구성되고, 그 위에 컨테이너 레이어가 Read/Write으로 생성된다. 따라서, 컨테이너가 사라지면 컨테이너 레이어도 삭제된다.

ASK: 이미지들은 어떻게 계속 쌓을 수 있고, 어떻게 동작하게 될까?

이미지 레이어는 스냅샷이므로, 파일 시스템이 여러겹 겹쳐져서 하나의 이미지가 되어 동작하는 것으로 이해할 수 있다. 해당 레이어들을 하나로 합쳐 하나의 파일 시스템처럼 동작시킬 수 있어야 한다는 뜻이다. 이를 가능하게 해주는 기술이 Union File System, UFS다.
UFS는 다음과 같은 역할을 수행한다.

Untitled

ASK: 이미지를 레이어로 쪼개는 기준이 무엇일까?

우리는 주로 Dockerfile을 이용해 이미지를 빌드한다. 그렇다면, Dockerfile을 통해 이미지 레이어도 구성된다는 뜻인데, 어떤 경우에 이미지 레이어가 새로 생성되고, 어떤 경우에 이미지 레이어가 생성되지 않는 것일까?
이 부분은 도커 버전에 따라 다르고, 도커 빌드 방법에 따라 조금씩 달라질 수 있다. 하지만 기본적인 골격은 “파일 시스템 내에 변화가 일어나는가”를 기준으로 이미지 레이어가 생성된다는 점이다.
예를 들어 파일 시스템에 변화를 주는 Dockerfile 커맨드들은 다음과 같다.

DOIT: Dockerfile을 이용한 이미지 레이어 실습

실습 단락들은 [Docker] Docker가 Image Layer를 구성하는 방법 블로그 글을 참고하였다.

FROM alpine:latest
COPY ./a.txt .
COPY ./a.txt .
COPY ./b.txt .
RUN mkdir -p /a
RUN mkdir -p /b
RUN mkdir -p /b

RUN mkdir -p b

CMD ["echo", "HELLO WORLD!1"]
CMD ["echo", "HELLO WORLD2!"]

자세히 알고 싶어서 참고한 자료보다도 더 최대한 가득가득 중복중복하게 채워서 실습을 해보았다.

 => [internal] load .dockerignore                                                                                                                         0.0s
 => => transferring context: 2B                                                                                                                           0.0s
 => [internal] load build definition from Dockerfile                                                                                                      0.0s
 => => transferring dockerfile: 229B                                                                                                                      0.0s
 => [internal] load metadata for docker.io/library/alpine:latest                                                                                          1.5s
 => [1/8] FROM docker.io/library/alpine:latest@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b                                    0.0s
 => [internal] load build context                                                                                                                         0.0s
 => => transferring context: 50B                                                                                                                          0.0s
 => CACHED [2/8] COPY ./a.txt .                                                                                                                           0.0s
 => CACHED [3/8] COPY ./a.txt .                                                                                                                           0.0s
 => CACHED [4/8] COPY ./b.txt .                                                                                                                           0.0s
 => CACHED [5/8] RUN mkdir -p /a                                                                                                                          0.0s
 => [6/8] RUN mkdir -p /b                                                                                                                                 0.3s
 => [7/8] RUN mkdir -p /b                                                                                                                                 0.3s
 => [8/8] RUN mkdir -p b                                                                                                                                  0.4s
 => exporting to image                                                                                                                                    0.1s
 => => exporting layers                                                                                                                                   0.1s
 => => writing image sha256:7c478b26034faf7d8b65057fcaac4464a216c5aa628d32898849308a52790637

docker inspect 명령어를 이용해 레이어 정보를 확인해보았다.

"GraphDriver": {
    "Data": {
        "LowerDir": "
	          /var/lib/docker/overlay2/e6tov8ibxiuy424mzsp1v94ar/diff
		        :/var/lib/docker/overlay2/ukfbimomyehfzxax54b6rfaft/diff
		        :/var/lib/docker/overlay2/dsgi5ogxy1jplf3zyigkoi8dk/diff
		        :/var/lib/docker/overlay2/guyf6pcoeyweoocyz5c5wkv3v/diff
		        :/var/lib/docker/overlay2/7fbzhe54ozo7hdg3096bzp6e4/diff
		        :/var/lib/docker/overlay2/m8f4uzbumdcuzg5kts4ckp6pr/diff
		        :/var/lib/docker/overlay2/1d0fa43a794e5fee5c0ed9f3714267c2efb697bc98f3e0d68b4a233acbb12531/diff",
        "MergedDir": "/var/lib/docker/overlay2/8ajd50mfs8fdh7k99t21lqz2n/merged",
        "UpperDir": "/var/lib/docker/overlay2/8ajd50mfs8fdh7k99t21lqz2n/diff",
        "WorkDir": "/var/lib/docker/overlay2/8ajd50mfs8fdh7k99t21lqz2n/work"
    },
    "Name": "overlay2"
},
"RootFS": {
    "Type": "layers",
    "Layers": [
        "sha256:d4fc045c9e3a848011de66f34b81f052d4f2c15a17bb196d637e526349601820",
        "sha256:5ae0aa6e54003522928b8dedb48651f3beafa58602a1b38446c0652c1fc32aa1",
        "sha256:5ae0aa6e54003522928b8dedb48651f3beafa58602a1b38446c0652c1fc32aa1",
        "sha256:09ea57408bd1e3b1ae74f63df084f8a21d69c885c965e6920c2f63d91bea4dce",
        "sha256:4e48221becc78783b749bad65bb2dc39b802cae6279bbb0d1a7191d543a1bb12",
        "sha256:4db836cd15321888a6e620f3e5466cb3d13be25d67bf013e552011b07210f485",
        "sha256:6a1e759fa4fabaf9ca4a95e4b65e2a7c54093e54c51817b72149bcfdee48647a",
        "sha256:6a1e759fa4fabaf9ca4a95e4b65e2a7c54093e54c51817b72149bcfdee48647a"
    ]
},

/var/lib/docker/image/overlay2/layerdb/sha256으로 가면 이미지 레이어들의 Hash 데이터를 볼 수 있다. 여기서 docker inspect에서 확인했던 값으로 이동해서 실제 디렉토리 값을 확인하면 레이어가 실제로 저장되어 있는 디렉토리를 찾을 수 있다. 그럼 레이어의 가장 아래, 베이스 이미지의 sha256 값인 d4fc045c9e3... 의 경로를 찾아보겠다.

Untitled

cat cache-id 를 통해 실제 디렉토리 주소를 알아냈다. 1d0fa43a794e5f... 저 값이 실제 주소다.

Untitled

/var/lib/docker/overlay2 는 실제 이미지 레이어들의 디렉토리 주소들을 모아놓은 곳이다. 여기서 아까 찾은 1d0fa43a79... 디렉토리로 이동해준다. diff 디렉토리 안에는 이미지 레이어의 파일들이 들어있다. 베이스 이미지 레이어이므로 alpine 이미지의 파일들일 것이다.

그러면 밑에서 두 번째 레이어인 sha256:5ae0aa6e5... 는 어떻게 찾을 수 있을까? 위와 같은 방법으로는 찾아지지 않는다. 도커의 overlay2는 도커 이미지 레이어 ID가 실제 디렉토리 값과 일치하지 않는다. 아까 docker inspect 명령어 결과로 나왔던 "GraphDriver”."Data"."LowerDir" 안에 있는 내용으로 경로를 찾아갈 수 있다. sha256 레이어 리스트의 경우 낮은 레이어부터 출력되지만, LowerDir의 레이어 리스트의 경우 높은 레이어부터 출력된다. alpine 베이스 이미지의 경로가 LowerDir에서 가장 아래쪽에 위치하고 있는 것을 확인할 수 있다.

overlay2란? 아까 Union File System에 대해 이야기했다. 도커는 ufs를 발전시켜서 overlay2라는 Storage Driver를 사용한다.

다시 실습으로 돌아와서…

베이스 레이어의 바로 상위 레이어는 COPY ./a.txt . 명령어의 수행 결과를 담은 파일 시스템을 diff로 들고 있을 것이다. a.txt가 diff 디렉토리 안에 포함되어 있는지 확인해보면 알 수 있다.

Untitled

하위 세번째 레이어 또한 바로 하위 레이어와 같은 명령어인 COPY ./a.txt . 명령어의 수행 결과물을 담고 있다. 앞서 이야기했듯, COPY 명령어는 파일의 변화를 만들지 않더라도 레이어를 생성한다.

이미지 레이어를 공간 효율적으로 만들 방법?

이미지 레이어 생성의 원리를 이해함으로써 Dockerfile을 잘 작성해 공간 효율적인 이미지를 만들어낼 수 있다. 예를 들어 다음과 같은 Dockerfile의 일부 내용이 있다.

RUN make /app
RUN rm -r $HOME/.cache

무엇이 문제일까? Dockerfile은 거의 모든 명령어별로 (거의 한 줄 한 줄) 이미지 레이어를 생성한다. 그렇다면 위 내용으로 이미지 레이어는 2개가 생성될 것이다. 4번 레이어는 파일 시스템에 /app이 포함되어 있고, 5번 레이어는 그저 지우는 내용일 뿐인데, 2개의 레이어가 모두 디스크의 용량을 차지하고 있게 된다. 위 내용은 아래와 같이 바꿀 수 있다.

RUN make /app \
    && rm -r $HOME/.cache

한 레이어 안에서 모든 작업을 다 처리할 수 있다. 따라서 make 명령어로 인해 생성된 캐시 데이터는 레이어에서 찾아볼 수 없게 된다. 그만큼 디스크 용량을 절약할 수 있다.

도커 빌드 과정

본 단락은 도커이미지레이어 4편 - 빌드과정에서 일어나는 일 블로그 글을 공부하고 정리한 내용입니다.

빌드 과정

docker build 명령어를 입력하면, Dockerfile에 적은 명령어가 도커 컨테이너 형태로 실행된다. 실행이 완료되면 docker commit으로 docker image를 생성한다.

  1. Dockerfile syntax 검사
  2. 마지막 명령어인지 검사
  3. 마지막 명령어가 아니라면 도커 컨테이너 실행
  4. 컨테이너가 실행되면 해당 명령어를 수행
  5. 명령어 수행이 끝나면 docker commit으로 이미지를 생성하고 컨테이너를 종료
  6. 2~5 과정을 마지막 명령어를 수행할 때까지 반복

명령어는 항상 바로 이전 명령어 수행을 위해 생성되었던 이미지를 베이스 이미지로 컨테이너를 만들어 수행된다. 마지막으로 docker commit된 도커 이미지가 최종 완성된, docker images 명령어를 통해 우리가 볼 수 있는 도커 이미지다.

주의할 점과 후기

도커의 버전이 변경되거나, 기반 기술의 변화 등으로 레이어 구성 방식은 변경(특정 명령어의 레이어 생성 규칙이 바뀌는 등)될 수 있다.
도커 이미지의 크기를 절약하기 위해 노력할 일이 많을지는 모르겠지만 공부해보니 재미있었고, 알쓸신잡으로 알아둘만 할 것 같다.

참고자료