본문 바로가기
K8S

Argo CD에서 이미지 변경 트리거 안되는 배포 트러블 슈팅

by Rainbound-IT 2025. 8. 25.
반응형

— GitOps + ECR 날짜태그 + Argo CD Image Updater + IRSA, 그리고 Helm으로 한 장 템플릿 감싸기까지

이 글은 제가 실제로 겪은 “ECR 이미지가 바뀌었는데 Argo CD가 변화를 못 잡는다” 문제를 완전 종료한 과정 정리입니다.

길지만, 그대로 따르면 끝납니다. (폴더 구조, Terraform, Helm, 어노테이션, GitHub Actions, 체크리스트 모두 포함)


문제의 본질: Argo CD는 “Git 변경”만 본다

컨테이너 이미지를 :latest 로 계속 덮어써도 Git 매니페스트가 그대로면 Argo CD는 “변화 없음”으로 봅니다.
해결 핵심은 두 가지 중 하나:

  • A안(불변 태그 + Git 반영): 매 빌드마다 새 태그(날짜/커밋SHA) 를 만들고, 그 태그를 Git에 반영(Helm/Kustomize/Plain YAML).
  • B안(Image Updater): Argo CD Image Updater에게 “레지스트리에서 새 태그가 뜨면 Argo CD Application에 값을 패치하라”고 맡김. (우리는 Git 파일을 직접 건드릴 필요 없음)

이 글은 B안(Argo CD Image Updater) 를 기준으로 정리합니다. (A안도 참고 스니펫 제공)


최종 아키텍처 한 장 요약

  • ECR: YYYYMMDD-HHMMSS 같은 불변 태그로 이미지를 푸시
  • Argo CD Image Updater: ECR을 주기 스캔 → 새 태그를 발견하면
    Application의 Helm 파라미터(image.tag)를 패치(write-back=argocd)
    → Argo CD가 자동 동기화(SyncPolicy automated) → 새 롤아웃
  • IRSA: Image Updater PodECR 조회 권한을 갖도록 ServiceAccount ↔ IAM Role 연결

Image Updater는 Helm/Kustomize 앱만 write-back=argocd로 패치할 수 있습니다. 순수 Directory(Plain YAML)는 write-back=git(레포에 커밋)로도 가능하지만 설정이 조금 더 복잡합니다. argocd-image-updater.readthedocs.io

 2


1) 레포 구조: 기존 argocd/kubernetes.yaml → Helm 차트로 감싸기

현재는 argocd/kubernetes.yaml 하나로 배포 중이었죠. 그 파일을 Helm 템플릿으로만 감싸면 Image Updater가 바로 인지합니다.

 
repo-root/
└─ argocd/                  # ← Argo CD Application의 path 그대로 유지
   ├─ Chart.yaml
   ├─ values.yaml
   └─ templates/
      └─ kubernetes.yaml    # ← 기존 파일, 일부만 템플릿 변수로 치환

 

Chart.yaml

 
apiVersion: v2
name: invitation
description: Wrap existing manifests as a Helm chart
type: application
version: 0.1.0
appVersion: "1.0.0"
 
 

values.yaml (핵심: tag는 초기값, 나중엔 Image Updater가 바꿉니다)

 
image:
  repository: 768157413559.dkr.ecr.ap-northeast-2.amazonaws.com/barun-dev-ecr
  tag: "20250825-120000"
  pullPolicy: IfNotPresent

ingress:
  host: "test.barunsoncard.store"
  certificateArn: "arn:aws:acm:ap-northeast-2:768157413559:certificate/c3ef0453-9472-4d61-9319-7ae901c0f9be"
 
 
 

templates/kubernetes.yaml — 기존 YAML에서 이미지/호스트/ACM ARN만 values로 치환

 
 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: invitation-web
  namespace: invitation
  labels:
    app.kubernetes.io/name: invitation-web
spec:
  replicas: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: invitation-web
  template:
    metadata:
      labels:
        app.kubernetes.io/name: invitation-web
    spec:
      containers:
        - name: app
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: "{{ .Values.image.pullPolicy }}"
          ports:
            - containerPort: 3000
              name: http
          env:
            - name: NODE_ENV
              value: "production"
          readinessProbe:
            httpGet: { path: "/", port: http }
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 6
          livenessProbe:
            httpGet: { path: "/", port: http }
            initialDelaySeconds: 30
            periodSeconds: 20
            timeoutSeconds: 3
            failureThreshold: 3
          resources:
            requests: { cpu: "100m", memory: "256Mi" }
            limits:   { cpu: "500m", memory: "512Mi" }
---
apiVersion: v1
kind: Service
metadata:
  name: invitation-svc
  namespace: invitation
  labels:
    app.kubernetes.io/name: invitation-web
spec:
  selector:
    app.kubernetes.io/name: invitation-web
  ports:
    - name: http
      port: 80
      targetPort: 3000
      protocol: TCP
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: invitation-alb
  namespace: invitation
  annotations:
    alb.ingress.kubernetes.io/scheme: internal
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/group.name: shared-alb
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80}, {"HTTPS":443}]'
    alb.ingress.kubernetes.io/certificate-arn: "{{ .Values.ingress.certificateArn }}"
    alb.ingress.kubernetes.io/backend-protocol: HTTP
    alb.ingress.kubernetes.io/healthcheck-path: /
    alb.ingress.kubernetes.io/healthcheck-interval-seconds: "15"
    alb.ingress.kubernetes.io/healthcheck-timeout-seconds: "5"
spec:
  ingressClassName: alb
  rules:
    - host: {{ .Values.ingress.host | quote }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: invitation-svc
                port:
                  number: 80
  tls:
    - hosts: [ {{ .Values.ingress.host | quote }} ]
      # ALB Ingress는 certificate-arn 어노테이션을 사용. secretName은 형식상 남김.
      secretName: dummy-tls-will-be-ignored-by-alb
 
 
 

ALB Ingress 어노테이션은 공식 문서의 규격을 따르세요. listen-ports, certificate-arn, backend-protocol 등은 컨트롤러 버전에 맞게 확인 필수. kubernetes-sigs.github.io

: Namespace 리소스는 Argo CD의 CreateNamespace=true 옵션 하나로 처리하는 걸 권장(중복 생성 경고 방지).


2) IRSA: Image Updater에게 ECR 조회 권한 주기

Image Updater PodECRDescribeImages/GetAuthorizationToken을 호출해야 새 태그를 스캔할 수 있습니다. 이 권한은 노드 Role이 아니라 해당 Pod의 ServiceAccountIRSA로 부여합니다. (보안상 최소권한 원칙) 

iamrole 

assign iam role

private repository policy

 

Terraform 예시: IRSA Role + Policy + SA 연결

 
# (입력 변수)
variable "eks_cluster_name" { type = string }
variable "aws_account_id"  { type = string }

data "aws_eks_cluster" "this" {
  name = var.eks_cluster_name
}

locals {
  oidc_issuer       = replace(data.aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "")
  oidc_provider_arn = "arn:aws:iam::${var.aws_account_id}:oidc-provider/${local.oidc_issuer}"
}

resource "aws_iam_role" "argocd_image_updater" {
  name = "ECRReadOnlyRole-ImageUpdater"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect = "Allow",
      Principal = { Federated = local.oidc_provider_arn },
      Action   = "sts:AssumeRoleWithWebIdentity",
      Condition = {
        StringEquals = {
          "${local.oidc_issuer}:sub" = "system:serviceaccount:argocd:argocd-image-updater"
        }
      }
    }]
  })
}

# AWS 관리형 ECR ReadOnly (DescribeImages 등 포함)
resource "aws_iam_role_policy_attachment" "image_updater_ecr_ro" {
  role       = aws_iam_role.argocd_image_updater.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}

# (선택) GetAuthorizationToken 보강 정책
resource "aws_iam_policy" "ecr_token" {
  name   = "ECRGetAuthorizationToken-ImageUpdater"
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect   = "Allow",
      Action   = ["ecr:GetAuthorizationToken"],
      Resource = "*"
    }]
  })
}
resource "aws_iam_role_policy_attachment" "image_updater_ecr_token" {
  role       = aws_iam_role.argocd_image_updater.name
  policy_arn = aws_iam_policy.ecr_token.arn
}
 
 
resource "kubernetes_service_account_v1" "argocd_image_updater" {
  metadata {
    name      = "argocd-image-updater"
    namespace = "argocd"
    annotations = {
      "eks.amazonaws.com/role-arn" = aws_iam_role.argocd_image_updater.arn
    }
  }
}
 

요지는 ServiceAccount에 role-arn 주석을 달아 주는 것. 그렇게 해야 Pod가 그 Role로 ECR API를 호출할 수 있습니다. AWS Documentation  eksctl.io

 


3) Argo CD Image Updater 설치(Helm) + ECR 레지스트리 지정

Helm 릴리스 예시(Terraform 없이 kubectl/helm으로 해도 동일 개념):

 
resource "helm_release" "argocd_image_updater" {
  name       = "argocd-image-updater"
  namespace  = "argocd"
  repository = "https://argoproj.github.io/argo-helm"
  chart      = "argocd-image-updater"
  version    = "0.14.0" # 사용 버전 확인

  set { name = "serviceAccount.create", value = "false" }
  set { name = "serviceAccount.name",   value = kubernetes_service_account_v1.argocd_image_updater.metadata[0].name }
  set { name = "config.log.level",      value = "info" }
  set { name = "extraEnv[0].name",      value = "AWS_REGION" }
  set { name = "extraEnv[0].value",     value = "ap-northeast-2" }

  values = [<<YAML
config:
  registries:
    - name: ecr-apne2
      prefix: 768157413559.dkr.ecr.ap-northeast-2.amazonaws.com
      api_url: https://api.ecr.ap-northeast-2.amazonaws.com
      ping: true
      credentials: aws     # IRSA(AWS SDK)로 인증
YAML
  ]
}
 
 

Image Updater는 Helm/Kustomize 앱만을 표준 지원(Directory 앱은 write-back=git로 사용). 쓰는 차트/전략/허용 태그는 공식 문서 기준으로 맞추는 게 안정적입니다. argocd-image-updater.readthedocs.io  2

 


4) Argo CD Application: write-back=argocd + Helm 경로 매핑

이제 Application에 어떤 이미지를 추적하고, Helm values의 어느 키를 바꿀지를 알려줍니다.

Terraform의 kubernetes_manifest(요지):

metadata = {
  name      = "aws-pipeline-test"
  namespace = "argocd"
  annotations = {
    # 1) 추적 이미지(별칭=app)
    "argocd-image-updater.argoproj.io/image-list"          = "app=768157413559.dkr.ecr.ap-northeast-2.amazonaws.com/barun-dev-ecr"

    # 2) 업데이트 전략 + 허용 태그 (YYYYMMDD-HHMMSS)
    "argocd-image-updater.argoproj.io/app.update-strategy" = "latest"
    "argocd-image-updater.argoproj.io/app.allow-tags"      = "regexp:^[0-9]{8}-[0-9]{6}$"

    # 3) 파일 수정 X, Application 파라미터만 패치
    "argocd-image-updater.argoproj.io/write-back-method"   = "argocd"

    # 4) Helm values 경로 매핑(별칭=app)
    "argocd-image-updater.argoproj.io/app.helm.image-name" = "image.repository"
    "argocd-image-updater.argoproj.io/app.helm.image-tag"  = "image.tag"
  }
}
spec.source = {
  repoURL        = var.git_repo_url
  targetRevision = var.git_target_revision
  path           = "argocd"
  helm = {
    valueFiles = ["values.yaml"]
    parameters = [
      { name = "image.repository", value = "768157413559.dkr.ecr.ap-northeast-2.amazonaws.com/barun-dev-ecr" },
      { name = "image.tag",        value = "20250825-120000" } # 초기값
    ]
  }
}
spec.syncPolicy = {
  automated = { prune = true, selfHeal = true }
  syncOptions = ["CreateNamespace=true"]
}
 

Image Updater 동작 원리: 스캔 주기마다 ECR의 태그를 확인 → 허용 패턴과 전략에 맞는 최신 태그를 선택 → Application의 Helm 파라미터(image.tag)만 패치 → Argo CD가 자동 Sync. argocd-image-updater.readthedocs.io


5) GitHub Actions: 이미지 빌드/푸시(날짜 태그)

Image Updater를 쓰면 매니페스트를 CI에서 수정할 필요는 없지만, 태그는 불변으로 밀어야 의미가 있습니다.

 
 
name: Build & Push to ECR (date tag)
on:
  push:
    branches: [ "main" ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set DATE_TAG (KST)
        run: echo "DATE_TAG=$(TZ=Asia/Seoul date +'%Y%m%d-%H%M%S')" >> $GITHUB_ENV

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v3
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Login to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build & Push
        env:
          ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }} # 768157413559.dkr.ecr.ap-northeast-2.amazonaws.com
          ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }} # barun-dev-ecr
        run: |
          docker build -t "$ECR_REGISTRY/$ECR_REPOSITORY:${{ env.DATE_TAG }}" .
          docker push "$ECR_REGISTRY/$ECR_REPOSITORY:${{ env.DATE_TAG }}"
 

ECR 인증에는 GetAuthorizationToken 권한이 필요합니다(관리형 정책 ECR ReadOnly에 포함). AWS Documentation+1,2


6) 동작 확인 / 운영 CLI

 
# Image Updater 로그(스캔/패치 기록)
kubectl -n argocd logs deploy/argocd-image-updater -f

# 즉시 스캔 트리거
kubectl -n argocd annotate application aws-pipeline-test \
  argocd-image-updater.argoproj.io/refresh=now --overwrite

# 애플리케이션 상태 (CLI)
argocd login <ARGOCD_SERVER> --username <id> --password <pwd> --insecure
argocd app get aws-pipeline-test
argocd app history aws-pipeline-test
 
 

7) 에러 로그로 보는 원인별 빠른 처방

  • skipping app 'xxx' of type 'Directory'
    → Application이 Directory 타입이라 write-back=argocd 미지원.
    Helm/Kustomize 전환(위 가이드) 또는 write-back=git로 전환. argocd-image-updater.readthedocs.io
  • AccessDeniedException: ecr:GetAuthorizationToken
    → IRSA Role/Policy 누락 또는 SA 주석 오타. ServiceAccount 주석Role 조건의 sub(namespace/name) 재확인. AWS Documentation
  • applications=0
    → 주석이 없는 App은 후보군에 안 잡힘. Application 메타데이터의 image-list 등 어노테이션 확인. argocd-image-updater.readthedocs.io
  • Ingress가 간헐적으로 엉뚱한 규칙 순서
    group.name / group.order / listen-ports 설정 누락/충돌 점검. (ALB 컨트롤러 버전에 따라 요구사항이 다를 수 있으니 공식 어노테이션 문서 확인) kubernetes-sigs.github.ioGitHub

8) 선택지 비교: write-back argocd vs git

  • argocd: 파일 수정 없이 Application 파라미터만 패치. 간단·안정적.
    단, Helm/Kustomize 앱만 지원. (Directory는 스킵) argocd-image-updater.readthedocs.io
  • git: 레포 파일을 직접 커밋. Directory 포함 광범위 지원.
    대신 PAT/SSH 자격/권한 설정이 필요하고, 브랜치·경로 지정 필요. (예: write-back-target) argocd-image-updater.readthedocs.io

9) “불변 태그” 전략, 왜 중요할까?

  • 롤백/추적/재현 가능: 20250825-153012 같은 태그는 언제나 같은 이미지를 가리킵니다.
  • 보안/정책: 어떤 정책은 “mutable tag 금지”를 요구. :latest 남용 방지.
  • Image Updater도 latest/newest-build/semver/digest 등 전략을 제공하므로, 날짜태그와 조합하면 운영이 깔끔합니다. argocd-image-updater.readthedocs.io

10) 자주 하는 질문(FAQ)

Q. 노드 IAM Role에 ECR ReadOnly 붙여놨는데 왜 또 IRSA가 필요?
A. 노드 Role은 kubelet이 이미지를 pull할 때 쓰는 자격입니다. Image Updater는 애플리케이션 Pod이므로 해당 Pod의 SA에 권한을 줘야 ECR API 호출이 됩니다. (IRSA) AWS Documentation

Q. Helm으로 반드시 바꿔야 하나요?
A. 아니요. Kustomize도 write-back=argocd로 아주 잘 됩니다. Directory만 write-back=argocd 미지원이라 선택지가 제한되는 것뿐. argocd-image-updater.readthedocs.io

Q. ALB Ingress에서 인증서/포트/그룹 순서가 헷갈립니다.
A. AWS Load Balancer Controller 어노테이션 규격을 확인해서 버전에 맞춰 쓰세요. 특히 listen-portscertificate-arn은 필수/선호 조합이 있습니다. kubernetes-sigs.github.io


11) 마무리 체크리스트

  • ECR에 날짜 태그로 이미지 푸시
  • 레포 argocd/를 Helm 차트로 구성, Deployment.image = {{ .Values.image.* }}
  • IRSA: argocd-image-updater SA ↔ IAM Role(ECR ReadOnly + GetAuthorizationToken) 연결
  • Image Updater Helm 설치(레지스트리 프리픽스/APIs 지정)
  • Application에 image-list / update-strategy / allow-tags / write-back=argocd / helm 경로 매핑
  • SyncPolicy 자동화(on)
  • 로그/CLI로 동작 확인: kubectl logs deploy/argocd-image-updater, argocd app get …

부록 A) Directory 그대로 쓰고 싶다면 (write-back=git)

Application 주석 예시:

 
metadata:
  annotations:
    argocd-image-updater.argoproj.io/image-list: app=768157413559.dkr.ecr.ap-northeast-2.amazonaws.com/barun-dev-ecr
    argocd-image-updater.argoproj.io/app.update-strategy: latest
    argocd-image-updater.argoproj.io/app.allow-tags: regexp:^\d{8}-\d{6}$
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/git-branch: main
    argocd-image-updater.argoproj.io/git-user: ci-bot
    argocd-image-updater.argoproj.io/git-email: ci-bot@example.com
    # 필요시 write-back-target 지정(Helm/Kustomize 경로/파일 등)
 
 

이 방식은 Image Updater에 레포 쓰기 권한을 Secret로 넣어줘야 합니다.


부록 B) (대안) CI가 매니페스트를 직접 바꾸는 A안

Image Updater 없이도 CI에서 values.yaml(image.tag) 를 바꿔 커밋/푸시하면 됩니다. (선호는 Image Updater)

 
- name: Set DATE_TAG
  run: echo "DATE_TAG=$(TZ=Asia/Seoul date +'%Y%m%d-%H%M%S')" >> $GITHUB_ENV

- name: Build & Push
  run: |
    docker build -t "$ECR/$REPO:${{ env.DATE_TAG }}" .
    docker push "$ECR/$REPO:${{ env.DATE_TAG }}"

- name: Bump Helm values
  run: |
    wget -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64
    chmod +x /usr/local/bin/yq
    yq -i '.image.tag = "${{ env.DATE_TAG }}"' argocd/values.yaml
    git config user.name "ci-bot"
    git config user.email "ci@example.com"
    git add -A
    git commit -m "chore: bump image to ${{ env.DATE_TAG }}"
    git push
 
 

Reference Notes (문서 확인 포인트 요약)

 

 

 

 

 

반응형

댓글