GitOps 하시면서, 배포 전에 뭐가 바뀔지 알고 계신가요

기술 블로그

PR에서 안 보이는 것

저희 배포는 GitOps입니다. 배포의 출발점이 Git이라는 뜻입니다. manifests 레포의 어떤 브랜치가 곧 그 환경이 지금 원하는 상태이고, ArgoCD가 그 브랜치를 따라 Application을 실제 클러스터에 동기화합니다. 그러니 무언가를 배포한다는 것은 이 레포에 커밋을 올린다는 것과 거의 같은 말이고, 그 변경을 검토하는 자리는 PR입니다.

문제는 PR 화면에 보이는 것과 실제로 배포되는 것 사이의 거리입니다. 리뷰어가 PR에서 마주하는 건 values YAML 몇 줄이 바뀐 diff입니다. 그런데 정작 클러스터에 꽂히는 건 그 몇 줄이 아니라, 여러 values 파일이 한 겹씩 merge되고 Helm이 렌더한 매니페스트 전체입니다. YAML 한 줄을 고쳤을 뿐인데 렌더 결과에서는 여러 Application의 여러 필드가 함께 움직일 수 있고, 반대로 크게 바꾼 것처럼 보여도 최종 매니페스트는 그대로일 수 있습니다.

리뷰어가 진짜 알고 싶은 건 "이 YAML 줄이 바뀌었나"가 아닙니다. "이 PR을 merge하면 ArgoCD가 어느 Application의 배포 결과를 어떻게 바꾸는가"입니다. 그런데 이 질문은 merge 전에는 확인하기가 어렵습니다. 확인하려면 각 브랜치를 받아 손으로 렌더해 비교해야 하니, 리뷰 자리에서 매번 할 수 있는 일이 아닙니다. 그렇다고 merge 후에 알게 되는 변경은 이미 리뷰의 기회를 지난 것입니다. 배포 결과가 눈에 보이는 시점이 리뷰가 끝난 다음이라면, 리뷰는 사실상 YAML 몇 줄을 훑은 것으로 끝나 버립니다.

그래서 저희 목표는 "이 값의 출처를 표시하자"가 아니었습니다. PR이 열리는 그 순간에, 기준 브랜치와 PR 브랜치의 배포 결과를 Application 단위로 비교해 리뷰 화면에 그대로 보여주자는 것이었습니다. 값의 출처 추적은 이 글의 조연입니다. 그 배포 diff를 읽을 때 "이 줄이 왜 이렇게 바뀌었지"를 함께 읽게 해주는 보조 정보로만 등장합니다.


브랜치가 곧 환경의 상태

저희 배포에서 manifests 레포의 브랜치는 문서가 아니라 상태 그 자체입니다. 어떤 환경이 지금 무엇을 원하는지가 그 환경에 대응하는 브랜치에 적혀 있고, ArgoCD가 그 브랜치와 클러스터를 계속 맞춥니다. 그러니 PR은 단순한 코드 리뷰가 아니라 "이 환경의 다음 상태를 이렇게 하자"는 제안의 단위가 됩니다. PR을 merge한다는 건 환경의 원하는 상태를 실제로 바꾸겠다는 결정입니다.

그 상태를 이루는 values가 왜 여러 겹으로 쌓이는지는 이미 본편 시리즈에서 다뤘습니다. 공통 기본층 위에 환경층이 얹히고 그 위에 변종층이 덮이는 구조, 어느 층이 무엇을 알고 무엇을 모르는지를 규율로 못박은 배경은 본편 "코드가 환경을 모르는 구조" 편을 참고하시면 됩니다. 여기서는 딱 한 문장만 전제로 두겠습니다. values는 우선순위가 낮은 층부터 높은 층까지 여러 겹으로 쌓여 하나로 merge되고, 위층이 아래층을 덮습니다.


Git diff가 아니라 배포 diff

리뷰에서 비교해야 할 대상은 파일이 아니라 배포 결과라는 것, 이게 저희가 도달한 지점이었습니다.

Git이 보여주는 diff는 파일이 어떻게 바뀌었는지를 알려줍니다. 하지만 앞서 말했듯 파일 변경과 배포 결과 변경은 일대일로 대응하지 않습니다. 여러 층이 merge되기 때문에, 한 층의 한 줄이 다른 층에 눌려 배포엔 아무 영향이 없을 수도 있고, 반대로 아래층 한 줄이 여러 Application의 렌더 결과를 동시에 흔들 수도 있습니다. 리뷰어가 필요한 건 "파일이 어떻게 바뀌었나"가 아니라 "merge와 렌더를 다 거친 뒤 배포 결과가 어떻게 바뀌나"입니다.

여기서 자연스럽게 떠오르는 질문이 있습니다. "ArgoCD에도 diff가 있는데, 왜 PR에서 다시 보여줘야 하지?" 저희가 필요했던 건 ArgoCD의 diff를 대체하는 도구가 아니었습니다. ArgoCD UI가 보여주는 App diff는 지금 클러스터에 떠 있는 live 상태와, ArgoCD가 현재 동기화 대상으로 알고 있는 브랜치의 desired 상태를 비교합니다. 그 브랜치에 변경이 이미 반영되고, ArgoCD가 그 상태를 따라가기 시작한 다음에야 강한 도구라는 뜻입니다.

그런데 GitOps에서 merge는 환경의 desired 상태를 바꾸겠다는 결정 그 자체입니다. 리뷰어가 알고 싶은 건 결정을 내린 뒤의 차이가 아니라, 결정을 내리기 전에 "이 PR을 merge하면 desired 상태가 어떻게 바뀌는가"입니다. PR 브랜치는 아직 ArgoCD의 동기화 대상이 아니고, 리뷰가 벌어지는 자리도 ArgoCD UI가 아니라 PR입니다. ArgoCD의 diff가 결정 후에 결정과 다른 자리에서 보인다면, 리뷰에 필요한 건 결정 전에 결정이 벌어지는 자리에서 보이는 diff였습니다.

그래서 저희는 비교의 기준을 렌더링 결과로 옮겼습니다. live와 desired의 차이가 아니라, 기준 브랜치의 desired와 PR 브랜치의 desired 사이의 차이를 봅니다. CI가 pull_request 트리거로 돌면서, 기준 브랜치(as-is)와 PR 브랜치(to-be)를 각각 받아 ArgoCD Application 단위로 Helm 렌더링을 실행합니다. 그리고 같은 Application의 as-is 매니페스트와 to-be 매니페스트를 비교합니다. 이 비교가 답하는 질문이 바로 "이 PR을 merge하면 ArgoCD가 이 Application에서 실제로 무엇을 바꾸는가"입니다. 파일 diff가 아니라 배포 diff이고, 이 계산을 사람의 손이 아니라 PR이 열릴 때마다 CI가 대신 해줍니다.


PR 코멘트에 남긴 것

이 비교의 산출물은 별도 대시보드가 아닙니다. 리뷰가 벌어지는 자리, 즉 PR 코멘트 그 자체입니다. 리뷰어가 이미 보고 있는 화면 안으로 배포 diff를 가져다 놓는 것이 핵심이었습니다. 맥락을 옮기지 않고, 열고 있던 PR에서 그대로 배포 영향도를 읽게 하려는 것입니다.

코멘트는 변경이 감지된 Application만 담습니다. 변경이 없는 Application까지 나열하면 코멘트만 길어지고 정작 봐야 할 곳이 묻히기 때문입니다. 변경이 있는 Application 각각은 접이식 섹션으로 들어갑니다. 섹션을 펼치면 그 Application의 as-is와 to-be 렌더 결과 차이가 diff 코드블록으로 보이고, 렌더 과정에서 걸린 warning과 error도 같은 코멘트 안에 정리됩니다. 리뷰어가 마주하는 건 흘러가는 로그가 아니라, 접었다 펼 수 있는 섹션과 diff 블록, 그리고 경고가 정돈된 리뷰용 화면입니다.

이렇게 하면 리뷰어는 "이 PR을 merge하면 ArgoCD가 무엇을 바꾸는가"에 대한 답을 PR을 떠나지 않고 바로 읽습니다. 어느 Application이 영향을 받는지, 그 Application의 매니페스트에서 어떤 줄이 어떻게 달라지는지, 렌더 과정에 문제는 없었는지가 한 코멘트에 모여 있습니다.


diff를 읽기 위한 보조, 값의 출처

배포 diff를 보다 보면 다음 질문이 자연스럽게 따라옵니다. "이 줄이 이렇게 바뀐 건 어느 values 층 때문이지?" 렌더 결과의 한 값은 여러 층이 merge된 끝의 승자라, diff만으로는 어느 층이 그 변화를 만들었는지 바로 보이지 않습니다. 그래서 변경이 감지된 Application에는 그 배포 diff를 읽는 보조로 값의 출처 리포트를 접이식으로 함께 붙였습니다.

출처를 만드는 방법은 merge를 그대로 따라가는 것입니다. 여러 values 파일을 Helm이 겹치는 순서와 똑같이, 우선순위가 낮은 것부터 높은 순서로 하나씩 deep merge합니다. 이때 양쪽이 모두 맵이면 재귀로 파고들고 한쪽이라도 leaf면 위층 값이 아래층 값을 덮는 규칙까지 Helm과 같게 두되, leaf를 덮을 때마다 그 leaf의 중첩 경로를 키로 삼아 "이 파일에서 이 값이 들어왔다"는 기록을 남깁니다.

핵심은 이 기록이 덮어쓰기가 아니라 누적이라는 점입니다. 한 경로에 기본층이 먼저 값을 넣고 환경층이 같은 경로를 덮으면, 그 경로의 이력에는 두 항목이 순서대로 쌓입니다. merge가 끝나면 각 경로마다 그 leaf가 거쳐 간 값들이 시간순으로 남습니다. 마지막 항목이 실제 배포되는 최종 값이자 그 출처이고, 바로 앞 항목이 그 값이 덮은 직전 값입니다. 디버깅에서 가장 먼저 궁금한 건 대개 "직전에 무엇이 있었나"라, 최종 출처와 함께 바로 이전에 눌린 값의 출처를 같이 남깁니다.

이 출처는 배포 diff가 붙는 Application, 그것도 변경 후(to-be) 렌더를 기준으로만 붙습니다. 산출물은 merge된 최종 values를 주석 달린 YAML로 내보내는 형태입니다. 각 leaf 옆에 그 값이 어느 파일에서 왔는지, 덮은 것이 있었다면 무엇을 눌렀는지를 주석 한 줄로 답니다.

replicaCount: 4                    # env-prod.yaml (덮음: base.yaml의 2)
resources:
  limits:
    cpu: "1000m"                   # app-sample.yaml (덮음: env-prod.yaml의 500m)
    memory: 512Mi                  # base.yaml

memory처럼 아무도 덮지 않은 값은 출처 하나만 붙고, cpu처럼 덮인 값은 최종 승자와 직전에 눌린 값이 함께 보입니다. diff에서 cpu가 바뀐 걸 본 리뷰어가 곧바로 "이건 변종층이 환경층 값을 덮은 것"임을 같은 코멘트에서 읽습니다. 출처 추적을 merge와 한 몸으로 계산하므로, 리포트의 출처와 실제 배포 값이 어긋날 구조적 여지도 없습니다.


리뷰 흐름이 어떻게 달라졌나

이 리포트가 바꾼 건 리뷰에서 던지는 질문 자체입니다. 예전 리뷰는 "무슨 파일이 바뀌었나"에서 출발했습니다. values 파일 diff를 보고, 이게 어느 환경에 어떻게 작용할지는 리뷰어가 머릿속에서 merge를 재실행해 짐작했습니다. 지금은 "어느 Application의 렌더 결과가 어떻게 바뀌었나"에서 출발합니다. 짐작할 대상이 이미 코멘트에 계산돼 있으니, 리뷰어는 그걸 검토하기만 하면 됩니다.

이 전환이 저희가 플랫폼에서 하려던 것과 맞닿아 있습니다. 첫째는 인지 부하를 덜어 내는 것입니다. 사람이 매번 머릿속에서 여러 층의 merge와 렌더를 재계산하던 수고를, PR이 열릴 때마다 CI가 대신 산출물로 남깁니다. 그 수고의 결과가 한 사람의 머릿속에만 남지 않고 코멘트로 다음 사람에게 이어집니다.

둘째는 셀프서비스입니다. 리뷰어도 작성자도 배포 영향을 확인하려고 플랫폼 팀이나 예전 작성자를 붙잡지 않아도 됩니다. "이거 merge하면 어디가 바뀌어요?"와 "이 값 왜 이렇게 됐어요?"에 대한 답이 이미 PR 안에 있기 때문입니다.

셋째는 추상화를 되돌려 주는 것입니다. 다층 values는 개발자가 환경별 차이를 직접 다루지 않게 감춰 주는 구조입니다. 감추는 게 목적이니 평소엔 감춘 채로 두는 게 맞습니다. 다만 배포를 결정하는 리뷰의 순간만큼은 그 추상화가 감춘 경로를 다시 펼쳐 보여야 합니다. 평소에는 감추고, 리뷰할 때 되돌려 주는 것. 그것도 지금 이 PR이 건드린 지점으로만 좁혀서. 그 균형을 PR 코멘트라는 자리에서 맞추려 했습니다.


merge 전에 답할 수 있게 된 질문

이 작업으로 모든 배포 문제가 사라진 것은 아닙니다. 대신 리뷰에서 가장 늦게 보이던 질문 하나가 앞으로 당겨졌습니다. "이 PR을 merge하면 ArgoCD가 실제로 무엇을 바꾸는가"를 merge 후의 ArgoCD 화면이나 사람의 머릿속 계산에 맡기지 않고, PR이 열리는 순간 코멘트에서 확인할 수 있게 됐습니다.

그 결과 리뷰의 출발점도 달라졌습니다. 예전에는 values 파일 몇 줄을 보고 여러 층의 merge와 Helm 렌더 결과를 짐작해야 했습니다. 지금은 변경된 Application과 렌더된 매니페스트 diff를 먼저 보고, 필요한 경우 같은 코멘트 안에서 값의 출처를 따라갑니다. 리뷰어가 확인하는 대상이 "파일 변화"에서 "배포 결과 변화"로 옮겨간 것입니다.

물론 이 리포트가 배포 전체를 보장하지는 않습니다. 비교하는 것은 기준 브랜치와 PR 브랜치를 각각 렌더한 desired 상태입니다. 실제 클러스터의 live 상태, 동기화 중 런타임에서 생기는 차이, 템플릿 바깥의 외부 조건까지 검증하지는 않습니다. 여러 겹의 오버라이드 자체를 줄여 주지도 않습니다. 출처 추적 역시 Helm의 merge 규칙을 별도로 따라간 결과라, 저희가 아직 반영하지 않은 규칙이나 미묘한 차이가 있으면 어긋날 여지도 남습니다.

그래도 이 도구가 맡은 자리는 분명합니다. 문제를 없애는 것이 아니라, 문제가 될 수 있는 변화를 리뷰 시점에 보이게 하는 것입니다. GitOps에서 merge가 환경의 desired 상태를 바꾸는 결정이라면, 그 결정 전에 배포 diff를 보는 일은 곁다리 기능이 아니라 리뷰의 일부에 가깝습니다. 저희가 얻은 것도 거기에 있습니다. 배포 판단을 merge 이후가 아니라 PR 안으로 가져온 것, 그리고 그 자리에서 "바뀐 파일"이 아니라 "바뀔 배포 결과"를 함께 보게 된 것입니다.

(본문에 등장하는 파일명·앱 이름·values 값은 설명을 위한 예시이며 실제 식별자가 아닙니다. 정량적 서술이 없는 운영상의 판단은 저자 팀의 자체 경험에 근거합니다.)


🚀플렉스팀 채용페이지 바로가기

☕flex Private Talk 신청하기

글이 마음에 드셨나요?
공유하기