WIKIDevOps와 CI/CD
플레이북

DevOps와 CI/CD

커밋 한 줄이 돌아가는 서버가 되기까지, 사람 손을 빼는 일.

DevOpsCI/CDJenkinsHelmArgoCDKubernetesGitOps인프라 기초
인프라백엔드 입문7분

한눈에

  • CI = 만들고 검증한다 (끝나는 지점은 '배포 가능한 이미지가 나왔을 때')
  • CD = 그걸 실제 환경에 올린다 (끝나는 지점은 '사용자가 쓸 때')
  • Jenkins는 클러스터에 접속하지 않는다. 이미지를 창고에 넣고 values.yaml의 태그 한 줄을 고치는 데서 끝
  • Helm = 쿠버 설정의 템플릿. 모양(차트)과 값(values)을 갈라 복붙을 없앤다
  • ArgoCD = 버튼이 아니라 감시자. Git과 클러스터를 대조해 차이를 메운다 (GitOps)
  • 배포를 촉발하는 건 이미지가 아니라 Git에 커밋된 태그 한 줄. 그래서 되돌리기가 git revert

도구 이름을 외우면 오래 못 간다. 왜 이런 구조가 생겼는지를 따라가면 각 도구가 무엇을 대신해 주는지가 저절로 읽힌다. 그래서 문제부터 시작한다.

이 문서는 전체 지도다. 도구 하나하나는 각론으로 따로 있다. Docker · Kubernetes · Helm · Jenkins · ArgoCD.

배포를 손으로 하면 어디서 터지나

빌드해서 서버에 올리는 일을 손으로 한다고 해 보자.

코드 받기
빌드
내 PC에서
jar 올리기
scp · FTP
옛 프로세스 죽이기
새로 띄우기

실제로 터지는 건 이런 것들이다.

빌드 — 내 PC 자바는 17인데 서버는 11이다
jar 올리기 — 어제 것을 덮어썼는데 되돌릴 게 없다
죽이고 띄우기 — 그 사이에 서비스가 끊긴다
띄우기 — 서버 환경변수를 나만 알고 있다
확인 — 안 뜨는데 로그가 어디 있는지 모른다

그리고 서버가 열 대면 이걸 열 번 한다. 서비스가 다섯이면 쉰 번이다. 아래 도구들은 전부 이 목록의 한 줄씩을 없애려고 나왔다. 그것 말고 다른 목적이 없다.

DevOps는 도구 이름이 아니다

개발(Development)과 운영(Operations)을 한 흐름으로 묶는 것이다.

예전에는 개발팀이 jar를 만들어 운영팀에 넘겼다. 넘기는 순간 책임이 갈리고, 문제가 나면 "코드가 이상하다"와 "서버가 이상하다"가 부딪힌다. 넘기는 행위 자체를 없애면 그 다툼이 사라진다.

CI와 CD를 가르는 선

하는 일끝나는 지점
CI (Continuous Integration)코드를 합치고 만들고 검증한다배포 가능한 이미지가 나온 시점
CD (Continuous Deployment)그 물건을 실제 환경에 올린다사용자가 쓰는 시점

선은 "물건이 만들어졌는가"다. CI의 결과물은 컨테이너 이미지 하나, CD의 결과물은 그 이미지로 도는 Pod이다.

커밋 한 줄이 Pod이 되기까지

Jenkins의 출력은 둘로 갈라지고, 클러스터에서 다시 합쳐진다
Jenkins의 출력은 둘로 갈라지고, 클러스터에서 다시 합쳐진다

이 그림의 요지는 Jenkins 다음이 두 갈래라는 것이다.

  • 앱 이미지는 Container Registry로 간다. 창고다
  • 태그 한 줄은 manifest 레포로 간다. values.yamltag: 를 새 값으로 고쳐 커밋한다

그리고 아래에서 다시 만난다. ArgoCD가 manifest를 읽어 클러스터를 맞추고, 쿠버네티스가 그 이미지를 창고에서 당겨 온다.

오해 1. Jenkins는 배포하지 않는다

가장 많이 어긋나는 지점이다. "젠킨스가 dev 서버에 배포한다"고 기억하기 쉬운데, Jenkins는 만드는 곳까지다.

이미지를 만들어 창고에 넣고, values.yaml 한 줄을 고쳐 커밋하고 끝난다. 클러스터에 손을 대지 않는다. 애초에 클러스터 접속 정보를 안 갖고 있다. 폐쇄망 보안 심사에서는 이게 오히려 장점이다.

kubectl apply를 하는 것은 ArgoCD다. 파이프라인을 어떻게 토막 내고 태그를 어떻게 짓는지는 Jenkins 노트에 있다.

컨테이너 이미지 — 통째로 얼리기

jar 하나만 옮기면 그 서버에 자바가 깔려 있어야 한다. 그래서 런타임과 라이브러리와 설정까지 통째로 한 파일로 굳힌다. 그것이 컨테이너 이미지다.

base 이미지 (자바 17이 깔린 리눅스)
   +  내 jar
   =  앱 이미지

결정적인 성질이 불변(immutable) 이라는 것이다. 한 번 만들면 안 바뀐다. 그래서 개발에서 돈 이미지와 운영에서 도는 이미지가 같음이 보장된다.

태그가 곧 버전이다. main-250827-79f1 같은 형태를 쓰면 브랜치·날짜·커밋이 한 줄에 들어가서, 지금 도는 서버가 어느 코드에서 나왔는지 역추적된다.

Java 하던 사람 기준으로 다리를 놓으면, java -jar app.jardocker run 이미지다. 차이는 하나다. JAR는 서버에 JDK가 있어야 돌지만, 이미지는 런타임까지 안에 들어 있어서 받는 쪽에 아무것도 미리 깔 필요가 없다.

더 자세한 건 Docker 노트에 있다.

Helm — 모양과 값을 가른다

서비스 하나를 쿠버에 올리려면 YAML이 여러 장 필요하다. Deployment · Service · Ingress · ConfigMap · HPA. 서비스가 다섯이면 그게 다섯 벌인데, 서로 다른 건 이름과 숫자 몇 개뿐이다.

Helm은 그걸 둘로 가른다.

# templates/deployment.yaml   모양. 값이 빈칸이다
spec:
  replicas: {{ .Values.replicaCount }}
  containers:
    - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
      resources:
        {{- toYaml .Values.resources | nindent 12 }}
# values.yaml   값. 이 파일만 환경마다 다르다
replicaCount: 3
image:
  tag: "main-250827-79f1"
resources:
  requests: { cpu: 500m, memory: 1Gi }
  limits:   { cpu: "2",  memory: 4Gi }
차트 한 벌에 values 여러 벌. 같은 이미지에 다른 숫자를 얹는다
차트 한 벌에 values 여러 벌. 같은 이미지에 다른 숫자를 얹는다

Pod을 몇 개 띄울지, CPU와 메모리를 얼마나 줄지가 전부 이 파일 안에 있다. 인프라 설정을 안다는 것은 결국 이 숫자에 근거를 댈 수 있다는 뜻이다.

템플릿 문법과 값이 겹칠 때의 우선순위는 Helm, 그 숫자를 정하는 기준은 쿠버네티스 노트에 있다.

오해 2. ArgoCD는 버튼이 아니다

ArgoCD에 SYNC 버튼이 있긴 하다. 수동 배포로 설정하면 그걸 누른다. 하지만 본질은 manifest 레포를 계속 지켜보다가, Git에 적힌 것과 실제 클러스터가 다르면 그 차이를 메우는 것이다.

Git에 적힌 것실제 클러스터판정
tag: 79f1tag: 79f1Synced
tag: a939tag: 79f1OutOfSync

화면에 뱃지가 둘 뜨는데 뜻이 다르다. Synced는 Git과 클러스터가 같은가, Healthy는 Pod이 실제로 잘 떠 있는가다. Synced인데 Degraded일 수 있다. 시킨 대로 배포는 됐는데 앱이 죽은 경우다.

이 방식을 GitOps라고 한다. Git이 진실의 원천이다. 누가 서버에 들어가 손으로 바꿔 놔도 ArgoCD가 "Git과 다르다"고 잡아낸다.

그리고 ArgoCD가 Pod을 직접 재생성하지도 않는다. Deployment를 갱신하는 데까지가 ArgoCD고, 그 다음은 쿠버네티스가 알아서 한다.

ArgoCD
Deployment의 image tag를 바꾼다
Kubernetes
선언된 상태가 달라졌다고 판단
롤링 업데이트
새것 하나 뜨고, 옛것 하나 내려가고
안 끊긴다

오해 3. dev와 prod를 가르는 건 Jenkins가 아니다

환경은 values 파일이 가른다. values/dev, values/prod 처럼 폴더가 곧 환경이다.

차트          한 벌
values/dev    replicaCount 1,  메모리 1Gi
values/prod   replicaCount 3,  메모리 4Gi

이미지는 환경마다 다시 만들지 않는다. 개발에서 검증한 그 이미지가 그대로 운영에 올라가야 "같은 것을 검증했다"가 성립한다. 다시 빌드하면 그 순간 검증이 뜻을 잃는다.

잘못 나갔을 때

되돌리는 방법이 셋인데 뜻이 다르다.

방법무엇을 하나언제
values.yaml의 tag를 옛 값으로 고쳐 커밋Git이 진실이므로 이게 정석대부분
ArgoCD History에서 이전 리비전 롤백클러스터만 되돌린다. Git은 그대로라 다시 어긋난다급할 때만
git revert 후 파이프라인 재실행소스까지 되돌린다코드 자체가 문제일 때

두 번째를 쓰면 반드시 Git도 맞춰 놔야 한다. 안 그러면 다음 동기화 때 문제가 다시 배포된다.

용어 한 줄 정리

Artifact — 빌드해서 나온 파일. jar · war · dist
Container Image — 앱 + 실행 환경을 통째로 굳힌 불변 파일
Registry — 이미지 창고. 태그가 곧 버전
Manifest — 쿠버에 "이렇게 있어라"고 적은 YAML
Helm Chart — 그 YAML의 템플릿 묶음
values.yaml — 템플릿의 빈칸을 채우는 값. 자원 설정이 여기 있다
Pod — 컨테이너가 도는 한 덩이 / Deployment — Pod을 몇 개로 유지할지의 선언
requests / limits — 최소 보장 자원 / 상한. 상한을 넘으면 죽거나 느려진다
GitOps — Git을 진실의 원천으로 삼는 운영 방식

손으로 익히려면

읽어서 아는 것과 손이 아는 것이 다르다. 반나절짜리 셋이면 흐름이 남는다.

k3d로 클러스터 하나
맥에서 도커만 있으면 된다
helm create로 차트 뼈대
템플릿과 values가 어떻게 갈리는지 눈으로
ArgoCD 걸기
values를 고쳐 커밋하면 아무것도 안 눌러도 Pod이 바뀐다

Jenkins는 건너뛰어도 된다. CI는 감이 있는 쪽이고, 흐릿한 것은 대개 Helm과 ArgoCD다.

그 다음이 진짜다. values.yaml의 숫자를 일부러 틀리게 넣어 망가뜨려 보는 것. 메모리 limits를 실제 사용량보다 낮게 잡아 OOMKilled를 직접 보고, CPU limits를 조여 응답이 느려지는 걸 재고, replicas를 0으로 만들어 Service는 살아 있는데 응답이 없는 상태를 본다.

정상 동작하는 설정에서는 배울 것이 없다. 한계와 실패 모드가 지식이다.