Development··5 min read

CI/CD 빌드 시간 절반으로 줄이기

12분 걸리던 CI/CD 파이프라인을 6분으로 줄인 과정을 공유한다.

PR 올리고 12분 동안 커피를 탔다

PR을 올리고 CI가 통과할 때까지 12분. 하루에 PR을 10개 넘게 올리는 날이면, 순수하게 CI 대기 시간만 2시간이 넘었다. 팀원이 5명이면 팀 전체로 하루 10시간의 대기 시간이 발생한다.

이건 커피 타임이 아니라 생산성 문제다.

어디서 시간을 먹고 있었나

GitHub Actions의 각 step 실행 시간을 쪼개봤다. Checkout 30초, Node.js 설치 20초, npm install 3분 30초, 린트 1분 30초, 타입 체크 2분, 테스트 3분, 빌드 2분.

npm install이 전체의 30%를 차지하고 있었다. 그리고 린트, 타입 체크, 테스트가 순차적으로 실행되고 있었다. 이건 병렬로 돌릴 수 있는 작업들인데.

(정확히는 12분 17초에서 시작했다.)

npm install을 15초로 줄인 방법

가장 먼저 한 건 npm 캐싱이었다. actions/cachenode_modules를 캐싱하고, package-lock.json의 해시를 키로 사용했다. 캐시 히트 시 3분 30초가 15초로 줄었다.

.npmrcprefer-offline=true를 추가한 것도 약간 도움이 됐다. 사소하지만 이런 것들이 쌓인다.

린트, 타입 체크, 테스트를 동시에

이 세 개는 서로 독립적인 작업이다. GitHub Actions의 matrix strategy로 병렬화했다. 3개의 job이 동시에 실행되니까 가장 오래 걸리는 작업의 시간만 소요된다.

순차 실행 시 6분 30초이던 구간이 3분으로 줄었다. 각 job마다 의존성 설치 오버헤드가 약간 생기지만, 캐싱 덕분에 30초 정도로 미미했다.

README만 고쳐도 전체 CI가 돌고 있었다

path filter를 추가해서 src/ 하위 파일이 변경된 경우에만 테스트와 빌드를 실행하도록 했다. 문서만 수정한 PR은 린트만 실행하고 1분 만에 통과된다.

솔직히 이건 좀 어이없었다. README 오타 하나 고치는데 12분 기다리고 있었으니.

테스트도 손을 봤다

Jest 설정에서 --changedSince 옵션을 활용해 변경된 파일과 관련된 테스트만 실행하도록 했다. PR 기준으로 base 브랜치와의 diff를 보고 관련 테스트만 돌리니 평균 3분에서 1분 30초로 줄었다.

전체 테스트는 main 브랜치에 머지될 때만 돌리도록 분리했다.

Docker 레이어 캐싱은 덤

배포 파이프라인에서 Docker 빌드도 시간을 잡아먹고 있었다. docker/build-push-action의 cache-from 옵션으로 이전 빌드의 레이어를 재사용하도록 했다. 의존성이 안 바뀐 빌드에서는 2분이 30초로 줄었다.

멀티스테이지 빌드를 적용해서 최종 이미지 크기도 800MB에서 200MB로 줄인 건 덤이다.

12분에서 5분 40초

팀 전체로 보면 하루에 5시간의 대기 시간을 절약한 셈이다. 하루 5시간 곱하기 20일 곱하기 12개월이면 1,200시간. 이 작업에 투자한 시간은 이틀이었다.

CI/CD 최적화는 한 번 하면 매일 복리로 돌아오는 투자다. 지금 셀프 호스팅 러너도 검토하고 있다. GitHub Actions 기본 러너는 2코어 7GB인데, 더 강력한 머신을 쓰면 빌드 시간을 더 줄일 수 있다.

느린 CI는 개발자를 컨텍스트 스위칭하게 만든다.

관련 글