ArgoCD는 Git에 적어 둔 매니페스트대로 쿠버네티스 클러스터를 맞춰 주는 배포 도구입니다. 이 글에서는 k3s 클러스터에 ArgoCD v3.5.3을 설치하고, 앱 등록, 첫 배포, 새 버전 배포, 롤백까지 직접 해 보면서 꼭 알아야 할 개념만 정리합니다.
1. ArgoCD가 하는 일
[CI/CD] CI/CD와 GitOps 개념 정리에서 정리한 것처럼, GitOps에서는 배포하고 싶은 상태를 Git에 커밋하면 클러스터 안의 에이전트가 그 상태를 가져와 적용합니다. 이 에이전트가 ArgoCD입니다. 그래서 ArgoCD를 쓰면 배포 명령을 직접 치는 대신 Git에 커밋하는 것이 배포가 됩니다.
처음 쓸 때 알아 둘 용어는 네 가지입니다. 화면과 CLI 출력에 계속 나오기 때문에 이것만 알면 나머지는 써 보면서 익힐 수 있습니다.
| 용어 | 뜻 |
| Application | ArgoCD의 배포 단위, "어느 Git 경로를 어느 클러스터와 네임스페이스에 배포할지"를 적은 설정 |
| Sync | Git에 적힌 상태를 클러스터에 적용하는 작업 |
| Sync Status | Git과 클러스터가 같으면 Synced, 다르면 OutOfSync |
| Health | 배포된 리소스가 실제로 잘 동작하는지, Healthy, Progressing, Degraded 등 |
ArgoCD는 파드 여러 개로 설치되지만, 역할은 크게 세 가지로 보면 됩니다. API Server는 웹 화면과 CLI 요청을 받고, Repo Server는 Git에서 매니페스트를 가져오며, Application Controller는 Git과 클러스터를 비교해서 sync를 실행합니다.

Application Controller가 Repo Server에서 받은 매니페스트와 클러스터 상태를 비교하고, API Server는 사용자와 Webhook 요청을 받습니다
2. 실습 환경
| 항목 | 내용 |
| 클러스터 | k3s v1.36.4, 노드 1대 |
| ArgoCD | v3.5.3, 공식 install.yaml로 설치 |
| 매니페스트 저장소 | 클러스터 안에 띄운 Git 서버, git://git-server.git.svc.cluster.local/manifest-repo.git |
| 예제 앱 | demo-api, 접속하면 "demo-api 버전"을 돌려주는 웹 서버 |
실습 환경은 외부 컨테이너 레지스트리에 접속할 수 없어서, ArgoCD 이미지는 GitHub 릴리스의 공식 바이너리로 같은 이름의 이미지를 만들어 넣었습니다. 실제 환경이라면 이 과정 없이 아래 설치 명령만 실행하면 됩니다. 매니페스트 저장소에는 Deployment와 Service 파일 두 개만 있습니다.
manifest-repo/
└── apps/
└── demo-api/
├── deployment.yaml
└── service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-api
spec:
replicas: 2
selector:
matchLabels:
app: demo-api
template:
metadata:
labels:
app: demo-api
spec:
containers:
- name: demo-api
image: registry.example.internal/demo/demo-api:v1
ports:
- containerPort: 8080
3. 설치와 로그인
3.1 설치
ArgoCD 문서의 설치 방법 그대로 argocd 네임스페이스를 만들고 install.yaml을 적용합니다. CRD가 커서 server-side apply를 씁니다.
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts \
-f https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.3/manifests/install.yaml \
| tail -n 4
namespace/argocd created
networkpolicy.networking.k8s.io/argocd-notifications-controller-network-policy serverside-applied
networkpolicy.networking.k8s.io/argocd-redis-network-policy serverside-applied
networkpolicy.networking.k8s.io/argocd-repo-server-network-policy serverside-applied
networkpolicy.networking.k8s.io/argocd-server-network-policy serverside-applied
Dex는 GitHub나 사내 계정으로 로그인(SSO)할 때 쓰는 구성 요소입니다. SSO를 붙이기 전이라면 0개로 줄여 두는 편을 권합니다. 쓰지 않는 파드가 떠 있으면 문제가 생겼을 때 볼 곳만 늘어나기 때문입니다.
# SSO(Dex)를 쓰지 않으므로 dex-server는 0개로
kubectl -n argocd scale deployment argocd-dex-server --replicas=0
kubectl -n argocd wait --for=delete pod \
-l app.kubernetes.io/name=argocd-dex-server --timeout=60s
kubectl -n argocd get pods
deployment.apps/argocd-dex-server scaled
NAME READY STATUS RESTARTS AGE
argocd-application-controller-0 1/1 Running 0 2m24s
argocd-applicationset-controller-84549767db-b7kcd 1/1 Running 0 2m26s
argocd-notifications-controller-57d4c66f69-5tl8m 1/1 Running 0 2m26s
argocd-redis-c55679569-ld62m 1/1 Running 0 2m26s
argocd-repo-server-7f9fdfbb74-q9rdm 1/1 Running 0 2m26s
argocd-server-5f785dd555-gppzr 1/1 Running 0 2m26s
3.2 CLI 로그인
argocd CLI는 GitHub 릴리스에서 받습니다. 처음에는 admin 계정 하나만 있고, 초기 비밀번호는 argocd-initial-admin-secret에 들어 있습니다. argocd-server를 외부에 열지 않았으니 port-forward로 연결했습니다.
# 내 PC의 8080 포트를 argocd-server의 443 포트로 연결
kubectl -n argocd port-forward svc/argocd-server 8080:443 >/dev/null 2>&1 &
sleep 3
# 설치 때 만들어진 admin 초기 비밀번호로 로그인(비밀번호는 화면에 출력하지 않음)
PASS=$(argocd admin initial-password -n argocd | head -1)
argocd login localhost:8080 --username admin --password "$PASS" --insecure
argocd account get-user-info
'admin:login' logged in successfully
Context 'localhost:8080' updated
Logged In: true
Username: admin
Issuer: argocd
Groups:
웹 화면도 같은 주소(https://localhost:8080)로 열립니다. 로그인한 뒤에는 admin 비밀번호를 바꾸고 argocd-initial-admin-secret은 지우는 것이 좋습니다. ArgoCD 문서도 비밀번호를 바꾼 뒤 이 시크릿을 지우라고 안내합니다.
4. 앱 등록과 첫 배포
4.1 Application 만들기
Application은 웹 화면에서 만들 수도 있지만, 저는 YAML로 만들어 Git에 같이 보관하는 방식을 권합니다. 화면에서 만든 설정은 누가 언제 바꿨는지 Git에 남지 않기 때문입니다. 처음에는 자동 sync 없이 수동으로 만들었습니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo-api
namespace: argocd
spec:
project: default
source:
repoURL: git://git-server.git.svc.cluster.local/manifest-repo.git
path: apps/demo-api
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: demo
syncPolicy:
syncOptions:
- CreateNamespace=true
| 필드 | 의미 |
| source.repoURL, path | 가져올 Git 저장소와 그 안의 디렉터리 |
| source.targetRevision | 따라갈 브랜치, main이면 main의 최신 커밋을 따라감 |
| destination | 배포할 클러스터와 네임스페이스, kubernetes.default.svc는 ArgoCD가 설치된 클러스터 |
| CreateNamespace=true | 대상 네임스페이스(demo)가 없으면 만들어 줌 |
kubectl apply -f app.yaml
sleep 10
argocd app get demo-api
application.argoproj.io/demo-api created
Name: argocd/demo-api
Project: default
Server: https://kubernetes.default.svc
Namespace: demo
URL: https://localhost:8080/applications/demo-api
Source:
- Repo: git://git-server.git.svc.cluster.local/manifest-repo.git
Target: main
Path: apps/demo-api
SyncWindow: Sync Allowed
Sync Policy: Manual
Sync Status: OutOfSync from main (0d5dbcc)
Health Status: Missing
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service demo demo-api OutOfSync Missing
apps Deployment demo demo-api OutOfSync Missing
Git에는 Deployment와 Service가 있는데 클러스터에는 아직 없으니 OutOfSync, Health는 Missing입니다. 자동 sync를 켜지 않았기 때문에 ArgoCD는 차이를 보여 주기만 하고 적용하지는 않습니다. 웹 화면(https://localhost:8080)에서도 같은 상태가 노란색으로 표시됩니다.

Application을 만든 직후의 화면으로, Service와 Deployment가 아직 클러스터에 없어 OutOfSync와 Missing으로 표시됩니다
4.2 첫 sync
argocd app sync demo-api > /dev/null
argocd app wait demo-api --health --timeout 120 > /dev/null
argocd app get demo-api | sed -n '/^Sync Status/,$p'
echo
kubectl -n demo get pods
# 서비스에 요청해서 실제로 뜬 버전 확인
kubectl -n demo port-forward svc/demo-api 9090:80 >/dev/null 2>&1 & PF=$!
sleep 2; curl -s localhost:9090; kill $PF
Sync Status: Synced to main (0d5dbcc)
Health Status: Healthy
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Namespace demo Running Synced namespace/demo created
Service demo demo-api Synced Healthy service/demo-api created
apps Deployment demo demo-api Synced Healthy deployment.apps/demo-api created
NAME READY STATUS RESTARTS AGE
demo-api-7c45584c69-89qz6 1/1 Running 0 1s
demo-api-7c45584c69-tc4qq 1/1 Running 0 1s
demo-api v1
sync 한 번으로 네임스페이스, Service, Deployment가 만들어지고, 서비스에 요청하면 v1이 응답합니다. 웹 화면에서는 Application 아래로 Service, Deployment, ReplicaSet, 파드가 트리로 이어집니다.

sync 후 화면으로, 하트는 Health(Healthy), 체크는 Sync 상태(Synced)이고 파드 2개가 떠 있습니다
5. 새 버전 배포
이제 CI가 하는 일을 흉내 내서, 매니페스트 저장소의 이미지 태그만 v2로 바꿔 커밋했습니다. 실제 파이프라인에서는 Jenkins가 이미지를 빌드한 뒤 이 커밋을 합니다.
# CI가 할 일: 매니페스트 저장소의 이미지 태그를 v2로 바꿔 커밋하고 push
cd /srv/devops-lab/work/manifest-repo
sed -i 's|demo-api:v1|demo-api:v2|' apps/demo-api/deployment.yaml
git commit -qam "demo-api: image v2"
git push -q origin main
# ArgoCD에 바로 확인하라고 요청(Webhook이 없으면 최대 3분 뒤에 알아챔)
argocd app get demo-api --refresh | grep -E '^(Sync|Health) Status'
argocd app diff demo-api
Sync Status: OutOfSync from main (f9bece0)
Health Status: Healthy
===== apps/Deployment demo/demo-api ======
116c116
< - image: registry.example.internal/demo/demo-api:v1
---
> - image: registry.example.internal/demo/demo-api:v2
ArgoCD는 기본 설정에서 최대 3분 간격으로 Git을 확인합니다. 그래서 커밋 직후에는 아무 변화가 없을 수 있고, --refresh로 바로 확인하게 하거나 GitHub Webhook을 연결하면 기다리지 않아도 됩니다. argocd app diff로 무엇이 바뀌는지 먼저 보고 sync했습니다.
argocd app sync demo-api > /dev/null
argocd app wait demo-api --health --timeout 120 > /dev/null
argocd app get demo-api | grep -E '^(Sync|Health) Status'
argocd app history demo-api
kubectl -n demo port-forward svc/demo-api 9090:80 >/dev/null 2>&1 & PF=$!
sleep 2; curl -s localhost:9090; kill $PF
Sync Status: Synced to main (f9bece0)
Health Status: Healthy
SOURCE git://git-server.git.svc.cluster.local/manifest-repo.git
ID DATE REVISION
0 2026-09-27 15:38:11 +0900 KST main (0d5dbcc)
1 2026-09-27 15:38:33 +0900 KST main (f9bece0)
demo-api v2
history에 배포한 커밋이 순서대로 남고, 응답도 v2로 바뀌었습니다. kubectl 명령은 한 번도 쓰지 않고 Git 커밋만으로 배포가 끝났습니다.
6. 자동 sync와 selfHeal
매번 sync를 누르지 않으려면 자동 sync를 켭니다. 여기서 자주 헷갈리는 것이 selfHeal입니다. 자동 sync는 Git이 바뀌었을 때 따라가는 기능이고, 누군가 클러스터를 직접 고친 것까지 되돌리는 것은 selfHeal입니다. 둘의 차이를 보려고 파드 수를 kubectl로 5개로 늘려 보았습니다.
# 1) 자동 sync만 켜기(selfHeal 없음)
argocd app set demo-api --sync-policy automated
kubectl -n demo scale deployment demo-api --replicas=5
sleep 20
echo "[selfHeal 꺼짐]"
argocd app get demo-api | grep -E '^Sync Status'
kubectl -n demo get deployment demo-api
# 2) selfHeal 켜기
argocd app set demo-api --self-heal
sleep 20
echo "[selfHeal 켜짐]"
argocd app get demo-api | grep -E '^Sync Status'
kubectl -n demo get deployment demo-api
deployment.apps/demo-api scaled
[selfHeal 꺼짐]
Sync Status: OutOfSync from main (f9bece0)
NAME READY UP-TO-DATE AVAILABLE AGE
demo-api 5/5 5 5 46s
[selfHeal 켜짐]
Sync Status: Synced to main (f9bece0)
NAME READY UP-TO-DATE AVAILABLE AGE
demo-api 2/2 2 2 67s
자동 sync만 켰을 때는 OutOfSync로 표시만 하고 파드 5개가 그대로 남았습니다. selfHeal을 켜자 Git에 적힌 2개로 돌아왔습니다.

자동 sync를 켜도 selfHeal이 꺼져 있으면 클러스터를 직접 고친 내용은 곧바로 되돌려지지 않습니다
자주 쓰는 설정은 아래 세 가지이고, 모두 기본값은 꺼져 있습니다.
| 설정 | 켜면 | 주의할 점 |
| 자동 sync | Git에 새 커밋이 생기면 알아서 sync | 운영 환경은 PR 승인 뒤 병합하는 규칙과 함께 쓰기 |
| selfHeal | kubectl로 바꾼 것도 Git 상태로 되돌림 | 장애 때 급하게 고친 값도 되돌아가므로 Git에 먼저 반영 |
| prune | Git에서 지운 리소스를 클러스터에서도 삭제 | PVC처럼 지워지면 안 되는 리소스는 따로 보호 |
저는 개발 환경은 세 가지를 모두 켜고, 운영 환경은 자동 sync와 selfHeal까지만 켠 뒤 prune은 리소스 정리가 필요할 때 수동으로 하는 구성을 권합니다. 삭제는 되돌리기 어려운 작업이라 한 번 더 확인하는 편이 안전하기 때문입니다.
7. 롤백
ArgoCD에도 롤백 명령이 있지만, 자동 sync가 켜진 앱에서는 동작하지 않습니다. 실제로 실행하면 거부되고, Git에서 되돌리면 그대로 따라옵니다.
# 자동 sync가 켜진 앱에서 ArgoCD 롤백 시도
argocd app rollback demo-api 0
# Git에서 되돌리기
cd /srv/devops-lab/work/manifest-repo
git -c user.name=ops-kim -c user.email=ops@example.com revert --no-edit HEAD > /dev/null
git push -q origin main
git log --format='%h %an %s'
argocd app get demo-api --refresh > /dev/null
argocd app wait demo-api --sync --health --timeout 120 > /dev/null
argocd app get demo-api | grep -E '^(Sync|Health) Status'
kubectl -n demo port-forward svc/demo-api 9090:80 >/dev/null 2>&1 & PF=$!
sleep 2; curl -s localhost:9090; kill $PF
{"level":"fatal","msg":"rpc error: code = FailedPrecondition desc = rollback cannot be initiated when auto-sync is enabled","time":"2026-09-27T15:39:18+09:00"}
531922d ops-kim Revert "demo-api: image v2"
f9bece0 jenkins-bot demo-api: image v2
0d5dbcc jenkins-bot demo-api: image v1
Sync Status: Synced to main (531922d)
Health Status: Healthy
demo-api v1
이렇게 막혀 있는 이유는 간단합니다. 자동 sync는 항상 Git의 최신 커밋을 따라가므로, ArgoCD에서만 이전 버전을 적용해도 곧 다시 새 버전으로 sync됩니다. 그래서 자동 sync 환경의 롤백은 git revert로 하고, 누가 언제 되돌렸는지도 Git 이력에 그대로 남습니다.
8. 자주 막히는 지점
가장 흔한 실수를 하나 재현했습니다. 레지스트리에 없는 이미지 태그(v9)를 커밋한 경우입니다.
# 레지스트리에 없는 태그(v9)를 실수로 커밋
cd /srv/devops-lab/work/manifest-repo
sed -i 's|demo-api:v1|demo-api:v9|' apps/demo-api/deployment.yaml
git commit -qam "demo-api: image v9" && git push -q origin main
argocd app get demo-api --refresh > /dev/null
sleep 40
argocd app get demo-api | grep -E '^(Sync|Health) Status'
kubectl -n demo get pods
Sync Status: Synced to main (ae48fed)
Health Status: Progressing
NAME READY STATUS RESTARTS AGE
demo-api-6f67b98457-fkmsx 0/1 ErrImagePull 0 40s
demo-api-7c45584c69-7cbjj 1/1 Running 0 54s
demo-api-7c45584c69-pk57t 1/1 Running 0 55s

Sync는 성공(Synced, Sync OK)했지만 새 파드가 이미지를 받지 못해 Health가 Progressing에 머물고, 기존 파드 2개는 계속 실행 중입니다
Sync Status는 Synced입니다. Git에 적힌 대로 적용하는 데는 성공했기 때문입니다. 대신 Health가 Progressing에 머물고 새 파드는 이미지를 받지 못합니다. 다행히 이 예제처럼 레플리카가 2개인 Deployment는 기본 롤링 업데이트 설정에서 새 파드가 준비될 때까지 기존 파드를 지우지 않아서, v1 파드 2개가 계속 요청을 받습니다. 이때 git revert 한 번으로 정상으로 돌아옵니다.
cd /srv/devops-lab/work/manifest-repo
git -c user.name=ops-kim -c user.email=ops@example.com revert --no-edit HEAD > /dev/null
git push -q origin main
argocd app get demo-api --refresh > /dev/null
argocd app wait demo-api --sync --health --timeout 120 > /dev/null
argocd app get demo-api | grep -E '^(Sync|Health) Status'
kubectl -n demo get pods
Sync Status: Synced to main (e0a084d)
Health Status: Healthy
NAME READY STATUS RESTARTS AGE
demo-api-6f67b98457-fkmsx 0/1 Terminating 0 50s
demo-api-7c45584c69-7cbjj 1/1 Running 0 64s
demo-api-7c45584c69-pk57t 1/1 Running 0 65s
그래서 배포가 끝났는지 볼 때는 Synced만 보지 말고 Health까지 봐야 합니다. CI에서 배포 완료를 기다린다면 argocd app wait에 --health를 붙입니다.
| 증상 | 원인 | 확인과 해결 |
| 커밋했는데 반영이 안 됨 | 최대 3분 간격 확인, 또는 자동 sync가 꺼짐 | argocd app get --refresh, Webhook 연결, syncPolicy 확인 |
| Synced인데 Progressing, Degraded | 이미지 태그 오류, 설정값 오류로 파드가 뜨지 않음 | kubectl get pods, describe로 이벤트 확인 후 Git에서 수정 |
| kubectl로 고친 값이 되돌아감 | selfHeal이 켜져 있음 | 같은 변경을 Git에 커밋 |
| rollback이 거부됨 | 자동 sync가 켜져 있음 | git revert로 되돌리기 |
| ComparisonError, failed to list refs | repoURL 오타, 인증 정보 누락, 지원하지 않는 Git 서버 방식 | argocd app get의 CONDITION 메시지로 저장소 주소와 인증 확인 |
표의 마지막 줄은 실습 중에 실제로 겪은 오류입니다. 처음에 Git 서버를 정적 파일만 내려주는 HTTP 서버로 띄웠더니 Application을 만들자마자 "failed to list refs: unexpected EOF"로 비교에 실패했고, git 프로토콜을 지원하는 서버로 바꾸자 해결되었습니다. 저장소 연결 문제는 앱이 아니라 저장소 쪽부터 확인하면 빨리 찾을 수 있습니다.
9. 정리
- ArgoCD는 Git에 적힌 상태와 클러스터를 비교해서 맞춰 주는 도구이고, Application에 저장소 경로와 배포 대상만 적으면 커밋이 곧 배포가 됩니다.
- 자동 sync는 Git 변경을 따라가고 selfHeal은 클러스터 직접 수정까지 되돌리므로, 급한 수정도 Git에 먼저 커밋하고 롤백은 git revert로 합니다.
- 배포 확인은 Synced만이 아니라 Health까지 보고, 반영이 늦으면 --refresh나 Webhook, 비교 오류는 저장소 연결부터 확인합니다.
다음 글에서는 Jenkins가 이미지를 빌드하고 매니페스트 저장소에 태그를 커밋하면, 이 글의 ArgoCD가 이어서 배포하는 흐름을 연결합니다.
참고
'DevOps > CI & CD' 카테고리의 다른 글
| [Jenkins] Jenkins 설치와 사용법 정리 (0) | 2026.09.27 |
|---|---|
| [GitHub] 브랜치, Pull Request, Webhook 사용법 정리 (0) | 2026.09.27 |
| [CI/CD] CI/CD와 GitOps 개념 정리 (0) | 2026.09.27 |