배포 자동화 리팩토링, 젠킨스에서 ArgoCD로 (2) — ArgoCD 개념 학습과 첫 PoC
왜 실제 서비스로 바로 시작하지 않았나
1편 끝에서 예고했던 질문부터 답하고 시작한다. ArgoCD를 처음 설치하고 나서, 가장 먼저 한 일은 실제 운영 중인 서비스가 아니라 아무 상관 없는 샘플 애플리케이션 두 개(React 프론트, Spring Boot 백엔드 데모)를 임시로 만들어서 배포해보는 것이었다.
이유는 간단하다. 실제 서비스는 Vault 연동, Istio 서비스 메시, 여러 개의 물리 클러스터, 교과목별 네임스페이스 분리 같은 게 다 얽혀있다. 이 상태에서 바로 ArgoCD로 전환을 시도하면, 뭔가 안 됐을 때 원인이 “ArgoCD 자체를 잘못 이해해서”인지 “우리 서비스 구조가 특이해서”인지 구분이 안 된다. 그래서 원칙을 하나 세웠다.
한 번에 변수 하나만 검증한다.
샘플 앱으로 ArgoCD라는 도구 자체의 동작 방식부터 몸에 익히고, 그다음에 실제 서비스의 복잡한 구조를 하나씩 얹어가는 순서로 갔다. 이 원칙은 이번 시리즈 전체에서 계속 반복해서 등장한다.
핵심 개념 세 가지
본격적으로 트러블슈팅 얘기로 들어가기 전에, ArgoCD를 처음 접하는 사람 기준으로 최소한의 용어부터 정리하고 가자.
Application — “이 Git 레포의 이 경로에 있는 헬름 차트를, 이 클러스터의 이 네임스페이스에 배포한다”를 선언하는 리소스. UI에서 클릭으로 앱을 등록하든 yaml로 직접 작성하든 결과는 같다.
1
2
3
4
5
6
7
8
9
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
source:
repoURL: <git 레포>
path: <차트 경로>
destination:
server: <클러스터 주소>
namespace: <네임스페이스>
AppProject — Application들이 접근할 수 있는 레포·클러스터·리소스 종류를 미리 제한해두는 울타리. “이 프로젝트 소속 앱들은 이 레포에서만, 이 클러스터에만, 이런 종류의 리소스만” 하는 식으로 범위를 좁힌다.
RBAC — 누가 무엇을 할 수 있는지 정하는 규칙. p, <역할>, <리소스>, <행동>, <대상>, <허용/거부> 형식의 정책 문장을 쓴다. 나중에 CI(젠킨스)한테 발급할 토큰의 권한도 이 방식으로 정한다 — 우리는 결국 “sync 하나만 가능하고, Git을 우회해서 배포하는 권한(override)은 없는” 토큰을 만들게 됐는데, 이 이야기는 4편에서 더 자세히 다룬다.
첫 삽질 — Repository 등록
가장 먼저 한 건 GitLab 레포를 ArgoCD에 등록하는 거였다. 처음엔 그냥 계정 비밀번호를 넣었는데 인증 실패가 났다 — GitLab이 Git 인증에는 일반 로그인 비밀번호 대신 Personal Access Token(PAT)을 요구하는 상태였다. PAT을 발급받아서 다시 넣으니 됐는데, 여기서 재밌는 걸 하나 알게 됐다.
GitLab은 PAT로 인증할 때 username 값을 아예 검증하지 않는다. 빈 문자열만 아니면 아무거나 넣어도 된다 — 신원 확인은 PAT 자체가 다 하기 때문이다.
두 번째 삽질 — “권한 없음”의 진짜 정체
레포 연결, 프로젝트 설정까지 끝내고 처음으로 sync 명령을 날렸는데 이런 에러가 떴다.
1
rpc error: code = PermissionDenied desc = permission denied
바로 “아, 권한 설정을 잘못했구나” 싶어서 프로젝트 정책부터 다시 뜯어봤다. 로그인 계정도 확인하고, 정책 문법도 다시 확인하고, 한참을 삽질했는데 — 알고 보니 원인은 완전히 다른 데 있었다. 애초에 그런 이름의 앱이 존재하지 않았다. 오타로 없는 앱 이름을 sync하려고 했던 거였다.
근데 왜 “그런 앱 없음”이 아니라 “권한 없음”으로 떴을까? 여기에 ArgoCD의 의도된 보안 설계가 있었다.
ArgoCD는 “이 앱이 존재하지 않음”과 “이 앱은 있지만 권한이 없음”을 의도적으로 구분하지 않고 똑같이
permission denied로 응답한다. 권한 없는 사람이 이름을 이것저것 대입해보면서 “어떤 이름의 앱이 실제로 존재하는지”를 알아낼 수 없게 막아둔 것이다.
이걸 알고 나니 이후로는 이 에러가 뜨면 권한 설정부터 의심하는 게 아니라, 대상 이름이 맞는지부터 확인하는 습관이 생겼다.
세 번째 삽질 — 네임스페이스, 그리고 또 한 번의 권한 벽
앱 이름을 바로잡고 다시 sync하니 이번엔 이런 메시지가 나왔다.
1
namespaces "..." not found
ArgoCD는 배포 대상 네임스페이스가 없으면 기본적으로 알아서 만들어주지 않는다. syncOptions에 CreateNamespace=true를 추가해서 해결했는데, 이번엔 또 다른 벽에 부딪혔다.
1
resource :Namespace is not permitted in project ...
여기서 배운 게 있다. AppProject는 리소스를 두 종류로 다르게 취급한다.
- 네임스페이스 범위 리소스(Deployment, Service 등) — 기본적으로 허용, 특정 종류만 막는 방식
- 클러스터 범위 리소스(Namespace, ClusterRole 등) — 기본적으로 전부 차단, 허용 목록에 명시한 것만 되는 방식
Namespace 자체가 클러스터 범위 리소스라서, 프로젝트 설정에 명시적으로 허용을 추가해줘야 했다. 한 프로젝트가 실수로 클러스터 전역 설정을 건드리는 걸 막기 위한 안전장치라고 이해했다.
1
2
3
clusterResourceWhitelist:
- group: ''
kind: Namespace
이 세 가지를 다 해결하고 나서야 처음으로 화면에 Synced / Healthy가 뜨는 걸 봤다.
ApplicationSet과 두 번째 클러스터
앱 하나 배포하는 게 되고 나서, 다음 단계는 같은 애플리케이션을 서로 다른 클러스터 두 개에 동시에 배포하는 것이었다. 실제 서비스가 스테이징 환경에서만 물리적으로 서로 다른 클러스터 두 대를 쓰고 있었기 때문에, 이 검증을 미리 해두는 게 중요했다.
처음엔 Application을 손으로 두 개(클러스터별로 하나씩) 만들어서 각각 잘 배포되는지부터 확인했다. 되는 게 확인된 다음, ApplicationSet(여러 Application을 자동으로 찍어내는 도구)으로 바꿔서 하나의 정의로 두 클러스터에 동시 배포되게 만들었다.
여기서도 흥미로운 삽질이 하나 있었다. ApplicationSet이 만들어낸 Application 두 개를 손으로 지워봤는데, 몇 초 뒤 kubectl apply를 다시 실행하지도 않았는데 자식 Application들이 알아서 다시 생겨났다. 처음엔 “왜 안 지워지지?” 싶었는데, 알고 보니 이것도 ArgoCD의 자기 치유(self-heal)와 같은 원리였다 — ApplicationSet 컨트롤러가 “이 목록대로 자식들이 항상 존재해야 한다”를 상시로 감시하고 있었던 거다. Application 하나가 클러스터 리소스를 감시하듯이, ApplicationSet은 그 한 단계 위(Application 자체)를 감시한다는 걸 이때 체감했다.
여기까지 확인된 것
샘플 앱 두 개로 여기까지 검증을 끝냈다.
- Repository/Project/Application의 기본 구조와 동작 원리
- 권한 관련 에러 메시지를 있는 그대로 믿지 않고 원인을 정확히 좁혀가는 법
- 클러스터 범위 리소스와 네임스페이스 범위 리소스의 권한 체계가 다르다는 것
- ApplicationSet으로 하나의 정의를 여러 클러스터에 동시 배포하는 것, 그리고 그 상위 구조도 자기 치유가 된다는 것
다음 편에서는 이 감을 가지고, 실제로 젠킨스가 관리해오던 진짜 서비스의 헬름 차트와 배포 스크립트를 직접 열어서 — 어떤 값이 실제로 쓰이고 있고 어떤 값이 이미 죽은 코드인지 하나하나 확인해가는 “전수조사” 과정을 다룬다. 짐작이 아니라 실제 파일을 읽어서 판단한다는 원칙이 이때부터 본격적으로 중요해진다.
(회사·인프라 관련 실명, 클러스터 식별자, 내부 호스트 주소 등은 전부 임의로 재구성해 게시했다.)