도커 컨테이너 빌드 및 배포 시간 최적화의 여정

Hoonkii·2022년 1월 20일
1

입사 초 사내 CI/CD 파이프라인에서 도커파일을 빌드하는 데 7~8분정도 걸렸었다. 사내 파이프라인은 마스터 브랜치에 PR들이 머지되면 자동으로 스테이징 환경에 배포되는데, 7~8분 정도 소요되는 것은 꽤 오래걸린다는 생각이 들었다. 마침 zenhub 이슈에 도커파일 빌드 시간 최적화 태스크가 있어서 해결해보고자 하였다.

  • 도커파일 분석

우리가 정의한 도커파일은 두 개였는데 하나는 백엔드 서버 하나는 머신러닝 워커였다. 백엔드 서버 도커 파일을 보면 python venv와 pipenv를 통해 의존성을 설치한다. 그리고 머신러닝 워커의 도커파일을 보면 머신러닝 데이터 처리와 관련된 여러 의존성을 설치하는 것을 확인할 수 있었다.

자 그럼 어떻게 빌드 시간을 최적화 할 수 있을까? 나는 세 가지 방법을 시도 하였고 두 가지 방법을 통해 도커 파일 빌드 및 배포 시간을 최적화 할 수 있었다.

  • Hadolint를 이용한 도커 이미지 다이어트

도커 이미지를 빌드하는 데 best practice가 공식 문서에 존재한다. 공식 문서의 best practice를 따르는 것도 좋지만, 내 파일이 best practice대로 잘 되었는지 정적 분석을 해줬으면 좋겠다는 생각이 들었다(마치 Pylint처럼..). 그와 관련해서 찾아보니 Hadolint가 있었다. Hadolint는 Dockerfile을 AST로 파싱 후 best practice를 지키는지 분석해주었다.

hadolint를 통해 다음과 같이 이미지를 개선하였다.

  • Run command들의 묶음을 통한 Layer 수 감소
  • pip install —no-cache-dir 옵션을 추가하여, 이미지 용량 감소
  • apt-get으로 설치한 dependency의 경우 apt-list를 삭제
  • —no-install-recommends ⇒ 추천되거나 제안된 패키지들을 설치하지 않음으로써 이미지 레이어 용량 줄이기.

이렇게 해서 빌드 했을 때 Layer수가 감소하였고, 도커 이미지 파일의 용량이 조금 줄어들었다. 빌드 및 배포 시간이 어느정도 감소되긴 했지만, 내가 원하는 수준 만큼 감소되지는 않았다.

  • Multi-Stage 빌드 도입. => 적용 실패

도커파일에서 빌드 이미지 두 개를 두고 컴파일 이미지랑 빌드 이미지로 구분한 다음 컴파일 이미지의 결과를 베이스 빌드 이미지로 구축하는 패턴을 Multi-stage 빌드라고 한다. 컴파일 이미지 구축 과정에서 실제 운영하는 데 쓸모 없는 데이터들을 삭제하기 때문에 이미지 용량을 줄일 수 있다고 한다.

이 부분은 하다가 build-image에서 venv를 계속 인식하지 못하는 상황이 발생했다.. 파이썬에서 멀티 스테이지빌드를 도입하는 것이 생각보다 쉽지 않았다. 나는 근본적인 물음을 던졌다. 아니 요즘 네트워크도 빠른데 도커 파일의 용량을 줄이는 것이 중요한 것일까? 어쩌면 빌드 속도가 느린게 문제가 아닐까? 라는 생각이 들었다.
실제로 CI 파이프라인을 보니 도커파일 빌드 속도 자체가 엄청 느리고 GCR에 푸시하는 시간은 상대적으로 빨랐다. 그래서 빌드 속도를 개선할 방법을 찾아보았다.

  • 도커 빌드 캐시 활용

도커 빌드 속도를 개선하기 위해서 찾아보다가 도커 빌드 캐시라는 개념을 알게되었다. 도커 이미지에는 기존 이미지에 추가적인 파일이 필요할 때 다시 다운로드 하는 것이 아니라 해당 파일을 추가하기 위해 쌓는 개념이다.
도커파일을 처음 빌드하여 레이어가 쌓이면 해당 레이어는 캐싱된다.

캐싱된 레이어를 이용할 수 있도록 도커파일을 잘 구성하는 것이 중요하다.

기존 도커 파일을 보자.

ENV VIRTUAL_ENV /env
ENV PATH /env/bin:$PATH

WORKDIR /app

COPY . /app

# Install Python packages
RUN pip install --upgrade pip && pip install pipenv
RUN pipenv sync

파이썬 프로젝트 안의 전체 소스코드를 한번에 복사한다. 복사한 이후 pipenv를 통해 패키지를 설치한다.
그런데 파이썬 프로젝트의 소스코드는 개발자가 작업하면서 계속 변하기 때문에 한 레이어에 있으면 빌드 캐시를 쓸 수가 없다.

그래서 이를 다음과 같이 개선했다.

ENV VIRTUAL_ENV /env
ENV PATH /env/bin:$PATH

COPY ./Pipfile.lock /
WORKDIR /

# Install Python packages
RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir pipenv && pipenv sync

WORKDIR /app
COPY . /app

CMD python manage.py collectstatic --no-input && gunicorn -b :$PORT base.wsgi --workers=5 --timeout=90

Pipfile.lock파일을 먼저 복사하고 pip install을 수행한다. 패키지 의존성 파일인 Pipfile.lock파일은 개발하면서 자주 변하는 부분이 아니다. 따라서 레이어를 분리하였다. 이렇게 되면 빌드 캐시를 사용할 수 있고 의존성을 설치하는데 걸리는 시간을 없앨 수 있다.

로컬에서 빌드해보니 처음 빌드했을 때는 오래걸리더라도 그 뒤에 소스코드를 살짝 변화시키고 다시 빌드하였을 때 도커 이미지 빌드 시간이 획기적으로 줄었다.

로컬 컴퓨터에서는 당연히 빌드 캐시를 쓸 수 있지만, 사내 CI/CD 시스템에서도 사용 가능한지 알아보아야 하였다. 왜냐면 CI/CD 파이프라인에서 해당 벤더사에서 제공하는 머신을 통해 빌드할 것이기 때문이었다.

우리는 Circle CI를 사용중이었다. https://circleci.com/docs/2.0/docker-layer-caching/ 문서에 보니 아래와 같이 docker_layer_caching옵션을 true로 하면 빌드시 레이어 캐시를 쓸 수 있다고 한다.

version: 2
jobs:
  build:
    docker:
      # DLC does nothing here, its caching depends on commonality of the image layers.
      - image: circleci/node:14.17.3-buster-browsers
        auth:
          username: mydockerhub-user
          password: $DOCKERHUB_PASSWORD  # context / project UI env-var reference
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: true
      # DLC will explicitly cache layers here and try to avoid rebuilding.
      - run: docker build .

실제로 적용하였을 때 원래 7~8분 정도 걸렸던 도커파일 빌드 및 배포 시간이 1~2분으로 줄었다!

이 정도 시간이면 충분하다는 생각이 들었고, 다른 해결해야될 이슈도 많아서 이 이슈는 위에서 설명한 두 가지 방법만으로 해결하였다.
다만 Multi-stage 빌드를 도입하지 못한 것이 아쉬운데 이건 technical dept로 남겨놓고 피쳐가 덜 바쁠 때 들여다봐야겠다.

profile
개발 공부 내용 정리

0개의 댓글