GitHub Actions 배포 속도 50% 단축: Docker Buildx 캐싱 실전 적용기

개발자에게 가장 고통스러운 시간 중 하나는 코드를 푸시한 후 CI/CD 파이프라인이 완료되기를 기다리는 "멍 때리는" 시간일 것입니다. 특히 MSA(Microservices Architecture) 환경이나 무거운 라이브러리를 포함하는 Node.js, Java Spring 프로젝트의 경우, Docker 이미지 빌드 단계에서만 5분 이상 소요되는 경우가 허다합니다. 최근 사내 프로젝트에서 배포 파이프라인이 10분을 넘어가면서 개발 생산성이 급격히 저하되는 문제가 발생했습니다. 원인은 매번 처음부터 다시 빌드되는 Docker 이미지였습니다.

본 글에서는 CI/CD 속도 개선을 위해 GitHub Actions의 Ephemeral Runner(일회성 러너) 환경에서 Docker Buildx와 GitHub Cache API를 연동하여 빌드 시간을 획기적으로 줄인 경험을 공유합니다.

GitHub Actions 최적화: 왜 로컬보다 느릴까?

로컬 개발 환경에서 docker build 명령어를 두 번째 실행할 때, 우리는 "Using cache"라는 메시지와 함께 빌드가 순식간에 끝나는 것을 경험합니다. Docker 데몬이 레이어(Layer)를 로컬 디스크에 저장해두고, 변경 사항이 없는 레이어는 재사용하기 때문입니다.

하지만 GitHub Actions와 같은 CI 환경은 기본적으로 Stateless(상태 없음)입니다. 워크플로우가 실행될 때마다 깨끗한 가상머신(VM)이 할당되고, 작업이 끝나면 폐기됩니다. 즉, 이전 빌드에서 생성한 Docker 레이어 캐시가 다음 빌드로 이어지지 않습니다. 결과적으로 매번 npm install이나 mvn package를 포함한 모든 레이어를 처음부터 다시 빌드하게 되며, 이는 엄청난 자원 낭비와 시간 지연을 초래합니다.

Problem: GitHub Actions의 기본 러너(Runner)는 빌드 간 상태를 공유하지 않습니다. 따라서 별도의 캐시 설정을 하지 않으면 Docker Layer Caching 기능이 전혀 작동하지 않아 빌드 시간이 매번 최대치로 소요됩니다.

이 문제를 해결하기 위해 많은 엔지니어들이 actions/cache 액션을 사용하여 소스 코드의 종속성(예: node_modules)을 캐싱하려고 시도합니다. 하지만 이는 소스 코드 레벨의 최적화일 뿐, Docker 이미지 빌드 프로세스 자체(Copy, Run 등의 명령어 실행)를 단축시키지는 못합니다. 진정한 해결책은 Docker의 레이어 자체를 외부 저장소에 캐싱하고 불러오는 것입니다.

실패했던 접근: Registry Cache 방식

처음에는 가장 직관적인 방법인 Registry Cache 방식을 시도했습니다. 이는 빌드된 캐시 레이어를 Docker Hub나 AWS ECR(Elastic Container Registry)에 업로드해두고, 다음 빌드 때 --cache-from 옵션으로 끌어와 사용하는 방식입니다.

하지만 이 방식에는 치명적인 단점이 있었습니다. 바로 네트워크 오버헤드입니다. 캐시를 저장소에 업로드하고 다시 다운로드하는 시간이 캐싱으로 절약되는 빌드 시간보다 더 오래 걸리는 역설적인 상황이 발생했습니다. 특히 레이어 용량이 큰 경우, ECR에서 캐시를 당겨오는 데만 2~3분이 소요되어 전체 CI/CD 속도 개선 효과가 미미했습니다. 우리는 네트워크 비용을 최소화하면서 GitHub 인프라 내부에서 빠르게 캐시를 교환할 방법이 필요했습니다.

해결책: Buildx와 GitHub Cache API (type=gha)

가장 효율적인 해결책은 Docker의 확장 빌드 도구인 Buildx와 GitHub Actions 전용 캐시 백엔드(type=gha)를 사용하는 것입니다. 이 설정은 Docker 빌드 캐시를 원격 레지스트리가 아닌 GitHub Actions의 자체 캐시 저장소에 저장합니다. 네트워크 지연이 거의 없으며 설정도 훨씬 간편합니다.

다음은 실제 프로덕션 환경에 적용하여 성능을 50% 이상 개선한 워크플로우 설정입니다.

# .github/workflows/deploy.yml
name: Build and Push Docker Image

on:
  push:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      # 1. Docker Buildx 설정 (필수)
      # docker-container 드라이버를 사용해야 고급 캐싱 기능을 사용할 수 있습니다.
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      # 2. Build 및 Push (핵심 설정)
      # cache-from과 cache-to 옵션을 type=gha로 설정합니다.
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: myuser/myapp:latest
          # GitHub Actions Cache API를 사용하여 캐시를 불러옵니다.
          cache-from: type=gha
          # 캐시를 저장합니다. mode=max는 중간 레이어까지 모두 캐싱하여 히트율을 높입니다.
          cache-to: type=gha,mode=max

위 코드에서 가장 중요한 부분은 cache-to: type=gha,mode=max입니다. 기본값인 mode=min은 최종 이미지의 레이어만 캐싱하지만, mode=max를 사용하면 멀티 스테이지 빌드(Multi-stage build)의 중간 빌드 결과물까지 모두 캐싱합니다. 의존성 설치 과정이 무거운 프로젝트라면 Docker Layer Caching 효과를 극대화하기 위해 반드시 mode=max를 사용해야 합니다.

성능 비교 분석

실제 Node.js 프로젝트(Next.js)에 해당 설정을 적용한 전후 비교 결과입니다. node_modules 설치와 빌드 과정이 포함된 Dockerfile을 기준으로 측정했습니다.

시나리오 빌드 시간 캐시 히트 여부 개선율
캐시 미적용 (Legacy) 8분 45초 Miss -
Registry Cache (ECR) 6분 20초 Hit (Slow Network) 약 27%
GitHub Cache (type=gha) 3분 10초 Hit (Fast) 약 63%

결과를 보면 type=gha를 적용했을 때 빌드 시간이 절반 이하로 줄어든 것을 확인할 수 있습니다. 특히 두 번째 실행부터는 npm install 단계가 0.1초 만에 캐시에서 로드되어 건너뛰어지는 것을 로그를 통해 확인할 수 있었습니다. 이는 DevOps 팁 중에서도 비용 절감과 직결되는 가장 강력한 최적화 기법 중 하나입니다.

주의사항 및 Edge Case

이 방식이 만능은 아닙니다. 실제 운영 시 고려해야 할 몇 가지 제약 사항이 있습니다.

첫째, GitHub Cache 용량 제한입니다. GitHub Actions는 리포지토리당 10GB의 캐시 제한을 둡니다. mode=max를 사용하여 너무 많은 중간 레이어를 저장하거나, 브랜치별로 캐시가 파편화되면 오래된 캐시가 삭제(Eviction)되어 캐시 히트율이 떨어질 수 있습니다. 캐시 키 관리 전략이 필요할 수 있습니다.

둘째, Scope 분리 문제입니다. PR(Pull Request) 브랜치에서 생성된 캐시는 기본적으로 기본 브랜치(main/master)에서 읽을 수 있지만, 반대의 경우는 보안 정책이나 설정에 따라 제한될 수 있습니다. scope 파라미터를 명시적으로 지정하여 캐시의 범위를 제어하는 것이 좋습니다.

마지막으로 Docker 공식 문서에 따르면 type=gha는 아직 실험적(Experimental) 기능에서 막 벗어난 단계이므로, Docker Buildx 버전을 최신으로 유지하는 것이 중요합니다.

Best Practice: .dockerignore 파일을 꼼꼼하게 작성하여 불필요한 파일 변경으로 인해 캐시가 깨지는 것을 방지하세요. 소스 코드 한 줄만 바뀌어도 COPY . . 이후의 레이어는 모두 다시 빌드된다는 점을 명심해야 합니다.
Docker 공식 가이드 확인하기

결론

GitHub Actions에서 docker buildxtype=gha 캐시 설정을 활용하는 것은 복잡한 스크립트 없이도 CI/CD 속도 개선을 달성할 수 있는 최고의 방법입니다. 50% 이상의 시간 단축은 단순히 기다리는 시간을 줄이는 것을 넘어, 개발자가 더 자주 배포하고 피드백을 받을 수 있는 '민첩한 개발 문화'를 만드는 기반이 됩니다. 지금 바로 여러분의 워크플로우에 cache-to: type=gha,mode=max 한 줄을 추가해 보시기 바랍니다.

OlderNewest

Post a Comment