CI/CD는 코드를 합치고, 검사하고, 운영에 내보내는 과정을 자동으로 돌리는 방식이고, GitOps는 그중 배포를 "Git에 커밋하면 클러스터가 알아서 따라오는" 방식으로 바꾼 것입니다. 이 글에서는 GitHub, Jenkins, ArgoCD로 이어지는 배포 흐름을 먼저 그림으로 보고, 로컬 Git 저장소로 배포 이력 확인, 롤백, 매니페스트 검사를 직접 해 봅니다.
1. CI와 CD를 한 줄씩
CI/CD는 한 단어처럼 쓰이지만 실제로는 세 가지를 묶은 말입니다. 팀에서 "CD 하고 있어요"라고 할 때 사람마다 뜻이 다른 경우가 많아서, 저는 아래처럼 나눠서 말합니다.
| 용어 | 한 줄 설명 | 예시 |
| CI (지속적 통합) | 코드를 자주 합치고, 합칠 때마다 자동으로 빌드와 테스트 | Pull Request를 열면 테스트가 자동으로 돌고 결과가 표시됨 |
| 지속적 전달 | 언제든 운영에 낼 수 있는 상태를 유지, 배포 시점은 사람이 결정 | 운영 배포는 승인 버튼을 눌러야 나감 |
| 지속적 배포 | 검사를 통과한 변경은 사람 개입 없이 운영까지 자동 반영 | main에 합치면 몇 분 뒤 운영에 반영됨 |
실무에서는 개발 환경은 지속적 배포, 운영 환경은 지속적 전달로 섞어 쓰는 경우가 많습니다. 저도 이 구성을 권합니다. 운영 반영 시점은 트래픽이나 공지 일정처럼 코드 밖의 사정으로 정해지는 일이 많기 때문입니다.
2. 커밋에서 운영까지의 흐름
이 시리즈에서 다루는 파이프라인은 GitHub, Jenkins, ArgoCD로 이루어집니다. 핵심은 Jenkins가 클러스터에 직접 배포하지 않는다는 점입니다. Jenkins는 이미지를 만들고 매니페스트 저장소의 이미지 태그만 바꿔 커밋하며, 클러스터 반영은 ArgoCD가 맡습니다.

윗줄이 CI, 아랫줄이 CD이고, 둘을 잇는 것은 매니페스트 저장소에 들어가는 커밋 하나입니다
그림을 순서대로 읽으면 이렇습니다.
- 개발자가 앱 저장소에 Pull Request를 합치면 GitHub가 Webhook으로 Jenkins에 알립니다.
- Jenkins가 빌드와 테스트를 하고, 컨테이너 이미지를 만들어 레지스트리에 올립니다.
- Jenkins가 매니페스트 저장소의 image 태그를 새 버전으로 바꿔 커밋합니다.
- ArgoCD가 바뀐 매니페스트를 보고 클러스터에 적용하고, 파드가 새 이미지를 받아 실행됩니다.
단계가 나뉘어 있으면 문제가 생겼을 때 볼 곳도 바로 정해집니다. 예를 들어 Jenkins 빌드는 성공했는데 운영에 반영되지 않았다면, 매니페스트 저장소에 커밋이 들어갔는지부터 보고 그다음 ArgoCD 상태를 보면 됩니다. 도구별 설치와 사용법은 아래 글에서 실습과 함께 정리했습니다.
| 도구 | 맡는 일과 실습 글 |
| GitHub | 코드와 매니페스트 보관, Pull Request 리뷰, Webhook 발송 [GitHub] 브랜치, Pull Request, Webhook 사용법 정리 |
| Jenkins | 빌드, 테스트, 이미지 생성, 매니페스트 태그 커밋 [Jenkins] Jenkins 설치와 사용법 정리 |
| ArgoCD | Git과 클러스터 비교, sync, 직접 수정 되돌리기 [ArgoCD] ArgoCD 설치와 사용법 정리 |
3. Push 방식과 Pull 방식
배포 명령을 누가 실행하느냐에 따라 두 방식으로 나뉩니다. Push 방식은 CI 서버가 마지막 단계에서 kubectl apply나 helm upgrade를 직접 실행하고, Pull 방식은 클러스터 안의 에이전트(ArgoCD)가 Git을 보고 스스로 적용합니다. GitOps는 Pull 방식입니다.

Push 방식은 클러스터 접속 권한이 CI 서버에 있고, Pull 방식은 클러스터 안의 ArgoCD가 Git을 가져와 적용합니다
| 항목 | Push 방식 | Pull 방식 |
| 클러스터 권한 | CI 서버가 가짐 | 클러스터 안에만 있음 |
| 배포 뒤 누가 직접 고치면 | 다음 배포 전까지 모름 | Git과 달라진 것이 OutOfSync로 보임 |
| 롤백 | 이전 파이프라인 재실행이나 수동 명령 | Git에서 커밋을 되돌리면 따라옴 |
저는 Pull 방식을 고르는 가장 큰 이유를 권한 위치로 봅니다. Push 방식에서는 빌드 서버가 뚫리면 운영 클러스터에 쓸 수 있는 권한까지 함께 넘어가지만, Pull 방식에서는 CI가 Git에 커밋할 권한만 있으면 됩니다.
4. 저장소를 두 개로 나누는 이유
그림 1에 GitHub 저장소가 두 개 있습니다. 앱 코드를 두는 앱 저장소와, 쿠버네티스 매니페스트(Deployment, Service 같은 YAML)를 두는 매니페스트 저장소입니다. ArgoCD 문서도 매니페스트를 앱 소스 코드와 다른 저장소에 두라고 강하게 권장합니다. 실무에서 체감되는 이유는 세 가지입니다.
| 이유 | 설명 |
| 빌드 무한 반복 방지 | CI가 태그를 앱 저장소에 커밋하면 그 커밋이 다시 빌드를 부름 |
| 배포 이력이 깔끔함 | 매니페스트 저장소의 git log가 그대로 배포 이력이 됨 |
| 권한 분리 | 개발자는 앱 저장소, 운영 설정은 매니페스트 저장소로 권한을 따로 줄 수 있음 |
두 번째 이유가 실제로 어떻게 쓰이는지는 다음 실습에서 바로 확인합니다.
5. 실습: 매니페스트 저장소로 배포 이력과 롤백 다루기
GitOps에서는 배포, 이력 확인, 롤백이 모두 Git 작업이 됩니다. 이것을 클러스터 없이 로컬 Git 저장소 두 개로 확인했습니다. Jenkins가 할 일은 스크립트로 대신했고, 실제 클러스터에서 ArgoCD가 이 커밋을 따라가는 과정은 [ArgoCD] ArgoCD 설치와 사용법 정리에서 다룹니다.
| 항목 | 내용 |
| Git | 2.43.0 |
| 앱 저장소 | app-repo, 커밋 작성자 dev-kim |
| 매니페스트 저장소 | manifest-repo, 커밋 작성자 jenkins-bot(CI 역할) |
| 매니페스트 검사 도구 | kubeconform v0.8.0 |
매니페스트 저장소에는 apps/demo-api/deployment.yaml 파일 하나만 있고, 처음 이미지 태그는 v1입니다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-api
namespace: demo
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
5.1 새 버전 반영하기
개발자가 코드를 고쳐 커밋하면, CI는 새 이미지를 만든 뒤 매니페스트의 태그만 v2로 바꿔 커밋합니다. 이때 커밋 메시지에 이미지를 만든 앱 커밋 해시를 남겨 두는 것이 요령입니다. 나중에 "지금 운영에 나간 게 어떤 코드인가"를 바로 찾을 수 있기 때문입니다.
#!/usr/bin/env bash
# 개발자가 앱 코드를 고치면 CI가 새 이미지 태그를 매니페스트에 커밋
set -e
cd /srv/cicd-lab
echo 'print("demo-api v2")' > app-repo/app.py
git -C app-repo commit -qam "fix: change response message"
APP_SHA=$(git -C app-repo rev-parse --short HEAD)
# 여기부터 CI(Jenkins)가 매니페스트 저장소에서 하는 일
cd manifest-repo
sed -i 's|demo-api:v1|demo-api:v2|' apps/demo-api/deployment.yaml
git commit -qam "demo-api: image v2 (app ${APP_SHA})"
5.2 배포 이력 확인하기
매니페스트 저장소의 로그와 마지막 변경 내용을 보고, 커밋 메시지의 앱 커밋 해시로 앱 저장소를 거슬러 올라갔습니다.
cd /srv/cicd-lab/manifest-repo
git log --format='%h %an %s'
git show --format='%h %s' HEAD -- apps/demo-api/deployment.yaml
# 커밋 메시지에 남긴 앱 커밋으로 거슬러 올라가기
APP_SHA=$(git log -1 --format=%s | sed -n 's/.*(app \([0-9a-f]*\)).*/\1/p')
git -C ../app-repo log --format='%h %an %s' -1 "$APP_SHA"
68eabd2 jenkins-bot demo-api: image v2 (app 1ef012d)
2fb4761 jenkins-bot demo-api: image v1 (app 6a28900)
68eabd2 demo-api: image v2 (app 1ef012d)
diff --git a/apps/demo-api/deployment.yaml b/apps/demo-api/deployment.yaml
index 9484870..3d0a616 100644
--- a/apps/demo-api/deployment.yaml
+++ b/apps/demo-api/deployment.yaml
@@ -15,6 +15,6 @@ spec:
spec:
containers:
- name: demo-api
- image: registry.example.internal/demo/demo-api:v1
+ image: registry.example.internal/demo/demo-api:v2
ports:
- containerPort: 8080
1ef012d dev-kim fix: change response message
출력은 세 부분으로 읽으면 됩니다. 첫 두 줄의 git log가 배포 이력이고, diff는 이번 배포에서 바뀐 것이 image 태그 한 줄뿐이라는 것을 보여 주며, 마지막 줄은 v2 이미지가 dev-kim의 "fix: change response message" 커밋에서 만들어졌다는 뜻입니다. 별도 배포 관리 도구 없이 git 명령만으로 누가, 언제, 무엇을 배포했는지 답할 수 있습니다.
5.3 롤백하기
v2에 문제가 생겼다고 가정하고 운영자(ops-kim)가 git revert로 되돌렸습니다.
cd /srv/cicd-lab/manifest-repo
git -c user.name=ops-kim -c user.email=ops@example.com revert --no-edit HEAD
git log --format='%h %an %s'
grep 'image:' apps/demo-api/deployment.yaml
[main 9ce5c67] Revert "demo-api: image v2 (app 1ef012d)"
Date: Sun Sep 27 13:40:04 2026 +0900
1 file changed, 1 insertion(+), 1 deletion(-)
9ce5c67 ops-kim Revert "demo-api: image v2 (app 1ef012d)"
68eabd2 jenkins-bot demo-api: image v2 (app 1ef012d)
2fb4761 jenkins-bot demo-api: image v1 (app 6a28900)
image: registry.example.internal/demo/demo-api:v1
git revert는 v2 커밋을 지우지 않고, 변경을 거꾸로 적용한 새 커밋을 하나 더 만듭니다. 그래서 이력에 "v1 배포, v2 배포, v2 되돌림"이 모두 남고 image는 다시 v1이 됩니다. 실제 환경에서는 이 커밋을 push하면 ArgoCD가 v1으로 다시 맞춥니다.
저는 롤백에 git reset과 강제 push는 쓰지 않습니다. 이력이 지워져 누가 무엇을 되돌렸는지 알 수 없게 되고, 같은 저장소를 쓰는 다른 사람의 작업까지 꼬일 수 있기 때문입니다. 또 ArgoCD 문서에 따르면 자동 sync가 켜진 앱에서는 ArgoCD의 롤백 기능을 쓸 수 없어서, 결국 Git에서 되돌리는 것이 표준 방법이 됩니다.
5.4 커밋 전에 매니페스트 검사하기
매니페스트가 틀리면 ArgoCD까지 가서야 실패합니다. 그래서 매니페스트 저장소에도 CI 검사를 거는 편이 좋습니다. kubeconform으로 정상 파일과, containerPort를 containerport로 잘못 쓴 파일을 함께 검사했습니다.
cd /srv/cicd-lab
./tools/kubeconform -strict -summary \
-schema-location 'tools/schemas/{{.ResourceKind}}{{.KindSuffix}}.json' \
manifest-repo/apps/demo-api/deployment.yaml deployment-bad.yaml | fold -s -w 86
echo "exit code: ${PIPESTATUS[0]}"
deployment-bad.yaml - Deployment demo-api is invalid: problem validating schema.
Check JSON formatting: jsonschema validation failed with
'file:///srv/cicd-lab/tools/schemas/deployment-apps-v1.json#' - at
'/spec/template/spec/containers/0/ports/0': missing property 'containerPort' - at
'/spec/template/spec/containers/0/ports/0': additional properties 'containerport' not
allowed
Summary: 2 resources found in 2 files - Valid: 1, Invalid: 1, Errors: 0, Skipped: 0
exit code: 1
두 파일 중 틀린 파일 하나를 정확히 짚었고, 종료 코드가 1이라 CI 단계를 실패로 만들 수 있습니다. 이 검사를 매니페스트 저장소 Pull Request의 필수 체크로 걸어 두면 잘못된 매니페스트는 main에 합쳐지지 않습니다. 필수 체크를 거는 방법은 [GitHub] 브랜치, Pull Request, Webhook 사용법 정리의 브랜치 보호 규칙에서 다룹니다.
6. 누군가 클러스터를 직접 고치면
장애 대응 중에 kubectl로 설정을 바로 고치는 일은 자주 있습니다. 이렇게 Git과 클러스터가 달라진 상태를 드리프트(drift)라고 하고, ArgoCD는 이것을 OutOfSync로 표시합니다. 여기서 많이 헷갈리는 것이 자동 sync와 selfHeal의 차이입니다.

자동 sync만 켜면 Git이 바뀔 때만 따라가고, 클러스터를 직접 고친 것까지 되돌리려면 selfHeal을 켜야 합니다
- 자동 sync: Git에 새 커밋이 생기면 따라갑니다. 클러스터를 직접 고친 것은 표시만 하고 되돌리지 않습니다.
- selfHeal: 클러스터를 직접 고친 것도 Git 상태로 되돌립니다.
selfHeal이 켜진 환경에서는 kubectl로 고쳐도 곧 원래대로 돌아가므로, 급한 수정도 매니페스트 저장소에 먼저 커밋하는 습관이 필요합니다. 두 설정을 실제로 켜고 꺼 보며 파드 수가 어떻게 바뀌는지는 [ArgoCD] ArgoCD 설치와 사용법 정리의 자동 sync와 selfHeal 실습에서 확인할 수 있습니다.
7. 정리
- CI는 합칠 때마다 자동 검사, 지속적 전달은 언제든 배포 가능한 상태, 지속적 배포는 자동 반영이고, 운영은 승인 후 반영으로 두는 구성이 현실적입니다.
- GitOps에서 Jenkins는 매니페스트 저장소에 이미지 태그를 커밋하는 데서 끝나고, 클러스터 안의 ArgoCD가 그 커밋을 가져와 적용합니다.
- 매니페스트 저장소를 따로 두면 git log가 배포 이력이 되고, 롤백은 git revert, 검사는 kubeconform 같은 도구를 Pull Request 필수 체크로 걸어 처리합니다.
참고
'DevOps > CI & CD' 카테고리의 다른 글
| [CI/CD] GitHub, Jenkins, ArgoCD로 dev/prd 배포 아키텍처 설계 (0) | 2026.09.27 |
|---|---|
| [ArgoCD] ArgoCD 설치와 사용법 정리 (0) | 2026.09.27 |
| [Jenkins] Jenkins 설치와 사용법 정리 (0) | 2026.09.27 |
| [GitHub] 브랜치, Pull Request, Webhook 사용법 정리 (0) | 2026.09.27 |