Github Actions를 사용한 도커 빌드 최적화 이야기(2)

도커파일을 잘 사용하여 빌드 캐시 재사용을 높이는 이야기

June Lim
onthelook
8 min readJan 10, 2024

--

Github Actions와 도커파일을 수정하고 배포가 빨라진 후 발견한 도커파일을 쓰는 노하우와, 참조하는 원리에 대해서 공유합니다.

배포된 prd 커밋에 대해서, 캐시가 참조되었는지를 확인합니다.

Build, tag, and push image to Amazon ECR에서 1분 19초 정도 소요된것으로 확인할 수 있듯이, 캐시가 되었습니다.

그런데 제가 새로운 캐시값을

**/Dockerfile.production, package.json, yarn.lock

위 세개 파일을 기준으로 해시값을 생성하도록 했고, 그 전 **/Dockerfile.production을 기반으로 만든 커밋이 없는데, 어떻게 도커 파일 빌드에서 캐시 참조가 가능한 것일까요?

우선 어떤 캐시를 참조했는지 알아보기 위해서 Cache Docker layers에서 확인할 수 있는 로그를 확인해보겠습니다.

Cache restored from key: Linux-docker-bc0bc9528e98ed07e7993f8a81500720f4150130833676925e4189a459fe93a0

위 해쉬를 가진 캐시를 참조하여서 불러온것을 확인할 수 있습니다.

그러면 어디서 생성된 캐시인지 알아보기 위해서 Actions의 Caches를 확인해보겠습니다.

해당 캐시와 어느 브랜치에서 생성이 된 캐시인지까지 표시가 되는데 main브랜치에서 생성이 된 캐시입니다.

지금 Github Action workflow의 구성상 main에서 생성된 캐시라고 한다면 Dev환경에서 만들어진 캐시 입니다.

그리고 다시 PRD의 빌드 로그를 자세히 들여다보니 CACHED로 가져올 수 있는건 다 가져왔다고 볼 수 있습니다.

DEV환경에서 만드는 key값의 기준이

‘**/Dockerfile.dev’, ‘package.json’, ‘yarn.lock’

해당 세개 파일의 해시를 기준으로 하고있고,

PRD환경에서 만드는 key값의 기준이

‘**/Dockerfile.production’, ‘package.json’, ‘yarn.lock’

해당 세개 파일의 해시를 기준으로하고 있습니다.

그런데 어떻게 캐시를 가져오는걸까요??

dev환경의 도커파일은 다음과 같습니다.

prd환경의 도커파일은 다음과 같습니다.

Docker 빌드 프로세스와 캐시 메커니즘을 좀 더 자세히 살펴보겠습니다.

Docker 빌드 과정에서의 캐시 사용 방식

1. 레이어별 캐시: Docker는 이미지를 여러 레이어로 구성합니다. RUN, COPY, ADD 명령은 별도의 레이어를 생성합니다. Docker는 빌드 시 이러한 레이어를 재사용하기 위해 캐시를 활용합니다.

2. 캐시 결정 기준: Docker는 현재 빌드 중인 레이어의 명령과 캐시에 있는 이전 레이어의 명령을 비교합니다. 명령이 동일하면, Docker는 캐시된 레이어를 재사용합니다.

3. 변경점 이후 레이어의 캐시 무시: 만약 Dockerfile의 특정 레이어에서 변경이 발생하면, 해당 레이어와 그 이후의 모든 레이어는 새롭게 빌드됩니다. 이전 레이어까지는 캐시가 유지됩니다.

Dockerfile.dev와 Dockerfile.production의 경우

- 공통된 초기 단계: 두 Dockerfile 모두 node:18-alpine3.16을 기반으로 하고, 종속성 설치 및 package.json, yarn.lock 파일을 복사하는 공통된 초기 단계를 가집니다.

- 캐시 재사용: 이러한 초기 단계는 캐시에 저장되며, 두 Dockerfile 중 어느 하나를 빌드할 때 이 캐시를 재사용할 수 있습니다. 특히 package.json과 yarn.lock이 동일하면, 종속성 설치 단계에서도 캐시를 재사용할 가능성이 높습니다.

- 변경점 이후의 단계: Dockerfile.dev와 Dockerfile.production 사이의 차이점은 주로 환경 변수 설정(NODE_ENV)과 실행 명령(CMD)에서 발생합니다. 이러한 변경점은 빌드 과정의 후반부에 위치하므로, 초기 단계에서 생성된 캐시는 그대로 유지되고, 변경점 이후의 레이어만 새로 빌드됩니다.

그리고 Build, tag, and push image to Amazon ECR의 상세로그 를 기반으로 어디까지 캐시가 되었는지 표시하자면

1. 기본 이미지 로드 및 메타데이터: node:18-alpine3.16 이미지의 메타데이터 로딩 과정. 이 단계는 매번 실행되며, 특정 빌드에 종속되지 않습니다.

2. 작업 디렉토리 설정 (WORKDIR /app): 이 단계는 캐시되었습니다. 이는 WORKDIR 명령이 변경되지 않았기 때문입니다.

3. 종속성 설치 (RUN apk add — no-cache g++ make python3 py3-pip curl-*** libressl-***): 이 단계도 캐시되었습니다. 이는 이전 빌드에서 동일한 종속성이 설치되었기 때문입니다.

4. package.json 및 yarn.lock 복사: 이 단계 역시 캐시되었습니다. 파일들이 변경되지 않았기 때문에 이전 빌드의 캐시를 사용할 수 있었습니다.

5. yarn install — frozen-lockfile: 이 단계는 부분적으로 캐시를 사용했습니다. 즉, 일부 레이어는 캐시되었지만, 새로운 종속성이 추가되거나 변경된 경우 일부 레이어는 새로 다운로드 및 설치해야 했습니다.

특히 이 부분에서 #11 CACHED라는 표시는 해당 단계가 캐시에서 불러와진 것을 의미합니다. 이는 yarn install 과정 중 일부가 이전 빌드의 캐시를 활용했음을 나타냅니다.

로그에 done이 표시된 부분들은 실제로 파일을 다운로드하고 설치하는 과정입니다. 예를 들어, #11 sha256:199500f3f0e2ceb39d1704a10c93dc44854b5fba2d6453825d9f40fccf675290 47.26MB / 47.26MB 1.0s done과 같은 표시는 새로운 종속성이 다운로드되어 설치되었음을 나타냅니다.

새로운 레이어의 추출: extracting 과정도 새로운 레이어가 생성되었음을 의미합니다. 예를 들어, extracting sha256:a2fbf128d6f6571fa8f05c89a714c3f6035944afda47aab082da656552f1567d 28.6s done는 새로운 레이어가 추출되었음을 나타냅니다.

결론

- 두 Dockerfile 사이에 명시적인 차이가 있음에도 불구하고, Docker 빌드 과정에서 공통된 초기 단계의 캐시를 재사용하여서 prd에서도 dev의 캐시를 참조하여 빌드 시간을 단축 시킬 수 있었습니다.

- dev의 캐시가 prd에서도 사용되게 되니, 패키지가 바꼈을때 원래는 dev에서도 한번, prd에서도 한번 새로 빌드를 했어야했는데, 이제는 dev에서 새로운 패키지를 기준으로 새로운 빌드를하고 캐시를 남겨주면, 그 이후 prd에서 빌드를 할때는 dev에서 남겨준 캐시를 사용할 수 있으니 두번의 반복 작업이 없게 되었습니다.

- 이 구조적인 이점을 가져가려면 Dockerfile에서 최대한 다른 환경에서도 유사하게, 공통적인 부분을 만드는 것이 전략적으로 유리합니다 .

--

--