Network & Server Factory

개인 공부 기록

DevOps/CI & CD

[CI/CD] CI/CD와 GitOps 개념 정리

1nfra 2026. 9. 27. 14:48
CI는 변경을 자주 합치고 합칠 때마다 자동으로 빌드와 테스트를 돌리는 방식이고, CD는 그 결과를 언제든 운영에 내보낼 수 있게(지속적 전달) 하거나 실제로 자동 반영하는(지속적 배포) 방식입니다. GitOps는 여기서 배포 단계를 "Git에 적힌 원하는 상태를 클러스터 안의 에이전트가 가져와 계속 맞추는" 방식으로 바꾼 것입니다. 이 글에서는 GitHub, Jenkins, ArgoCD가 파이프라인의 어디에 서는지와 드리프트, 롤백을 개념 수준에서 정리합니다.

1. CI, 지속적 전달, 지속적 배포

CI/CD라는 말은 흔히 한 덩어리로 쓰이지만, 실제로는 세 가지 서로 다른 약속을 가리킵니다. 저는 용어를 Martin Fowler의 정의에 맞춰 씁니다. 도구 이름보다 "무엇을 보장하는가"로 나눠야 팀 안에서 같은 말을 다르게 이해하는 일이 줄기 때문입니다.

 

Martin Fowler는 지속적 통합(Continuous Integration)을 팀원 각자가 자기 변경을 적어도 하루에 한 번 공동 코드베이스에 합치고, 합칠 때마다 테스트를 포함한 자동 빌드로 검증하는 개발 방식이라고 설명합니다. 지속적 전달(Continuous Delivery)은 소프트웨어를 언제든 운영에 배포할 수 있는 상태로 유지하는 것이고, 지속적 배포(Continuous Deployment)는 모든 변경이 파이프라인을 통과하면 자동으로 운영에 반영되는 것입니다. 그래서 지속적 배포를 하려면 지속적 전달이 먼저 되어 있어야 합니다.

 

구분 보장하는 것 운영에서 자주 깨지는 지점
지속적 통합 합친 코드가 항상 빌드되고 테스트를 통과함 깨진 빌드를 방치해 다음 변경도 계속 실패함, Fowler는 문제 커밋을 되돌려 먼저 복구하라고 권함
지속적 전달 어떤 커밋이든 운영에 낼 수 있는 상태, 배포 시점은 사람이 결정 배포 절차 일부가 수동이라 "배포 가능"이 실제로는 아님
지속적 배포 파이프라인을 통과한 변경은 사람 개입 없이 운영 반영 테스트가 약하면 결함이 그대로 운영까지 감

 

GitOps 구성에서는 이 구분이 매니페스트 저장소에서 드러납니다. CI가 새 이미지 태그를 운영 매니페스트에 바로 커밋하고 ArgoCD가 자동 sync하면 지속적 배포이고, 운영 매니페스트 변경을 Pull Request 승인 뒤에 합치도록 두면 지속적 전달입니다. 저는 개발 환경은 자동 반영, 운영 환경은 승인 후 반영처럼 환경마다 다르게 두는 편이 현실적이라고 봅니다. 운영 반영 시점은 트래픽이나 공지 일정처럼 코드 밖의 사정에 따라 정해지는 경우가 많기 때문입니다.

2. 커밋에서 운영까지의 파이프라인

이 글과 이어지는 시리즈에서 다루는 파이프라인은 GitHub, Jenkins, ArgoCD로 이루어집니다. 개발자가 앱 저장소(애플리케이션 코드 저장소)에 코드를 합치면 GitHub이 Webhook으로 Jenkins에 알리고, Jenkins가 빌드와 테스트를 거쳐 이미지를 만들어 레지스트리에 올립니다. 그다음 Jenkins는 클러스터에 직접 배포하지 않고 매니페스트 저장소의 이미지 태그만 바꿔 커밋합니다. 클러스터 안의 ArgoCD가 이 변경을 보고 클러스터를 맞춥니다.

Jenkins의 일은 매니페스트 저장소에 커밋하는 데서 끝나고, 클러스터 반영은 ArgoCD가 맡습니다

 

단계 하는 일 멈추는 대표 원인
변경 합치기 브랜치 작업 후 Pull Request로 main에 합침 리뷰, 필수 체크 미통과
빌드 시작 GitHub Webhook이 Jenkins에 push 이벤트 전달 Webhook 주소, 서명 비밀값, 방화벽
빌드·테스트 컴파일, 단위 테스트, 정적 분석 테스트 실패, 빌드 에이전트 자원 부족
이미지 컨테이너 이미지 빌드 후 레지스트리에 push 레지스트리 인증, 저장 공간
태그 반영 매니페스트 저장소의 image 태그 수정 후 커밋 저장소 쓰기 권한, 동시 커밋 충돌
동기화 ArgoCD가 차이를 감지하고 클러스터에 적용 매니페스트 오류, 이미지 pull 실패

 

단계를 이렇게 나눠 두면 장애가 났을 때 어디를 볼지 바로 정해집니다. 예를 들어 Jenkins 빌드는 성공했는데 운영에 반영되지 않았다면 CI 쪽이 아니라 매니페스트 커밋이 실제로 들어갔는지, ArgoCD가 그 커밋을 보고 있는지부터 확인하면 됩니다.

 

이 시리즈는 그림 1의 흐름을 도구별로 나눠 설명합니다. 이론 글에서 각 도구의 구조를 정리하고, 이후 도구별 활용 글에서 실제 설정을 다룬 뒤 연동 글에서 GitHub Webhook으로 Jenkins 빌드를 시작해 매니페스트 태그를 바꾸고 ArgoCD가 sync하는 흐름을 실제로 이어 붙입니다.

 

역할 도구와 맡는 일 이어지는 글
저장소 GitHub: 앱 저장소와 매니페스트 저장소, Pull Request 검토, Webhook 발송 [GitHub] 브랜치, Pull Request, Webhook 정리
빌드 Jenkins: 빌드, 테스트, 이미지 빌드와 push, 매니페스트 태그 커밋 [Jenkins] Jenkins 구조와 파이프라인 정리
배포 ArgoCD: 매니페스트 감시, 클러스터와 비교, sync와 드리프트 처리 [ArgoCD] ArgoCD 동작 원리 정리

3. Push 방식과 Pull 방식 배포

배포 단계를 누가 실행하느냐에 따라 두 방식으로 나눌 수 있습니다. Push 방식은 CI 서버가 파이프라인 마지막에 kubectl apply나 helm upgrade를 직접 실행합니다. Pull 방식은 클러스터 안에서 동작하는 에이전트가 Git 저장소를 확인해 원하는 상태를 가져오고, 스스로 클러스터에 적용합니다. GitOps는 Pull 방식을 전제로 합니다.

Push 방식은 클러스터 자격 증명이 CI 서버에 있고, Pull 방식은 에이전트가 클러스터 안에서 Git을 가져옵니다

 

항목 Push 방식 Pull 방식
배포 실행 CI 서버가 클러스터 API에 접속해 적용 클러스터 안의 에이전트(ArgoCD)가 적용
자격 증명 CI 서버가 클러스터에 쓸 수 있는 권한을 가짐 클러스터 밖으로 쓰기 권한을 내보내지 않음
네트워크 CI 서버에서 클러스터 API로 들어가는 연결 필요 클러스터에서 Git으로 나가는 연결만 있으면 됨
배포 뒤 변경 다음 배포 전까지 알 수 없음 Git과 계속 비교해서 OutOfSync로 드러남
롤백 이전 파이프라인 재실행이나 수동 명령 Git 커밋을 되돌리면 에이전트가 따라감

 

저는 Pull 방식의 가장 큰 장점을 자격 증명의 위치로 봅니다. Push 방식에서는 빌드 서버가 뚫리면 곧바로 운영 클러스터에 쓸 수 있는 권한까지 넘어갑니다. 같은 맥락에서 ArgoCD 문서는 자동 sync를 쓰면 CI/CD 파이프라인이 ArgoCD API 서버에 직접 접근할 필요 없이 Git에 커밋하고 push하는 것만으로 배포가 된다고 설명합니다.

 

다만 Pull 방식에도 조건이 붙습니다. ArgoCD가 자기 클러스터가 아닌 다른 클러스터까지 관리하면 그 클러스터의 자격 증명은 ArgoCD가 보관하게 됩니다. 또 GitHub Webhook으로 ArgoCD를 바로 깨우려면 GitHub에서 ArgoCD API 서버로 들어오는 연결이 필요합니다. 여러 클러스터를 관리할 때는 ArgoCD가 그 클러스터의 API 서버로 접속해야 하므로, 그 방향의 연결도 필요합니다. 그래서 "Pull이면 인바운드가 전혀 필요 없다"는 말은 Webhook 없이 주기적 확인만 쓰고 ArgoCD가 자기 클러스터만 관리할 때에 한해서 맞습니다.

4. OpenGitOps의 네 가지 원칙

GitOps라는 말도 도구마다 뜻이 조금씩 달라서, CNCF 샌드박스 프로젝트인 OpenGitOps가 GitOps Principles v1.0.0으로 네 가지 원칙을 정리했습니다. 저는 이 원칙을 체크리스트로 씁니다. 원칙 하나가 빠지면 GitOps 도구를 쓰고 있어도 GitOps의 장점이 사라지기 때문입니다.

 

원칙 뜻 지키지 않으면 생기는 일
선언형 Declarative, 원하는 상태를 명령이 아니라 선언으로 적음 kubectl scale, set image 같은 명령은 Git에 남지 않아 다음 sync 때 사라짐
버전·불변 Versioned and Immutable, 원하는 상태를 변경 불가능한 이력과 함께 보관 latest 태그처럼 같은 이름이 다른 이미지를 가리키면 매니페스트가 그대로라 배포할 변경도, 이력도 생기지 않음
자동 가져오기 Pulled Automatically, 에이전트가 원하는 상태를 소스에서 자동으로 가져옴 CI가 클러스터에 직접 밀어 넣으면 Git을 거치지 않는 배포 경로가 생기고 자격 증명도 밖에 둬야 함
지속적 조정 Continuously Reconciled, 에이전트가 실제 상태를 계속 관찰하고 원하는 상태를 적용하려 함 누군가 클러스터를 직접 고쳐도 알아채지 못하고 어긋난 상태가 계속됨

 

운영에서 가장 자주 어기는 것은 두 번째 원칙입니다. 이미지 태그를 latest로 고정해 두면 새 이미지를 올려도 매니페스트에는 변화가 없어서 ArgoCD 입장에서는 배포할 것이 없습니다. 그래서 저는 이미지 태그를 빌드마다 바뀌는 값(버전이나 커밋 해시)으로 붙이고, 그 값을 매니페스트에 커밋합니다. 같은 이유로 ArgoCD 문서는 Helm이나 Kustomize의 원격 의존성도 버전을 고정하라고 권합니다. 고정하지 않으면 같은 Git 커밋이 날마다 다른 매니페스트를 만들 수 있기 때문입니다.

5. 앱 저장소와 매니페스트 저장소를 나누는 이유

그림 1에서 GitHub 저장소가 두 개인 이유가 여기에 있습니다. ArgoCD 문서는 쿠버네티스 매니페스트를 애플리케이션 소스 코드와 다른 Git 저장소에 두는 것을 강하게 권장하고, 이유를 다섯 가지로 듭니다. 제가 중요하다고 보는 순서로 정리하면 아래와 같습니다.

 

이유 설명
무한 루프 방지 CI가 매니페스트를 앱 저장소에 커밋하면 그 커밋이 다시 빌드를 부름
권한 분리 앱 저장소와 매니페스트 저장소의 커밋 권한을 따로 부여, 개발자가 운영 설정을 바로 바꾸지 못하게 함
깨끗한 이력 매니페스트 저장소 로그가 곧 배포 이력, 개발 커밋이 섞이지 않음
설정만 변경 replicas 하나 바꾸려고 전체 빌드를 돌리지 않아도 됨
여러 서비스 여러 저장소의 서비스를 한 단위로 배포할 때 둘 곳이 명확함

 

첫 번째 무한 루프는 같은 저장소에 매니페스트를 두고 CI가 태그를 커밋하게 만들면 바로 생기는 문제입니다. 한 저장소에서 막으려면 커밋 작성자나 메시지로 빌드를 건너뛰는 예외 조건을 넣어야 하는데, 이런 예외는 시간이 지나면 잊히기 쉽습니다. 저장소를 나누면 루프가 구조적으로 생기지 않습니다.

6. 드리프트와 조정 루프

드리프트(drift)는 Git에 적힌 원하는 상태와 클러스터의 실제 상태가 달라진 것을 말합니다. 장애 대응 중에 kubectl edit으로 설정을 고치거나, kubectl rollout undo로 이전 버전을 되살리는 것이 대표적인 원인입니다. ArgoCD는 두 상태를 계속 비교해서 같으면 Synced, 다르면 OutOfSync로 표시합니다.

자동 sync를 켜도 selfHeal이 꺼져 있으면 클러스터를 직접 고친 내용은 곧바로 되돌려지지 않습니다

 

여기서 자주 오해하는 부분이 있습니다. ArgoCD 문서에 따르면 자동 sync를 켜도 기본값에서는 클러스터를 직접 바꾼 것만으로 다시 sync하지 않습니다. 자동 sync는 같은 커밋에 대해 한 번만 시도하기 때문입니다. selfHeal을 켜야 실제 상태가 Git과 달라졌을 때 다시 sync하므로, "kubectl로 고쳤는데 곧 원래대로 돌아왔다"는 현상은 selfHeal이 켜져 있다는 뜻입니다.

 

selfHeal이 켜진 환경에서 장애 대응을 할 때는 순서가 중요합니다. 클러스터를 먼저 고치면 ArgoCD가 곧바로 되돌리므로, 저는 급한 조치라도 매니페스트 저장소에 먼저 커밋하거나, 불가피하면 해당 애플리케이션의 자동 sync를 잠시 끄고 조치한 뒤 같은 내용을 Git에 반영합니다. 반대로 HPA(Horizontal Pod Autoscaler)가 파드 수를 조절하는 Deployment는 ArgoCD 문서 권고대로 replicas를 매니페스트에 적지 않습니다. 적어 두면 HPA가 바꾼 값과 Git 값이 계속 어긋나서 OutOfSync가 되고, sync할 때마다 파드 수가 Git 값으로 돌아가기 때문입니다. ArgoCD의 구성 요소와 자동 sync, prune, selfHeal의 판단 규칙과 기본값은 [ArgoCD] ArgoCD 동작 원리 정리에서 이어서 다룹니다.

7. 매니페스트 저장소로 보는 배포 이력과 롤백

GitOps에서 배포 이력과 롤백이 Git 작업이 된다는 점을 로컬 저장소로 확인했습니다. 클러스터와 GitHub 없이 로컬에 앱 저장소(app-repo)와 매니페스트 저장소(manifest-repo)를 만들고, Jenkins가 할 일을 스크립트로 대신했습니다. 매니페스트는 아래 Deployment 하나입니다.

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

 

두 저장소를 만들고 v1 매니페스트를 커밋합니다. 매니페스트 커밋 메시지에는 이미지가 만들어진 앱 커밋 해시를 남겼습니다.

#!/usr/bin/env bash
# app-repo: 앱 코드, manifest-repo: 쿠버네티스 매니페스트
set -e
cd /srv/cicd-lab && rm -rf app-repo manifest-repo

git init -q -b main app-repo
git -C app-repo config user.name dev-kim
git -C app-repo config user.email dev@example.com
echo 'print("demo-api v1")' > app-repo/app.py
git -C app-repo add . && git -C app-repo commit -qm "feat: first release"

git init -q -b main manifest-repo
git -C manifest-repo config user.name jenkins-bot
git -C manifest-repo config user.email ci@example.com
mkdir -p manifest-repo/apps/demo-api
cp deployment.yaml manifest-repo/apps/demo-api/
APP_SHA=$(git -C app-repo rev-parse --short HEAD)
git -C manifest-repo add .
git -C manifest-repo commit -qm "demo-api: image v1 (app ${APP_SHA})"

 

개발자가 코드를 고치면 CI는 새 이미지를 만든 뒤 매니페스트의 태그만 v2로 바꿔 커밋합니다. 실제 파이프라인에서는 이 부분이 Jenkins 파이프라인의 마지막 단계가 됩니다.

#!/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})"

 

이제 매니페스트 저장소의 로그와 마지막 커밋의 변경 내용을 보고, 커밋 메시지의 앱 커밋 해시로 어떤 코드가 배포되었는지 거슬러 올라갔습니다.

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

 

로그 두 줄이 그대로 배포 이력이고, diff는 무엇이 바뀌었는지를 한 줄로 보여 줍니다. 커밋 메시지에 앱 커밋 해시를 남겨 두어서 "지금 운영에 나간 이미지가 어떤 코드인가"에도 바로 답할 수 있습니다. 저는 이 추적성을 GitOps의 가장 실용적인 이점으로 봅니다.

 

v2에 문제가 생겼다고 가정하고 운영자가 되돌렸습니다.

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는 이전 커밋을 지우지 않고, 변경을 거꾸로 적용한 새 커밋을 만듭니다. 그래서 누가 언제 무엇을 되돌렸는지가 이력에 남고, image는 다시 v1이 됩니다. 실제 환경이라면 이 커밋을 push한 뒤 ArgoCD가 다음 Git 확인 때(Webhook을 받으면 바로) 차이를 감지하고, 자동 sync가 켜져 있으면 v1으로 sync합니다. ArgoCD 문서도 자동 sync가 켜진 애플리케이션에는 ArgoCD 자체의 롤백 기능을 쓸 수 없다고 설명하므로, 자동 sync 환경의 롤백은 Git 되돌리기가 사실상 표준 방법입니다. git reset으로 이력을 지우고 강제 push하는 방법은 다른 사람의 작업과 감사 이력을 함께 지우므로 저는 쓰지 않습니다.

 

매니페스트 저장소에도 CI 검사를 두면 잘못된 매니페스트가 ArgoCD까지 가기 전에 걸러집니다. kubeconform으로 정상 매니페스트와 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이 나오므로, 이 검사를 매니페스트 저장소의 Pull Request 필수 체크로 걸어 두면 병합 전에 막을 수 있습니다. 원하는 상태가 선언형 파일로 있기 때문에 가능한 검사입니다.

8. 정리

  • 지속적 통합은 자주 합치고 자동으로 검증하는 것, 지속적 전달은 언제든 배포 가능한 상태, 지속적 배포는 통과한 변경의 자동 반영입니다.
  • GitOps는 클러스터 안의 에이전트가 Git에 선언한 상태를 가져와 계속 맞추는 Pull 방식이고, 이미지 태그는 latest 대신 빌드마다 바뀌는 값을 커밋합니다.
  • 매니페스트 저장소를 앱 저장소와 나누면 그 로그가 곧 배포 이력이 되고, 자동 sync 환경에서는 변경과 롤백을 kubectl 대신 Git 커밋과 git revert로 합니다.

참고

728x90
서울
--:--:--
-전체 글
-카테고리
오늘 방문