Containerization/Docker

[Docker] Dockerfile에서 명령어 순서가 중요한 이유 (w/ 도커 이미지 레이어 구조)

쉬지마 이굥진 2024. 5. 16. 16:34

Dockerfile이란?

도커파일로 이미지를 만들어보기 전에, 도커 파일이 무엇인지 자세히 알아보고 가자. 도커 파일이란?

  • 도커 이미지를 빌드하고 설정하는 데 사용되는 텍스트 파일
  • 도커 이미지를 어떻게 구성할지를 정의함
  • 도커 이미지를 만들기 위한 명령어와 설정 정보를 담고 있음
    • 베이스 이미지
    • 빌드 시 실행할 명령어
    • 복사할 파일
    • 환경 변수

공식 문서를 참조해서 도커 파일 속 용어들의 의미를 가지고 와 봤다.

도커파일 예시

  • FROM: 베이스 이미지 선정
    • 반드시! 있어야 하는 명령어
    • 여러겹의 이미지를 쌓아 가는데, 그 층 중 가장 기본이 되는 이미지
  • WORKDIR: work directory 선정
    • 도커파일 뒤에 오는 모든 지시자(RUN, CMD, COPY, ADD 등)에 대한 작업 디렉토리를 설정
    • 리눅스 명령어의 cd와 비슷한 역할
  • COPY: 복사할 파일 선정 (ex: 작업한 서비스 파일들)
    • 현재 로컬 컴퓨터에 있는 파일을 도커 컨테이너 안으로 복사하는 기능
  • RUN: "이미지 빌드 시" 실행할 명령어
    • 여러번 사용 가능
    • 보통 라이브러리 설치할 때 쓰임
  • CMD: "컨테이너 생성 시" 실행할 명령어
    • 이미지로부터 컨테이너를 생성해서 최초로 실행할 때 수행됨
      • 반면, RUN 명령어는 이미지를 빌드할 때 실행됨
  • ENTRYPOINT: "컨테이너가 생성되고 최초로" 실행할 명령어 (ex: 서버 실행)

도커 이미지 레이어(Layers) 구조

도커파일이 뭔지 배웠으면 알아둬야 할 개념이 있다. '레이어' 구조이다. 위에서 언급한 것 처럼, 도커 파일 도커 이미지를 빌드하기 위한 스크립트 파일로, 각 명령어는 이미지의 새로운 레이어를 만들며, 이 레이어들이 스택처럼 쌓여 최종 이미지를 형성한다.

 

즉, 도커는 Dockerfile을 읽어들여 각 줄마다 이미지 레이어를 만든다. 하지만 주의할 점이 모든 줄마다 레이어를 만드는 것이 아니라 파일 시스템에 변화가 발생하는 경우만 이미지 레이어를 생성한다는 특징이 있다.

 

도커 파일을 사용하여 이미지를 생성하는 과정은 매우 자동화되어 있으며, 이를 통해 어플리케이션 배포를 일관되고 예측 가능하게 만든다.

++ 참고

도커 빌드 엔진은 이미지 레이어의 공간 효율과 안정성을 위해 꾸준히 빌드 방식을 변경하고 개선해가고 있다. 따라서, 도커 엔진 버전, 빌드 라이브러리의 종류에 따라 결과물은 조금씩 차이가 있을 수 있다.

 

레이어와 캐시

  • 레이어
    • 도커 이미지는 여러 개의 레이어로 구성되며, 각 레이어는 이전 레이어 위에 추가되는 변경사항들을 저장한다.
    • 이는 이미지를 재구성할 때 필요한 데이터만 다시 다운로드하거나 수정하게 해준다.
  • 캐시 활용
    • 빌드 프로세스 중에 도커는 이전 빌드에서 사용되었던 레이어의 캐시를 재사용함으로써 빌드 시간과 자원을 절약할 수 있다.
    • 변경되지 않은 레이어는 다시 다운로드하거나 빌드하지 않는다.

Dockerfile의 각 명령어가 레이어를 형성하는 방법

 

Dockerfile의 명령 순서가 중요한 이유

도커 파일의 명령 순서가 중요한 이유는 바로 레이어 캐시 때문이다. 레이어의 순서가 캐시 매커니즘에 크게 영향을 미치기 때문에, 도커 파일에서 명령어의 순서가 매우 중요하게 되는 것이다.

 

잘못된 명령어 순서는 불필요한 레이어의 재빌드를 유발할 수 있어 효율성과 속도를 저하시킨다. 예를 들면 소스 코드 파일을 먼저 복사한 후 👉 의존성 파일을 설치하는 순서일 경우, 소스 코드에 변경이 생길 때마다 의존성 레이어도 재빌드해야하기 때문에 비효율적인 명령어 순서라고 할 수 있다.

 

순서 최적화

위에서 언급한 사례와 같이 비효율적인 순서의 명령 순서일 경우, 명령어의 순서를 조정해야만 한다.

 

의존성 파일(go.modgo.sum)을 소스 코드 전체를 복사하기 전에 먼저 복사하고 설치하도록 해서, 소스 코드의 변경이 의존성 레이어에 영향을 주지 않게 할 수 있다. 이를 통해 불필요한 의존성 다운로드를 줄일 수 있게 하는 것이다.

변경 전
변경 후

두 번째 이미지는, 의존성 파일을 먼저 복사하고 필요한 컴파일 작업을 수행하는 Dockerfile로서 첫 번째 이미지보다 더 최적화된 Dockerfile 이다.

 

더 나아가, 공간 효율적인 Dockerfile 작성 방법

도커 이미지 레이어의 순서 및 원리를 이해하는 것에서 더 나아가, 효과적으로 Dockerfile을 작성하는 방법에 대해 알아보고 마치겠다.

 

도커 이미지는 배포되는 과정에서 여러번 pull & push 되기 때문에 이미지의 크기가 작을수록 배포 속도가 향상되며 disk 공간이 낭비되지 않는다.

 

앞서 언급했듯 도커 이미지는 층이 만들어져있기 때문에, 최종 이미지에 사용되지 않는 파일들이 중간 레이어에 남아있을 수 있다. 이미지 크기를 효율적으로 관리하기 위해선, 이러한 중간 레이어의 불필요한 크기를 줄여야 한다. 이를 해결하기 위한 좋은 방법은?!

 

레이어로 만들어질 필요 없는 커맨드들을 이전 커맨드와 합쳐서 실행시키면 된다. 

 

이렇게만 보면 추상적으로 느껴질 수 있기에 Docker Docs에 언급되어 있는 예시를 보며 이해를 해보자.

 

🔹Dockerfile (before)

# syntax=docker/dockerfile:1
FROM ubuntu:18.04
LABEL org.opencontainers.image.authors="org@example.com"
COPY . /app
RUN make /app
RUN rm -r $HOME/.cache
CMD python /app/app.py

 

🔸Dockerfile (after)

# syntax=docker/dockerfile:1
FROM ubuntu:18.04
LABEL org.opencontainers.image.authors="org@example.com"
COPY . /app
RUN make /app \
    && rm -r $HOME/.cache
CMD python /app/app.py

 

위 도커 파일의 before와 after는 어떻게 다를까?

 

🔹첫 번째 도커파일에서는, 4번째 라인이 RUN make /app 을 통해 일련의 빌드 과정을 수행하고, 5번째 라인에서 RUN rm -r $HOME/.cache를 수행해 이미지에 불필요한 캐시를 삭제하고 있다. 그러나 이 경우 4번, 5번 라인이 각각 실행되기 때문에, 5번 라인에서 캐시를 삭제하더라도 4번의 이미지 레이어에서는 여전히 캐시가 남아있게 된다.

즉, 캐시를 삭제하는 행위가 이미지의 크기를 줄이는데 1도 영향을 주지 못한다는 얘기다.

 

🔸두 번째 도커파일대로 변경하면, 기존에 빌드 후 👉 캐시를 삭제하던 과정이 하나의 레이어로 합쳐지면서 이미지 레이어 내 해당 캐시 데이터는 어디에도 존재하지 않게 된다. 이제는 해당 캐시 크기만큼 이미지의 크기가 줄어드는 효과를 볼 수 있다.

 

마치며

단순히 Dockerfile은 작성만 하면 되는 줄 알았는데 .. 객체지향에서의 객체처럼 Dockerfile 역시 명령어 수행 순서나 구조를 어떻게 짜느냐에 따라 결과도 천차만별로 달라질 수 있음을 알았다. 역시 개발은 그냥 돌아가는 것만이 능사가 아니라 최적화를 고민하고 어떻게 짜야 더 효율적인 구조일지를 고민하는 것만이 살길이란 걸 깨달은 순간이다. 


References

https://docs.docker.com/build/guide/layers/

https://docs.docker.com/reference/dockerfile/

https://github.com/Rachel-3/docker-pro/blob/main/keyword/Rachel-3/%EC%9C%A4%EC%B1%84%EB%A6%BC.md

https://creboring.net/blog/how-docker-divide-image-layer/