ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Infra] Docker 이미지 최적화
    프로젝트/뉴스타 2024. 6. 13. 07:32

    Docker 이미지 최적화 왜 궁금했을까❓

    Docker를 이용하여 이미지 파일을 만들어보니 약 5.1GB로 생각보다 이미지의 크기가 컸다. 큰 용량으로 인해 빌드 시간이 오래 걸려 배포가 늦어진다는 문제점이 존재했다. 이를 해결하고자 Docker 이미지 최적화를 찾아봤는데 Multi Stage Build, 작은 Base 이미지 등을 활용하여 줄이는 방법이 있었다. 이에 대해 알아보고 프로젝트에 적용해보려고 한다.
     

    [Infra] Dockerfile & Docker-Compose 작성

    Dockerfile & Docker-Compose 작성 왜 궁금했을까❓뉴스타 서비스의 경우 Docker를 이용하여 어플리케이션을 띄우기로 했다. 그래서 Dockerfile과 Docker-compose 작성법에 대해서 알아보고 뉴스타 서비스에 적

    pslog.co.kr

    위 포스팅을 통해 Docker 문법에 대해서 학습할 수 있다.

    1. Docker 이미지 저장 방식

    • Docker 이미지를 pull 받으면 여러 개의 조각으로 분리되어 다운받아 진다.
    $ docker pull nginx:latest
    
    Using default tag: latest
    latest: Pulling from library/nginx
    c499e6d256d6: Already exists
    74cda408e262: Pull complete
    ffadbd415ab7: Pull complete
    Digest: sha256:282530fcb7cd19f3848c7b611043f82ae4be3781cb00105a1d593d7e6286b596
    Status: Downloaded newer image for nginx:latest
    docker.io/library/nginx:latest
    • 이렇게 분리되어 받아진 데이터를 레이어(Layer)라고 한다.
    • Docker 이미지가 빌드될 때, Dockerfile에 정의된 명령문을 실행하면서 만들어지며 각각 독립적으로 저장되고 읽기 전용이라 수정할 수 없다.
    "sha256:c3a984abe8a88059915bb6c7a1d249fd1ccc16d931334ac8816540b0eb686b45"
    "sha256:99134ec7f247e5a211c7205fec587bf72a6d4aac339b21858b892e9c04f78920"
    "sha256:d37eecb5b7691ec21bd19989e37f8bb4d20b340a775591d0f3db5897d606b0e4"
    • 레이어가 어떻게 저장되는지 알고 싶다면 docker image inspect 명령어를 통해 확인할 수 있다.
    • 위 해시값은 Host 머신 내에 어딘가에 실제 레이어 내용이 저장된 것을 찾을 수 있을 것이다.

    • 위 그림과 같이 Docker Container는 실행이 되면 모든 읽기 전용 레이어들을 쌓고 마지막에 쓰기 전용 레이어를 추가한다.
    • Container에서 발생하는 결과물들이 쓰기 전용 레이어에 기록되는 것으로 많은 Docker Container를 실행하더라도 읽기 전용 레이어는 변하지 않고 Container마다의 쓰기 전용 레이어에 데이터가 쌓이기 때문에 서로 겹치지 않게 된다.
    • 이러한 레이어들은 캐싱되어 재사용되기 때문에 빌드 시간을 단축할 수 있다는 장점이 있다.
    • 또한, Dockerfile에 정의된 모든 명령어가 레이어가 되는 것은 아니고 RUN, ADD, COPY 만 레이어가 생성되며 메타 데이터를 다루는 CMD, LABEL, ENV, EXPOSE 등은 임시 레이어로 생성되지만 저장되지 않아 Docker 이미지 크기에 영향을 주지 않는다.

    2. Docker 이미지 최적화 방안

    2.1. 가벼운 Base 이미지 사용

    • 기본 Base 이미지에는 불필요한 패키지들이 포함되어 있다.
      • name:version: 가장 기본이 되는 이미지로 운영에 필요한 대부분의 패키지들이 포함되어 있다.
      • name:version-slim: Container를 실행하기 위한 최소한의 패키지만 설치된 이미지이다.
      • name:version-alpine: Container에서 사용하기 위해 만들어진 Alpine 리눅스를 기반으로 배포된 이미지로 가장 용량이 적다.
    • Python을 사용할 때는 slim을 사용해야 하는데 그 이유는 Python은 내부 구현체로 C를 사용하고 많은 패키지들이 C를 활용하여 구현되어 있다.
    • 즉, glibc라는 라이브러리를 활용해야 하는데 alpine의 경우 다른 패키지들과 다르게 C 라이브러리가 musl을 사용하여 추가적인 작업으로 인해 시간과 크기가 증가되는 문제점이 존재한다.

    2.2. 불필요한 레이어 제거

    • 과거 Docker에서는 이미지 레이어의 개수가 성능에 영향을 줬지만 현재는 아니라고 한다. 하지만, 가독성과 유지 보수를 고려한다면 도움이 될 것이라 생각된다.
    RUN apt-get update && \
        apt-get -y install apt-transport-https \
          ca-certificates \
          curl \
          gnupg2 \
          software-properties-common && \
        curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg > /tmp/dkey; apt-key add /tmp/dkey && \
        add-apt-repository \
          "deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
          $(lsb_release -cs) \
          stable" && \
       apt-get update && \
       apt-get -y install docker-ce
    • 위와 같이 체이닝을 활용하면 1개의 레이어로 줄일 수 있으며 가독성이 증가와 중복 설치를 방지할 수 있다.

    2.3. 프로젝트 코드를 복사하는 부분은 아래로 설정

    COPY src src
    COPY build.gradle gradlew settings.gradle gradle .
    • 위 Dockerfile을 보면 COPY가 2번 실행되어 2개의 레이어가 만들어지고 캐싱될 것이다.
    • src의 경우 변경이 잦아 캐시가 자주 초기화 될 것이고 그 다음에 실행되는 COPY 또한 레이어 캐시가 초기화 될 것이다.
    • 따라서, 프로젝트 코드는 변화가 빈번하지 않은 명령문 다음에 오는 것이 이미지 빌드 시간을 단축시킬 수 있다.
    COPY build.gradle gradlew settings.gradle gradle .
    COPY src src

     

    2.4. Production 패키지만 설치

    • 배포할 어플리케이션을 빌드할 경우에는 개발용 패키지나 파일이 필요하지 않다.
    • 필요한 패키지만 추출을 하는 방법과 node에서는 아래 코드를 통해 development 패키지를 제거할 수 있다.
    npm install --production

    2.5. 멀티 스테이지 빌드 사용

    • 1개의 Dockerfile에 FROM 명령어를 여러 개 작성하는 방식이다.
    • FROM을 기준으로 스테이지를 구분한다고 하면 특정 스테이지 빌드 과정에서 생성된 것 중에 사용되지 않거나 불필요한 것은 무시하고 필요한 부분만을 가져와서 새로운 베이스 이미지에서 새로운 이미지를 생성하는 것이다.
    FROM node:alpine AS builder
    
    WORKDIR /usr/src/app
    
    COPY package.json .
    
    RUN npm install --force --production
    
    COPY . .
    
    RUN npm run build
    
    FROM nginx:latest
    
    RUN rm -rf /etc/nginx/conf.d/*
    
    COPY ./default.conf /etc/nginx/conf.d/default.conf
    
    COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
    
    CMD [ "nginx", "-g", "daemon off;"]
    • 프론트 React의 경우 빌드 파일만 필요하기 때문에 첫 번째 FROM에서는 빌드 파일만을 추출한다.
    • 그리고 두 번째 FROM에서는 --from builder를 이용하여 빌드 파일을 가져와서 nginx 이미지에 COPY하여 정적 파일을 반환하도록 하고 있다.
    • 이를 통해, 첫 번째 빌드 과정에서 생기는 패키지 설치 파일과 소스 코드 등을 이미지에서 제거하여 용량을 크게 줄일 수 있다.

     

    3. 프로젝트 적용

    3.1. Spring Boot

    FROM openjdk:17-alpine AS builder
    
    WORKDIR /usr/src/app
    
    COPY build.gradle gradlew settings.gradle gradle .
    
    COPY src src
    
    RUN chmod +x gradlew
    
    RUN ./gradlew clean bootJar
    
    FROM openjdk:17-alpine
    
    WORKDIR /usr/src/app
    
    ARG JAR_FILE=build/libs/*.jar 
    
    COPY --from=builder /usr/src/app/${JAR_FILE} app.jar
    
    RUN apk add tzdata && ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
    RUN echo Asia/Seoul > /etc/timezone
    
    ENTRYPOINT ["java","-jar", "-Dspring.profiles.active=prod", "app.jar"]
    • Base 이미지를 alpine을 사용하고 FROM을 2개 사용하여 멀티 스테이지 빌드를 이용했다.
    • 이미지 크기가 650MB에서 372MB로 감소했다.

    3.2. FastAPI

    FROM python:3.9.13-slim
    
    WORKDIR /app/
    
    COPY . .
    
    RUN pip install --upgrade pip
    
    RUN pip install -r requirements.txt
    
    RUN ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
    RUN echo Asia/Seoul > /etc/timezone
    
    ENV APP_ENV=prod
    
    CMD uvicorn --host=0.0.0.0 --port 8000 app.main:app
    # setup
    fastapi==0.110.0
    uvicorn==0.28.0
    requests==2.31.0
    typing_extensions==4.10.0
    starlette==0.36.3
    scipy==1.12.0
    packaging==21.3
    
    # gensim
    numpy==1.26.4
    smart-open==7.0.1
    gensim==4.3.2
    tqdm==4.66.2
    
    # kobart
    python-mecab-ko==1.3.3
    python-mecab-ko-dic==2.1.1.post2
    pytorch-lightning==1.2.1
    torch==1.7.1
    transformers==4.3.3
    kobart @ git+https://github.com/SKT-AI/KoBART@30c5eb7b593828d6ec2d767eeedb2f2ed02c5c2a
    
    # pytest
    pytest==8.1.1
    python-dotenv==1.0.1
    
    # elastic Search
    elastic-transport==8.12.0
    elasticsearch==7.10.0
    
    # crawling
    beautifulsoup4==4.12.3
    pandas==2.2.1
    
    # Database
    PyMySQL==1.1.0
    SQLAlchemy==2.0.28
    pydantic==2.6.3
    pydantic_core==2.16.3
    • Base 이미지를 slim을 사용하고 불필요한 패키지 파일을 제거해서 정리했다.
    • 이미지 크기가 4.43GB에서 3.74GB로 감소했다.

    3.3. React

    FROM node:alpine AS builder
    
    WORKDIR /usr/src/app
    
    COPY package.json .
    
    RUN npm install --force --production
    
    COPY . .
    
    RUN npm run build
    
    FROM nginx:latest
    
    RUN rm -rf /etc/nginx/conf.d/*
    
    COPY ./default.conf /etc/nginx/conf.d/default.conf
    
    COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
    
    CMD [ "nginx", "-g", "daemon off;"]
      • Base 이미지를 alpine을 사용하고 FROM을 2개 사용하여 멀티 스테이지 빌드를 이용했다. 또한, production 옵션을 줘서 배포에 불필요한 패키지를 설치하지 않았다.
      • 이미지의 크기가 385MB에서 189MB로 감소했다.
Designed by Tistory.