콘텐츠로 이동

40 · 검증 매트릭스

이 도구가 "검증됐다"고 말할 때 그 말이 무엇을 뜻하는지 정의한다. 정의는 문서가 아니라 internal/matrix/matrix.go 의 데이터이고, 이 문서는 그 데이터를 읽는 법과 돌리는 법을 적는다.

1. 왜 있는가

이 도구는 사람이 머릿속에 담을 수 있는 것보다 형태가 많다. 구축은 맨바닥에서 시작할 수도, 이미 있는 클러스터 위일 수도 있고, 노드는 1대이거나 여러 대이며, 데이터플레인 3종 · 인증서 모드 4종 · 게이트웨이 노출 2종 · 레지스트리 4종이 곱해진다. 전수 조합은 수백 가지고, 수백 번 구축하는 사람은 없다.

그래서 정직한 질문은 "다 돌렸는가"가 아니라 "몇 개를 돌리면 모든 값과, 실제로 깨졌던 모든 조합을 밟는가" 이다.

2026-08 에 시나리오 6개를 손으로 써서 돌렸더니 결함 13건이 나왔다. 그중에는 모든 신규 구축을 실패시키던 것(set -e 아래의 명령 치환)도 있었다. 결함들은 난해하지 않았다 — 아무도 걸어보지 않은 길이었을 뿐이다. 매트릭스는 그 길이 "아무도 안 걸어본 길"이 아니게 만드는 장치다.

2. 차원

차원
nodes 1, 2
dataplane cilium-gw, cilium-traefik, canal-traefik
pki none, private-ca, byo-cert
exposure node-ips, lb-pool, none
registry embedded, upstream
gitops true, false
network online, airgap
operation build, grow, resume, reapply, upgrade

operation 이 차원인 이유: 이번에 나온 결함 대부분은 첫 구축이 아니라 그 다음에 한 일에서 나왔다. 노드 증설, 중단 후 재개, 같은 문서 재적용은 각각 다른 코드 경로다.

network 가 차원인 이유: 문서에 단어 하나 바꿔 적는 것이 아니라 하네스가 실행 전에 노드의 egress 를 DROP 한다. 밖으로 나가려는 스텝은 고객사에서가 아니라 여기서 실패한다. 이 축 하나가 스토리지 이미지 다섯 개 누락, 프리플라이트가 자기 반입물을 거부하는 결함 등을 드러냈다 — 온라인에서는 전부 보이지 않는 것들이다.

3. 선정 규칙

케이스는 다음 두 조건을 만족해야 한다. 둘 다 internal/matrix 의 테스트가 하드웨어 없이 매 커밋 검사한다.

  1. 모든 차원의 모든 값이 최소 한 케이스에 등장한다 (Uncovered)
  2. RequiredPairs 의 모든 조합이 한 케이스 안에서 함께 등장한다 (UncoveredPairs)

2번이 핵심이다. 값별 커버리지만으로는 이번 교착을 못 잡는다 — 단일 노드도 통과하고 Gateway API 도 통과하는데, 둘이 만나면 cilium-operator 가 복제본 2를 한 노드에 놓지 못해 재시작이 영원히 안 끝났다. 그래서 실제로 깨졌던 조합은 목록으로 고정한다.

각 케이스에는 Why 가 필수다. 근거를 적지 않은 케이스는 다음에 누가 지운다.

4. 돌리는 법

scripts/build.sh                                   # 운영자가 쓰는 그 바이너리
NODE_PASSWORD=... scripts/matrix.sh                # 전 케이스
NODE_PASSWORD=... scripts/matrix.sh -run idc-single # 한 케이스

노드 주소 외에 지정하는 값들. 기본값이 문서용 대역(RFC 5737)인 것은 공개 저장소의 기본값이 실재하는 주소를 겨누지 않게 하기 위해서다 — 그대로 두면 kube-vip 이 붙을 인터페이스가 없어 VIP 케이스가 vip-interface 에서 멈춘다.

변수
MALMOK_LAB_VIP VIP 케이스가 쓸 주소. 노드와 같은 세그먼트의 빈 주소
MALMOK_LAB_LB_POOL 로드밸런서 풀 CIDR
MALMOK_LAB_AIRGAP_VERSION 노드에 반입해 둔 RKE2 릴리스. 없으면 에어갭 케이스는 무엇을 놓아야 하는지 말하고 skip 한다
MALMOK_LAB_MIRROR pull-through 캐시 주소. 기본 꺼짐 — 캐시를 통과한 초록은 캐시 없는 고객의 설치를 증명하지 않는다

에어갭 케이스는 두 가지를 노드에 미리 놓아야 한다. RKE2 릴리스 아티팩트와, 플랫폼 이미지 번들이다. 번들은 MALMOK_IMAGE_ARCHES=amd64 scripts/airgap-images.sh <tag> 로 만들어 아티팩트 옆에 둔다 — 케이스가 와이프 뒤에 /var/lib/rancher/rke2/agent/images/ 로 옮긴다. 미리 거기 넣어둘 수 없다: 와이프가 /var/lib/rancher 를 통째로 지운다.

  • 케이스마다 두 노드를 완전 초기화하고 재부팅한다. 따라서 파괴해도 되는 구간에만 겨눈다 (MALMOK_LAB_SERVER / MALMOK_LAB_AGENT 로 지정).
  • 재부팅은 장식이 아니다. rke2-uninstall 은 Cilium 이 NIC 에 붙인 tc/eBPF 프로그램을 걷어내지 않고, 남은 프로그램이 다음 구축의 노드 간 통신을 떨군다 — 포트 검사가 그것을 방화벽으로 보고한다.
  • 하네스는 빌드된 바이너리를 실행한다. 엔진 패키지를 직접 부르면 아무도 쓰지 않는 경로를 검증하게 된다.
  • RKE2 버전은 채널 서버에서 읽는다. 버전을 상수로 박은 스위트는 사람들이 실제로 설치하는 버전을 검증하지 않는다.
  • 인증서 자재는 매번 생성한다 (root → intermediate → wildcard leaf). 디스크에 둔 자재는 만료되고, 아무도 안 건드렸는데 반년 뒤 실패하는 스위트는 사람들이 믿지 않게 된다.

5. 케이스가 통과했다는 것

작업별로 하는 일이 다르다.

operation 하는 일
build 맨바닥에서 구축
grow 1노드로 구축 후, 문서에 에이전트를 추가해 재적용
resume l1-bootstrap/service 시작 시점에 SIGKILL, 같은 명령으로 재실행
reapply 구축 후 같은 문서를 다시 적용 — 바뀐 스텝이 0이어야 통과
upgrade 이전 마이너로 구축한 뒤 현재 릴리스로 이동 — 전 노드의 kubelet 이 새 버전을 보고해야 통과. stable 에서 출발하면 stable 과 latest 가 같은 릴리스를 가리키는 동안 매번 skip 했고, 실제로 매트릭스 전 실행에서 한 번도 돌지 않았다

그 뒤 공통 확인 (전부 운영자 계정에서 sudo 없이):

  • Ready 노드 수가 케이스가 말한 수와 같다
  • kubectl 이 PATH 에 있다 (kubeconfig 만 있고 도구가 없으면 그 클러스터는 구축한 기계에서 못 쓴다)
  • cilium-gw 케이스는 GatewayClass 가 Accepted
  • 모든 파드가 Running 또는 Completed 로 안정된다. Pending 은 통과가 아니다 — 이 도구가 요청했는데 클러스터가 주지 못한 파드다. 30초 간격으로 30회까지 기다린다: 세 초짜리 ContainerCreating 을 결함으로 보고하지 않기 위해서다

6. 매트릭스를 넓힐 때

  1. Values() 에 값을 추가한다 → 커버리지 테스트가 실패한다
  2. 그 값을 밟는 케이스를 Cases() 에 추가하고 Why 를 적는다
  3. 그 값이 다른 값과 만나야만 깨지는 성질이면 RequiredPairs() 에 넣는다

이 순서를 지키면 "값을 추가했는데 아무도 안 돌리는" 상태가 존재할 수 없다.

7. 아직 매트릭스 밖인 것

정직하게 적는다. 이것들은 하드웨어나 외부 서비스가 없어서 빠졌다.

  • 3서버 HA 조인 (l1-join-server) — 세 번째 VM 필요
  • 프록시 망 모드 — 프록시 필요. 폐쇄망은 network 축으로 들어왔다
  • acme-dns01 — 공인 DNS 와 ACME 계정 필요
  • external / internal 레지스트리 — Harbor · Hauler 필요
  • byo-csi — 현장 CSI 필요. 이 단계는 StorageClass 존재만 확인한다
  • longhorn · nfs 스토리지 — 이번 릴리스가 설치하지 않는다. 문서가 지정하면 StorageClass 없는 클러스터를 만드는 대신 실행을 멈춘다