개요
이전에 6주간 네이버 부스트캠프에서 그룹 프로젝트를 진행했습니다.
적은 개발 기간 속에서 기획, 아키텍처 설계, 기능 개발까지 모두 구현하느라, 정리가 안된 항목들이 많이 있었는데요.
그중 하나가 바로 CI 파이프라인이었습니다.
위의 사진들에서 볼 수 있듯이 프로젝트 배포에 필요한 도커 이미지를 만드는데 많은 시간을 소요하고 있었습니다.
이렇게 긴 시간을 차지하고 있는 CI 파이프라인은 개발자들의 개발 속도를 늦추게 했습니다. 실제로 저는 그러한 경험을 겪었는데요. 배포 환경에서 발생한 버그를 수정하기 위해 빠르게 코드를 고쳤어도 CI 파이프라인의 속도가 느려 배포 환경에 적용이 원할하게 되지 않아 기다림을 계속했던 기억이 있습니다.
그룹 프로젝트의 전반적인 인프라 및 파이프라인 구축을 맡은 저로서는 팀원들의 기다리는 모습을 보며 꼭 개선을 해야겠다는 다짐을 했습니다.
그렇게 해서, 이번에는 실제로 CI 파이프라인을 개선해 본 결과와 그 과정을 공유해보려고 합니다.
본문
프로젝트 구조
개선 과정을 공유하기 전에 프로젝트 환경이 어떻게 구성되었는지 이해해야 밑의 글을 확실히 이해할 수 있을 것이라고 판단해 작성하려고 합니다.
저희는 단일 리포지토리에서 모든 서비스를 관리하는 모노레포 방식을 채택했습니다. 프로젝트는 크게 apps와 packages 두 디렉토리로 나뉩니다.
Copy
README.md
├── apps (주요 프로그램)
│ ├── client
│ ├── hub
│ ├── nginx
│ └── server
├── packages (공통 모듈)
│ ├── cli
│ ├── cloud-graph
│ ├── ncloud-sdk
│ └── terraform
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
apps 디렉토리에는 실제 서버에 배포되는 프로그램들이, packages 디렉토리에는 여러 앱에서 공통으로 사용되는 모듈들이 위치해 있습니다.
의존성 관리
모노레포에서 중요한 것은 각 애플리케이션과 패키지 간의 의존성 관리입니다. 저희 프로젝트에서는 다음과 같은 의존성을 가지고 있습니다.
- React 기반 애플리케이션은 cloud-graph와 terraform 패키지를 사용
- NextJS 애플리케이션은 현재 독립적으로 운영
- NestJS 기반 백엔드는 ncloud-sdk 패키지에 의존
이러한 의존성 구조로 인해 빌드 프로세스에 특별한 주의가 필요합니다. 예를 들어, 백엔드 서버의 도커 이미지를 생성할 때는 다음과 같은 순서로 진행되어야 합니다:
- ncloud-sdk 및 백엔드 의존성 설치
- ncloud-sdk 빌드
- 백엔드 빌드
도구 선택
pnpm
패키지 매니저로는 pnpm을 선택했습니다. 선택 이유는 두 가지입니다.
- 효율적인 저장소 관리: 각 서비스와 모듈에서 공통으로 사용되는 npm 모듈을 중앙 저장소에 저장함으로써 프로젝트의 전체 크기를 최적화할 수 있습니다.
- 워크스페이스 기능: pnpm이 제공하는 워크스페이스 기능을 활용하면 모노레포 구축이 매우 간단해집니다.
Turborepo
모노레포 관리 도구로는 Turborepo를 도입했습니다. 주된 이유는 다음과 같습니다.
- 캐싱 기능: npm 명령어 실행 결과를 캐싱할 수 있어 빌드 시간을 크게 단축할 수 있습니다.
- 증분 빌드: 변경된 부분만 선택적으로 빌드할 수 있어 개발 생산성이 향상됩니다.
프로젝트 환경은 간략하게 이정도만 설명하도록 하고 그럼 CI 파이프라인 작업으로 넘어가겠습니다.
도커 이미지 경량화 및 속도 개선
가장 먼저 진행했던 것은 도커 이미지 사이즈 경량화 및 이미지 빌드 속도 개선입니다.
아무래도 CI 파이프라인에서 속도가 느린 요인으로 그 역할(?)을 톡톡히 하고 있는 듯 했습니다.
또한 이렇게 오랜 시간이 걸려 만들어진 도커 이미지의 사이즈는 NestJS를 사용하고 있는 백엔드를 기준으로 2GB에 육박했습니다.
백엔드가 packages의 의존성이 없었던 초기에는 멀티 스테이지 빌드와 이미지 레이어 감소를 통해 도커 이미지를 300MB까지 경량화할 수 있었습니다. 하지만 프로젝트가 발전하면서 백엔드가 packages의 여러 모듈에 의존성을 갖게 되었고, 이러한 의존성 환경에서는 최적화를 위해 작성했던 기존의 Dockerfile이 더 이상 정상적으로 동작하지 않게 되었습니다.
프로젝트의 시간 제약으로 인해 우선 COPY . . 명령어를 사용한 기본적인 Dockerfile을 임시로 적용하게 되었습니다. 이 과정에서 이미지 크기가 300MB에서 2GB로 크게 증가했고, 이는 여러 문제를 야기했습니다.
- CI/CD 파이프라인 성능 저하
- Github Actions에서 컨테이너 레지스트리로의 이미지 Push 속도 저하
- 배포 서버에서 이미지 Pull 시간 증가
- 비용 증가
- 컨테이너 레지스트리의 스토리지 사용량 증가로 인한 요금 상승
- 빌드 효율성 저하
- 불필요한 devDependencies까지 설치되면서 의존성 설치 시간이 크게 증가
그래서 도커 이미지 경량화를 통해 두 가지 주요 목표를 달성하고자 했습니다.
- CI 파이프라인 성능 개선
- 이미지 Push/Pull 시간 단축
- 컨테이너 레지스트리 비용 절감
- 배포 프로세스 최적화
- 빌드 시간 단축
- 더 빠른 배포 사이클 구현
Turborepo의 prune을 활용해 빠르게 의존성 추출하기
아까 위에서 모노레포 환경에서의 Dockerfile을 제대로 만들지 못하고, 결국 기초적인 Dockerfile을 활용해 여러 문제를 발생시켰다고 말한 거 같습니다.(자랑은 아닙니다 ㅎㅎ;;)
하지만 왜 빠르게 모노레포 환경에 맞춘 Dockerfile을 만들지 못했느냐라고 묻는다면, 순수 Dockerfile 스크립트로 필요한 의존성 부분만 추출해서 이를 메인 프로그램과 결합시키는 것이 꽤나 복잡하고 어려운 작업이었기 때문입니다.
예를 들어 아까 백엔드에서는 ncloud-sdk 모듈을 의존하고 있다고 말씀을 드렸는데, ncloud-sdk 모듈을 의존한 상태로 성공적인 Dockerfile을 만들기 위해서는 모노레포 디렉토리와 똑같은 환경을 가진 Dockerfile을 만들어야 합니다.
말 그래도 기존 프로젝트 저장소에서 다른 부분을 제외하고 백엔드와 ncloud-sdk가 똑같은 위치에 있어야 한다는 뜻입니다.
그렇게 하지 않으면 의존성을 설치할 때 오류가 나는 문제가 발생합니다. 지금은 해당 문제의 원인을 빠르게 파악하고 Dockerfile을 만들 수 있지만 기존 Dockerfile에서 문제가 발생해 급히 새로운 Dockerfile을 만들어야 했던 때에는 이러한 문제를 빠르게 파악할 수 없었습니다. 그래서 기본적인 Dockerfile를 만들게 된 것인데요.
하지만 Turborepo에서 제공하는 prune —docker 명령어를 통해서 apps에 있는 프로그램의 얽혀있는 의존성을 빠르게 분석하고 필요한 의존성만 추출할 수 있습니다.(아래 사진은 turbo prune server —docker 시 나오는 결과물입니다.)
turbo prune —docker 를 입력하게 되면 out 폴더가 생기게 되고 out 폴더 안에는 full과 json 두 개의 폴더와 lock이나 workspace 파일 등을 확인할 수 있습니다.
- full 폴더: 타겟을 빌드하는 데 필요한 패키지들을 포함한 전반적인 소스코드를 보관합니다.
- json 폴더: 타겟을 빌드하는 데 필요한 패키지들이 명시되어 있는 package.json 등만 보관합니다.
- 그외 나머지 파일: workspace 파일은 모노레포 환경에서의 빌드를 위해서 lock 파일은 의존성을 설치할 때 빠르게 참조할 수 있도록 하기 위해 포함됩니다.
이렇게 나눠진 파일은 Dockerfile을 만들게 될 때 다음 순서로 활용됩니다.
- 먼저 json 폴더를 옮겨서 의존성 설치
- 그 다음 full 폴더를 활용해서 빌드 진행
참고로 json 폴더를 먼저 옮겨와서 의존성 설치를 진행하는 이유는 package.json은 소스 코드보다 덜 변경되는 파일이기 때문입니다. 따라서 이미지 레이어 캐싱을 통해 빠르게 도커 이미지를 만들도록 할 수 있습니다.
# 1번: 소스코드를 포함해서 전부 가져와서 의존성을 실행할 경우
# 소스 코드가 변경되면 의존성이 똑같아도 캐싱이 무효화 돼서 캐싱처리 안됨
# 소스 코드 변경에 따라 의존성만 설치할 뿐인데도 이후 과정이 캐싱처리 되지 않고 새로 실행됨
COPY ./out/full/ .
RUN pnpm install
# 2번: json 폴더를 통해 의존성만 먼저 가져와서 의존성을 설치하고 이후 소스코드를 가져오게 될 경우
COPY ./out/json/ .
# package.json은 바뀌는 경우가 소스코드 보다 적으므로 대다수의 경우에서 캐싱처리 될 수 있음
RUN pnpm install
# 이후부터는 소스 코드 변경으로 인해서 캐싱처리가 안될 확률이 높음
COPY ./out/full/ .
RUN pnpm build
이를 통해 빠르게 Dockerfile을 만들 수 있게 됐는데요, 기존 Dockerfile과 개선된 Dockerfile은 다음과 같습니다.
기존
FROM node:20
WORKDIR /usr/src/app
COPY . .
RUN npm install -g pnpm && pnpm install && pnpm build
ENV DATABASE_URL=''
ENV MYSQL_HOST=1
ENV MYSQL_PORT=3306
ENV REDIS_HOST=3
ENV REDIS_PORT=6379
ENV NCLOUD_ACCESS_KEY=0
ENV NCLOUD_SECRET_KEY=0
ENV PORT=3000
ENV NODE_ENV=development
EXPOSE 3000
개선
FROM node:20-alpine AS prune
WORKDIR /prune
COPY . .
RUN npm install -g turbo && turbo prune server --docker
FROM node:20-alpine AS dependencies
WORKDIR /dependencies
COPY --from=prune /prune/out/json/ .
RUN npm install -g pnpm && pnpm install --frozen-lockfile --prod
FROM node:20-alpine AS build
WORKDIR /build
COPY --from=prune /prune/out/json/ .
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY --from=prune /prune/out/full/ .
RUN apk add --no-cache openssl && cd /build/apps/server && pnpm build && cd /build/packages/ncloud-sdk && pnpm build
FROM node:20-alpine AS production
WORKDIR /app
RUN apk add --no-cache openssl
COPY ./apps/server/prisma ./apps/server/prisma
COPY --from=dependencies /dependencies/node_modules ./node_modules
COPY --from=dependencies /dependencies/pnpm-workspace.yaml .
COPY --from=dependencies /dependencies/package.json .
COPY --from=dependencies /dependencies/packages/ncloud-sdk/node_modules ./packages/ncloud-sdk/node_modules
COPY --from=build /build/packages/ncloud-sdk/dist ./packages/ncloud-sdk/dist
COPY --from=dependencies /dependencies/packages/ncloud-sdk/package.json ./packages/ncloud-sdk/package.json
COPY --from=dependencies /dependencies/apps/server/node_modules ./apps/server/node_modules
COPY --from=build /build/apps/server/dist ./apps/server/dist
ENV NODE_ENV=''
ENV DATABASE_URL=''
ENV REDIS_HOST=''
ENV REDIS_PORT=''
ENV NCLOUD_ACCESS_KEY=''
ENV NCLOUD_SECRET_KEY=''
CMD [ "node", "./apps/server/dist/src/main.js" ]
build 한 번으로 모든 모노레포 서비스 빌드 끝내기
근데 막상 개선된 Dockerfile을 만들고 나니 이런 의문이 들었습니다. 왜 굳이 Dockerfile에서 프로그램 빌드를 해야하는 걸까? 생각해보면 그랬습니다. 프로젝트 전반이 타입스크립트로 이루어진 프로젝트입니다. 타입스크립트의 빌드 결과물은 자바스크립트로 변환되는 것이며, 자바스크립트의 빌드 결과물은 바이트 코드로 변경되는 것이 아닌(물론 실제로 실행될 때는 바이트 코드와 비스무리한 뭔가로 컴파일 된 다음 실행되긴 하지만 그건 넘어갑시다^^) 번들링이 되는 것 뿐인데, 굳이 Dockerfile에서 빌드를 함으로써 도커 이미지 빌드 시간을 증가시켜야 하나 싶었습니다.
게다가 Turborepo를 이용하고 있었기 때문에 로컬 프로젝트 저장소에서 빌드 캐시를 활용한다면, 프로젝트를 빌드하는 시간 동안 매우 빠르게 처리될 수 있을 것이라 판단했습니다.
그래서 저희는 기존 Dockerfile에서 진행하고 있는 빌드 작업을 제외하고 로컬 환경에서 빌드 후 나온 dist 폴더를 Dockerfile에 옮기는 것으로 빌드 속도를 개선했습니다.
FROM node:20-alpine AS prune
WORKDIR /prune
COPY . .
RUN npm install -g turbo && turbo prune server --docker
FROM node:20-alpine AS dependencies
WORKDIR /dependencies
COPY --from=prune /prune/out/json/ .
COPY --from=prune /prune/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=prune /prune/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
RUN npm install -g pnpm && pnpm install --frozen-lockfile --prod
FROM node:20-alpine AS production
WORKDIR /app
RUN apk add --no-cache openssl
COPY ./apps/server/prisma ./apps/server/prisma
COPY --from=dependencies /dependencies/node_modules ./node_modules
COPY --from=dependencies /dependencies/pnpm-workspace.yaml .
COPY --from=dependencies /dependencies/package.json .
COPY --from=dependencies /dependencies/packages/ncloud-sdk/node_modules ./packages/ncloud-sdk/node_modules
COPY ./packages/ncloud-sdk/dist ./packages/ncloud-sdk/dist
COPY ./packages/ncloud-sdk/package.json ./packages/ncloud-sdk/package.json
COPY --from=dependencies /dependencies/apps/server/node_modules ./apps/server/node_modules
COPY ./apps/server/dist ./apps/server/dist
ENV NODE_ENV=''
ENV DATABASE_URL=''
ENV REDIS_HOST=''
ENV REDIS_PORT=''
ENV NCLOUD_ACCESS_KEY=''
ENV NCLOUD_SECRET_KEY=''
CMD [ "node", "./apps/server/dist/src/main.js" ]
이후 이미지 빌드 속도를 측정해보았습니다. 테스트 케이스는 다음과 같이 세 가지 경우로 나누었습니다. (각 테스트마다 docker system prune -a를 활용해 빌드 캐시 및 이미지를 제거 후 측정했습니다. 추가로 로컬 환경에서 실행한 결과입니다.)
기존 Dockerfile을 활용해서 도커 컴포즈 환경에서 front, front-hub, back 이미지 빌드 ⇒ 도커 컴포즈 빌드 시간 143초 = 143초
새롭게 만든 Dockefile을 활용해서 도커 컴포즈 환경에서 front, front-hub, back 이미지 빌드(Turborepo 캐싱 미적용) ⇒ 빌드 시간 20초 + 도커 컴포즈 빌드 시간 27초 ⇒ 47초
새롭게 만든 Dockefile을 활용해서 도커 컴포즈 환경에서 front, front-hub, back 이미지 빌드(Turborepo 캐싱 적용) ⇒ 빌드 시간 1초 미만 + 도커 컴포즈 빌드 시간 27초 ⇒ 28초
여기서 주의할 점은 turbo build 시 나오는 결과물을 명확하게 캐싱할 수 있도록 turbo.json 파일의 outputs 부분에 이러한 경로를 잘 명시해두어야 한다는 점입니다.
잘 명시해 두지 않을 경우 캐싱이 이루어지지 않으며, 빌드 결과물이 제대로 나오지 않는 문제가 발생할 수 있습니다.
"build": {
"inputs": ["$TURBO_DEFAULT$", "!README.md", "!CHANGELOG.md"],
// outputs 항목에 경우에 따라 나오는 빌드 결과물 경로를 잘 포함해야 합니다.
// 예를 들어 저같은 경우 .next/**를 없앨 경우 캐싱은 잘 되지만 빌드된 NextJS 결과물이 나오지 않는 현상이 발생했습니다.
"outputs": ["dist/**", ".next/**"]
},
자세한 내용은 아래 문서를 참고해주세요.
Configuring turbo.json | Turborepo
베이스 이미지 활용하기
마지막으로 베이스 이미지를 활용했습니다. 기존에는 베이스 이미지를 사용하지 않고 도커 이미지를 빌드할 때마다 프로덕션 환경에 의존성을 설치하는 과정을 반복했습니다.
문제라고 볼 건 아니지만, 이러한 과정 또한 시간을 잡아먹는 요소이기 때문에 의존성을 미리 설치해 놓은 베이스 이미지를 따로 만들어 두고, 도커 이미지를 빌드할 때 마냥 베이스 이미지에서 가지고 있는 의존성 외에 추가적인 의존성을 보유하고 있다면, pnpm install을 통해 추가 의존성을 설치하도록 하였습니다.
그렇게 최종적으로 개선된 Dockerfile은 다음과 같습니다.
FROM node:20-alpine AS prune
WORKDIR /prune
COPY . .
RUN npm install -g turbo && turbo prune server --docker
FROM registry/cloud-canvas-server-base AS dependencies
COPY --from=prune /prune/out/json/ .
COPY --from=prune /prune/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=prune /prune/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
RUN pnpm install --frozen-lockfile --prod
FROM node:20-alpine AS production
WORKDIR /app
RUN apk add --no-cache openssl
COPY ./apps/server/prisma ./apps/server/prisma
COPY --from=dependencies /dependencies/node_modules ./node_modules
COPY --from=dependencies /dependencies/pnpm-workspace.yaml .
COPY --from=dependencies /dependencies/package.json .
COPY --from=dependencies /dependencies/packages/ncloud-sdk/node_modules ./packages/ncloud-sdk/node_modules
COPY ./packages/ncloud-sdk/dist ./packages/ncloud-sdk/dist
COPY ./packages/ncloud-sdk/package.json ./packages/ncloud-sdk/package.json
COPY --from=dependencies /dependencies/apps/server/node_modules ./apps/server/node_modules
COPY ./apps/server/dist ./apps/server/dist
ENV NODE_ENV=''
ENV DATABASE_URL=''
ENV REDIS_HOST=''
ENV REDIS_PORT=''
ENV NCLOUD_ACCESS_KEY=''
ENV NCLOUD_SECRET_KEY=''
CMD [ "node", "./apps/server/dist/src/main.js" ]
또한 베이스 이미지를 사용했을 때와 그렇지 않은 경우도 나눠서 테스트를 진행해보았습니다. (각 테스트마다 docker system prune -a를 활용해 빌드 캐시 및 이미지를 제거 후 측정했습니다. 추가로 로컬 환경에서 실행한 결과입니다.)
베이스 이미지 사용 안했을 때 백엔드 이미지 빌드 속도 ⇒ 16초(의존성 설치 시간 7.5초)
베이스 이미지 사용했을 때 백엔드 이미지 빌드 속도 ⇒ 9초(의존성 설치 시간 0.9초)
이렇게 다음과 같은 과정을 거친 백엔드 도커 이미지는 캐싱 적용시 빌드 시간 9초(기존 대비 약 20배 감소), 도커 이미지 사이즈 240MB(기존 대비 약 10배 감소)라는 크기를 가지게 되었습니다!
Github Actions 속도 개선
도커 이미지 최적화를 끝내고 이 다음에는 본격적으로 CI 파이프라인 속도 개선 작업에 돌입했습니다.
아무래도 CI 파이프라인 또한 결과적으로 Dockerfile을 만드는 것이 중심이었기 때문에, 기존에 로컬 환경에서 진행했던 캐싱 등의 작업을 그대로 CI 파이프라인에서 구현할 수 있게끔 하는 것이 중요했습니다.
그래서 이후 나오는 내용들은 로컬 환경에서 적용했던 캐싱을 어떻게 Github Actions에서도 활용할 수 있었는지에 대한 이야기입니다.
Turbo Remote Cache
Turborepo는 Remote Cache 시스템을 지원합니다. 이를 활용해 캐싱 결과물을 원격 서버에 보관하고 이를 활용해서 어떠한 곳에서 Turbo 명령어를 실행해도 캐싱 결과물을 참조하여 빠른 실행을 보장할 수 있습니다.
저희 같은 경우에는 처음에는 자체적으로 Turbo Remote Cache 서버를 구축해서 이를 사용해보자는 이야기가 나왔으나. 아무래도 그럴려면 서버를 따로 구축하거나 서버리스 리소스를 추가적으로 활용해야 했기에 비용이 발생하는 이유로 고민이 되었는데요.
조금 더 찾아본 결과 Vercel에서 조금이긴 하지만 무료 요금제로 Remote Cache을 제공하고 있다는 것을 알게 되었고 프로젝트의 결과물 누출에 대해서 사이드 프로젝트 특성 상 걱정할 일이 아니었기 때문에 Vercel 기반의 Remote Cache을 결정하게 되었습니다.
방법은 따로 설명하지 않겠지만, Turborepo에서 제공하는 문서가 설명을 아주 잘 해놓았기 때문에 이것을 보고 따라하시면 될 거 같습니다!
ps. 무료 요금제(Hobby)로 이용해도 사이드 프로젝트 용으로 사용하기에 적절할 거 같습니다.
결과적으로 Remote Cache를 적용하여 turbo build 명령어 시간을 획기적으로 단축할 수 있었습니다!
Remote Cache 적용 전 ⇒ 약 50초
Remote Cache 적용 후 ⇒ 약 6초
pnpm Cache
다음은 pnpm cache입니다. pnpm 의존성을 캐싱하는 이유는 아까 도커 이미지 경량화 부분에서 로컬에서 빌드를 진행하고 빌드로 나온 결과물을 Dockerfile에서 쓰기 때문입니다. Github Actions에서 빌드를 하기 위해서는 빌드 전 의존성을 설치하는 과정을 추가해야 합니다.
그리고 이렇게 의존성을 설치하는 과정을 Github Actions에서도 캐싱할 수 있도록 하였습니다.
pnpm 공식 문서에서 pnpm으로 설치된 의존성을 어떻게 캐싱할 수 있는지 잘 설명되어 있기에 이 역시 생략하겠습니다. 아래 문서를 참고해주세요!
결과적으로 pnpm install 시간 또한 줄일 수 있었습니다.
pnpm Cache 적용 전 ⇒ 20초
pnpm Cache 적용 후 ⇒ 5초
Registry Cache
앞서 저희는 도커 이미지 최적화를 통해 로컬 환경에서 빌드 시간을 크게 단축했습니다. 하지만 CI/CD 환경, 특히 Github Actions에서는 새로운 문제에 직면했습니다. Github Actions는 매 실행마다 새로운 환경을 제공합니다. 이는 로컬 환경과 달리 이전 빌드에서 생성된 도커 이미지 레이어 캐시를 활용할 수 없다는 것을 의미합니다. 결과적으로 CI/CD 파이프라인에서는 매번 전체 이미지를 처음부터 다시 빌드해야 하는 비효율이 발생했습니다.
이 문제를 해결하기 위해 우리는 외부 레지스트리를 캐시 저장소로 활용하는 방법을 선택했습니다. BuildKit의 --cache-from과 --cache-to 기능을 사용하면, 이전 빌드의 캐시를 다운로드하고 새로운 캐시를 업로드할 수 있습니다.
하지만 여기서 주의할 점이 있습니다. Registry Cache 기능은 Docker Buildx에서 지원하는 기능입니다. 따라서 이 기능을 사용하기 위해서는 반드시 Github Actions 워크플로우에서 docker/setup-buildx-action@v3를 설정해야 합니다.
조금 더 Registry Cache에 대해 궁금하시다면 아래 문서를 참고해주세요!
이렇게 Registry Cache를 적용하게 되면, 실제로 도커 이미지를 빌드할 때 이미지 레이어 캐시 히트 비율이 몇 퍼센트인지 확인할 수 있습니다.
paths-filter 액션을 활용한 선택적 CI 구현
마지막으로 CI 파이프라인이 실행될 때 선택적으로 변경된 부분만 도커 이미지를 빌드하도록 하였습니다.
이때 사용된 paths-filter 액션은 다른 분들도 한 번 이용해보시면 좋을 거 같습니다!
paths-filter를 통해 상태 변경을 감지하는 부분을 라벨링 하여 묶을 수 있고, 이를 활용해 모노레포의 Github Actions CI 효율성을 극대화 할 수 있습니다.
https://github.com/dorny/paths-filter
그래서 저는 apps에 있는 메인 프로그램 1개와 그 메인 프로그램에서 사용하고 있는 공통 모듈을 묶어서 front, front-hub, back으로 라벨링을 진행하였습니다.
- uses: dorny/paths-filter@v3
id: changes
with:
base: ${{ github.repository_default_branch }}
filters: |
front:
- './apps/client/**'
- './packages/terraform/**'
- './packages/cloud-graph/**'
front-hub:
- './apps/hub/**'
back:
- './apps/server/**'
- './packages/ncloud-sdk/**'
- './packages/terraform/**'
예를 들어 front 항목에서 apps/client, packages/terraform, packages/cloud-graph 중 어느 한 곳에서라도 소스 코드의 변경이 일어났다면, front label의 상태가 true로 바뀌고 이렇게 바뀐 상태를 if 조건으로 감지해서 도커 이미지 빌드 및 푸시를 할 수 있도록 설정했습니다.
- name: Docker front image build and push
if: steps.changes.outputs.front == 'true'
uses: docker/build-push-action@v6
with:
context: .
file: ./apps/client/Dockerfile.CI
push: true
tags: |
cloud-canvas.kr.ncr.ntruss.com/front:dev
cloud-canvas.kr.ncr.ntruss.com/front:${{ github.sha }}
cache-from: type=registry,ref=cloud-canvas.kr.ncr.ntruss.com/front:buildcache
cache-to: type=registry,ref=cloud-canvas.kr.ncr.ntruss.com/front:buildcache,mode=max
그래서 이렇게 변경이 없는 경우에는 실행이 되지 않지만.
front 라벨에서 감지하고 있는 부분에서 변경이 일어날 경우 front 이미지를 빌드 및 푸시하는 것을 확인할 수 있습니다.
결과적으로 이렇게 Github Actions CI 파이프라인을 최적화 함으로써 기존보다 빌드 시간을 최대 2.5배(5분 55초 → 2분 30초)를 단축할 수 있게 되었습니다!
이전 환경에서의 Github Actions(front, front-hub, back 이미지 빌드) ⇒ 5분 55초
개선된 환경에서의 Github Actions(front, front-hub, back 이미지 빌드) ⇒ 2분 30초
아무것도 빌드하지 않을 때 Github Actions ⇒ 34초
참고자료
모노레포에서 Github Actions 현명하게 사용하기
Configuring turbo.json | Turborepo
https://github.com/dorny/paths-filter