Crossplane v2.3 Pipeline Inspector로 Composition Function 디버깅하기
Composition Function을 쓰기 시작하면 한 번쯤은 이런 상황을 겪는다. XR을 만들었는데 리소스가 안 만들어지고, kubectl describe를 봐도 "function pipeline failed"만 나오고, 정확히 어느 단계의 어느 함수가 뭘 받아서 뭘 뱉었는지는 깜깜하다. 사실상 추측 디버깅이었다.
v2.2에서 alpha로 들어왔던 Pipeline Inspector가 v2.3에서 beta로 승격됐다. OpenTelemetry 통합도 같이 들어와서 이제는 함수 파이프라인을 진짜로 들여다볼 수 있게 됐다. 우리 팀이 지난 2주간 도입하면서 정리한 내용을 공유한다.
Pipeline Inspector가 정확히 뭘 보여주나
이름이 좀 추상적인데, 실제로는 gRPC interceptor다. Crossplane이 composition function을 호출할 때마다 RunFunctionRequest와 RunFunctionResponse를 그대로 인터셉트해서 별도 socket으로 흘려보낸다. 그 socket의 반대편을 우리가 사이드카로 띄워두면, 함수 입출력을 실시간으로 볼 수 있다.
좀 더 구체적으로 보면 이런 것들이 잡힌다:
- 각 함수에 들어간 desired/observed state의 전체 스냅샷
- 함수가 반환한 결과 (다음 함수에 넘겨질 desired state)
- 함수 실행 시간, 에러 메시지, conditions
- 파이프라인 전체에서 어떤 함수가 어떤 순서로 호출됐는지
kubectl describe나 events에서는 절대 안 보이는 정보다. 특히 함수가 여러 개 체이닝된 파이프라인이라면, 중간 단계의 desired state를 볼 수 있는 것 자체가 게임 체인저다.
설치와 활성화
먼저 Crossplane을 v2.3 이상으로 올려야 한다. Helm 기준으로 feature flag를 활성화한다.
# crossplane-values.yaml
args:
- --enable-pipeline-inspector
- --pipeline-inspector-socket=/var/run/crossplane/pipeline-inspector.sock
# inspector socket을 위한 emptyDir 마운트
volumes:
- name: inspector-socket
emptyDir: {}
volumeMounts:
- name: inspector-socket
mountPath: /var/run/crossplane
그리고 같은 emptyDir를 공유하는 사이드카를 띄운다. 우리는 처음에 공식 reference inspector를 썼다.
extraContainers:
- name: pipeline-inspector
image: ghcr.io/crossplane-contrib/pipeline-inspector:v0.3.0
args:
- --socket=/var/run/crossplane/pipeline-inspector.sock
- --output=otel
- --otel-endpoint=otel-collector.observability:4317
volumeMounts:
- name: inspector-socket
mountPath: /var/run/crossplane
--output 옵션이 v2.3에서 추가됐다. stdout, file, otel 세 가지를 지원한다. 개발 환경에서는 stdout이 제일 편하고, 운영에서는 otel로 collector에 보내는 게 정석이다.
주의: socket 경로는 Crossplane core 컨테이너와 inspector 사이드카가 동일하게 봐야 한다. mount path가 어긋나면 inspector가 조용히 아무것도 못 받는다. 우리도 이거 하루 잡았다.
실제 디버깅 시나리오
설치만 해두면 뭐가 좋은지 와닿지 않을 테니, 우리가 실제로 풀었던 케이스 두 개를 보여주겠다.
케이스 1: 함수가 받는 입력이 의도와 다르다
EKS 클러스터를 만드는 XR을 정의했는데, 두 번째 함수(function-patch-and-transform)에서 NodeGroup의 instance type이 항상 t3.medium으로 박혔다. XR spec에는 m5.large로 줬는데 말이다.
OTel trace를 보니 첫 번째 함수가 desired state를 만들 때 패치를 잘못 걸어서 instance type 필드를 통째로 덮어쓰고 있었다. trace 한 줄로 끝났다.
crossplane.composition.function.run {
function.name: "compose-eks"
function.duration_ms: 142
desired.composed.eks-nodegroup.spec.forProvider.instanceTypes: ["t3.medium"]
}
이전에는 이런 거 잡으려면 함수를 로컬에서 띄워서 protobuf 입출력 찍어보고… 한나절짜리 작업이었다.
케이스 2: 파이프라인이 무한 reconcile
XR이 자꾸 reconcile만 돌고 ready=true가 안 됐다. 함수 자체는 에러 없이 끝나는데, 매번 desired state가 미묘하게 달랐다.
Pipeline Inspector trace를 시간 순으로 보니, 두 번의 연속 reconcile에서 한 함수가 metadata.annotations에 timestamp를 박고 있었다. 매번 다른 값이라 Crossplane은 "달라졌으니 다시 적용" 판단을 무한히 했다. 함수 코드 한 줄 수정으로 끝.
이 케이스는 Pipeline Inspector 없었으면 진짜 못 찾았다. observed/desired diff를 사람 눈으로 봐서는 보이지 않는 변화였다.
OTel 통합으로 운영 단계로 올리기
개발 환경에서 stdout으로 보는 건 좋은데, 운영에서는 trace로 묶어서 봐야 한다. v2.3의 OTel 통합이 여기서 빛난다.
# otel-collector config 일부
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 5s
# Crossplane function trace만 추출
filter/crossplane:
spans:
include:
match_type: regexp
services: ["crossplane.*"]
exporters:
otlphttp/tempo:
endpoint: http://tempo:4318
이렇게 띄우면 Tempo/Jaeger 어디서든 service.name=crossplane로 함수 파이프라인 trace를 볼 수 있다. XR 하나 만들 때마다 트레이스가 한 줄로 떠서, 각 함수 span에 입출력이 attribute로 박힌다.
우리는 추가로 Tempo metrics-generator를 켜서 function duration histogram을 Prometheus로 뽑고 있다. 어느 함수가 p99 1초를 넘기는지 알람을 걸어둘 수 있다.
카드를 다 까보면 보이는 한계
좋은 얘기만 한 것 같으니 단점도 솔직히 적는다.
첫째, 부하가 적지 않다. desired state 전체를 매 함수 호출마다 trace에 넣다 보니, XR이 많은 클러스터에서는 OTel collector에 부담이 간다. 우리는 sampling을 10%로 깎았다. 디버깅이 목적이면 어차피 한두 케이스 잡으면 충분하다.
둘째, observed state에 시크릿이 그대로 박힌다. Crossplane v2.3에서 attribute redaction 옵션이 들어왔지만 아직 완벽하진 않다. 운영 cluster에서 켤 때는 access를 trace storage 레벨에서 막아야 한다.
셋째, beta다. v2.3 GA 직후라 API가 안정화 단계다. inspector socket protocol이 v2.4에서 또 바뀔 수 있다는 메인테이너 코멘트가 있었다. 사이드카 이미지 버전 핀 잘 잡아두자.
정리
Composition Function 도입한 팀이라면 Pipeline Inspector는 거의 무조건 켤 가치가 있다. 디버깅 시간을 한나절에서 5분으로 줄여준다. 우리 팀은 도입 첫 주에만 그동안 묵혀뒀던 함수 버그 세 개를 찾아서 고쳤다.
운영 환경에 적용한다면 OTel 통합으로 통합 관측 스택에 묶고, sampling과 redaction을 신경 쓰면 된다. 개발 환경이라면 그냥 stdout 모드로 띄워두고 함수 한 번 호출해보는 것만으로도 차이를 느낄 수 있을 것이다.
혹시 Pipeline Inspector 운영 케이스 있으면 댓글 남겨주시면 좋겠다. 특히 trace storage 비용 절감 팁 공유 환영.