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 |
- 배포 속도는 개선시킨 사항이 아님. 워크플로우마다 존재하는 편차에 해당.