— 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 Pod가 ECR 조회 권한을 갖도록 ServiceAccount ↔ IAM Role 연결
Image Updater는 Helm/Kustomize 앱만 write-back=argocd로 패치할 수 있습니다. 순수 Directory(Plain YAML)는 write-back=git(레포에 커밋)로도 가능하지만 설정이 조금 더 복잡합니다. argocd-image-updater.readthedocs.io
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 Pod가 ECR에 DescribeImages/GetAuthorizationToken을 호출해야 새 태그를 스캔할 수 있습니다. 이 권한은 노드 Role이 아니라 해당 Pod의 ServiceAccount에 IRSA로 부여합니다. (보안상 최소권한 원칙)
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-ports와 certificate-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 (문서 확인 포인트 요약)
- Image Updater 지원 범위/전략/쓰기 방법: Helm/Kustomize 대상, write-back=argocd/git 지원, 최신/semver/digest 전략 등. argocd-image-updater.readthedocs.io, 2, 3
- IRSA: ServiceAccount에 eks.amazonaws.com/role-arn 주석, OIDC 연동, Pod가 AWS SDK로 권한 받아 API 호출. AWS Documentation , eksctl.io
- ECR 권한: ecr:GetAuthorizationToken 필요, 관리형 정책 사용. AWS Documentation
- AWS Load Balancer Controller 어노테이션: listen-ports, certificate-arn 등 버전별 규격. kubernetes-sigs.github.io
'K8S' 카테고리의 다른 글
| [kubernetes] kind 명령어 정리 (0) | 2025.09.06 |
|---|---|
| [kubernetes] kind 설치 및 간단한 실행방법 (0) | 2025.09.06 |
| 일반 서비스 (echo-service)와 Ingress Controller의 차이 (0) | 2025.08.07 |
| kubernetes scale down 시 연결 끊기는 현상(Race Condition) (0) | 2025.07.17 |
| [kubernetes] API groups 를 활용하여 리소스 제어 (0) | 2025.03.04 |
댓글