배포 자동화 리팩토링, 젠킨스에서 ArgoCD로 (4) — 실제 적용과 트러블슈팅
새로 만드는 게 아니라 “이어받는” 것이었다
3편에서 정리한 values 구조를 실제로 적용하는 단계에서, 처음에 놓치고 있던 전제가 하나 있었다. 이건 아무것도 없는 곳에 새로 배포하는 게 아니라, 지금 이 순간에도 젠킨스가 관리하고 있는 살아있는 서비스를 ArgoCD가 이어받는 것이었다. 이 차이가 이번 편에서 겪은 문제 대부분의 근본 원인이다.
Namespace를 Git으로 관리하기로 한 이유
배포 대상 네임스페이스가 없을 때 ArgoCD가 자동으로 만들어주는 옵션(CreateNamespace=true)이 있긴 한데, 이건 정말 아무 설정 없이 네임스페이스만 딱 만든다. 실제로는 이 네임스페이스에 Istio 사이드카 자동 주입을 위한 라벨이 붙어있어야 했는데, 이 옵션으로는 라벨까지는 못 넣는다.
그래서 Namespace 자체를 헬름 템플릿으로 만들어서 Git이 직접 관리하게 했다.
1
2
3
4
5
6
7
8
apiVersion: v1
kind: Namespace
metadata:
name:
labels:
istio-injection: enabled
annotations:
argocd.argoproj.io/sync-wave: "-1"
sync-wave: "-1"이 핵심이다. 이게 없으면 Namespace가 아직 안 만들어진 상태에서 그 안에 들어갈 Deployment 같은 다른 리소스가 먼저 적용을 시도하는 경쟁 상태가 생길 수 있다. 이 값을 낮게 줘서 “다른 것보다 무조건 먼저 만들어라”를 명시했다.
릴리즈 편입 문제 — 조용히 어긋나 있던 이름표
이 서비스를 ArgoCD Application으로 처음 등록했을 때, sync 버튼을 누르지도 않았는데 기존에 떠있던 Deployment와 그 하위 리소스들이 화면에 바로 나타났다. 처음엔 신기하다고만 생각했는데, 여기 조심해야 할 함정이 하나 숨어있었다.
기존 리소스를 확인해보니 이런 annotation이 붙어있었다.
1
2
meta.helm.sh/release-name: aidt-lm-service
meta.helm.sh/release-namespace: <네임스페이스>
이건 헬름 엔진이 자동으로 붙이는 “이 리소스가 어느 헬름 릴리즈 소유인지”를 나타내는 꼬리표다. 지금까지 젠킨스가 배포할 때 릴리즈 이름을 도메인과 무관하게 고정값으로 써왔기 때문에, 모든 도메인의 릴리즈 이름이 똑같았다.
문제는 새로 만든 ArgoCD Application 이름이었다. 도메인을 구분하려고 Application 이름에 도메인을 붙여서 지었는데(예: aidt-lm-service-math), 릴리즈 이름을 따로 지정하지 않으면 ArgoCD는 기본값으로 Application 이름을 릴리즈 이름으로 쓴다. 즉 그대로 뒀으면 이 annotation 값이 기존 값과 다르게 덮어써질 뻔했다.
당장 문제가 터지는 건 아니다. ArgoCD 자체 동기화는 이 annotation을 안 보고 그냥 적용하기 때문이다. 근데 전환 과도기에 실수로 옛날 젠킨스 잡을 한 번이라도 다시 돌리면, 헬름이 “어? 나는 이 릴리즈 이름인데 리소스는 다른 이름 소유로 돼 있네” 하면서 소유권 충돌 에러를 낸다. 그래서 Application에 릴리즈 이름을 기존 값 그대로 명시해서 맞춰줬다.
1
2
3
source:
helm:
releaseName: aidt-lm-service
이 한 줄로 “기존 것을 그대로 이어받는다”는 게 명확해졌다.
젠킨스 쪽에서 나온 진짜 버그 두 개
기존 배포 로직을 걷어내고 “이미지 태그만 Git에 반영하는” 새 스테이지를 만드는 과정에서, 실제로 동작하지 않았을 뻔한 버그를 두 개 발견했다.
하나, 브랜치 체크아웃 누락. 새 스테이지는 값 파일이 있는 레포를 clone하고, 태그를 patch하고, 커밋해서 push하는 순서로 짜였다. 근데 clone은 기본 브랜치로 되는데, push는 특정 브랜치 이름으로 하드코딩돼 있었다. 로컬에 그 이름의 브랜치가 없으니 push 자체가 실패할 뻔했다. clone 시점부터 원하는 브랜치를 명시하는 걸로 고쳤다.
1
git clone -b ${target_branch} ...
둘, 도구 자체가 없었음. 값 파일에서 필드 하나만 정확히 바꾸기 위해 yq(YAML 버전 jq)라는 도구를 쓰기로 했는데, 정작 젠킨스 에이전트에 이 도구가 설치돼 있지 않았다. yq: not found 에러를 보고서야 알았다 — 새 도구를 파이프라인에 도입할 땐, 에이전트 환경에 그 도구가 실제로 있는지부터 확인해야 한다는 당연한 걸 새삼 깨달았다.
자동 동기화, 한 번에 다 켜지 않은 이유
이미지 태그가 Git에 잘 반영되는 걸 확인한 다음, 마지막으로 남은 건 “Git이 바뀌면 사람이 버튼을 안 눌러도 자동으로 반영되게” 만드는 것이었다. ArgoCD에는 이걸 위한 옵션이 두 개 있다.
automated— Git에 새 커밋이 올라올 때만 자동으로 반영selfHeal— 여기에 더해서, Git 변경이 없어도 클러스터 쪽이 어떤 이유로든 어긋나면 즉시 원래대로 되돌림
팀 안에서 kubectl로 클러스터를 직접 수동으로 만지는 일이 종종 있다는 걸 감안하면, 최종적으로는 selfHeal까지 켜는 게 맞다고 판단했다. 근데 한 번에 다 켜지는 않았다. 이 서비스의 배포 전략이 스테이징 환경에서는 Recreate(파드를 전부 내렸다가 새로 띄우는 방식)였는데, selfHeal이 예상치 못한 순간에 이 전략을 발동시키면 순간적인 다운타임이 생길 수 있어서다.
그래서 1단계로 automated(정확히는 그 안의 prune 옵션)만 먼저 켜고, 며칠 지켜본 다음 문제없으면 selfHeal을 추가하는 순서로 갔다. 운영 환경은 배포 전략이 RollingUpdate(순차 교체)라 이 위험이 적어서, 나중에 운영까지 확대할 때는 이 단계를 그대로 반복할 필요가 없다고 판단했다.
팀 실제 사용 패턴에 맞춘 파라미터 하나 추가
전환 작업 도중 실제로 필요해진 상황이 하나 있었다. 이미지는 이미 빌드·배포됐는데, 배포 설정(ConfigMap·Secret에 들어가는 값)만 바뀌어서 그것만 다시 반영하고 싶은 경우다. 이런 상황을 위해 젠킨스 잡에 파라미터를 하나 추가했다 — 켜면 이미지 빌드·배포 관련 스테이지는 전부 건너뛰고, 설정값 반영 스테이지만 실행되게.
이 과정에서 하나 배운 게 있다. 소스 코드를 가져오는 스테이지를 그냥 꺼도 될 줄 알았는데, 알고 보니 이 스테이지가 앱 소스만이 아니라 설정값 반영에 필요한 스크립트가 들어있는 레포까지 같이 가져오고 있었다. 즉 겉보기엔 꺼도 될 것 같은 스테이지가 사실은 뒤쪽 스테이지의 숨은 전제조건이었던 거다. 이런 식으로 스테이지 사이에 보이지 않는 의존 관계가 있는지 확인 안 하고 토글을 만들면, 그 토글이 오히려 새로운 실패를 만들 수 있다는 걸 배웠다.
다음 편 예고
여기까지 오면서 실제로 부딪힌 문제와 판단들을 다뤘다. 마지막 편에서는 지금 이 전환이 어디까지 왔고, 아직 안 끝난 게 뭔지, 그리고 프론트엔드·스테이징·운영으로 확대해가는 계획을 솔직하게 정리하면서 시리즈를 마무리한다.
(회사·인프라 관련 실명, 클러스터 식별자, 내부 호스트 주소 등은 전부 임의로 재구성해 게시했다.)