이미지 레이어 이해하기

이미지 레이어 이해하기

컨테이너 이미지는 여러 레이어로 이루어져 있고, 각 레이어는 한 번 만들어지면 불변이에요. 이 가이드에서는 레이어가 무엇인지, 컨테이너가 사용할 파일시스템이 어떻게 만들어지는지, 그리고 직접 레이어를 만들어 보면서 원리를 이해해요.

출처: 문서

본문

설명

이미지란 무엇인가요?에서 배웠듯이, 컨테이너 이미지는 레이어로 구성돼요. 그리고 각 레이어는 한 번 생성되면 불변(immutable)이에요. 그런데 이게 정확히 무슨 뜻일까요? 또 그 레이어들은 컨테이너가 사용할 수 있는 파일시스템을 만드는 데 어떻게 쓰일까요?

이미지 레이어

이미지의 각 레이어는 파일시스템 변경 세트(추가, 삭제, 수정)를 담고 있어요. 이론적인 이미지를 하나 살펴볼까요.

  • 첫 번째 레이어는 apt 같은 패키지 매니저와 기본 명령을 추가해요.
  • 두 번째 레이어는 의존성 관리를 위해 Python 런타임과 pip를 설치해요.
  • 세 번째 레이어는 애플리케이션의 특정 requirements.txt 파일을 복사해 넣어요.
  • 네 번째 레이어는 애플리케이션의 특정 의존성들을 설치해요.
  • 다섯 번째 레이어는 애플리케이션의 실제 소스 코드를 복사해 넣어요.

이 예시는 레이어를 이미지 간에 재사용할 수 있게 해 주기 때문에 유용해요. 예를 들어 또 다른 Python 애플리케이션을 만들고 싶다고 해 볼게요. 레이어링 덕분에 동일한 Python 베이스를 활용할 수 있어요. 이렇게 하면 빌드가 빨라지고, 이미지를 배포하는 데 필요한 저장 공간과 대역폭도 줄어들어요. 이미지 레이어링은 다음과 비슷하게 생겼어요.

레이어를 사용하면 다른 사람의 베이스 레이어를 재사용해 이미지를 확장할 수 있고, 여러분의 애플리케이션이 필요한 데이터만 추가하면 돼요.

레이어 쌓기

레이어링은 콘텐츠 주소 지정 저장소(content-addressable storage)와 유니온 파일시스템(union filesystem) 덕분에 가능해요. 다소 기술적이지만, 어떻게 동작하는지 설명할게요.

  • 각 레이어는 다운로드된 후 호스트 파일시스템의 자신만의 디렉터리로 추출돼요.
  • 이미지에서 컨테이너를 실행하면 레이어들이 서로 위에 쌓여 새로운 통합된 뷰를 만드는 유니온 파일시스템이 생성돼요.
  • 컨테이너가 시작되면 chroot를 사용해 루트 디렉터리가 이 통합 디렉터리의 위치로 설정돼요.

유니온 파일시스템이 생성될 때, 이미지 레이어 외에도 실행 중인 컨테이너 전용 디렉터리가 하나 만들어져요. 이 덕분에 컨테이너가 파일시스템을 변경하더라도 원래 이미지 레이어는 그대로 유지될 수 있어요. 그래서 동일한 베이스 이미지에서 여러 컨테이너를 실행할 수 있는 거예요.

직접 해보기

이 실습 가이드에서는 docker container commit 명령을 사용해 새 이미지 레이어를 수동으로 만들어 봐요. 보통은 Dockerfile을 사용하기 때문에 실제로 이 방식으로 이미지를 만드는 경우는 드물지만, 어떻게 작동하는지 이해하는 데는 훨씬 좋아요.

베이스 이미지 만들기

첫 번째 단계에서는 이후 단계에서 사용할 나만의 베이스 이미지를 만들어 봐요.

  • Docker Desktop을 다운로드하고 설치해요.
  • 터미널에서 다음 명령을 실행해 새 컨테이너를 시작해요.
    $ docker run --name=base-container -ti ubuntu
    
    이미지가 다운로드되고 컨테이너가 시작되면 새 셸 프롬프트가 보여요. 이것은 컨테이너 안에서 실행되는 거예요. (컨테이너 ID는 달라질 수 있어요.)
    root@d8c5ca119fcd:/#
    
  • 컨테이너 안에서 다음 명령을 실행해 Node.js를 설치해요.
    $ apt update && apt install -y nodejs
    
    이 명령이 실행되면 컨테이너 안에 Node가 다운로드되어 설치돼요. 유니온 파일시스템의 관점에서 보면, 이런 파일시스템 변경은 이 컨테이너 고유의 디렉터리 안에서 일어나요.
  • 다음 명령을 실행해 Node가 설치됐는지 확인해요.
    $ node -e 'console.log("Hello world!")'
    
    콘솔에 "Hello world!"가 나타나면 돼요.
  • Node가 설치됐으니 이제 변경 사항을 새 이미지 레이어로 저장할 준비가 됐어요. 이 레이어에서 새 컨테이너를 시작하거나 새 이미지를 만들 수 있어요. 이를 위해 docker container commit 명령을 사용해요. 새 터미널에서 다음 명령을 실행해요.
    $ docker container commit -m "Add node" base-container node-base
    
  • docker image history 명령으로 이미지의 레이어를 확인해요.
    $ docker image history node-base
    
    다음과 비슷한 출력이 보여요.
    IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
    9e274734bb25   10 seconds ago   /bin/bash                                       157MB     Add node
    cd1dba651b30   7 days ago       /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
          7 days ago       /bin/sh -c #(nop) ADD file:6089c6bede9eca8ec…   110MB
          7 days ago       /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
          7 days ago       /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
          7 days ago       /bin/sh -c #(nop)  ARG LAUNCHPAD_BUILD_ARCH     0B
          7 days ago       /bin/sh -c #(nop)  ARG RELEASE                  0B
    
    맨 위 줄의 "Add node" 코멘트를 주목하세요. 이 레이어에는 방금 만든 Node.js 설치가 포함돼 있어요.
  • 이미지에 Node가 설치됐음을 확인하려면 이 새 이미지로 새 컨테이너를 시작해 보세요.
    $ docker run node-base node -e "console.log('Hello again')"
    
    터미널에 "Hello again" 출력이 나오면 Node가 설치되어 정상 작동한다는 뜻이에요.
  • 이제 베이스 이미지를 다 만들었으니, 그 컨테이너를 제거할 수 있어요.
    $ docker rm -f base-container
    
  • 베이스 이미지 정의

    베이스 이미지는 다른 이미지를 빌드하기 위한 토대예요. 어떤 이미지든 베이스 이미지로 사용할 수 있어요. 다만 일부 이미지는 의도적으로 빌딩 블록으로 만들어져, 애플리케이션의 토대나 시작점을 제공해요.

    이 예시에서는 node-base 이미지가 실제로는 아무것도 하지 않기 때문에 배포하지 않겠지만, 이후 다른 빌드에 사용할 수 있는 베이스가 돼요.

앱 이미지 빌드하기

베이스 이미지가 있으니 이제 그 이미지를 확장해 추가 이미지를 빌드할 수 있어요.

  • 방금 만든 node-base 이미지로 새 컨테이너를 시작해요.
    $ docker run --name=app-container -ti node-base
    
  • 이 컨테이너 안에서 다음 명령을 실행해 Node 프로그램을 만들어요.
    $ echo 'console.log("Hello from an app")' > app.js
    
    이 Node 프로그램을 실행하려면 다음 명령을 사용해 화면에 메시지가 출력되는 걸 볼 수 있어요.
    $ node app.js
    
  • 다른 터미널에서 다음 명령을 실행해 이 컨테이너의 변경 사항을 새 이미지로 저장해요.
    $ docker container commit -c "CMD node app.js" -m "Add app" app-container sample-app
    
    이 명령은 sample-app이라는 새 이미지를 만들 뿐 아니라, 컨테이너 시작 시 기본 명령을 설정하는 추가 구성을 이미지에 더해요. 여기서는 컨테이너가 시작될 때 자동으로 node app.js를 실행하도록 설정하는 거예요.
  • 컨테이너 밖의 터미널에서 다음 명령을 실행해 업데이트된 레이어를 확인해요.
    $ docker image history sample-app
    
    그러면 다음과 같은 출력이 보여요. 맨 위 레이어의 코멘트가 "Add app"이고, 그다음 레이어는 "Add node"인 걸 확인할 수 있어요.
    IMAGE          CREATED              CREATED BY                                      SIZE      COMMENT
    c1502e2ec875   About a minute ago   /bin/bash                                       33B       Add app
    5310da79c50a   4 minutes ago        /bin/bash                                       126MB     Add node
    2b7cc08dcdbb   5 weeks ago          /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
          5 weeks ago          /bin/sh -c #(nop) ADD file:07cdbabf782942af0…   69.2MB
          5 weeks ago          /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
          5 weeks ago          /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
          5 weeks ago          /bin/sh -c #(nop)  ARG LAUNCHPAD_BUILD_ARCH     0B
          5 weeks ago          /bin/sh -c #(nop)  ARG RELEASE                  0B
    
  • 마지막으로, 새로운 이미지로 새 컨테이너를 시작해요. 기본 명령을 지정했으니 다음 명령을 사용할 수 있어요.
    $ docker run sample-app
    
    여러분의 Node 프로그램에서 만든 인사말이 터미널에 나타나는 걸 볼 수 있어요.
  • 이제 컨테이너를 다 사용했으니 다음 명령으로 제거해요.
    $ docker rm -f app-container
    

추가 자료

배운 내용을 더 깊이 파고들고 싶다면 다음 자료를 확인해 보세요.

  • docker image history
  • docker container commit

다음 단계

앞서 힌트를 드렸듯이, 대부분의 이미지 빌드는 docker container commit을 사용하지 않아요. 대신 이러한 단계를 자동화해 주는 Dockerfile을 사용해요. Dockerfile 작성하기

더 알아보기 (Learn more)