Logo

GitHub Actions의 캐시(Cache) 액션으로 패키지 설치 최적화하기

어느 프로그래밍 언어를 사용하든 요즘 대부분의 소프트웨어 프로젝트는 수많은 다른 패키지에 의존하기 마련인데요. 로컬 환경에서 소프트웨어 개발을 할 때는 이러한 외부 패키지를 최초에 딱 한 번만 설치하면 되지만 항상 새롭게 셋업되는 CI 서버에서는 이 작업을 매번 다시 해야합니다.

이번 포스팅에서는 깃허브에서 제공하는 캐시(Cache) 액션을 사용하여 CI 서버에서 발생할 수 있는 불필요한 패키지 재설치를 예방해보겠습니다.

GitHub Actions의 액션(Action)이란?

먼저 GitHub Actions에서 액션(Action)이 무엇을 의미하는지 간단하게 짚고 넘어가겠습니다. GitHub Actions는 일반적으로 CI(Continuous Integration, 지속 통합) 또는 CD(Continuous Deployment, 지속 배포)와 같은 자동화를 위해서 사용되는데요. 이를 위해 워크플로우(workflow)를 구성하다보면 거의 필연적으로 여러 작업(job)에서 반복적으로 처리되야 할 일들이 생겨나기 마련입니다.

액션(Action)은 이렇게 빈번하게 필요한 반복 단계를 재사용하기 용이하도록 GitHub Actions에서 제공되는 일종의 작업 공유 매커니즘인데요. 이 액션은 하나의 코드 저장소 범위 내에서 여러 워크플로우 간에서 공유를 할 수 있을 뿐만 아니라, 공개 코드 저장소를 통해 액션을 공유하면 깃허브 상의 모든 코드 저장소에서 사용이 가능해집니다.

누구나 GitHub Marketplace를 통해 깃허브 뿐만 아니라 다양한 써드 파티 업체들이 공개해놓은 액션을 쉽게 검색하고 써볼 수 있습니다.

GitHub Actions에 대한 소개와 핵심 개념은 별도로 정리해 두었으니 관련 포스팅를 참고 바랍니다.

깃허브의 캐시 액션

깃허브가 제공하는 캐시(Cache) 액션을 사용하면 GitHub Actions에서 워크플로우가 실행될 때 필요한 파일 중에서 거의 잘 바뀌지 않는 파일들을 깃허브의 캐시에 올려놓고 CI 서버로 내려받을 수 있습니다. 설치해야하는 패키지가 많은 프로젝트의 경우, 깃허브의 캐시 액션을 잘 활용하면 워크플로우의 성능을 최적화하는데 상당한 도움이 됩니다. 프로젝트에서 의존하고 있는 외부 패키지를 매번 네트워크를 통해 원격 패키지 저장소로 부터 내려받는 대신에 깃허브의 캐시에 저장해두고 활용할 수 있기 때문이죠.

깃허브의 캐시 액션은 주어진 키에 해당하는 데이터가 깃허브의 캐시에 존재하는 경우 해당 파일을 CI 서버의 특정 경로에 내려받아 줍니다. 반면에 주어진 키에 해당하는 데이터가 깃허브의 캐시에 존재하지 않는 경우에는 CI 서버의 특정 경로에 있는 파일을 모두 캐시에 저장하여 다음에 워크플로우가 실행될 때 해당 데이터가 캐시에 존재하도록 해줍니다.

지금부터 실습 프로젝트를 하나 생성하고 깃허브의 코드 저장소에 올린 후에 실제로 GitHub Actions에서 캐시 액션을 사용해보겠습니다.

실습 프로젝트와 코드 저장소 생성

실습을 위해서 외부 패키지에 의존하는 프로젝트가 하나 필요할 것 같아요. Create React App을 통해서 간단한 React 앱을 하나 뚝딱 만들고 시작하겠습니다.

$ npx create-react-app github-actions-cache

Creating a new React app in /Users/daleseo/temp/github-actions-cache.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...


(... 생략 ...)

We suggest that you begin by typing:

  cd github-actions-cache
  npm start

Happy hacking!

본인 깃허브에 계정에 새로운 코드 저장소(repository)를 하나를 만들고 위에서 생성한 React 프로젝트를 올립니다. (저는 github-actions-cache이라는 이름으로 실습 코드 저장소를 만들겠습니다.)

$ cd github-actions-cache
$ git remote add origin https://github.com/DaleSchool/github-actions-cache.git
$ git branch -M main
$ git push -u origin main
Enumerating objects: 22, done.
Counting objects: 100% (22/22), done.
Delta compression using up to 8 threads
Compressing objects: 100% (22/22), done.
Writing objects: 100% (22/22), 285.94 KiB | 8.41 MiB/s, done.
Total 22 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/DaleSchool/github-actions-cache.git
 * [new branch]      main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.

GitHub Actions 워크플로우 생성

프로젝트에 .github/workflows/라는 폴더를 만든 후, 그 안에 cache.yml이라는 이름의 YAML 파일을 하나 생성합니다.

코드 저장소에서 push 이벤트가 발생되면 워크플로우가 실행되도록 설정하고 cache라는 간단한 작업(job)을 추가합니다. cache 작업은 총 2단계로 이뤄지는데 첫번째 단계에서는 체크아웃 액션을 이용하여 코드 저장소에 올려둔 프로젝트의 코드를 CI 서버로 내려받고 두번째 단계에서는 npm을 이용하여 package.json 파일에 명시되어 있는 패키지들을 CI 서버에 설치합니다.

GitHub에서 제공하는 체크아웃(Checkout) 액션에 대해서는 별도의 포스팅에서 자세히 다루었으니 참고 바랍니다.

.github/workflows/cache.yml
name: Our Workflow
on: push
jobs:
  cache:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci

cache.yml 파일을 GitHub의 코드 저장소로 올린 후 Actions 탭에 들어가면 다음과 같은 워크플로우 실행 로그를 확인할 수 있을 것입니다.

Log
☑️ Set up Job
☑️ Run actions/checkout@v3
☑️ Run npm ci
▶ Run npm cinpm WARN deprecated source-map-resolve@0.6.0: See https://github.com/lydell/source-map-resolve#deprecatednpm WARN deprecated svgo@1.3.2: This SVGO version is no longer supported. Upgrade to v2.x.x.added 1415 packages, and audited 1416 packages in 28s177 packages are looking for funding  run `npm fund` for details6 moderate severity vulnerabilitiesTo address all issues (including breaking changes), run:  npm audit fix --forceRun `npm audit` for details.☑️ Post Run actions/checkout@v3
☑️ Complete Job

Run npm ci 단계를 보면 패키지를 1415개를 설치하는데 총 28초가 걸린 것을 볼 수 있는데요.

실제 프로젝트에서는 당연히 이것보다 설치해야할 패키지가 훨씬 많아지겠죠? CI 서버에 패키지 설치하는데만 보통 수분이 소요될 수 있을 것입니다.

워크플로우를 실행할 때마다 이렇게 매번 똑같은 패키지를 내려받으면 여러 가지 측면에서 낭비가 발생합니다. CI가 느려질 뿐만 아니라 네트워크 대역폭을 많이 쓰는 등 결국은 개발 생산성 저하와 유지보수 비용의 증가로 이어질 것입니다.

캐시 액션 사용

워크플로우 YAML 파일에서는 steps 키 하위의 uses 키에 사용하고자 하는 액션의 위치를 {소유자}/{저장소명}@{참조자}의 형태로 명시하는데요. GitHub에서 제공하는 캐시 액션의 소유자는 actions이고, 저장소 이름은 cache이며 현재 포스팅 시점에서 사용 가능한 최신 버전은 v3입니다.

이에 따라 실습 워크플로우에 캐시 액션을 추가해주겠습니다.

.github/workflows/cache.yml
name: Our Workflow
on: push
jobs:
  cache:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/cache@v3        with:          path: ~/.npm          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}      - run: npm ci

여기서 주의깊게 볼 부분은 with 키를 이용해서 캐시 액션에 넘기고 있는 2개의 입력값인데요.

path 인자로는 우분투 운영체제에서 npm이 패키지 저장소에서 내려받은 패키지를 CI 서버에 저장해두는 경로를 지정하고 있고요. key 인자로는 깃허브의 캐시에서 데이터를 읽거나 쓸 때 사용되는 식별자를 명시합니다.

다시 말해 CI 서버의 path에 위치한 디렉토리나 파일을 key를 통해서 캐시에 올리거나 내려받게 됩니다.

캐시 히트(hit)의 경우, 즉 캐시에서 주어진 key에 해당하는 파일들이 저장되어 있다면, CI 서버 상의 path로 해당 파일들을 내려받아 줍니다. 캐시 미쓰(miss)의 경우, 즉 캐시에서 주어진 key에 해당하는 파일들이 저장되어 있지 않다면, 해당 작업의 종료 시점에 CI 서버 상의 path에 있는 파일들을 캐시에 저장합니다.

여기서 조심할 부분은 새로운 패키지를 설치하거나 기존 패키지를 제거하거나 버전을 업그레이드할 때는 깃허브의 캐시에 저장해놓은 패키지를 사용하면 안 되겠죠? 따라서 패키지 설치 내역에 변경이 있을 때는 키도 함께 변경이 될 수 있도록 GitHub Actions의 hashFiles() 내장함수를 이용하여 package-lock.json 파일의 SHA 해시값을 키에 포함시켜줍니다. 뿐만 아니라 워크플로우를 여러 운영체제에서 실행할 경우를 대비하여 GitHub Actions의 runner.os 컨텍스트(context)도 키에 포함시키고 있습니다.

이제 cache.yml 파일의 변경 내용을 GitHub의 코드 저장소로 올리면 워크플로우가 실행될 것입니다.

Log
☑️ Set up Job
☑️ Run actions/checkout@v3
☑️ Run actions/cache@v3
▶ Run actions/cache@v3Cache not found for input keys: Linux-node-8f192a58c56108cb7532746bc925611ec228332771a9983ab738220e0b03b5b8☑️ Run npm ci
▶ Run npm cinpm WARN deprecated source-map-resolve@0.6.0: See https://github.com/lydell/source-map-resolve#deprecatednpm WARN deprecated svgo@1.3.2: This SVGO version is no longer supported. Upgrade to v2.x.x.added 1415 packages, and audited 1416 packages in 27s177 packages are looking for funding  run `npm fund` for details6 moderate severity vulnerabilitiesTo address all issues (including breaking changes), run:  npm audit fix --forceRun `npm audit` for details.☑️ Post Run actions/cache@v3
Post job cleanup./usr/bin/tar --posix --use-compress-program zstd -T0 -cf cache.tzst -P -C /home/runner/work/github-actions-cache/github-actions-cache --files-from manifest.txtCache Size: ~38 MB (39344370 B)Cache saved successfullyCache saved with key: Linux-node-99a1d11713e027e9d5df465eafb83e00829dc398e3b7e415b8cde8e65444d092☑️ Post Run actions/checkout@v3
☑️ Complete Job

Actions 탭에 들어가면 워크플로우 실행 로그를 확인해보면,

  • Run actions/cache@v3 단계에서 입력된 키에 해당하는 키가 없었다고 나옵니다.
  • Run npm ci 단계를 보면 패키지를 설치하는 속도에 큰 차이가 없는 것을 볼 수 있습니다.
  • Post Run actions/cache@v3 단계를 보면 캐시에 약 38MB의 크기의 압축 파일을 저장하는 것이 확인됩니다.

이처럼 캐시 액션을 설정 후에 최초 워크플로우 실행 시에는 전혀 빨리지는 않습니다. 왜냐하면 기존에 깃허브의 캐시에 데이터를 저장한 적이 없기 때문이죠. 하지만 막 워크플로우를 한 번 실행하였기 때문에 프로젝트에서 필요한 모든 패키지가 깃허브의 캐시에 저장되었습니다.

이제 Actions 탭에서 실습 워크플로우를 수동으로 재실행해볼까요?

Log
☑️ Set up Job
☑️ Run actions/checkout@v3
☑️ Run actions/cache@v3
▶ Run actions/cache@v3Received 39344370 of 39344370 (100.0%), 40.5 MBs/secCache Size: ~38 MB (39344370 B)/usr/bin/tar --use-compress-program zstd -d -xf /home/runner/work/_temp/8b2db391-0bb4-43bf-b7bd-efbe64691fa7/cache.tzst -P -C /home/runner/work/github-actions-cache/github-actions-cacheCache restored successfullyCache restored from key: Linux-node-99a1d11713e027e9d5df465eafb83e00829dc398e3b7e415b8cde8e65444d092☑️ Run npm ci
▶ Run npm cinpm WARN deprecated source-map-resolve@0.6.0: See https://github.com/lydell/source-map-resolve#deprecatednpm WARN deprecated svgo@1.3.2: This SVGO version is no longer supported. Upgrade to v2.x.x.added 1415 packages, and audited 1416 packages in 12s177 packages are looking for funding  run `npm fund` for details6 moderate severity vulnerabilitiesTo address all issues (including breaking changes), run:  npm audit fix --forceRun `npm audit` for details.☑️ Post Run actions/cache@v3
Post job cleanup.Cache hit occurred on the primary key Linux-node-99a1d11713e027e9d5df465eafb83e00829dc398e3b7e415b8cde8e65444d092, not saving cache.☑️ Post Run actions/checkout@v3
☑️ Complete Job

이 번에는 무언가가 달라진 것을 볼 수 있는데요.

  • Run actions/cache@v3 단계를 보면 깃허브의 캐시에서 주어진 키에 해당하는 38 MB의 압축 파일을 읽어와서 압축을 풀고 있는 것을 볼 수 있습니다.
  • Run npm ci 단계에서 동일한 개수의 패키지를 설치하는데 걸리는 속도가 12초로 기존 대비 2배 이상 빨라진 것을 볼 수 있습니다.
  • Post Run actions/cache@v3 단계에서는 캐시 히트(hit)의 경우이기 때문에 캐시에 데이터를 저장하지 않는다고 나옵니다.

캐시 히트 여부 출력값 활용

깃허브의 캐시 액션은 캐시 히트(hit) 여부, 즉 캐시에 주어진 키에 해당하는 데이터가 존재하는지 여부를 cache-hit라는 출력값에 담아서 제공해주는데요. 캐시 액션을 사용한 다음에 나오는 작업 단계(step)에서 이 출력값을 읽어서 활용할 수 있습니다.

간단하게 캐시 히트 여부를 실행 로그에 출력해주는 작업 단계를 실습 워크 플로우에 추가해볼까요? GitHub Actions의 steps 컨텍스트를 통해 <step_id>.outputs.cache-hit를 읽어와 후속 작업의 실행 여부를 if 속성으로 설정해줍니다. (캐시 액션을 사용하는 작업 단계에 id 속성을 추가해주는 부분 빼먹지 않도록 주의하세요.)

Log
name: Our Workflow
on: push
jobs:
  cache:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/cache@v3
        id: npm-cache        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      - if: steps.npm-cache.outputs.cache-hit == 'true'        run: echo 'npm cache hit!'      - if: steps.npm-cache.outputs.cache-hit != 'true'        run: echo 'npm cache missed!'      - run: npm ci

이제 워크플로우 실행 로그를 확인해보면 캐시에 주어진 키에 해당하는 데이터가 존재하였기 때문에 npm cache hit만 출력이 되는 것을 볼 수 있습니다. npm cache miss를 출력하는 작업 단계는 if 속성에 명시된 조건이 거짓이므로 수행되지 않습니다.

Log
☑️ Set up Job
☑️ Run actions/checkout@v3
☑️ Run actions/cache@v3
☑️ Run echo 'npm cache hit!'▶ Run echo 'npm cache hit!'npm cache hit!🚫 Run echo 'npm cache missed!'☑️ Run npm ci
☑️ Post Run actions/cache@v3
☑️ Post Run actions/checkout@v3
☑️ Complete Job

실제 워크플로우에서는 cache-hit 출력값을 불필요한 작업 단계를 생략하기 위해서 많이 사용되고 있습니다. 예를 들어, 사설 패키지 저장소를 사용하는 경우 GitHub의 캐시에 패키지가 이미 존재한다면 굳이 해당 저장소에 접근하기 위한 인증 단계를 거칠 필요가 없을 것입니다.

실습 코드

본 포스팅에서 작성한 YAML 파일과 워크플로우 실행 결과는 아래 코드 저장소에서 확인하실 수 있습니다.

https://github.com/DaleSchool/github-actions-cache

마치면서

지금까지 깃허브 캐시에 잘 바뀌지 않는 파일들을 저장하기 위해서 사용되는 캐시(Cache) 액션을 워크플로우에 어떻게 설정하고 사용하는지에 대해서 살펴보았습니다. 참고로 하나의 코드 저장소(repository)에는 총 10 GB 캐시 용량이 주어지며 일주일이 넘게 접근이 되지 않은 파일들을 자동으로 캐시에서 삭제됩니다. Cache 액션에 대한 좀 더 자세한 내용은 GitHub Marketplace를 참고 바랍니다.

GitHub Actions 관련 포스팅은 GitHub Actions 태그를 통해서 쉽게 만나보세요!