Github Actions를 통해서 ECS에 배포하는 간단한 CI/CD 파이프라인을 사용하고 있다. QA를 하는 과정에서 1시간에 여러 번의 PR이 머지되면서 여러 번의 Github Actions의 workflow가 실행됐는데, 매번 꽤 빌드 속도가 오래 걸렸다. 확인해 보니 dockerfile 빌드하는 과정에서 캐시가 제대로 안 걸려있었는데, 이번에는 Docker 빌드를 빠르게 할 수 있는 buildkit에 대해서 알아보려고 한다.
사실, 우리가 맥에서 도커 데스크탑을 다운로드해서, 직접 도커를 실행한다면 우리는 이미 buildkit을 쓰고 있는 셈이 된다. BuildKit은 기존의 도커 빌더를 대체하는 새로운 백엔드로, Docker Desktop과 Docker Engine 23.0 버전에서 기본 빌더로 사용되기 때문이다. 공식 레퍼런스는 아래와 같다.
- [docker] build/buildkit : https://docs.docker.com/build/buildkit/
BuildKit
Introduction and overview of BuildKit
docs.docker.com
해당 페이지에서 언급한 BuildKit의 기능은 다음과 같다.
- 캐시를 통해서 재빌드를 방지
- 병렬 빌드
- 빌드 컨텍스트에서 변경된 파일만 빌드 간에 증분적으로 전송
- 빌드 컨텍스트에서 사용되지 않은 파일 전송을 감지하고 건너 뜀
뭐 대충 생각해보면 대충 각각의 명령어들을 부분적으로 독립/병렬적으로 실행한다는 의미인 것 같다. 이번 포스팅에서는 docker에서 사용하는 빌더인 BuildKit과 함께 Github Actions에서 이를 사용하기 위한 방법을 소개하려고 한다.
본론
BuildKit?
기존의 도커는 다른 빌드 엔진을 사용했다. 그 과정에서 BuildKit이 개발되었고, 그 특유의 빠른 속도에 도커 데스크탑 23 버전에서부터는 기본 빌더로 자리 잡게 되었다. BuildKit이 이렇게 빠른 이유로는 바로 LLB(Low-Level Build)이라고 부르는 기술을 사용하기 때문이다.
LLB는 고수준인 dockerfile과는 다른 저수준의 빌드 언어로, 마치 프로그래밍 언어에 있어서 저급, 고급 언어의 차이와 비슷하다. LLB는 Dockerfile의 명령어를 추상화하여 관리한다. 예를 들면, RUN, COPY, ADD와 같은 명령어를 추상화된 형태로 변환해서 관리하고, 각 추상화된 형태가 어떤 입력과 출력을 갖는지 정의한다. 이를 통해서 변경된 부분만 재빌드하여 다시 합치는 방법을 사용한다.
정말 말도 안되게 좋은 아이디어이다. 구현도 엄청나게 힘들 듯으로 보인다. 공식 레퍼런스에 따르면 gokerfile, mokerfile 등 다양하게 더 작은 단위로 빌드를 하고 합친다고 한다.
- [github] moby/buildkit : https://github.com/moby/buildkit#exploring-llb
GitHub - moby/buildkit: concurrent, cache-efficient, and Dockerfile-agnostic builder toolkit
concurrent, cache-efficient, and Dockerfile-agnostic builder toolkit - moby/buildkit
github.com
이론적인 부분은 여기까지고 실제로 도커 데스크탑 23 버전부터 탑재되어 있다고 했으니, 직접 docker를 사용하면 어떨까? 먼저 nest.js로 만들어져 있는 프로젝트를 빌드해 보았더니 41초가 걸렸다.
➜ marsboy-test (development) docker build . -f scheduler.dev.dockerfile -t first
[+] Building 41.6s (19/19) FINISHED
간단하게 API 서버의 Controller 레이어에 API route하나를 지우고 다시 실행해보니, 1.5초로 둘어들었다.
➜ marsboy-test (development) docker build . -f scheduler.dev.dockerfile -t second
[+] Building 1.5s (19/19) FINISHED
앞서, buildKit의 설명에 따르면 바뀐 부분만 새롭게 빌드한다고 되어있으니 아주 약간 건드려서 재빌드는 조금만 되어 빠른 실행 결과를 보장하는 것으로 보인다.
Bulidx (feat. GIthub Actions)
다음은 Buildx에 대한 내용이다. Github Actions에서 buildKit을 사용하기 위해서는 buildx를 사용하면 된다. Buildx는 Docker CLI의 플러그인으로, 도커 빌드 기능을 확장한 CLI이다. 앞서 선보인 BuildKit를 포함하고, 다른 것들도 지원한다. 예를 들면 아래와 같다.
- 멀티플랫폼 빌드 지원
- 향상된 캐싱 메커니즘
- 빌드 드라이버
Github Actions를 사용하다 보면 로컬에서 도커라이징을 했을 때와 다르게 오래 걸릴 수가 있다. Github Actions의 성질을 생각해 보면 필요할 때마다 인스턴스를 띄워 workflow를 실행하는 방식이기 때문에 어찌 보면 당연한 일이다. 이러한 Github Actions에서 도커 캐시를 사용하려면 다음과 같이 작성할 수 있다.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: user/app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Buildx의 캐시 동작 방식
Docker Buildx의 캐시 메커니즘은 다음과 같은 방식으로 이루어진다. 먼저 도커 이미지는 여러 레이어로 구성되며, 각 레이어는 Dockerfile의 각 명령어(RUN, COPY 등)에 대응한다.
FROM node:14
WORKDIR /app
COPY package.json ./ # 레이어 1
RUN npm install # 레이어 2
COPY . . # 레이어 3
RUN npm run build # 레이어 4
이제 각 레이어는 이전 레이어의 변경 사항에 기반하여, Buildx는 이러한 레이어의 내용을 해시값으로 변환하여 캐싱하고, 동일한 레이어는 재사용되고, 변경된 레이어부터만 다시 빌드한다.
그렇다면, 우리가 사용하는 Github Actions에서 Buildx의 캐시는 어디로 가는 걸까? buildx는 다양한 캐시 저장소를 지원한다. 로컬, 레지스트리 등을 지원하지만 Github Actions를 사용한다면 gha라는 타입의 Github Actions 캐시를 사용하게 된다.
cache-from: type=gha
cache-to: type=gha,mode=max
실제로 레포지토리에서 Actions -> Cache를 보면 깃허브 액션의 캐시를 살펴볼 수 있다.
다음으로는 이러한 캐시 모드의 옵션이다. cache-to와 cache-from이라는 키워드를 통해서 캐시의 타입을 지정하고, 어디서 캐시를 받아올 것인 지 설정할 수 있다. 아래의 깃허브 액션 workflow 스크립트처럼 설정하여 캐시를 저장할 수 있다.
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
cache-from: |
type=gha
type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:cache
cache-to: |
type=gha,mode=max
type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:cache,mode=max
여기서 cache-to를 사용해서 모드를 설정할 수 있는데, 각각 설정은 다음과 같다.
- max : 모든 레이어를 캐시 ( 가장 완전한 캐싱 )
- min : 최종 이미지에 포함되지 않는 레이어를 캐시
- inline : 이미지 메타데이터에 캐시 정보 포함
위와 같이 설정함으로써 캐시를 사용하여 github actions에서 도커 이미지를 빌드하는 시간을 대폭 단축할 수 있다.
참고
- [kakao] Github Actions에서 도커 캐시를 사용하기 : https://fe-developers.kakaoent.com/2022/220414-docker-cache/
감사합니다.
'DevOps > docker' 카테고리의 다른 글
docker image의 환경 변수 그리고 우선 순위에 대하여 (1) | 2025.02.04 |
---|---|
docker은(는) 사용자의 컴퓨터를 손상시킵니다. "'com.docker.vmnetd'에 악성 코드가 포함되어 있어서 열리지 않았습니다." 문제 해결 방법 (2) | 2025.01.14 |
꼭 알아야 하는 CMD와 ENTRYPOINT의 차이점 (0) | 2024.12.09 |
docker multi-stage build에 대해서 ( spring ) (0) | 2024.11.25 |
[Jenkins] DinD vs DooD 그리고 DooD 설정법 (1) | 2024.10.30 |