[Infra] NestJS & Github Action 빌드 개선하기 (1 : CI build - ci.yml 개선)

2024. 5. 22. 23:09Programming/Infra Structure


목차

  1. 목적
  2. CI 빌드 살펴보기
  3. ci.yml 파일에서 불필요한 과정 찾기
    1. Prisma ORM Setting
    2. Install Package
    3. 최종적으로 완성된 ci.yml 파일
  4. RECOMMEND FOR YOU & REFERENCE

목적

내가 직접 인프라를 관리하고 배포를 하는 등의 DevOps 작업을 한 건 이번 프로젝트가 처음이었다.

그런데 이상하리라만치 배포 속도가 느렸고, 이를 개선하고자 한다.

CI 빌드. 평균적으로 58초 가량 걸린다 (최근 빌드 25개 계산)
CD 빌드. 평균적으로 174초 가량 걸린다 (최근 빌드 중 성공한 21개 계산)


CI 빌드 살펴보기

우선 현재 사용중인 CI 빌드의 yml 파일을 살펴보자.

name: CI

on:
  pull_request:
    branches:
      - dev
      - prod

jobs:
  ci-build:
    runs-on: ubuntu-20.04

    steps:
      - name: checkout
        uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: install package manager - pnpm
        run: npm install -g pnpm

      - name: env set
        working-directory: ./
        run: |
          touch .env
          echo POSTGRESQL_DB_URL=${{secrets.POSTGRESQL_DB_URL}} >> .env
          echo JWT_PRIVATE=${{secrets.JWT_PRIVATE}} >> .env
          echo JWT_SECRET=${{secrets.JWT_SECRET}} >> .env
          echo PORT=${{secrets.PORT}} >> .env
          echo SALT=${{secrets.SALT}} >> .env
          echo ORIGIN=${{secrets.ORIGIN}} >> .env

      - name: Install package
        run: pnpm i

      - name: prisma set
        run: |
          npx prisma db push
          npx prisma migrate deploy
          npx prisma generate

      - name: Test unit
        run: pnpm test:prod

      # - name: Test E2E
      #   run: pnpm test:e2e

      - name: test
        run: |
          pnpm build
          timeout ${{ vars.SLEEP }} nohup node dist/main.js > app.log || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
          cat app.log

 

dev 또는 prod 브랜치에서 PR이 등록될 때마다 실행된다.

 

ubuntu 20.04 버전에서 실행되며, node-version은 20으로 가져간다.

pnpm을 설치하고, .env 파일을 생성해 환경변수를 설정한 다음 패키지 설치와 ORM 세팅을 진행한다.

마지막으로 가장 중요한 테스팅을 3단계에 거쳐 실행한다.

첫 번째로는 단위 테스트를 실행하고, 두 번째로는 통합 테스트를 실행한다 (다만 통합 테스트 코드는 아직 작성하지 못하였으므로 실행할 수 없어 주석처리해두었다).

마지막으로는 실제 실행을 하고, 일정 시간 (GH Action 환경변수로 등록해두었다) 이후 자동으로 종료한 이후 로그를 출력하도록 설정했다.


ci.yml 파일에서 불필요한 과정 찾기

어디서 어떤 작업을 하느라 시간이 오래 걸리는 걸까?

가장 오래 걸렸던 작업을 찾아보자.

 

최근 실행된 작업 6개를 기준으로 살펴보았을 때, 평균 실행 시간이 1.5초 이상인 작업의 평균 실행시간은 다음과 같았다.

  1. Prisma ORM Setting : 14.5sec / 최대 17sec
  2. Execute Test : 14.3sec / 최대 15sec
  3. Unit Test : 7.6sec / 최대 8sec
  4. Install Package : 6.8sec / 최대 7sec
  5. Set Up Node.js : 1.8sec / 최대 5sec
  6. Install Package Manage - pnpm : 1.6sec / 최대 2sec

yml 파일만으로 줄일 수 있는 부분은 Prisma ORM Setting, Install Package 정도라고 생각한다.


Prisma ORM Setting

prisma orm setting step을 좀 더 자세히 살펴보자.

  npx prisma db push
  npx prisma migrate deploy
  npx prisma generate
  1. db를 prisma에 동기화하고,
  2. deploy 환경에서 동기화를 진행한 후
  3. prisma/client를 생성하는 방식이다.

효율적으로 진행하려면 어떻게 해야할지 몇 가지 고민을 해보았다.

 

그때 이런 생각이 들었다. 굳이 동기화를 해야 할까?

 

prisma/client는 매번 생성해주어야 하는 것이 맞지만, 동기화는 크게 필요 없다고 생각한다.

로컬 환경에서 사용할 수 있는 act로 테스트를 진행한 후 GH Action에 PR을 올렸다.

수정 후 실행 시간

 

결과적으로 해당 부분을 커밋하였을 때, 약 7초 (기존 대비 12% 가량) 정도의 실행 시간 감소가 일어났다.

괄목할 만한 성과이다.


Install Package

Install Package step을 보자. (중간의 .env 세팅은 제외하였다)

- name: install package manager - pnpm
  run: npm install -g pnpm

- name: Install package
  run: pnpm i

 

이 코드를 보면서 나는 두 가지 고민을 했다.

 

첫 번째는 "굳이 패키지 매니저와 패키지 설치를 따로 나누어야 할까?"라는 고민이었고,

두 번째로는 "패키지를 매 번 새로 설치해야 할까?"라는 고민이었다.

 

pnpm을 사용하는 이유 중 하나가 중복으로 설치를 하지 않아서 때문이었는데, 그 장점이 상쇄되는 느낌이 들었다.

 

때문에 GH Action에서 캐싱을 할 수 있는 방법을 찾아보던 중 actions/cache@v3 패키지를 찾을 수 있었다. (Reference 참조)

해당 액션을 사용하여 node_modules를 캐싱하고, 일치할 경우 굳이 설치하지 않음으로써 설치에 드는 시간을 줄일 수 있다.

 

다음과 같이 코드를 작성해 액션을 사용하였다.

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

- name: Install package
  if: steps.cache.outputs.cache-hit != 'true'
  run: |
    npm install -g pnpm
    pnpm i --frozen-lockfile

 

Caching dependencies에서 \*\*/node\_modules에 있는 파일들을 캐싱하고, key는 ${{ runner.os }}-node-${{ hashFiles('\*\*/pnpm-lock.json') }}로 지정한다.

restore-keys에서는 ${{ runner.os }}-node 또는 ${{ runner.os }}를 prefix로 포함하는 키가 있는지 확인하고, 일치하는 키가 있을 경우 가장 최근의 캐시를 가져온다.

 

첫 캐싱 실행 시간

 

해당 부분을 수정 후 올렸는데, 첫 캐싱이라 그런지 시간이 조금 더 걸렸다.

 

그렇다면 다른 부분을 수정하고 다시 올린다면 전체적인 시간이 줄어들까?

run all-job rerunning을 통해 모든 작업을 재실행했다.

 

그리고 실패했다.

 

아니 도대체 왜...? 라고 묻고 싶었지만, pnpm은 글로벌로 설치한 탓에 캐싱이 안되는 모양이었다.

일반 설치를 시도해봤지만 여전히 실패하고, 결국 pnpm은 따로 깔기로 했다...

 

그렇게 yml 파일을 수정하고 다시 한 번 커밋하여 캐싱의 위력을 확인해본 결과

캐싱 이후 빌드

 

2초 가량 줄였다!

 

prisma 빌드 작업을 최소화하여 줄였을 때 대비해서는 약 4%, 최적화되지 않았던 빌드 대비해서는 15% 이상 절감한 셈이다.


완성한 ci.yml 파일

더보기
name: CI

on:
  pull_request:
    branches:
      - dev
      - prod

jobs:
  ci-build:
    runs-on: ubuntu-20.04

    steps:
      - name: checkout
        uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 20

      - name: install pnpm
        run: npm i -g pnpm

      - name: Caching dependencies
        id: cache
        uses: actions/cache@v3
        with:
          path: "**/node_modules"
          key: ${{runner.os}}-node-${{ hashFiles('**/pnpm-lock.json')}}
          restore-keys: |
            ${{ runner.os }}-node-
            ${{ runner.os }}
              
      - name: Install package
        if: steps.cache.outputs.cache-hit != 'true'
        run: pnpm i --frozen-lockfile

      - name: env set
        working-directory: ./
        run: |
          touch .env
          echo POSTGRESQL_DB_URL=${{secrets.POSTGRESQL_DB_URL}} >> .env
          echo JWT_PRIVATE=${{secrets.JWT_PRIVATE}} >> .env
          echo JWT_SECRET=${{secrets.JWT_SECRET}} >> .env
          echo PORT=${{secrets.PORT}} >> .env
          echo SALT=${{secrets.SALT}} >> .env
          echo ORIGIN=${{secrets.ORIGIN}} >> .env

      - name: prisma set
        run: npx prisma generate

      - name: Test unit
        run: pnpm test:prod

      # - name: Test E2E
      #   run: pnpm test:e2e

      - name: test
        run: |
          pnpm build
          timeout ${{ vars.SLEEP }} nohup node dist/main.js > app.log || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi
          cat app.log

RECOMMEND FOR YOU

[Infra] NestJS & Github Action 빌드 개선하기 (2 : CD build - cd.yml 파일 개선) : https://iio-nff.tistory.com/5

REFERENCE

Github Actions 캐시 조금 더 알아보기(Pozafly님) : https://pozafly.github.io/dev-ops/cache-and-restore-keys-in-github-actions/