# ArgoCD Image Updater - 무엇을 최신으로 볼 것인가, 그 결과를 어디에 쓸 것인가

GitOps로 배포하면 매니페스트의 이미지 태그가 곧 배포 버전입니다. 그런데 CI가 새 이미지를 밀었을 때 그 태그를 매니페스트에 반영하는 주체가 없습니다. CI에게 매니페스트 레포 쓰기 권한을 주면 되지만 그러면 CI가 두 레포에 걸치고 커밋 루프를 만들기 쉽습니다.

ArgoCD Image Updater는 이 자리를 채웁니다. 레지스트리를 주기적으로 보고 새 태그를 찾아 매니페스트에 반영합니다. 설정은 애너테이션 몇 줄이라 간단해 보이지만 실제로는 서로 독립적인 두 결정을 해야 합니다. 무엇을 최신으로 판정할지(update strategy)와 그 결과를 어디에 쓸지(write-back method)입니다. 이 글에서는 두 축의 선택지가 각각 어디서 무너지는지 정리합니다.

# 1. 두 축은 서로 독립이다

애너테이션을 보면 두 축이 한 덩어리처럼 보입니다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  annotations:
    # 축 1: 어떤 이미지를 어떤 기준으로 최신이라 볼 것인가
    argocd-image-updater.argoproj.io/image-list: api=registry.example.com/api
    argocd-image-updater.argoproj.io/api.update-strategy: semver
    argocd-image-updater.argoproj.io/api.allow-tags: regexp:^v\d+\.\d+\.\d+$

    # 축 2: 판정 결과를 어디에 쓸 것인가
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/git-branch: main

앞의 셋은 "레지스트리에서 무엇을 고를까", 뒤의 둘은 "고른 것을 어디에 기록할까"입니다. 잘못된 조합이 만드는 증상이 서로 다르므로, 문제가 생겼을 때 어느 축인지 먼저 갈라야 합니다. 새 태그가 아예 감지되지 않으면 1축, 감지는 되는데 배포에 반영되지 않거나 되돌아가면 2축입니다.

# 2. 무엇을 최신으로 볼 것인가

전략은 네 가지입니다. 문서의 정의를 그대로 옮기면 이렇습니다.

전략 문서 설명 판정 기준
semver Update to the latest version of an image considering semantic versioning constraints 시맨틱 버전
newest-build (구 latest) Update to the most recently built image found in a registry 이미지 빌드 시각
digest Update to the latest version of a given version (tag), using the tag's SHA digest 고정 태그의 다이제스트
alphabetical (구 name) Sorts tags alphabetically and update to the one with the highest cardinality 태그 이름 사전순

이름만 보면 newest-build가 가장 직관적입니다. 그런데 여기가 함정이 가장 많습니다.

# 2-1. newest-build가 보는 것은 푸시 시각이 아니다

문서가 명시합니다.

the build date of the image, and not the date of when the image was tagged or pushed

이미지 안에 기록된 빌드 시각을 봅니다. 레지스트리에 올라온 순서가 아닙니다. 두 값이 어긋나는 경우가 실제로 흔합니다. 오래된 커밋을 다시 빌드해 핫픽스로 밀면 빌드 시각은 지금이지만 코드는 과거 것입니다. 반대로 캐시된 레이어를 재사용하는 빌드는 빌드 시각이 예전에 머무를 수 있습니다.

더 결정적인 경우가 있습니다.

If you are using reproducible builds ... the latest or newest-build strategy will not be able to determine which tag to update to.

재현 가능한 빌드는 타임스탬프를 고정값으로 박습니다. 그러면 모든 이미지의 빌드 시각이 같아져 순서를 매길 수 없습니다. 빌드 재현성을 확보하는 작업과 이 전략이 정면으로 충돌합니다. 이 경우 동점 처리로 태그 이름 역순 정렬이 쓰이는데, 그건 사실상 alphabetical이 되어 버립니다.

비용도 있습니다.

these will count into your pull limits. So unless you are not affected by these pull limits, it is not recommended to use the latest or newest-build update strategy with images hosted on Docker Hub.

빌드 시각을 알려면 태그마다 매니페스트와 config를 받아 봐야 합니다. 태그가 수백 개면 폴링 한 번에 요청이 수백 번 나갑니다. 요청 수 제한이 있는 레지스트리에서는 이것만으로 한도를 씁니다. allow-tags로 후보를 좁히는 것이 선택이 아니라 필수인 이유입니다.

# 2-2. semver는 규칙에 안 맞는 태그를 조용히 버린다

Argo CD Image Updater will omit any tags from your registry that do not match a semantic version.

무시입니다. 오류가 아닙니다. 태그를 v1.4.2 형식으로 밀다가 어느 시점부터 2026.01.26-a1b2c3d 같은 형식으로 바꿨다면, 그 뒤로는 후보가 하나도 없어 업데이트가 조용히 멈춥니다. 로그를 보지 않으면 몇 주 뒤에 알게 됩니다.

v 접두사, 프리릴리스 표기(1.4.2-rc.1), 빌드 메타데이터(1.4.2+build.5)의 처리도 확인이 필요합니다. 제약을 함께 거는 편이 안전합니다.

    argocd-image-updater.argoproj.io/api.update-strategy: semver
    # 1.x 안에서만 올린다 - 메이저 업그레이드는 사람이 결정
    argocd-image-updater.argoproj.io/api.allow-tags: regexp:^v1\.\d+\.\d+$

# 2-3. alphabetical은 사전순이지 숫자순이 아니다

tags returned by the registry in a lexical way (by name, in descending order)

v9v10을 사전순으로 비교하면 v9가 큽니다. 버전 비슷한 태그에 이 전략을 쓰면 어느 순간 뒤로 가고 그 뒤로는 영원히 거기 머뭅니다. 날짜 태그(2026-01-26)처럼 사전순과 시간순이 일치하도록 설계된 태그에만 안전합니다. 자릿수가 고정된 형식이어야 한다는 뜻입니다.

# 2-4. digest가 가장 예측 가능하다

digest는 태그를 고정한 채 그 태그가 가리키는 다이제스트가 바뀌면 반영합니다. 즉 "stable 태그가 가리키는 실체가 바뀌면 따라간다"입니다.

    argocd-image-updater.argoproj.io/image-list: api=registry.example.com/api:stable
    argocd-image-updater.argoproj.io/api.update-strategy: digest

매니페스트에는 다이제스트가 기록되므로, 배포된 실체가 무엇인지 Git에 남습니다. 태그 선택 로직이 없으니 2-1~2-3의 함정이 전부 사라지고 대신 어떤 이미지를 stable로 올릴지는 CI가 결정합니다. 판단을 이미지 파이프라인 쪽으로 옮기는 구조입니다.

가변 태그가 배포에 만드는 문제 자체는 이미지 태그는 포인터다 (opens new window)에 정리했는데, digest 전략은 그 문제를 "태그는 움직이되 그 결과를 Git에 다이제스트로 고정한다"로 푸는 방식입니다.

전략을 고르는 기준을 정리하면 이렇습니다.

상황 전략
릴리스 태그가 semver로 일관됨 semver + allow-tags 제약
CI가 승격 태그(stable)를 관리 digest
자릿수 고정 날짜 태그 alphabetical
그 외 태그 규칙부터 정리

newest-build는 목록에 넣지 않았습니다. 빌드 시각에 의존하는 특성이 재현 가능한 빌드와 충돌하고 폴링 비용이 크기 때문에, 다른 선택지가 있으면 그쪽이 낫습니다.

# 3. 결과를 어디에 쓸 것인가

판정이 맞아도 쓰기 방식이 틀리면 배포에 반영되지 않습니다. 두 가지가 있습니다.

argocd: directly modifies the Argo CD Application resource in the cluster, updating the application's source parameters

git: uses Git to permanently store its parameter overrides along with the Application's resource manifests

argocd 방식이 기본값이고 설정이 필요 없어서 처음에는 이쪽으로 시작하게 됩니다. 문제는 이 문장입니다.

pseudo-persistent. If you delete the Application resource from the cluster and re-create it, changes made by Image Updater will be gone.

클러스터의 오브젝트만 고치므로 그 오브젝트가 다시 만들어지면 사라집니다. 그리고 Application 자체를 GitOps로 관리하고 있다면 이 상황이 늘 발생합니다. App-of-Apps나 ApplicationSet으로 Application을 생성하는 구성에서는 ArgoCD가 Git 기준으로 Application을 동기화하므로, Image Updater가 써 넣은 파라미터가 다음 동기화에서 지워집니다.

증상이 고약합니다. 이미지가 올라갔다가 얼마 뒤 예전 태그로 되돌아가고, 다시 Image Updater가 올리고, 또 되돌아갑니다. 원인이 두 컨트롤러의 상반된 목표라서 어느 한쪽 로그만 봐서는 안 보입니다.

Application을 Git으로 관리한다면 선택지는 git 하나뿐입니다.

    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/git-branch: main
    argocd-image-updater.argoproj.io/write-back-target: helmvalues:./values-prod.yaml

# 4. .argocd-source-<appName>.yaml이라는 보이지 않는 파일

git 방식에서 대상을 지정하지 않으면 기본 동작이 이렇습니다.

By default, git write-back will create or update .argocd-source-<appName>.yaml

레포 루트에 점으로 시작하는 파일이 생깁니다. 여기에 파라미터 오버라이드가 들어가고, ArgoCD는 이 파일을 values나 kustomization보다 우선 적용합니다.

이것이 나중에 혼란을 만듭니다. values-prod.yaml에서 이미지 태그를 직접 고쳤는데 반영되지 않는 상황이 생기고 파일이 점으로 시작해 디렉터리 목록에서 눈에 잘 안 띕니다. 원인을 찾으려면 그 파일의 존재를 알고 있어야 합니다.

대상을 명시하면 이 파일 없이 원하는 곳에 씁니다.

write-back-target 동작
미지정 .argocd-source-<appName>.yaml 생성
kustomization:<경로> kustomize edit set image를 실행한 것과 같은 결과
helmvalues:<경로> 지정한 values 파일의 해당 키를 갱신

values 파일을 사람도 고치는 레포라면 helmvalues를 명시하는 편이 낫습니다. 값이 한 곳에만 있게 되어, 나중에 왜 이 태그가 배포됐는지 추적할 때 볼 파일이 하나입니다.

# 5. 커밋 루프와 자격 증명

git 방식은 커밋을 만듭니다. 그 커밋이 CI를 트리거하고, CI가 새 이미지를 빌드하고, Image Updater가 다시 커밋하는 순환이 생길 수 있습니다. 매니페스트 레포와 애플리케이션 레포가 같은 모노레포일 때 특히 그렇습니다. 커밋 메시지 템플릿에 CI 건너뛰기 표시를 넣거나, CI 트리거 경로에서 매니페스트 디렉터리를 제외해야 합니다.

    argocd-image-updater.argoproj.io/git-repository: https://git.example.com/manifests.git
    argocd-image-updater.argoproj.io/commit-message-template: |
      build: bump {{ range .AppChanges }}{{ .Image }} to {{ .NewTag }}{{ end }} [skip ci]

보호된 브랜치라면 직접 푸시가 막히므로 PR 모드를 씁니다. 여기서 자격 증명 제약이 하나 걸립니다.

PR creation requires credentials that carry a bearer token - either a personal access token (PAT) or a GitHub App. SSH keys cannot be used

SSH 키로는 PR을 못 만듭니다. 커밋·푸시는 SSH로 되는데 PR 생성만 안 되므로, SSH 배포 키로 설정해 두고 PR 모드를 켜면 그 지점에서만 실패합니다. 토큰 기반 자격 증명이 따로 필요합니다.

브랜치를 나눠 쓸 수도 있습니다.

    # base:target - main에서 분기해 별도 브랜치에 커밋
    argocd-image-updater.argoproj.io/git-branch: "main:image-updater-{{.SHA256}}"

다만 이 표기법에는 제약이 있습니다.

The base:target shorthand ... is not supported when pullRequest is configured

PR 모드와 조합할 수 없습니다. 둘을 함께 쓰려고 하면 설정이 무시되거나 예상과 다르게 동작합니다.

# 6. 애초에 Image Updater를 써야 하는가

대안이 둘 있고 비교하면 이렇습니다.

Image Updater CI가 직접 매니페스트 커밋
트리거 레지스트리 폴링 빌드 성공 시점
반영 지연 폴링 주기만큼 즉시
레지스트리 요청 주기적 폴링 비용 없음
CI 권한 매니페스트 레포 쓰기 불필요 두 레포 쓰기 필요
어떤 이미지인지 판정 태그 정렬 규칙에 의존 CI가 방금 만든 것을 안다
레포 밖 이미지 추적 가능 (서드파티 이미지) 불가

핵심 차이는 마지막 두 줄입니다. CI는 자기가 방금 무엇을 빌드했는지 정확히 압니다. 정렬 규칙으로 추측할 필요가 없으므로 2절의 함정이 통째로 사라집니다. 반면 Image Updater는 자기가 빌드하지 않은 이미지도 추적할 수 있습니다. 서드파티 컴포넌트를 최신으로 유지하는 용도라면 CI로는 못 하는 일입니다.

기준을 정하면, 우리가 빌드하는 이미지는 CI가 커밋하고 외부 이미지 추적에 Image Updater를 쓰는 조합이 각자의 강점을 살립니다. 하나로 통일해야 한다면, CI가 여러 레포에 쓰기 권한을 갖는 것을 피하고 싶은지가 판단 기준이 됩니다.

# 7. 직접 확인하는 방법

증상을 축으로 나눠 좁힙니다.

# 1축: 후보 태그를 실제로 몇 개나 봤는가
kubectl -n argocd logs deploy/argocd-image-updater --tail=200 | grep -i "considering\|no new\|tags"

# 특정 이미지의 태그 목록 - allow-tags 정규식과 맞춰 본다
crane ls registry.example.com/api | head -30

# 정규식이 의도대로 거르는지 로컬 확인
crane ls registry.example.com/api | grep -E '^v1\.[0-9]+\.[0-9]+$'

# 2축: 어디에 썼는가 - .argocd-source 파일이 생겼는지
git -C ./manifests ls-files | grep argocd-source

# Application에 파라미터 오버라이드가 붙었는지 (argocd 방식일 때)
kubectl -n argocd get application myapp \
  -o jsonpath='{.spec.source.helm.parameters}{"\n"}'

# 되돌아가는 현상 추적 - Application 리소스 변경 이력
kubectl -n argocd get events --field-selector involvedObject.name=myapp

1축에서 후보가 0이면 allow-tags나 semver 규칙 문제(2절), 후보는 있는데 갱신이 없으면 정렬 기준 문제입니다. 2축에서 파라미터가 붙었다가 사라지면 3절의 되돌림입니다.

# 8. 트러블슈팅

증상 원인 조치
새 태그를 감지하지 못함 semver 규칙에 안 맞아 전부 제외됨 태그 형식 확인, allow-tags 조정
어느 시점부터 업데이트 정지 태그 형식이 중간에 바뀜 로그에서 후보 수 확인
버전이 거꾸로 감 alphabetical의 사전순 비교 semver로 전환 또는 자릿수 고정
재현 빌드 도입 후 갱신 안 됨 newest-build가 빌드 시각에 의존 semver 또는 digest로 전환
레지스트리 요청 한도 초과 태그마다 메타데이터 조회 allow-tags로 후보 축소, 폴링 주기 상향
태그가 올라갔다 되돌아감 argocd 방식 + Application을 Git 관리 write-back-method: git
values를 고쳤는데 무시됨 .argocd-source-*.yaml 오버라이드 write-back-target 명시
커밋이 끝없이 반복됨 매니페스트 커밋이 CI 트리거 커밋 메시지에 CI 스킵, 경로 제외
PR만 생성 실패 SSH 키로는 PR 불가 PAT 또는 GitHub App 자격 증명

# 9. 마무리

  • 설정은 판정(update strategy)과 기록(write-back method) 두 축입니다. 감지가 안 되면 1축, 반영이 안 되거나 되돌아가면 2축입니다.
  • newest-build는 푸시 시각이 아니라 이미지 빌드 시각을 봅니다. 재현 가능한 빌드와 충돌하고 태그마다 메타데이터를 받아 오므로 폴링 비용이 큽니다.
  • semver는 규칙에 안 맞는 태그를 오류 없이 버립니다. 태그 형식이 바뀌면 업데이트가 조용히 멈춥니다.
  • alphabetical은 사전순이라 v10v9보다 작습니다. 자릿수 고정 태그에만 안전합니다.
  • argocd 방식은 클러스터 오브젝트만 고칩니다. Application을 Git으로 관리하면 두 컨트롤러가 서로 되돌리는 싸움이 됩니다.
  • git 방식 기본값은 .argocd-source-<appName>.yaml을 만들고 이 파일이 values보다 우선합니다. 존재를 모르면 진단이 오래 걸립니다.
  • 우리가 빌드하는 이미지라면 CI가 직접 커밋하는 쪽이 판정 문제 자체를 없앱니다. Image Updater의 고유 가치는 외부 이미지 추적입니다.

GitOps 동기화에서 컨트롤러와 Git이 서로 되돌리는 문제 일반은 ArgoCD ignoreDifferences가 동작하지 않는 세 가지 이유 (opens new window)에 정리했습니다.

# 참고