CI CD 최적화 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

1. 개요: 현재 CI가 너무 오래 걸림


2. 관련 개념들

a. 증분 빌드

이전에 빌드한 결과를 재활용하고, 변경된 부분만 다시 빌드하는 방식

  • git diff, file hash 등을 기준으로 변경 여부 탐지
  • TurboRepo, Nx는 변경된 패키지만 의존 그래프에 따라 빌드

b. 내용 기반 해싱

파일의 내용 자체를 해싱해서 변경 여부 판단

  • 단순히 수정 시간이나 파일 이름 기준이면 오류 가능성 있음
  • 내용이 바뀌지 않았다면 동일 hash → 캐시 재사용 가능 ( 수정했다가 원상복구한 상황에서 수정 시간은 업데이트되었으나 내용이 바뀌지 않아 캐시 재사용 가능)

c. 병렬 실행

여러 작업을 동시에 실행하여 전체 시간 줄임

  • cpu 리소스가 남는데 직렬로만 실행하면 낭비
  • 여러 패키지의 빌드/테스트는 서로 독립적이라 병렬 처리 적합
  • CI 플랫폼의 매트릭스 실행

→ 도커 이미지 빌드와 배포는 의존 관계가 있어 병렬 실행이 어려움. 여러 테스트를 병렬적으로 실행하는 구조는 가능할 것

d. 원격 캐싱

이전 빌드 결과를 클라우드 또는 S3, Redis 등에 저장해서 다음 빌드에 재사용

  • 같은 커밋이라면 모든 CI 서버에서 같은 캐시를 받아 쓸 수 있음
  • 개발자 로컬과 CI 간에도 캐시 공유 가능

e. 캐시 볼륨 마운트

컨테이너가 종료돼도 사라지지 않는 디스크 공간에 빌드 결과 캐시를 저장하는 방식

  • node_modulers, .turbo, .next/cache 등을 캐시 볼륨에 마운트

→ 온프레미스 Runner를 운영하는 환경에서 이용 가능하나, Github action을 사용하는 상황에서는 일회성 VM을 이용하기 때문에 불가능한 방식


3. 도입할 방법들 검토

a. 병렬 작업 활성화: 어플리케이션과 Node exporter 컨테이너 병렬 실행 (이미 도입됨)

  • 도커 이미지를 pull해오는 작업을 병렬적으로 실행 (waitFor: [-] 설정 활용)
#!/bin/bash
set -e

export IMAGE_TAG=$(curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/attributes/IMAGE_TAG)
export PROJECT_ID=$(curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/attributes/PROJECT_ID)

echo "[INFO] IMAGE_TAG=${IMAGE_TAG}"
echo "[INFO] PROJECT_ID=${PROJECT_ID}"

echo "Docker 설치"
apt-get update
apt-get install -y docker.io

echo "Docker 데몬 시작 중"
systemctl start docker
systemctl enable docker

echo "node_exporter 컨테이너 실행"
docker run -d \
  --name node_exporter \
  --net=host \
  --pid=host \
  --restart unless-stopped \
  prom/node-exporter

echo "gcloud CLI 설치"
apt-get install -y apt-transport-https ca-certificates gnupg curl
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" \
  | tee /etc/apt/sources.list.d/google-cloud-sdk.list
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg \
  | apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
apt-get update && apt-get install -y google-cloud-sdk

echo "GCP Artifact Registry 인증"
gcloud auth configure-docker asia-northeast3-docker.pkg.dev --quiet

echo "ai-cpu 이미지 Pull"
docker pull asia-northeast3-docker.pkg.dev/dev-ongi-3-tier/dev-ongi-ai-repo/ai-cpu:${IMAGE_TAG}

echo "FastAPI 앱 컨테이너 실행"
docker run -d \
  --name ai-cpu \
  -e PROJECT_ID="$PROJECT_ID" \
  -p 8000:8000 \
  --restart unless-stopped \
  asia-northeast3-docker.pkg.dev/dev-ongi-3-tier/dev-ongi-ai-repo/ai-cpu:${IMAGE_TAG}
  • 기존 코드에서도 docker run -d를 이용하여 비동기 실행하고 있음
  • -d 옵션은 detach mode로, 실행 이후 즉시 다음 명령으로 넘어감

b. Docker 캐싱 전략 개선

  • 도커 이미지 레이어 분리
    • 변경이 잦은 패키지는 하단에 배치하여
    • 변경이 일어났을 때, 이전 레이어는 재활용하며 변경된 부분만 재빌드되도록 함
  • 내용 해시를 통해 코드 변경이 있을 경우에만 캐시 무효화

c. Docker 이미지 SHA(Secure Hash Algorithm)

  • 도커 캐시는 “파일 해시 기준”이고, SHA 태그 전략은 “커밋 기준”
  • 즉, 도커 캐시는 파일에 변경 사항이 없으면 캐시를 이용하고 SHA 태그 전략은 커밋이 동일하면 캐시를 이용함
  • 파일 해시 기준일 경우, 커밋 기준을 포함함
    • 파일 변경 사항 없이 커밋 추가된 경우: SHA 전략 기준으로는 캐시 무효화, 도커 캐시 기준으로는 캐시 사용
    • 동일 커밋 기준: SHA 전략과 도커 캐시 모두 캐시 사용

→ 결론: 도커 캐시 방식이 캐시 히트(Cache Hit)이 높음. 재빌드가 더 적게 발생하여 지연 시간 절약

d. VM 인스턴스 생성 & 스크립트 실행 통합 (metadata, 이미 도입됨)

  • 기존 방식

    • startup.sh 내부에서 gcloud auth login (인증 과정)부터 진행하여 지연
  • 잘못 생각했던 부분

    • 기존 방식에서 vm 생성 시, 서비스 계정 설정하고
    • startup.sh를 metadata 설정했음
    • 이는 실행 스크립트(startup.sh) 진행 시, 인증된 서비스 계정 상태로, gcloud auth login과 gcloud auth configure-docker가 필요하지 않음
    • 불필요한 인증 과정이 추가되어 지연 시간 증가했었음 → 과정 생략함
            **echo ▶ 템플릿 생성: ${_TEMPLATE_NAME}
            gcloud compute instance-templates create "${_TEMPLATE_NAME}" \
              --network=dev-ongi-vpc \
              --subnet=dev-ongi-private-subnet \
              --machine-type=e2-standard-4 \
              --region=asia-northeast3 \
              --boot-disk-size=50GB \
              --image-family=ubuntu-2204-lts \
              --image-project=ubuntu-os-cloud \
              --metadata="ssh-keys=$$SSH_KEYS,IMAGE_TAG=${_IMAGE_TAG},PROJECT_ID=$$PROJECT_ID" \
              --metadata-from-file=startup-script=.gcp/scripts/startup.sh \
              --service-account=github-ai-cd-builder@dev-ongi-3-tier.iam.gserviceaccount.com \
              --scopes=https://www.googleapis.com/auth/cloud-platform \
              --tags=dev-ai**
    

e. 의존성 캐싱

  • 깃헙 액션에서 의존성 파일의 hash 값을 기반으로 캐싱
  • 의존성 변경되는 경우에만 재설치
- name: Cache dependencies
  uses: actions/cache@v3
  with:
    path: |
      ~/.cache/pip
    key: pip-${{ hashFiles('requirements.txt') }}
    restore-keys: |
      pip-
  • 의존성 캐싱은 Runner 머신의 홈 디렉토리 하단에 위치한 ~/.cache/pip에 저장됨
  • ~/.cache/pip: pip가 다운로드한 wheel, tar.gz 파일 등 저장해두는 저장소
  • 캐시가 깃헙 내부에 저장되며, Runner가 바뀌더라도 key 기반 복원됨

→ 도커 캐싱이 적용되었기 때문에 의존성이 변경되지 않으면 의존성 레이어가 재사용됨. 이로 인해 따로 의존성 캐싱을 적용하지 않아도 됨.

f. packer를 활용한 vm image bake (추후 고려 사항)

  • 기존에는 docker image만을 사용하여 ubuntu에 도커 설치 과정이 따로 필요했음
  • packer를 이용하면 도커 설치, gcloud 인증까지 포함한 vm 이미지를 생성함
  • 추가적인 세팅 과정 없이 vm을 이용 가능

→ 환경 내 자동 설치 측면: 기존 방식도 startup.sh 파일을 통해 도커 설치 및 gcloud 인증 진행함. 동일 인스턴스 내에서는 도커 이미지를 따로 띄우더라도 설치 및 인증된 환경을 공유하여 이용할 수 있음. 동일 환경에서 도커 컨테이너를 띄우는 현재 환경에서는 장점이 크지 않음.

→ 배포 시간 측면: 도커 캐싱 적용 후, 이미지 빌드 지연 시간 단축 성공. startup.sh 실행 시간도 단축하기 위해서는 vm bake가 방법이 될 수 있음. 그러나 도커 이미지 레이어 캐싱 이후 단계로 적용되야 하기 때문에 캐싱 효과에 대한 검토 요망.


4. 도입한 방법: 도커 캐시 적용

a. 도커 캐시 적용 상세 설명

  • GHA(GitHub Action) 캐시를 활용하여 도커 레이어 저장
  • 이후 빌드 시, 도커 파일의 각 명령어에 대해 내용 해시를 비교하여 변경된 레이어만 재빌드하고, 이전 레이어는 캐시에서 복원하여 빌드 속도 최적화

b. Docker file 수정

# 전
FROM python:3.10.17-slim

WORKDIR /app

RUN apt-get update && apt-get install -y \
    git wget \
    libglib2.0-0 libgl1 \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

COPY app/ app/
COPY gunicorn.conf.py .

ENV PYTHONPATH=/app

EXPOSE 8000

CMD ["gunicorn", "-c", "gunicorn.conf.py", "app.main:app"]

# 후
FROM python:3.10.17-slim

WORKDIR /app

RUN apt-get update && apt-get install -y \
    git wget \
    libglib2.0-0 libgl1 \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
COPY gunicorn.conf.py .

RUN pip install --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

COPY app/ app/

ENV PYTHONPATH=/app

EXPOSE 8000

CMD ["gunicorn", "-c", "gunicorn.conf.py", "app.main:app"]

  • 변경 사항: 자주 변경되지 않는 gunicorn 설정(멀티 프로세스 설정)을 상단으로 이동하여 도커 레이어 캐시 재사용성을 높임

c. dev-deploy.yaml 수정


# 전
jobs:
  build-docker:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v4
        with:
          ref: dev

      - name: Set up Python 3.10.17
        uses: actions/setup-python@v5
        with:
          python-version: '3.10.17'

      - name: Build Docker Image
        env:
          TAG: dev-${{ github.sha }}
        run: |
          docker build -t asia-northeast3-docker.pkg.dev/dev-ongi-3-tier/dev-ongi-ai-repo/ai-cpu:$TAG .
          docker push asia-northeast3-docker.pkg.dev/dev-ongi-3-tier/dev-ongi-ai-repo/ai-cpu:$TAG

# 후
jobs:
  build-docker:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v4
        with:
          ref: dev

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Set up Python 3.10.17
        uses: actions/setup-python@v5
        with:
          python-version: '3.10.17'

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: asia-northeast3-docker.pkg.dev/dev-ongi-3-tier/dev-ongi-ai-repo/ai-cpu:dev-${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha, mode=max
  • 변경 사항:
    • buildx 툴킷을 사용하여 도커 레이어 캐싱 사용 가능하도록 설정
    • gha(github action) 캐시 저장소에 max(모든 도커 레이어 저장)로 레이어 캐싱

d. 결과

개선
이미지 빌드 5m 49s 48s 86.2%
배포 3m 27s 3m 15s -
소요 시간 9m 16s 4m 3s
  • 배포 속도는 개선시킨 사항이 아님. 워크플로우마다 존재하는 편차에 해당.

참고 자료

https://tech.inflab.com/20231101-optimizing-ci-pipeline/