-
Notifications
You must be signed in to change notification settings - Fork 0
docker 자동 배포 파이프라인 시간 단축
docker를 활용한 자동 배포 구축을 완료한 뒤 main 브랜치에 merge 될 때마다 배포가 자동으로 잘 되었습니다.
하지만 github action CI 서버에서 이미지 빌드 후 docker hub에 업로드하고 원격 서버에서 docker hub로부터 이미지를 받아서 container를 띄우는 과정이 5분 넘게 소요되었습니다.
그래서 docker의 특성을 잘 활용해서 시간을 줄여보기로 했습니다.
배포가 5분 넘게 걸린 원인은 크게 두 가지였습니다.
첫 번째, 캐싱이 이루어지지 않습니다.
이미지를 빌드할 때 yarn install을 실행하여 패키지를 설치할 때 캐싱이 이루어지지 않아서 패키지의 변경 사항이 없는 경우에도 패키지를 처음부터 다시 설치합니다.
두 번째, 병렬 처리가 이루어지지 않습니다.
저희 프로젝트는 모노 레포 구조로 workspace가 분리되어 있고 각 workspace를 image로 만들고 docker-compose로 여러 container를 관리하고 있습니다.
여기서 각 workspace의 이미지를 빌드하고 push하는 과정은 독립적이기 때문에 병렬로 처리해도 큰 문제가 발생하지 않습니다.
그런데 여러 개의 image 빌드와 push를 순차적으로 진행하기 때문에 시간이 오래 걸렸습니다.
그러면 지금부터 캐싱과 병렬 처리를 통해 시간을 단축해보겠습니다.
구현에 앞서 docker image의 layer 구조에 대해 알아야 합니다.
다음과 같은 Dockerfile이 있습니다.
# syntax=docker/dockerfile:1
FROM ubuntu:latest
RUN apt-get update && apt-get install -y build-essentials
COPY main.c Makefile /src/
WORKDIR /src/
RUN make build
위 Dockerfile을 가지고 이미지를 빌드하면 layer들이 쌓이는 형태로 이미지가 생성됩니다.
이 상태에서 세 번째 layer에서 사용하는 main.c 파일이 변경되었을 때 해당 layer부터 이후 layer가 모두 영향을 받게 됩니다.
변화가 없는 앞 두 layer는 기존에 존재하던 layer를 사용하고 그 이후의 layer는 다시 생성합니다.
출처 : https://docs.docker.com/build/cache/
그렇다면 node를 기준으로 패키지를 설치하는 과정을 layer 단위로 살펴보겠습니다.
아래 Dockerfile은 package.json과 yarn.lock을 가지고 패키지를 설치하는 Dockerfile입니다.
FROM node:20-alpine
COPY package.json yarn.lock ./
RUN yarn install
위 Dockerfile로 이미지를 빌드한 결과는 다음과 같습니다.
만약 pakcage.json과 yarn.lock 파일의 변화가 없다면 yarn install은 실행되지 않고 기존 layer를 그대로 사용하게 됩니다.
그렇다면 패키지가 추가되어 package.json과 yarn.lock이 변경된다면 그 이후의 layer는 다시 생성됩니다.
특히 패키지를 설치하는 yarn install은 패키지가 많을 수록 I/O 작업이 많아지기 때문에 오랜 시간이 걸리고 용량도 많이 차지합니다.
즉 저는 캐싱의 가장 큰 목적을 **“이미 설치한 패키지의 중복 설치를 막는다”**라고 정했습니다.
이제 구현을 시작하겠습니다.
우선 package.json의 변경이 없는 이상 yarn install은 실행되지 않고 기존 layer를 재활용한다고 했습니다.
그런데 문제가 저희 프로젝트에서는 docker image를 빌드하는 곳이 github action CI 서버인데 이 CI 서버는 지속적으로 실행되는 서버가 아니라 각 실행마다 별도의 환경에서 독립적으로 실행됩니다.
즉, 이전 layer 정보를 저장하고 있지 않기 때문에 언제나 새로운 layer를 생성합니다.
다행히 github action에서는 cache action을 제공해주기 때문에 독립적으로 실행되더라도 캐싱을 사용할 수 있습니다.
가장 쉬운 방법은 lock 파일의 SHA 해시 값을 키에 포함해서 패키지를 캐싱하는 방법입니다.
이 방법을 사용하면 package.json과 yarn.lock 파일이 변경되지 않으면 캐싱된 패키지를 사용하기 때문에 빌드 시간을 단축할 수 있습니다.
하지만 package.json과 yarn.lock 파일이 조금이라도 변경된다면 모든 패키지를 다시 설치하게 됩니다.
패키지가 하나 추가되었다고 기존에 설치했던 패키지도 모두 설치하는 것은 비효율적이라고 생각이 들었습니다.
그러다 문득 docker image layer 구조를 잘 활용하면 추가된 패키지만 설치할 수 있겠다는 생각이 들었습니다.
바로 원격 저장소에 저장된 가장 최신 버전 이미지를 그대로 가져온 다음에 package.json을 복사해서 yarn install을 진행하는 방식입니다.
가장 최신 버전 이미지를 가져오면 그 layer들을 그대로 가져오게 되고 yarn install에 해당하는 layer에는 이전에 설치했던 패키지들이 그대로 담겨있게 됩니다.
그 다음에 변경된 package.json을 복사한 뒤 yarn install을 실행합니다.
이렇게 하면 이미 설치된 패키지들은 다시 설치되지 않고 추가된 패키지만 설치하게 됩니다.
즉 base 이미지를 원격 저장소에 저장된 자기 자신의 이미지의 최신 버전으로 사용하면 됩니다.
재귀적으로 자기 자신을 사용한다고 보면 되겠습니다.
물론 처음 빌드할 때는 원격 저장소에 자기 자신이 없기 때문에 그 때는 node 이미지를 base로 사용해야 합니다.
일종의 캐시 역할을 수행하면서 오직 변경된 패키지만 설치할 수 있게 되었습니다.
배포 pipeline은 얼마나 단축되었을까요?
먼저 base 이미지를 node로 지정했을 때입니다.
총 5분 50초가 걸렸습니다.
https://github.com/boostcampwm-2024/refactor-web39-OctoDocs/actions/runs/12855189480
base 이미지를 자기 자신으로 지정했을 때입니다.
총 3분 33초가 걸렸습니다.
https://github.com/boostcampwm-2024/refactor-web39-OctoDocs/actions/runs/12855161978
정확히 어떠한 작업에서 시간이 단축되었는지 확인해보겠습니다.
기존 패키지가 설치된 layer는 이미 docker hub에 존재하기 때문에 그 이후 layer만 push하여 push 시간이 1분 23초에서 12초로 단축되었습니다.
기존 패키지가 설치된 layer는 이미 원격 서버에 존재하기 때문에 그 이후 layer만 pull하여 pull 시간이 68초에서 14초로 단축되었습니다.
그렇다면 패키지 하나를 추가했을 때도 시간이 단축되는지 확인해보겠습니다.
사용하지 않는 node-cache 패키지를 추가하고 자동 배포를 진행해보았습니다.
총 3분 47초가 걸렸습니다.
https://github.com/boostcampwm-2024/refactor-web39-OctoDocs/actions/runs/12855274900
여전히 처음보다 시간이 상당히 단축된 모습을 확인할 수 있습니다.
이렇게 단순 캐싱으로는 대응할 수 없었던 패키지 변경 상황에 대해 대응할 수 있게 되었습니다.
저희 프로젝트는 모노 레포 구조로 되어있고 총 3개의 workspace를 사용하고 있고 CI 서버에서 총 4개의 이미지를 빌드한 뒤 docker hub로 푸시하고 있습니다.
현재는 순차적으로 진행하고 있기 때문에 시간이 오래 걸리는 상황이라 병렬 처리로 시간을 단축해보겠습니다.
&를 통해 백그라운드로 실행하고 wait으로 모든 백그라운드 작업이 끝날 때까지 기다리면 됩니다.
# Docker 이미지 빌드
- name: docker image build
run: |
docker build -f ./services/backend/Dockerfile.prod -t summersummerwhy/octodocs-backend . &
docker build -f ./services/nginx/Dockerfile.prod -t summersummerwhy/octodocs-nginx . &
docker build -f ./services/websocket/Dockerfile.prod -t summersummerwhy/octodocs-websocket . &
wait
# Docker 이미지 푸시
- name: docker image push
run: |
docker push summersummerwhy/octodocs-modules &
docker push summersummerwhy/octodocs-backend &
docker push summersummerwhy/octodocs-nginx &
docker push summersummerwhy/octodocs-websocket &
wait
https://github.com/boostcampwm-2024/refactor-web39-OctoDocs/actions/runs/12855356801
빌드 시간이 45초에서 36초로 단축되었고 푸시 시간이 12초에서 6초로 단축되었습니다.
node 패키지를 가지고 있는 이미지의 base 이미지를 자기 자신으로 사용하면서 큰 문제가 발생했습니다.
바로 배포를 거듭할 수록 layer의 수가 굉장히 늘어난다는 점이었습니다.
계속 이전 layer들을 모두 가져와서 사용하기 때문에 빌드를 거듭할수록 layer의 수가 계속 쌓였습니다.
중간 layer에서 불필요한 데이터 때문에 용량이 커질 수 있고 성능이 저하될 수 있기 때문에 이 문제를 해결해야 했습니다.
실제로 한 번 빌드를 했을 때 layer의 변화를 확인해보았습니다.
"sha256:a0904247e36a7726c03c71ee48f3e64462021c88dafeb13f37fdaf613b27f11c",
"sha256:cf526f148e101e89a673e698c83790d31f99d970b9f8d3a152e548191b5e2a03",
"sha256:2cc32dc37aa3d94c30cb8597fce44b3f6b81ba139934d0f64959e9dc9f69231d",
"sha256:a63b27d40558f37f29da4970b031cf7989bb61fc4d9547a9fe553d6486207c44",
"sha256:fd538c1714667e332690419200e12e924df53014463efb135290929d34ca0f7d",
"sha256:99ae75ec0d557856cd7f66d5c481bc0cdc5f9fb53faf477779c6724586f9c9ed",
"sha256:ee3b8deb03a6cb52707a2fc6d6bdb0e77184aa086fa1f7433ae320061b20277f",
"sha256:51665942b4f24ac0d98e24e6f79563cc133c58e2a75a1c6625f6a5fca0ad324d",
"sha256:373141948df02928e2c56e8fd1262fc2013f67a5d22d8519c07edc0543a1a40a",
"sha256:c12bfad522cc8c2b6a3d66a99d37b1c6005f995c2dc36f9809660fbd303d4f81",
"sha256:13d85ac0e920889242c0017898ad63db77b826e2487a336ec854b2cb4363d157",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
"sha256:e3a8bf0d7d67bb08942ff440ffda6a86e53a76c7af567210c775dcc7bf227185"
"sha256:a0904247e36a7726c03c71ee48f3e64462021c88dafeb13f37fdaf613b27f11c",
"sha256:cf526f148e101e89a673e698c83790d31f99d970b9f8d3a152e548191b5e2a03",
"sha256:2cc32dc37aa3d94c30cb8597fce44b3f6b81ba139934d0f64959e9dc9f69231d",
"sha256:a63b27d40558f37f29da4970b031cf7989bb61fc4d9547a9fe553d6486207c44",
"sha256:fd538c1714667e332690419200e12e924df53014463efb135290929d34ca0f7d",
"sha256:99ae75ec0d557856cd7f66d5c481bc0cdc5f9fb53faf477779c6724586f9c9ed",
"sha256:ee3b8deb03a6cb52707a2fc6d6bdb0e77184aa086fa1f7433ae320061b20277f",
"sha256:51665942b4f24ac0d98e24e6f79563cc133c58e2a75a1c6625f6a5fca0ad324d",
"sha256:373141948df02928e2c56e8fd1262fc2013f67a5d22d8519c07edc0543a1a40a",
"sha256:c12bfad522cc8c2b6a3d66a99d37b1c6005f995c2dc36f9809660fbd303d4f81",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
"sha256:7a8ff9564f7a9e1bc46d50162bdc1d24f673ba2a4ebf5ee96eb6260906727a0c",
"sha256:2b63038ae599be710e79da45b277cd4b96045106bfdf992136ac7630c54c781e",
"sha256:279fad124104f8ceefc2ba7958a5f646ebad4ce2aeac84fc2615941d86f7990d",
"sha256:8a7dc1faea2c780a1cea4884015f9bfa4e4bc2e666fb335652d6b6bf9c4c95e5",
"sha256:2efe8f920c67b043ea395c95c43a732f2158153ea4a57579f816d7ec9175e06a",
"sha256:bd01293ff4c0f14975a056026dc31cddfeb757c81aae930532b121190d476430",
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
"sha256:20b861a54eafe30903393bc6e97c357b8cea7652aa792ee4e7d2d742118f9a44"
layer가 6개가 늘어났습니다.
이를 위해 layer의 개수가 50개가 넘어가면 octodocs-modules를 빌드할 때 자기 자신을 base로 하지 않고 node 이미지를 base로 해서 layer의 개수를 초기화하기로 결정했습니다.
먼저 octodocs-modules 이미지를 빌드할 때 node 이미지를 base로 사용하는 경우는 다음 두 가지입니다.
- 원격 저장소에 octodocs-modules가 없을 때
- layer의 수가 50개가 넘어갈 때
위 두 경우 중 하나에 해당하면 Dockerfile.init을 사용해서 node 이미지를 base로 사용하고 그렇지 않으면 Dockerfile을 사용해서 octodocs-modules 이미지를 base로 빌드하는 코드를 작성했습니다.
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://hub.docker.com/v2/repositories/summersummerwhy/octodocs-modules)
if [ "$STATUS" -eq 404 ]; then
echo "octodocs-modules not found"
docker build -f ./services/module/Dockerfile.init -t summersummerwhy/octodocs-modules .
else
echo "octodocs-modules found"
docker build -f ./services/module/Dockerfile -t summersummerwhy/octodocs-modules .
LAYERS=$(docker inspect --format '{{len .RootFS.Layers}}' summersummerwhy/octodocs-modules)
if [ $LAYERS -gt 50 ]; then
echo "too many layers"
docker build -f ./services/module/Dockerfile.init -t summersummerwhy/octodocs-modules .
fi
fi
layer 수가 50개가 넘어갔을 때 걸리는 시간은 7분 10초가 되었네요.
https://github.com/boostcampwm-2024/refactor-web39-OctoDocs/actions/runs/12859630622
이 workflow가 최대한 실행되지 않도록 하기 위해 빌드할 때 layer 수를 줄여야 합니다.
현재 octodocs-modules를 빌드하는 Dockerfile입니다.
# COPY 명령어는 여러 디렉토리를 목적지로 파일 복사하는 것이 불가능하다.
# 빌드 스테이지에서 COPY 명령어를 여러 번 생성해서 여러 디렉토리로 파일을 복사한다.
# 그 후 그 디렉토리 자체를 다음 스테이지에서 그대로 복사하여 layer의 개수를 줄인다.
FROM node:20-alpine as builder
WORKDIR /app
# 호이스팅을 위해
COPY package.json yarn.lock ./
COPY apps/backend/package.json ./apps/backend/
COPY apps/frontend/package.json ./apps/frontend/
COPY apps/websocket/package.json ./apps/websocket/
# node_modules를 가지고 있는 이미지
# 이 이미지를 기반으로 각 workspace 별 이미지를 만들면
# yarn install 레이어를 공유하게 된다.
FROM summersummerwhy/octodocs-modules:latest
# 호이스팅을 위해
COPY --from=builder /app/apps /app/apps
# 의존성 설치
RUN yarn install --check-files
COPY를 여러 번 사용해서 layer가 여러 개 생성되는데 이 layer 수를 줄이기로 결정했습니다.
우선 저희 프로젝트는 모노 레포 구조이고 각 workspace는 apps 디렉토리 있습니다.
그리고 중복 패키지를 최대한 방지하기 위해 yarn hoisting을 적용하였고 container마다 패키지를 공유하기 위해 오직 패키지만 가지고 있는 octodocs-modules라는 이미지를 base로 해서 각 workspace 별 이미지를 만들었습니다.
그래서 여러 개의 package.json 파일을 각 workspace 디렉토리로 복사해야 하는데 COPY 명령어는 여러 개의 목적지를 설정하는 것이 불가능합니다.
그래서 COPY 명령어를 여러 개 사용한 것인데 COPY 명령어 하나마다 layer가 생성되기 때문에 너무 많은 layer가 생성됩니다.
이 문제를 해결하기 위해 multi stage build를 도입했습니다.
builder stage에서 COPY 명령어를 4개 사용해서 서로 다른 목적지에 파일을 복사한 뒤 다음 stage에서 디렉토리 통째로 복사하면 됩니다.
# COPY 명령어는 여러 디렉토리를 목적지로 파일 복사하는 것이 불가능하다.
# 빌드 스테이지에서 COPY 명령어를 여러 번 생성해서 여러 디렉토리로 파일을 복사한다.
# 그 후 그 디렉토리 자체를 다음 스테이지에서 그대로 복사하여 layer의 개수를 줄인다.
FROM node:20-alpine as builder
WORKDIR /app
# 호이스팅을 위해
COPY package.json yarn.lock ./
COPY apps/backend/package.json ./apps/backend/
COPY apps/frontend/package.json ./apps/frontend/
COPY apps/websocket/package.json ./apps/websocket/
# node_modules를 가지고 있는 이미지
# 이 이미지를 기반으로 각 workspace 별 이미지를 만들면
# yarn install 레이어를 공유하게 된다.
FROM summersummerwhy/octodocs-modules:latest
# 호이스팅을 위해
COPY --from=builder /app/apps /app/apps
# 의존성 설치
RUN yarn install --check-files
multi stage build를 적용한 뒤 이미지를 빌드할 때마다 늘어나던 layer의 개수가 6개에서 2개로 줄어들었습니다.
- octodocs-modules 이미지 push 시간 71초 감소 (83s -> 12s)
- octodocs-modules 이미지 pull 시간 54초 감소 (68s -> 14)
- octodocs-modules 이미지 빌드할 때마다 늘어나던 layer 수 4개 감소 (6개 -> 2개)
- websocket, backend, websocket 이미지 빌드 시간 9초 감소 (45s -> 36s)
- websocket, backend, websocket 이미지 푸시 시간 6초 감소 (12s -> 6s)