Kubernetes · GitOps

서로 다른 두 도구,
똑같이 빠뜨린 루트

Argo CD와 Flux는 애플리케이션 의존성 트리를 거의 반대 방식으로 표현한다. 한쪽은 신호가 전부 부모에 있고, 다른 쪽은 자식이 직접 선언한다. 그런데 어느 쪽이든 증거를 쥔 객체를 빼고 감사하면 똑같은 거짓 실패가 나온다. 따져 보면 이유도 같다.

애플리케이션 묶음이 제대로 된 의존성 트리인지 확인하는 검사가 있다고 하자. 이름 패턴만 비슷한 무관한 앱 더미와 가려내야 하니, 결국 이 질문에 답해야 한다. 이 자식은 더 큰 구조의 일부로 관리되고 있는가, 아니면 혼자 떨어져 있는가. 두 GitOps 도구는 구조가 전혀 다른 신호로 이 질문에 답한다. 그런데 감사 범위를 덜 잡으면 둘 다 똑같은 방식으로 실패했다.

Argo CD는 신호가 부모에 있다

전형적인 Argo CD App-of-Apps에서는 source.directory.recurse: true를 단 루트 Application이 자식 Application 매니페스트가 모인 디렉터리를 가리킨다. 문서만 보지 않고 실제로 리컨실하는 Argo CD v3.4.5 컨트롤러에서 확인했다. 자식에는 구분할 만한 내용이 아무것도 없다. ownerReferences도 없고, 스펙 어디에도 "나는 트리에 속해 있다"는 표시가 없다. 단독 앱이 아니라 관리받는 앱이라는 증거는 루트의 recurse 플래그 하나뿐이고, 자식 쪽에서는 그 루트에 대해 아무 말도 하지 않는다.

그러니 자식만 받은 검사는 둘을 구분할 수 없다. 예를 들어 "우리 팀 앱"으로 범위를 좁히면서 다른 팀이 관리하는 공유 루트를 일부러 뺀 감사가 그렇다. 제대로 관리되는 자식 둘과, 우연히 비슷하게 생긴 무관한 단독 Application 둘이 똑같아 보인다. 어느 경우든 FAIL을 낸다. 확인할 근거를 받지 못했으니 확인할 수 없다고 답하는 것이고, 그 답은 맞다.

Flux는 자식이 신호를 선언한다

Flux는 반대다. 자식 Kustomization이 spec.dependsOn: [{name: infra}]처럼 자기가 의존하는 부모 이름을 직접 적어 소속을 밝힌다. 언뜻 보면 이것만으로 충분할 것 같다. 자식이 이미 부모가 누군지 말하고 있으니 바깥의 루트 객체를 볼 필요가 없어 보인다.

그렇지 않다. dependsOn에 적힌 이름은 그냥 문자열이기 때문이다. CRD 스키마만 보지 않고 실제 flux install에서 확인해 보니, kustomize-controller는 자식에게 부모를 가리키는 ownerReferences를 찍어 주지 않는다. 그래서 자식의 라이브 상태만 봐서는 infra가 실제로 있고 제대로 관리되는 대상인지 알 수 없다. 오타일 수도 있고, 몇 달 전에 지운 Kustomization을 가리키는 낡은 참조일 수도 있다. payment-api와 order-api가 둘 다 dependsOn: [{name: infra}]를 선언하고 있고 검사가 이 둘만 받았다면, 같은 문자열을 두 번 볼 뿐 그 이름이 무언가로 이어지는지는 확인하지 못한다. 그래서 일부러 FAIL을 낸다. 자식이 주장하는 이름과 실제 리소스가 뒷받침하는 이름은 별개의 사실이고, 손에 쥔 증거는 그중 하나뿐이었다.

실패는 같고, 원리는 반대다

Argo CD는 증거가 부모에 있고 자식에는 아무 흔적도 없다. Flux는 증거가 자식에 있고 부모 쪽에서 되짚어 주는 확인이 없다. 실패하는 이유는 구조적으로 다르다. 하나는 자식이 아무 말도 안 해서고, 다른 하나는 자식의 주장만으로는 검증이 안 돼서다. 그래도 결과는 같다. 검사기에 넘기는 입력에서 루트를 빼면 제대로 된 구조와 우연의 일치를 구분할 수 없다.

같은 단서가 두 번 따로 확인됐다

먼저 확인한 건 Argo CD였다. 독립적으로 도는 App-of-Apps 예제를 실제 클러스터에 올려서 봤다. 그러자 같은 루트 누락 문제가 Flux의 dependsOn 트리에도 있는지 궁금해졌고, 같은 방식으로 답을 얻었다. 실제 flux install에 루트 infra Kustomization을 두고, 거기에 dependsOn을 거는 의존 앱 둘을 만든 다음, 검사기에 자식만 넘기는 픽스처를 따로 짰다. 예상한 대로, 예상한 이유로 실패했다.

도구도 다르고 트리를 표현하는 구조도 다른데, 루트를 빼면 안 된다는 단서는 양쪽에서 똑같이 성립했다. 원리가 비슷해서 그런 게 아니다. "이 객체가 트리에 속한다는 걸 증명하라"는 질문에 답하려면, 질문받는 객체 말고 적어도 하나의 객체가 바깥에서 더 필요하기 때문이다.

일반 규칙

계층 안의 소속을 확인하는 검사라면 검토할 리프만이 아니라 구조의 루트까지 입력에 넣어야 한다. GitOps 앱 트리만의 얘기가 아니다. 소유권 그래프, 의존성 그래프, 조직도 모양의 권한 감사처럼 "이게 정말 구조의 일부인가"를 묻는 곳이면 어디든 마찬가지다. "내가 소유한 것만"이나 "지금 검사하는 것만"으로 좁힌 쿼리는 빠짐없어 보여도, 필요한 증거가 처음부터 범위 밖에 있어서 구조상 질문에 답하지 못할 수 있다.

더 볼 자료

kyhsa93/k8s-playbook(Kubernetes 배포 실수를 유형별로 모으고, 매니페스트에서 그 실수를 찾아내는 검사기를 붙여 둔 내 예제 프로젝트. Argo CD와 Flux 라이브 컨트롤러 검증 기록과, 각 실패를 일부러 재현하는 픽스처가 있다)