🏷️ latest 태그, 지금 당장 버려야 하는 이유 — 컨테이너 이미지 태그 불변성(Immutability) 완전 정복
by gasbugs2026. 4. 14.
반응형
"latest를 쓰면 편하긴 한데… 왜 실무에선 쓰지 말라고 할까?" 오늘 그 이유를 파헤쳐 봅니다.
🎯 이 글에서 다루는 것
컨테이너 이미지 태그가 mutable(가변)하면 어떤 일이 벌어지는가
Immutable 태그란 무엇이고, 왜 정석으로 불리는가
실무에서 많이 쓰는 태그 전략 패턴 (SemVer, Git SHA, 날짜 기반)
ECR·GCR·Harbor에서 Tag Immutability 강제 설정하는 방법
CI/CD 파이프라인에서 불변 태그를 자동화하는 예제
📌 도입 / 배경
컨테이너를 처음 배울 때 누구나 docker pull nginx:latest 한 줄로 시작합니다. 간편하고, 항상 최신 버전을 받을 수 있을 것 같고, 딱 봐도 직관적이죠.
그런데 실제 운영 환경에서 latest 태그를 그대로 쓰다가 낭패를 본 사례는 셀 수도 없이 많습니다.
분명히 어제 잘 됐는데 오늘 배포하니까 갑자기 앱이 죽었어요.
이 문장의 원인 중 상당수가 바로 mutable 태그 때문입니다. 같은 태그 이름인데 이미지 내용이 바뀌는 것, 이게 핵심 문제입니다.
컨테이너 생태계가 성숙해지면서, 지금은 이미지 태그는 immutable(불변)하게 관리하는 것이 사실상 업계 표준으로 자리 잡았습니다. Kubernetes, GitOps, DevSecOps 어느 쪽을 파고들어도 결국 이 원칙을 만나게 됩니다.
🔍 Mutable vs Immutable 태그, 정확히 뭐가 다른가?
Mutable 태그란?
같은 태그 이름으로 다른 이미지를 덮어쓸 수 있는 상태입니다.
# 오전 10시 빌드
docker build -t myapp:latest .
docker push myapp:latest
# → latest = commit abc123 이미지
# 오후 3시 빌드 (코드 변경 후)
docker build -t myapp:latest .
docker push myapp:latest
# → latest = commit def456 이미지 (오전 이미지는 사라짐)
latest 외에도 stable, production, v1 같은 태그도 같은 방식으로 덮어쓰면 mutable이 됩니다.
Immutable 태그란?
한 번 push한 태그는 절대 다른 이미지로 덮어쓸 수 없는 정책입니다.
# 특정 Git SHA나 버전으로 태그
docker build -t myapp:1.4.2 .
docker push myapp:1.4.2
# → 1.4.2는 이 이미지로 영구 고정
# 나중에 같은 태그로 push 시도 시
docker push myapp:1.4.2
# ❌ ERROR: Tag immutability enabled. Cannot overwrite existing tag.
핵심은 "태그 = 특정 이미지의 고유 식별자" 가 된다는 점입니다.
⚡ Mutable 태그가 만들어내는 현실적 문제들
1️⃣ 재현 불가능한 빌드/배포 환경
latest 태그를 쓰면 오늘 배포한 이미지와 일주일 후 롤백 시도 시 이미지가 달라질 수 있습니다. docker pull myapp:latest가 그 사이 덮어써져 있다면 롤백 자체가 의미를 잃습니다.
2️⃣ 보안 감사의 어려움
"이 이미지에 어떤 라이브러리가 들어있었나?"를 추적해야 할 때, mutable 태그는 근거를 제공하지 못합니다. CVE가 터졌을 때 "그 시점에 배포된 이미지가 정확히 무엇인지" 를 증명할 수 없습니다.
3️⃣ Kubernetes에서의 캐시 문제
Kubernetes는 같은 태그의 이미지를 노드에서 캐시합니다. imagePullPolicy: IfNotPresent 설정 시, 태그가 같으면 새 이미지로 업데이트된 줄도 모르고 기존 캐시를 계속 씁니다.
# 위험한 설정 예시
spec:
containers:
- name: myapp
image: myapp:latest # ← mutable 태그
imagePullPolicy: IfNotPresent # ← 캐시 우선, 항상 pull 안 함
4️⃣ 동시 배포 시 Race Condition
멀티 인스턴스 환경에서 latest를 동시에 pull하면 인스턴스마다 서로 다른 이미지를 실행하는 상황이 생길 수 있습니다. 이건 장애 원인 찾기가 정말 지옥입니다.
# 태그 패턴: v* 로 시작하는 태그만 immutable 적용
Tag Pattern: v*
Repository Pattern: **
Google Artifact Registry
gcloud artifacts repositories update myapp-repo \
--location=us-central1 \
--update-immutable-tags=true # 태그 불변 정책 활성화
🤖 GitHub Actions CI/CD에서 불변 태그 자동화
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set image tag
id: tag
run: |
# Short SHA + 날짜 조합
echo "IMAGE_TAG=$(date +%Y%m%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build image
run: docker build -t ${{ secrets.ECR_REGISTRY }}/myapp:${{ env.IMAGE_TAG }} .
- name: Push image
run: docker push ${{ secrets.ECR_REGISTRY }}/myapp:${{ env.IMAGE_TAG }}
- name: Update Kubernetes manifest
run: |
# GitOps 방식: 매니페스트의 태그를 새 이미지 태그로 교체
sed -i "s|image: myapp:.*|image: myapp:${{ env.IMAGE_TAG }}|g" k8s/deployment.yaml
git commit -am "chore: update image tag to ${{ env.IMAGE_TAG }}"
git push
이렇게 하면 배포할 때마다 고유한 태그가 자동 생성되고, Kubernetes 매니페스트에 기록이 남습니다. 나중에 "그때 배포된 이미지가 뭔지"를 Git 히스토리로 확인할 수 있죠.
⚠️ 주의사항 / 흔한 실수
🚫 latest를 운영 환경에 사용하지 말 것 개발·테스트에서 편의상 쓰는 건 괜찮지만, staging과 production에서는 절대 금지입니다.
🚫 Immutable 설정 후 태그 충돌 처리 미비 같은 태그를 재push하면 레지스트리가 오류를 반환합니다. CI 파이프라인에서 태그 생성 로직을 반드시 유니크하게 설계해야 합니다. (같은 커밋을 두 번 빌드하면? → SHA는 같음 → 오류 발생 → 빌드 번호 조합 추천)
🚫 Digest를 태그와 혼동하는 경우 이미지 다이제스트(sha256:abc...)는 이미지 내용의 해시로 원래부터 불변입니다. 태그가 불변이어야 한다는 건 태그 이름이 항상 같은 다이제스트를 가리켜야 한다는 의미입니다.
🚫 레지스트리 정책 없이 컨벤션만으로 관리 "우리는 latest 안 쓰기로 약속했어요"는 충분하지 않습니다. ECR Immutability 같은 레지스트리 수준의 강제 정책이 있어야 실수를 원천 차단할 수 있습니다.
✅ 정리 / 마무리
구분
Mutable 태그
Immutable 태그
재현성
❌ 동일 태그 ≠ 동일 이미지
✅ 동일 태그 = 동일 이미지
보안 감사
❌ 이력 추적 불가
✅ 배포 이미지 완전 추적
롤백
❌ 이미지가 사라질 수 있음
✅ 항상 정확한 버전으로 복귀
협업
❌ 혼선 가능
✅ 명확한 커뮤니케이션
운영 리스크
🔴 높음
🟢 낮음
컨테이너 이미지 태그 불변성은 단순한 컨벤션이 아니라, GitOps·DevSecOps·재현 가능한 빌드 의 기반이 되는 핵심 원칙입니다. 지금 당장 레지스트리에 Immutability 정책을 켜고, CI 파이프라인에서 Git SHA 기반 태그 자동화를 적용해 보세요.
다음 단계로는 Image Signing(Cosign) 이나 SBOM(Software Bill of Materials) 으로 이미지 무결성 검증까지 확장하면 보안 수준이 한 단계 더 올라갑니다.