Github Actions - Cache
Cache
캐시는 워크플로우 실행마다 다시 받아오거나 빌드해야하는 무거운 파일들을 저장해뒀다가 다음 실행 때에 재사용할 수 있도록 하는 저장소 같은 개념으로 프로젝트 내부의 node_modules나 pip, Gradle/Maven같은 라이브러리나 빌드 산출물과 같이 재사용 가능한 중간 결과들을 캐시해서 재사용하도록 한다.
이렇게 cache해서 사용하면 의존성의 설치, 컴파일 속도를 크게 줄여주고 러너의 사용 시간을 줄여 비용을 절감할 수 있으며 외부 레지스트리 지연 시에도 캐시로 빠르게 실행이 가능하다.
여기서 나는 조금 혼동이 생겼다.
기존에는 로컬에서 서버에 필요한 라이브러리를 모두 설치하고 소스코드와 라이브러리를 포함한 형태로 빌드하여 그 산출물을 통해서 서버를 실행했었는데 github actions를 사용해서 러너 내부에서 소스코드를 체크아웃 하고, 빌드에 필요한 라이브러리들을 설치하고, 이걸 통해서 빌드를 한 다음에 빌드된 산출물만 실제 운영을 위한 서버로 전달하고 이것만 갖고 서버를 실행하는 형태로 하는게 좋다고 한다.
운영 서버의 경우는 이런 저런 열린 구멍들이 존재하는 경우에 외부 공격에 취약해지기 때문에 공격로를 줄이기 위해 고안된 방식 같아 보인다.
아무튼 이런 방식으로 인해서 결국엔 workflow의 내부에서 프로젝트 자체를 checkout도 해야하고, 필요한 프로그램(node.js, java, python 등의 런타임들)도 설치해야하고, 라이브러리들도 설치하는 과정이 필요한데 이때 이 cache라는 걸 통해서 설치할때 가져온 압축파일(tar파일들..)을 남겨두고 그걸 통해서 라이브러리를 압축 해제를 해서 저장하는 방식을 사용해 tar파일이 설치되는 시간을 아낄수 있게 된다는 것이다.
Cache의 사용법
cache의 사용법은 아래 홈페이지에서 확인이 가능하다
https://github.com/marketplace/actions/cache
Cache - GitHub Marketplace
Cache artifacts like dependencies and build outputs to improve workflow execution time
github.com
그래서 한번 캐쉬를 하는 과정을 확인해보자면
먼저 기본적인 github acitons의 틀을 잡아주고
steps부터 작성을 해볼텐데 위에서 말했던거 처럼 결국엔 시작은 소스를 checkout하는 것이 필요하다.
그리고 이 소스를 컴파일 하고 빌드를 할 도구가 필요하다.
이 때는 setup을 사용해서 설치를 해주면 된다.
이건 어떤 언어를 사용하는 지에 따라 다르다고 보면 된다
(자바의 경우는 컴파일을 위해서 java 가 필요하기에 설치해야하나 빌드하는 도구는 보통 소스 내부에 포함되어 있어서 소스에 있는 빌드 도구를 사용해서 빌드가 가능하다, 이런 경우엔 자바만 필요하다. 또한 node.js 같은 경우는 node.js에 npm을 같이 설치하기에 컴파일을 해주는 도구와 빌드를 해주는 도구가 한번에 설치가 되기에 이 때도 node.js를 설치해주기만 하면 된다. 이런식으로 매번 달라지니 언어별로 생각하면서 구성해야할 듯 싶다.)
우리는 node.js를 사용할 것이기 때문에 node.js를 설치하면서 npm을 같이 설치하기에 node.js만 설치해주는 방식을 사용하면 된다.
node.js를 설치하는 방법은
actions의 marketplace에서 보면 아래와 같이 사용방법이 있으니 이걸 보고 기준으로 설정해주도록 하자
우린 설정 중에 어떤 node를 깔지에 대해서 설정하는 것과, 패키지 매니저가 package.json에 설정되어 있다면 자동으로 캐시하는 부분만 false로 설정해주도록 하자
그러면 러너 내부에서 node.js를 설치하게 된다.
이제야 cache를 쓸 준비가 다 됐다.
cache는 actions/cache@v4를 사용한다.
그리고 with를 통해서 추가적인 설정들을 지정하는데
먼저 path로 어떤 디렉터리/파일을 캐시의 대상으로 할지에 대해서 지정하는 설정이다.
이건 캐시를 저장하기 위해서, 캐시를 가져와서 복원하기 위해서 둘다 사용된다.
그리고 이건 언어에 따라서 지정되어 있는 형태가 고정되어 있으니 찾아 보면서 캐싱해야할 부분들이 어떤걸지 맞춰서 지정하면 된다.
node.js의 경우는 npm install 혹은 npm ci를 할때 라이브러리를 package.json 혹은 package-lock.json을 통해서 확인하고 그걸 네트워크에서 tar와 같은 형태로 받아 내린다.
이때 ~/.npm에 이 파일이 저장되고 이걸 풀어서 node-modules를 구성하게 된다.
그래서 이 ~/.npm을 캐싱하게 되면 네트워크를 통해서 tar파일을 내려받는 시간이 필요하지 않고 바로 그냥 ~/.npm에서 압축만 풀어서 node-modules를 구성할 수 있어 더 빠르게 빌드 환경을 구축할 수 있게 된다.
이제 캐시를 찾기 위한 key를 지정한다.
key는 os와 lockfile의 hash값을 통해서 지정하는데 형태는 보통
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
└───────┬──────┘ └─┬─┘ └──────────────┬──────────────┘
OS 라벨 임의 라벨 lockfile 해시(변하는 부분)
- runner.os : ubuntu / windows 와 같이 runner가 실행되는 os를 출력함
- hashFiles('**/package-lock.json') : 어딘가에 있는 package-lock.json을 해시값으로 변형함
이런식으로 구성된다.
이 key 값은 그냥 문자열로 구성되며 찾기 위한 임의의 key값으로 os나 lockfile이 각각 실제로 어떤 기능을 하지는 않는다.
key 값을 통해 기존의 캐시가 존재하는지를 찾고 만약 이 key로 된 캐시가 찾아볼 수 없다면 새롭게 라이브러리를 구성했다 혹은 라이브러리의 수정이 진행된 상태와 같이 정확하게 일치하는 캐시가 없다라고 생각 해서 설정에 따라 복원을 시도하고 새로 캐시를 생성하거나 그냥 캐싱하지 않고 새로 캐시를 생성하는 되는 기준이 된다.
이렇게만 지정하면 캐시를 key를 기준으로 찾고 만약 완전 일치하는게 없다면 캐싱을 진행하지 않고 ~/.npm을 새로 만들게 된다.
근데 만약 추가로 설정을 넣어주면 조금 방향이 달라지게 된다.
restore-keys는 만약 너가 전달한 key와 완전하게 매칭되는게 없을 경우에 기존에 만들었던 key중 restore-keys로 전달한 문자열과 prefix가 일치하고 가장 최신의 cache를 가져와서 path에 있는 파일을 복원한다.
이제 캐시 설정은 끝났고 라이브러리들을 설치해주도록 하자.
라이브러리 설치는 프로젝트 디렉터리로 이동하고
npm ci를 통해서 설치해주도록 하자
npm ci는 package.json을 보고 라이브러리를 설치하는 npm install과는 다르게 package-lock.json을 보고 라이브러리를 설치한다.
또한 workflow에서는 의미 없으나 npm installdms 기존의 node-modules에 라이브러리를 추가 및 수정하는 과정을 통해서 생성하나 npm ci는 기존의 node-modules를 제거하고 새로운 node-modules를 생성한다.
그리고 npm install은 package-lock.json을 변경하기도 하나 npm ci는 package-lock.json을 수정하지 않는다.
아무튼 배포하기 위해서는 npm ci를 사용해줘야 한다.
최초의 workflow의 실행 시점에는 캐시가 없기 때문에 npm ci를 할때 ~/.npm을 생성하게 된다.
그리고 이후 캐시가 존재하나 완전 매치되는 캐시가 없어 restore-keys를 통해서 캐시를 하는 경우에는 npm ci에서 ~/.npm을 보고 변경 사항이 존재할 때 수정 진행하게 된다(기존에 있던 라이브러리가 제거되는 경우는 제거하지 않음(tar파일) 새로운거만 추가됨).
만약 완전히 동일한 캐시가 존재하는 상태에서의 workflow의 실행의 경우는 npm ci에서는 ~/.npm만을 사용해서 라이브러리를 구성한다.
그리고 이제 빌드 작업을 진행해준다.
이 때도 프로젝트 위치로 러너를 옮겨주고(스탭간의 cd는 유지 되지 않음)
빌드 명령어를 실행시켜준다
이러면 캐시를 하는 npm의 빌드 작업을 수행하는 workflow가 완성 되었다.
name : Cache Workflow for Node.js
on :
push :
branches :
- dev
jobs :
cache_test :
runs-on : ubuntu-latest
steps:
- name : checkout_source_file
uses : actions/checkout@v4
- name : node.js module install
uses : actions/setup-node@v5
with :
node-version : 18
package-manager-cache: false
- name : cache ~/.npm
uses : actions/cache@v4
with :
path : ~/.npm
key : ${{runner.os}}-node-${{ hashFiles('**/package-lock.json')}}
restore-keys: |
${{runner.os}}-node-
- name : install dependency
run : |
cd my-app
npm ci
- name : npm build
run : |
cd my-app
npm run build
여기서 추가로 기존에 캐시가 없는 경우는 job의 기준으로 저장되는데 actions/cache@v4 스탭이 있는 job이 종료될때 actions/cache이 자동으로 job의 후에 작업할 post-job훅을 생성해서 자동으로 캐시를 저장하게 된다
이제 app.js파일을 수정해보고
commit & push를 해보자.
이제 workflow가 돌아간걸 보면 최초의 push로 실행되는 workflow는 dependency를 설치하는데 17초가 걸린걸 볼 수 있는데
이제 app.js를 다시 수정하고
commit & push 해보면
이렇게 14초로 줄어든걸 볼 수 있다.
그리고 추가로 최초 workflow를 보면
이렇게 job의 최종 step 이후에 캐시를 저장하는 것을 볼 수 있는데 이는 최초에 캐시가 없는 경우(기존에 캐시가 없는 경우, 매칭되지 않은 경우를 포함)는 job의 기준으로 저장되는데 actions/cache@v4 스탭이 있는 job이 종료될때 actions/cache이 자동으로 job의 후에 작업할 post-job훅을 생성해서 자동으로 캐시를 저장하게 된다
그리고 이 캐시는 Actions의 좌측 메뉴에서 Caches를 보면
이렇게 캐시가 저장되어 있는 것을 볼 수 있다.
이게 캐시의 사용법이다.