왜 CI 스토리지가 조용히 새는가
CI 아티팩트는 빌드 산출물, 테스트 리포트, 커버리지 데이터, 컨테이너 이미지 레이어, 캐시로 구성된다. 문제는 이들 대부분이 기본 보존 기간이 길거나 무제한이라는 점이다. 하루 수백 번 도는 파이프라인에서 파이프라인당 수백 MB가 쌓이면, 월 단위로 수 TB에 도달한다. 특히 매 커밋마다 생성되는 node_modules 캐시나 디버그 심볼은 최신 것 몇 개만 의미가 있는데도 계속 적재된다. 스토리지 비용은 물론, 아티팩트 목록 API가 느려져 파이프라인 시작 자체가 지연되는 2차 피해도 발생한다.
보존 대상을 계층으로 나눠라
모든 아티팩트를 같은 기준으로 지우면 사고가 난다. 롤백이나 규제 대응에 필요한 산출물까지 날아가기 때문이다. 먼저 아티팩트를 목적별로 분류하고 각각 다른 정책을 적용한다.
| 분류 | 예시 | 권장 보존 |
|---|---|---|
| 배포 산출물 | 릴리스 바이너리, 프로덕션 이미지 | 90일~영구(태그 기준) |
| 디버그/진단 | 테스트 로그, 커버리지, 코어 덤프 | 7~14일 |
| 중간 캐시 | 의존성 캐시, 빌드 중간물 | 최신 N개만 |
| PR 검증물 | 프리뷰 빌드 | PR 종료 시 즉시 삭제 |
파이프라인 레벨에서 만료 지정
가장 효과가 큰 건 애초에 생성 시점에 만료를 박아두는 것이다. GitLab CI는 expire_in, GitHub Actions는 retention-days를 지원한다. 배포물만 예외로 두고 나머지는 짧게 잡는다.
build:
stage: build
script:
- make build
artifacts:
paths: [dist/]
expire_in: 3 days # 진단용은 짧게
release:
stage: deploy
script:
- make package
artifacts:
paths: [release/]
expire_in: never # 릴리스는 태그 정책으로 별도 관리
rules:
- if: '$CI_COMMIT_TAG'
레지스트리 이미지는 별도로 청소한다
컨테이너 레지스트리는 아티팩트 만료와 별개로 관리해야 하는 최대 비용원이다. 태그 정규식 기반으로 "최신 N개 + 릴리스 태그"만 남기고 나머지 SHA 태그를 정리한다. 아래는 레지스트리 API를 이용한 정리 스크립트의 핵심 로직이다.
import re, requests
KEEP_RECENT = 10
tags = requests.get(f"{REG}/v2/{IMAGE}/tags/list").json()["tags"]
# 릴리스 태그는 보존, 나머지는 최신순 정렬 후 오래된 것 삭제
protected = [t for t in tags if re.match(r"^v\d+\.\d+\.\d+$", t)]
transient = sorted(set(tags) - set(protected), reverse=True)
for tag in transient[KEEP_RECENT:]:
digest = requests.head(
f"{REG}/v2/{IMAGE}/manifests/{tag}",
headers={"Accept": "application/vnd.oci.image.manifest.v1+json"},
).headers["Docker-Content-Digest"]
requests.delete(f"{REG}/v2/{IMAGE}/manifests/{digest}")
print(f"deleted {tag}")
삭제 후 GC를 잊지 마라
매니페스트를 지워도 블롭은 남는다. 레지스트리 종류에 따라 garbage-collect를 실행하거나 클라우드 레지스트리의 정리 정책을 켜야 실제 용량이 회수된다. 이 단계를 빼먹어 "지웠는데 용량이 그대로"인 사례가 흔하다.
비용을 먼저 측정하라
정책을 바꾸기 전에 무엇이 얼마를 쓰는지 봐야 한다. 대부분의 CI/레지스트리는 사용량 메타데이터를 제공하므로, 상위 소비 프로젝트를 집계해 우선순위를 정한다.
SELECT project, ROUND(SUM(size_bytes)/1e9, 1) AS gb,
COUNT(*) AS artifacts
FROM ci_artifacts
WHERE created_at < NOW() - INTERVAL '30 days'
GROUP BY project
ORDER BY gb DESC
LIMIT 20;
주의점: 지우기 전에 참조를 확인하라
공격적인 정리는 두 가지를 깨뜨린다. 첫째, 롤백 경로다. 프로덕션에 떠 있는 이미지 태그를 정리 대상에 넣으면 노드 재기동 시 이미지 풀에 실패한다. 삭제 전 실행 중인 배포가 참조하는 다이제스트를 제외 목록에 넣어야 한다. 둘째, 규제·감사 요건이다. 빌드 재현성이나 SBOM 보관이 의무인 조직은 최소 보존 기간이 법적으로 정해져 있을 수 있다. 정책은 드라이런으로 먼저 돌려 삭제 대상 목록을 로그로 남기고, 며칠 관찰한 뒤 실제 삭제를 활성화하는 순서가 안전하다. 마지막으로 캐시를 너무 짧게 잡으면 캐시 미스로 빌드 시간이 늘어 컴퓨트 비용이 스토리지 절감분을 상쇄할 수 있으니, 캐시 적중률을 함께 모니터링해 균형점을 찾는다.