현재 진행중인 Remember Me 프로젝트에서는 GCP + Docker + Github Actions로 CI/CD 환경을 만들어 작업하고 있습니다. CI/CD 환경을 만들어 굉장히 편리해졌지만, 커밋 후 반영되기까지 기다려야 하는 시간이 2분 후반에서 3분까지 걸렸습니다. 아직 프로젝트 스케일이 작아서 적은 시간이기는 하지만, 이 속도를 개선해볼 수 있지 않을까 고민해보았습니다.
찾아보니 반복되는 작업들, 그 중에서도 Gradle 의존성을 주입받는 과정과 Docker 레이어를 캐싱하여 속도를 개선할 수 있었습니다. 추가적으로, 불필요한 파일은 워크플로우에서 제거하여 서버의 다운타임을 최소화하고자 했습니다. 그리고 제 프로젝트에는 적용하지 않았지만, jobs를 분리하여 병렬적으로 처리하여 속도를 개선하는 방법에 대해서도 함께 정리해보았습니다.
우선 개선 전에는 대략 3분 정도의 시간이 평균적으로 소요되었습니다.
1. 의존성 주입부분 캐싱하기
Github Actions에서는 매 Workflow를 실행할 때마다 Gradle 빌드에 필요한 의존성들을 모두 다운받습니다. 즉, 매번 이전에 다운받은 의존성을 모두 다시 다운받는 불필요한 과정을 거치게 됩니다.
그래서 이전에 다운받았던 의존성들을 캐싱하여, 의존성이 변경되지 않은 경우 새로 다운받지 않고 이전 캐싱값을 불러오도록 하여 속도를 개선시켜보겠습니다.
actions/cache@v3
을 사용하여 Gradle 의존성을 캐싱해주었습니다.
actions/cache@v3
문서 참고 : https://github.com/actions/cache/blob/main/examples.md#java—gradle
여기서 사용한 with 변수들에 대해 알아보겠습니다.
우선 key
는 hashfiles의 파일들을 해싱한 키값입니다. 이는 해당 파일을 해싱한 키값으로 해당 파일이 변경되었는 지 판단합니다. 만약 파일을 해싱한 키값이 달라졌다면, 해당 파일이 변경되었다는 의미이므로 의존성을 다시 다운받습니다. 그리고 restore key
는 만약 key값이 다를때, 이전에 캐싱된 키와 같은 지 찾는 데 활용됩니다.
로그를 보면 다음처럼 캐싱하는 단계에서 추가적으로 5s가 걸리지만, 이후 Gradle 의존성 다운받는 부분은 훨씬 빨라졌습니다 ⚡
이제는 이전과 의존성이 달라지지 않을 경우, 따로 다운받지 않고 캐싱된 의존성을 가져와서 사용합니다. Gradle Build 작업을 1분대에서 10-20초대까지 작업속도를 개선시켰습니다.
2. 도커 레이어 캐싱하기
다음으로는 Docker 레이어를 캐싱해보겠습니다.
기본적으로는 Github Actions는 Docker 레이어를 캐싱하지 않습니다. 따라서 매 Workflow마다 Dockerfile의 모든 레이어를 새롭게 생성하게 됩니다. 여기서 Docker 레이어를 캐싱하도록 설정해주면 속도를 개선시킬 수 있습니다.
Buildx
와 docker/build-push-action
을 통해서 Docker 레이어를 캐싱해보겠습니다.
docker/build-push-action
참고 문서 : https://github.com/docker/build-push-action
기존 gradle.yml 파일에서 Buildx
와 docker/build-push-action
액션을 추가해주었습니다. 우선 Buildx
는 docker/build-push-action
액션을 사용하기 위한 빌드툴입니다. 아래 문서를 보면, 캐시를 사용하기 위해서는 Buildx
를 다운받아 주어야 합니다.
그리고 docker/build-push-action
의 cache- from과 to는 캐싱하고자 하는 출처와 캐시를 저장하는 위치를 뜻합니다. 그리고 이번에 알게되었는데 gha는 GitHub Actions의 줄임말(!)이었습니다. gha에 캐시를 저장하고, 캐시를 찾을때도 gha를 조회한다는 의미인 것 같습니다.
참고로 캐시는 7일동안 보관되고, 하나의 Repository당 캐시가 10GB를 넘어가면 예전 캐시들은 자동 삭제된다고 합니다.
사실 현재까지의 제 프로젝트에서는 Docker 레이어가 간단하기 때문에 Docker 레이어 캐싱을 통해서 시간 단축이 딱히 이루어지지는 않았습니다. 다만 후에 Docker 레이어가 많아질 경우, 훨씬 개선될 것 같습니다. ㅎㅎ
3. 불필요한 파일은 Workflow에서 제외하기
추가적으로 제 백엔드 코드가 최대한 빠르게 반영되고, 서버가 멈춰있는 시간을 줄여 프론트 개발자가 작업하기 편한 환경을 만들어주고 싶었습니다. 그래서 CI/CD가 불필요한 파일은 Workflow에서 제외해주고 싶었습니다.
특히 저는 Readme 파일을 수시로 업데이트해주는데, 이때마다 Workflow가 불필요하게 실행되는 걸 막고 싶었습니다. 그래서 다음처럼 path-ignore
를 통해 readme.md 등의 파일은 제외시켜주었습니다.
이제 Readme 파일의 수정으로는 Workflow가 실행되지 않게 되었습니다!
4. jobs를 병렬적으로 분리하기
마지막으로는 jobs를 분리하여 병렬적으로 실행하여 속도를 개선하는 방법입니다.
GitHub Actions는 가장 상위 개념인 Workflow에 여러 jobs라는 작업 단위로 실행됩니다. 그리고 jobs 마다 steps라는 작업 단위로 순차적으로 실행됩니다.
기본적으로는 jobs들끼리는 병렬적으로 실행되지만, 순차적으로 실행되야하는 작업은 다음처럼 순서를 지정해줄 수 있습니다. needs :
을 통해서 이전에 수행되야하는 작업을 지정해주었습니다. (build → deploy
)
만약 병렬적으로 실행되어도 가능한 작업인 경우, jobs를 분리하여 병렬적으로 구성하면 CI/CD 속도가 빨라지게 됩니다. 현재 프로젝트에서는 간단하게 빌드 → 배포
의 2단계가 순차적으로 이뤄져야 하기에 해당되지 않지만, 후에 필요할 상황을 위해서 정리해보겠습니다. 예를 들면,
- 여러 test를 실행할 경우
- Front, Backend 각각을 Build할 경우
이처럼 jobs들이 순차적으로 실행될 필요가 없는 경우에는, jobs를 분리하여 병렬적으로 실행하면 CI/CD 속도를 개선시킬 수 있습니다. (단, jobs마다 runner 환경이 셋팅되는 시간이 발생합니다.)
개선 결과⚡
결과적으로 기존 167s에서 99s로 68초, 약 1분 개선되었습니다! 특히 Build 부분에서 속도가 개선되었습니다. 구체적으로 확인해보면 제 프로젝트에서는 특히 Gradle 의존성을 캐싱한 게 속도를 크게 향상시켰습니다. 아직 프로젝트가 작아서 크게 차이가 나지는 않지만, 프로젝트가 클수록 개선효과가 더 크게 느껴질 것 같습니다. ⚡🤓
이렇게 하루에 커밋할 때마다 1분이 아껴지고, 하루에 30번 커밋한다 가정하고 한달 프로젝트면 30 * 30 = 900분이 아껴질 수 있겠습니다. 서비스가 클 수록 이러한 속도 개선이 엄청난 생산성을 가져올 것 같다는 생각이 듭니다..!