GitHub Actions 기반의 CI 속도 개선 [Vue.js Build]

Denia·2024년 2월 25일
0

TroubleShooting

목록 보기
20/25
post-thumbnail

저는 요즘에도 혼자서 MyGarden 프로젝트를 계속해서 업데이트 하고 있습니다.

코드를 업데이트 하면서 매번 느끼는 점은 CICD는 정말 귀찮은 일이라는 것입니다.

그래서 저는 MyGarden 프로젝트에 GitHub ActionsDocker를 활용해서 CICD를 도입했습니다.

제 프로젝트에 CICD를 도입한 관련 글은 [Vue + Spring] Github Actions를 이용한 CI/CD 구축하기 (+ Jacoco PR Comment 기능)를 봐주시면 됩니다.😊

CICD를 도입하고 나서는 코드 수정만 하면 Test, Build 모두 알아서 진행이 되니, 생산성이 많이 올라갔습니다.

※ 개인 프로젝트라서 아직 도입을 안 하셨더라도, 꼭 도입해보시기를 추천드립니다!!

CI/CD를 도입하고 나서는, PR을 머지하고 배포가 완료될 때까지 기다리기만 하면 됩니다.

근데 가끔씩 지금 내가 수정한 기능이 정상적으로 동작 하는지를, 직접 눈으로 결과를 보고 싶을 때가 있습니다. 그럴 때는 어쩔 수 없이 모니터 앞에서 CI/CD가 끝날 때까지 기다려야 했습니다.

이를 기다리는 시간은 정말 지루하고 아깝습니다.

그래서 이 시간을 지금보다 더 줄여보기로 했습니다.


문제 파악

시간을 줄이기 위해서 GitHub ActionsWorkflow들을 살펴봤습니다.

Workflow에 대해서 살펴보기 전에, 현재 프로젝트에 적용중인 CI/CD에 관해서 간단히 설명드리겠습니다.

CI

  • Front(vue) 쪽 코드를 수정했을 때, node를 사용하여 npm run build가 정상적으로 되는지 확인합니다.
  • Back(spring) 쪽 코드를 수정했을 떄, gradle을 통해 TestBuild가 정상적으로 되는지 확인합니다.

CD

  • PR이 정상적으로 master 브랜치에 머지가 되면, 다음의 로직을 따라서 배포가 됩니다.
    1. gradle을 통해 Build
    2. Docker로 이미지를 만든 후 ghcr.io (Github Action Container Registry)Push
    3. GitHub Actions Runner를 통해 AWS EC2Docker 이미지를 Pull 하고 실행

제일 먼저 CI에서 Front쪽 코드를 수정했을 때, 진행되는 Workflow를 점검 했습니다.

해당 전체 과정이 총 17초 정도 걸리는 것을 볼 수 있으며, Install Dependencies에서 무려 7초나 걸린다는 사실을 알 수 있습니다.

Install Dependencies를 해결하면, 적어도 약 30%이상의 성능 향상이 있을 것 같습니다.


해결 방법 탐색

아무래도 FrontDependencies는 자주 변경이 되지 않을 것 같아서, 캐시를 활용하면 좋을 것 같다는 생각이 들었습니다.

GitHub Actionsnpm cache 관련해서 검색을 하다보니, actions/cache를 알게 되었습니다.

GitHub Actions에서는 쉽게 cache를 사용할 수 있도록 cache관련 action이 있습니다. (actions/cache GitHub)

매번 Install Dependencies 하는 것 대신에, 의존성의 변경이 있을 때만 딱 1번 의존성을 설치하고 해당 의존성을 캐싱해두면 문제를 해결할 수 있을 것 같습니다.
(※의존성을 캐싱하기 위해서는 node_modules폴더를 캐싱하면 되겠습니다.)

사용 방법은 간단했습니다.

해당 step만 추가해주면 됩니다.

주석으로 설명을 달아뒀습니다.

- name: Cache dependencies
  id: cache
  uses: actions/cache@v4
  with:
    # 캐싱할 대상을 정합니다. (대상의 path를 지정)
    # 여기서는 npm에서 의존성이 설치되는 디렉터리인 node_modules를 지정
    path: '**/node_modules'
    
    # 캐싱의 기준은 key를 기준으로 합니다.
    # 의존성이 변경되면, 함께 변경되는 파일인 package-lock.json을 key로 잡아줍니다.
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    
    # key가 유효하지 않은 경우 runner의 운영체제 값과 node라는 suffix를 key로 복구합니다.
    # 결과적으로 package-lock.json이 변경되지 않았다면 캐싱된 node_modules를 사용합니다.
    # 만약 복구될 캐시가 없다면 아래에서 사용할 cache-hit는 false가 됩니다.
    restore-keys: |
      ${{ runner.os }}-node-

cache action을 추가하고 나서는, 다음 step에서 cache hit를 기준으로 Install Dependencies를 할지 말지 결정하면 됩니다.

- name: Install Dependencies
  # cache-hit이 true가 아니면 의존성을 설치합니다.
  if: steps.cache.outputs.cache-hit != 'true'
  run: npm ci

- name: Build with npm
  run: npm run build

해당 workflow가 정상적으로 동작하게 된다면, Install Dependencies를 매번 하지 않아도 되기 때문에 큰 성능 향상이 있을 것 같습니다.


해결 방법 적용

캐싱을 적용 전, yaml 코드

name: Vue build test

on:
  pull_request:
    branches:
      - master
    paths:
      - my-garden-fe/** # 해당 경로의 파일이 수정되었을 때만, 해당 workflow 실행

env:
  NODE_VERSION: 20

jobs:
  front-build:
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: ./my-garden-fe # npm을 실행할 기본 경로 지정

    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: npm # npm 캐싱
          cache-dependency-path: my-garden-fe/package-lock.json

      - name: Install Dependencies
        run: npm install

      - name: Build with npm
        run: npm run build

캐싱을 적용 후, yaml 코드

name: Vue build test

on:
  pull_request:
    branches:
      - master
    paths:
      - my-garden-fe/** # 해당 경로의 파일이 수정되었을 때만, 해당 workflow 실행

env:
  NODE_VERSION: 20

jobs:
  front-build:
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: ./my-garden-fe # npm을 실행할 기본 경로 지정

    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Cache node modules
        id: cache
        uses: actions/cache@v4
        with:
          path: '**/node_modules'
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Install Dependencies
        if: steps.cache.outputs.cache-hit != 'true'
        run: npm ci

      - name: Build with npm
        run: npm run build

달라진 부분들에 대해서 설명을 드리겠습니다.

1. setup-node action의 cache 기능 제거

# 변경 전
- name: Use Node.js
  uses: actions/setup-node@v4
  with:
    node-version: ${{ env.NODE_VERSION }}
    cache: npm # npm 캐싱
    cache-dependency-path: my-garden-fe/package-lock.json

# ========================================================================

# 변경 후
- name: Use Node.js
  uses: actions/setup-node@v4
  with:
    node-version: ${{ env.NODE_VERSION }}
  • setup-node에서 cache 기능을 추가하게 되면, /home/runner/.npm를 캐싱합니다. (.npm 안에는 패키지 캐시와 관련된 정보만을 담고 있습니다.)

    패키지 캐시란 ?

    • 패키지 캐시는 패키지 매니저가 인터넷에서 패키지를 다운로드하거나 로컬에서 패키지를 설치할 때, 이미 다운로드한 패키지를 로컬에 저장하여 같은 패키지를 다시 다운로드하지 않고 이전에 다운로드한 것을 사용함으로써 시간과 대역폭을 절약할 수 있게 도와준다.
    • 패키지 캐시는 주로 .npm 또는 .yarn 디렉토리에 저장되며, 이 디렉토리는 패키지 매니저가 자동으로 생성하고 관리
    • 캐시된 패키지는 해당 패키지의 버전과 메타데이터를 포함하며, 필요할 때 패키지 매니저가 이를 사용하여 종속성을 설치하거나 업데이트
    • 패키지 캐시의 존재는 패키지 매니저가 빌드 시간을 줄이고 패키지를 효율적으로 관리할 수 있도록 도와줌
  • .npm 폴더가 있으면 의존성을 설치할 때 시간을 줄여주긴 하지만, 저희는 의존성 폴더 자체를 캐싱해서 사용할 예정이기 때문에 그다지 필요가 없습니다.

    • .npm 캐싱 keypackage-lock.json 입니다.

    • 의존성이 추가될 때마다 package-lock.json가 변경되기 때문에, 해당 캐시는 초기화가 됩니다.

    • 저희는 의존성이 추가되면, 바로 해당 의존성 폴더를 캐싱해서 사용합니다.

    • 캐싱된 의존성 폴더가 있으면 의존성을 재설치 하지 않아도 됩니다.

    • 그러므로 의존성을 설치할 때 시간을 줄일 필요가 없으며, 해당 .npm 캐시를 내려 받는 시간도 손해입니다.
      (약 40MB를 내려받는데, 1초 정도 걸리는 것 같습니다. → 1초를 손해봄)

2. node_modules 폴더 캐싱 추가 & cache-hit에 따라서 Install Dependencies 진행

# 변경 전
- name: Install Dependencies
  run: npm install

# =========================================================================

# 변경 후
- name: Cache node modules
  id: cache
  uses: actions/cache@v4
  with:
    path: '**/node_modules'
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

- name: Install Dependencies
  if: steps.cache.outputs.cache-hit != 'true'
  run: npm ci
  • 위에서 설명드린 내용 그대로 입니다. cache action을 추가했습니다.
    • cache hit를 기준으로 Install Dependencies를 할지 말지 결정합니다.

3. npm install 대신에 npm ci 사용

- name: Install Dependencies
  run: npm install

# =========================================================================

- name: Install Dependencies
  if: steps.cache.outputs.cache-hit != 'true'
  run: npm ci
  • CI/CD 에서는 npm ci가 더 옳은 명령어라고 해서 수정했습니다.
    • npm installnpm ci 차이
      • npm install은 종속성을 추가하거나 업데이트하기 위해 사용
      • npm cipackage-lock.json에 정의된 정확한 종속성으로 빠르고 일관된 설치를 보장하기 위해 CI 환경에서 사용

결과 확인

  • node modules 의존성이 캐싱되는 첫 workflow를 제외하고 비교하면, 17s12s (약 30%의 성능 향상)
  • workflow 기준
상황설명시간 (초)
cache action 추가 전npm 의존성 캐싱 X, .npm 캐시 적용17
cache action 추가 후 첫 빌드npm 의존성 캐싱 X, .npm 캐시 적용27
cache action 추가 후 두번째 빌드npm 의존성 캐싱 O, .npm 캐시 적용13
cache action 추가 후 세번째 빌드npm 의존성 캐싱 O, .npm 캐시 제거12

cache action 추가 전 (npm 의존성 캐싱 X, .npm 캐시 적용) → 17초

cache action 추가 후 첫 빌드(npm 의존성 캐싱 X, .npm 캐시 적용) → 27초

  • 첫 빌드라서 아직 npm 의존성이 캐싱되어 있지 않습니다. 그래서 node_modules를 캐싱하는 과정이 있습니다.

  • Post Cache node modulesnode moudle을 캐시로 등록하기 위해 업로드하는 시간이 7초 걸림

cache action 추가 후 두번째 빌드 (npm 의존성 캐싱 O, .npm 캐시 적용) → 13초

  • node module을 캐시로 가져왔기 때문에 Install Dependencies가 진행되지 않은 것을 보실 수 있습니다.

  • Cache node modulesnode module을 캐시로 가져오는 시간이 2초 걸림
    (cache-hittrue라서, Install Dependencies가 진행되지 않음)

  • 여기서 Post Cache node modules진행한 job에 대해서 cleanup을 합니다.

cache action 추가 후 세번째 빌드 (npm 의존성 캐싱 O, .npm 캐시 제거) → 12초

  • 위에서 말씀드렸던 .npm 폴더에 대한 캐싱을 삭제했더니, 해당 폴더를 다운로드 하는 시간이 줄어들어 1초 더 빨라졌습니다.


참고 자료

profile
HW -> FW -> Web

0개의 댓글