Kubernetes 2025.04.24 16 min read

[하이브리드 Kubernetes 구축기 6] “5대 모두 Ready” 다음에 확인할 것: 운영 검증과 회고

5노드 구축 후 etcd membership, Cilium, metrics와 Tailscale 경로를 확인했다. 상태 조회로 확인한 결과와 아직 수행해야 할 장애·복구 시험을 나눠 정리했다.

하이브리드 Kubernetes 구축기 6/6 · ← 이전 글


기록 시기: 2025년
이 글은 2025년 구축·운영 기록을 기준으로 정리했다. CLI 출력은 포트폴리오 문서화를 위해 일부 열을 생략했다. *.example, 198.51.100.0/24, 100.64.0.0/24, 합성 member ID·이름·시각·리소스 수치는 공개용 예시이며 실제 환경과 무관하다. 노드 운영 기간은 검증 지표로 사용하지 않았다. 명령 블록의 $는 일반 사용자 프롬프트이며 입력하지 않는다. 운영체제 관리자 권한이 필요한 명령에만 sudo를 표시했다.

클러스터를 만든 뒤 kubectl get nodes에서 다섯 줄의 Ready를 확인했다. 다만 Ready 상태만으로 etcd quorum, Pod 통신, DNS나 백업 복구까지 확인할 수는 없었다.

구축 후 점검에서 확인한 결과남은 검증 항목을 정리했다. RTO, RPO와 성능 향상 폭은 장애 시험과 반복 측정 후에 기록하기로 했다.

검증 대상

구성은 control plane 3대와 worker 2대다.

노드 위치 역할 Kubernetes underlay
core-a OCI control-plane, etcd OCI 사설망 10.0.0.0/24
core-b OCI control-plane, etcd OCI 사설망 10.0.0.0/24
core-c OCI control-plane, etcd OCI 사설망 10.0.1.0/24
edge-a OCI worker OCI 사설망 10.0.1.0/24
edge-b On-Premise worker 사설 LAN + Tailscale subnet routing

OCI 네 노드끼리의 Kubernetes 트래픽은 OCI 사설망과 LPG를 사용한다. Tailscale은 모든 노드의 관리 SSH와 edge-b ↔ OCI private subnet underlay에 사용한다. Cilium VXLAN은 그 경로 위에서 Pod 패킷을 운반한다.

1. 노드 상태: 다섯 대가 모두 Ready인가

노드 목록과 버전부터 확인했다.

$ kubectl get nodes -o wide
# AGE와 민감 주소 열 생략
NAME     STATUS   ROLES                       VERSION          INTERNAL-IP
core-a   Ready    control-plane,etcd,master   v1.33.5+rke2r1   10.0.0.3
core-b   Ready    control-plane,etcd,master   v1.33.5+rke2r1   10.0.0.2
core-c   Ready    control-plane,etcd,master   v1.33.5+rke2r1   10.0.1.3
edge-a   Ready    -                           v1.33.5+rke2r1   10.0.1.2
edge-b   Ready    -                           v1.33.5+rke2r1   192.168.0.188

다섯 kubelet이 현재 control plane에 상태를 보고하고 있고 버전도 일치했다. 애플리케이션이 장애를 견디는지는 이 조회로 확인하지 못했다.

2. etcd: 세 Pod가 아니라 세 voter인가

etcd Pod 개수와 실제 membership을 함께 확인했다.

$ kubectl -n kube-system get pods -l component=etcd -o wide
NAME          READY   STATUS    IP         NODE
etcd-core-a   1/1     Running   10.0.0.3   core-a
etcd-core-b   1/1     Running   10.0.0.2   core-b
etcd-core-c   1/1     Running   10.0.1.3   core-c
$ kubectl -n kube-system exec etcd-core-a -- \
  etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/var/lib/rancher/rke2/server/tls/etcd/server-ca.crt \
  --cert=/var/lib/rancher/rke2/server/tls/etcd/server-client.crt \
  --key=/var/lib/rancher/rke2/server/tls/etcd/server-client.key \
  member list -w table
+------------+---------+-----------------+-----------------------+------------+
| ID         | STATUS  | NAME            | PEER ADDRS            | IS LEARNER |
+------------+---------+-----------------+-----------------------+------------+
| 1a2b3c4d5e6f7001 | started | core-a-a1b2c3d4 | https://10.0.0.3:2380 | false      |
| 1a2b3c4d5e6f7002 | started | core-b-b2c3d4e5 | https://10.0.0.2:2380 | false      |
| 1a2b3c4d5e6f7003 | started | core-c-c3d4e5f6 | https://10.0.1.3:2380 | false      |
+------------+---------+-----------------+-----------------------+------------+

세 멤버가 모두 started이고 learner가 아니라는 사실을 확인했다. 이 구성은 정상적인 3-member quorum의 전제는 충족하지만, 실제로 한 멤버를 중단했을 때 API가 얼마나 빨리 회복되는지까지 증명하지는 않는다. 또한 logical corruption이나 세 노드 동시 손실에는 etcd snapshot이 필요하다.

3. Cilium과 Hubble: DaemonSet 수보다 데이터 경로를 본다

모든 노드에 Cilium agent가 하나씩 준비됐는지, operator와 Hubble이 준비됐는지 확인했다.

$ kubectl -n kube-system get daemonset cilium
$ kubectl -n kube-system get deployment cilium-operator hubble-relay hubble-ui
NAME     DESIRED   CURRENT   READY   AVAILABLE
cilium   5         5         5       5

NAME              READY   UP-TO-DATE   AVAILABLE
cilium-operator   2/2     2            2
hubble-relay      1/1     1            1
hubble-ui         1/1     1            1

agent 내부 상태도 확인했다.

$ kubectl -n kube-system exec daemonset/cilium -c cilium-agent -- \
  cilium-dbg status
Kubernetes:            Ok
KubeProxyReplacement:  True
Cilium:                Ok   1.18.1
Cilium health daemon:  Ok
Routing:               Network: Tunnel [vxlan]   Host: BPF
Hubble:                Ok
Cluster health:        5/5 reachable
Encryption:            Disabled

Cilium 5/5, Hubble Ok, Cluster health 5/5를 확인했다. Encryption: Disabled도 함께 확인했다. OCI 내부의 Cilium VXLAN 자체는 암호화되지 않았다. Tailscale WireGuard 암호화는 온프레미스와 OCI 사이에서 Tailscale peer 구간에 적용된다.

다음 단계에서는 Cilium이 제공하는 실제 연결성 테스트를 별도 namespace에서 실행해 DNS, service, NetworkPolicy 경로를 반복 검증해야 한다.

# 테스트 리소스를 생성하므로 홈랩/검증 namespace에서만 실행한다.
$ cilium connectivity test --test-concurrency 1

이 글에서는 위 연결성 테스트의 성공률과 소요 시간을 측정하지 않았다. 실행할 때는 일시, Cilium 버전, 실패한 케이스와 원본 출력을 함께 남길 계획이다.

4. 리소스 메트릭: Ready와 관측 가능성은 다르다

$ kubectl top nodes

문서화 과정의 첫 점검에서는 core-b의 metric이 잠시 비어 있었지만, 짧은 간격을 두고 다시 조회하자 다섯 노드 모두 반환됐다. CPU와 메모리 값은 시점에 따라 달라 공개 출력에서는 범위만 남겼다.

NAME     CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)
core-a   342m         17%      3880Mi          32%
core-b   287m         14%      3650Mi          30%
core-c   301m         15%      3725Mi          31%
edge-a   418m         20%      4760Mi          39%
edge-b   136m         0%       3290Mi          27%

원본 metrics API에서도 core-bNodeMetrics가 정상 반환되는 것을 재확인했다.

$ kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes/core-b
{
  "kind": "NodeMetrics",
  "metadata": {"name": "core-b"},
  "usage": {"cpu": "312345678n", "memory": "4038656Ki"}
}

첫 조회에서 값 누락과 NotFound가 나타났지만 재조회에서는 정상으로 돌아왔다. 수집 주기 사이의 일시적인 공백으로 판단했다. 반복된다면 metrics-server 로그, APIService availability, 해당 노드의 kubelet 인증서와 10250/TCP 경로를 확인해야 한다.

$ kubectl get apiservice v1beta1.metrics.k8s.io
$ kubectl -n kube-system logs deploy/rke2-metrics-server --since=30m
$ kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes

이번 재조회에서는 다섯 노드가 모두 정상 응답했다. 장기간 안정적으로 수집되는지는 시계열의 수집 성공률과 alert 이력을 더 봐야 한다.

5. Tailscale: “설치됨”이 아니라 실제 경로를 확인한다

관리 SSH와 온프레미스 underlay가 실제로 Tailscale을 통과하는지 확인했다.

$ tailscale status
$ tailscale ping edge-b
pong from edge-b (100.64.0.5) via 198.51.100.24:41641 in 11ms

이 출력은 특정 시점에 DERP relay가 아니라 direct UDP 경로가 성립했다는 뜻이다. 11ms는 단일 관측값일 뿐 SLA나 평균 지연시간이 아니다. NAT 상태가 달라지면 DERP로 전환될 수 있으므로 반복 측정과 경로 변화 기록이 필요하다.

온프레미스 edge-b에서 OCI 사설 IP로 가는 경로도 함께 본다.

$ ip route get 10.0.0.3
$ tailscale ping core-a
$ nc -vz 10.0.0.3 9345
$ nc -vz 10.0.1.3 6443
Connection to 10.0.0.3 9345 port [tcp/*] succeeded!
Connection to 10.0.1.3 6443 port [tcp/*] succeeded!

이 검증으로 edge-b → Tailscale subnet router → OCI private subnet → RKE2 endpoint 경로가 열린 것은 확인할 수 있다. 반대 방향의 Pod 트래픽과 subnet router 장애 시 route 철회·blackhole 여부는 별도 장애 시험 대상이다.

6. 이제 해야 할 failure test

상태 조회 뒤에 남은 장애 시험은 아래와 같다. 모두 아직 계획 또는 반복 측정이 필요한 항목이다.

주의: 아래 테스트는 노드와 control plane을 실제로 중단한다. 홈랩 또는 격리된 검증 환경에서만 수행하고, etcd snapshot·애플리케이션 백업·out-of-band 콘솔·복구 명령·관찰용 외부 단말을 먼저 준비한다. 한 번에 하나의 fault만 주입한다. SSH가 Tailscale에 의존한다면 tailscaled 중단 전에 반드시 별도 콘솔을 확보한다.

6-1. control plane 한 대 중단

사전 상태를 저장한다.

$ sudo rke2 etcd-snapshot save --name pre-control-plane-test
$ kubectl get nodes
$ kubectl -n kube-system get pods -l component=etcd

외부 관찰 단말에서는 API ready endpoint를 짧은 간격으로 기록한다.

$ while true; do
  date -Is
  kubectl --request-timeout=3s get --raw=/readyz || true
  sleep 1
done

콘솔이 확보된 상태에서 control plane 한 대만 중단한다.

# 장애 주입 대상에서 실행. 나머지 두 etcd 멤버가 정상인지 먼저 확인한다.
$ sudo systemctl stop rke2-server

# 테스트 종료 후 반드시 복구
$ sudo systemctl start rke2-server

측정할 값은 다음과 같다.

  • 마지막 성공 요청과 첫 실패 요청 시각
  • 첫 실패와 안정적인 연속 성공이 시작된 시각
  • etcd leader 변경 여부와 member 복귀 시간
  • 테스트 동안 발생한 API 오류율

6-2. worker 한 대 중단과 Pod 재스케줄

테스트 애플리케이션은 replica 3개, readiness probe, PodDisruptionBudget, 여러 노드에 퍼지도록 topology spread를 먼저 구성한다.

$ kubectl -n portfolio-test get deploy,pod,pdb -o wide
$ kubectl -n portfolio-test get pods -w -o wide
# 홈랩 worker 한 대에서만 실행
$ sudo systemctl stop rke2-agent

# 테스트 종료 후 복구
$ sudo systemctl start rke2-agent

외부 부하 발생기에서 요청 성공률과 p95 latency를 동시에 기록해야 “재스케줄됐다”를 사용자 관점의 복구로 연결할 수 있다. ARM64 OCI worker와 AMD64 온프레미스 worker가 섞여 있으므로, 이미지가 multi-arch인지도 사전에 확인한다.

$ docker buildx imagetools inspect registry.example/portfolio/echo-server:1.0.0

6-3. Tailscale subnet router 장애

현재 core-b/core-c가 광고하는 더 구체적인 /24edge-a가 광고하는 /16은 서로 다른 prefix다. longest-prefix match에서 /24가 우선될 뿐, 서로 다른 prefix 사이에는 동일-prefix HA의 failover 관계가 없다. /16/24 장애 시 자동 승계하는 예비 route로 취급해서는 안 된다. 광고 주체가 사라지는 동안 경로 철회가 지연되거나 의도하지 않은 경로가 남아 blackhole이 생길 수 있으므로, 중첩 route와 장애 동작을 ADR에 명시해야 한다.

한 router를 중단했을 때 route가 언제 철회되고 실제 도달성이 어떻게 변하는지 다음 명령으로 기록한다.

$ tailscale status
$ tailscale ping core-b
$ ip route get 10.0.0.3
$ nc -vz 10.0.0.3 9345

Tailscale을 원격 SSH 세션에서 바로 중단하면 복구 경로도 잃는다. OCI 콘솔과 온프레미스 하이퍼바이저 콘솔을 동시에 확보하고, 승인된 route 한 개만 내린 뒤 route 철회 시간, blackhole 구간과 RKE2 연결 회복 시간을 측정한다.

subnet router HA가 목표라면 /16을 예비 경로처럼 두는 것이 아니라 동일한 /24 prefix를 둘 이상의 router가 광고하도록 설계해야 한다. 예를 들어 10.0.0.0/2410.0.1.0/24 각각에 대해 동일 prefix의 복수 광고자를 두고, Tailscale의 route 선택과 장애 전환을 실제로 검증해야 한다.

6-4. etcd snapshot 복원 리허설

스냅샷을 만든 뒤에는 격리 환경에서 실제로 복원해봐야 한다.

$ sudo rke2 etcd-snapshot list
$ sudo sha256sum \
  /var/lib/rancher/rke2/server/db/snapshots/pre-control-plane-test-example

실제 복원은 운영 클러스터에 덮어쓰지 않고 격리 환경에서 수행한다. 복원 후에는 Kubernetes object 수, 핵심 Secret의 존재 여부, 애플리케이션의 데이터 일관성을 검증한다. 스냅샷에는 Secret이 들어 있으므로 포트폴리오 저장소에는 파일 자체가 아니라 시각, 크기, checksum, 복원 결과만 공개한다.

7. RTO와 RPO: 현재 숫자를 쓰지 않는 이유

이번 점검에서는 장애부터 복구까지의 시간과 데이터 손실 범위를 측정하지 않았다.

장애 시나리오 현재 확인한 사실 아직 필요한 측정 현재 표기
control plane 1대 장애 etcd 3 voter 구성 API 오류 구간, leader 전환, 안정화 시간 RTO 미측정
worker 1대 장애 worker 2대가 Ready Pod 재스케줄 시간, 요청 오류율 RTO 미측정
subnet router 장애 정상 시 Tailscale 경로 도달 route 철회·blackhole 구간, 동일 /24 다중 광고 구성 후 전환 시간 RTO 미측정
etcd 데이터 손상 snapshot 명령과 멤버 상태 확인 격리 복원 시간과 데이터 검증 RTO/RPO 미측정
상태 저장 애플리케이션 장애 워크로드별로 다름 볼륨·DB 백업 주기와 복원 시험 워크로드별 정의 필요

RPO를 확인하려면 마지막으로 복원 가능한 백업을 만든 시각과 데이터 저장 방식을 알아야 한다. RTO는 Pod가 Running으로 바뀐 시각에서 끝내지 않고, 사용자가 안정적으로 정상 응답을 받기 시작한 시각까지 측정할 계획이다.

장애 시험을 세 번 이상 반복해 중앙값과 최악값을 기록할 계획이다. 그전에는 목표값을 실측값처럼 사용하지 않는다.

RTO target: 아직 정의하지 않음
RTO observed: 반복 장애 시험 전이므로 미측정
RPO target: 워크로드별 정의 예정
RPO observed: 복원 시험 전이므로 미측정

8. 현재 구조의 한계

현재 운영에서 남아 있는 제약은 다음과 같다.

  • edge-b는 온프레미스의 단일 물리 장애 영역에 있다. 전원, ISP, 하이퍼바이저 장애가 함께 영향을 줄 수 있다.
  • ARM64와 AMD64가 섞여 있어 모든 이미지가 어느 worker에서나 실행되는 것은 아니다. multi-arch manifest 또는 nodeSelector가 필요하다.
  • Cilium VXLAN 암호화는 비활성화돼 있다. 온프레미스 구간의 Tailscale WireGuard 보호를 클러스터 전체 암호화로 일반화할 수 없다.
  • Tailscale control plane, NAT와 DERP 경로에 외부 의존성이 있다.
  • /24/16 중첩 광고는 longest-prefix match일 뿐 HA pair가 아니다. 서로 다른 prefix 사이의 failover를 기대하면 blackhole이 생길 수 있으며, HA에는 동일 /24의 다중 광고와 장애 시험이 필요하다.
  • 3-member etcd는 한 멤버 장애를 견딜 수 있는 quorum 구조지만, 지역 전체 장애나 잘못된 변경, 데이터 손상에 대한 백업은 별개다.
  • kubectl top은 재조회에서 다섯 노드 모두 정상 반환됐지만, 순간적인 수집 공백은 Ready에 드러나지 않는다. 장기 안정성은 반복 조회와 시계열로 판단해야 한다.
  • eBPF 전환 전후 부하 테스트는 하지 않았다. kube-proxy replacement와 eBPF datapath를 적용한 것은 확인했다.

9. 공개 포트폴리오에 남길 증거

나중에 결과를 다시 확인할 수 있도록 명령과 출력, 검증 조건을 함께 남기려고 한다.

docs/
├─ architecture/
│  ├─ physical-network.md
│  └─ kubernetes-data-path.md
├─ adr/
│  ├─ 001-rke2-and-cilium.md
│  ├─ 002-tailscale-subnet-routing.md
│  └─ 003-mtu-and-vxlan.md
├─ incidents/
│  ├─ dns-boot-race.md
│  ├─ etcd-learner.md
│  ├─ cilium-bootstrap.md
│  └─ host-firewall.md
├─ evidence/
│  └─ 2025/
│     ├─ nodes-sanitized.txt
│     ├─ etcd-members-sanitized.txt
│     ├─ cilium-status-sanitized.txt
│     └─ failure-test-summary.md
└─ runbooks/
   ├─ node-rejoin.md
   ├─ etcd-snapshot-restore.md
   └─ tailscale-route-failure.md

앞으로의 시험 기록에는 아래 항목을 포함할 계획이다.

  • 실행한 명령과 도구 버전
  • 테스트 전제 조건과 성공 기준
  • 민감 정보를 제거한 원본 출력
  • 실패한 시도와 수정 사항
  • 관찰 시작·종료 시각
  • RTO/RPO 계산 방식
  • 재실행 가능한 Ansible, manifest 또는 Runbook

반대로 다음 항목은 공개하지 않는다.

  • /etc/rancher/rke2/rke2.yaml 원본
  • server/agent token과 kubeconfig client key
  • Tailscale auth key, ACL 내부 식별자, tailnet suffix
  • 공인 IP·NAT endpoint와 방화벽 전체 규칙
  • etcd snapshot, Secret과 실제 데이터

공개 화면에는 다음 정도만 남긴다.

$ kubectl get nodes                         # 5/5 Ready
$ kubectl -n kube-system exec etcd-core-a -- \
  etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/var/lib/rancher/rke2/server/tls/etcd/server-ca.crt \
  --cert=/var/lib/rancher/rke2/server/tls/etcd/server-client.crt \
  --key=/var/lib/rancher/rke2/server/tls/etcd/server-client.key \
  member list -w table                      # 3 voters, learner 0
$ kubectl -n kube-system get ds cilium      # 5/5 Ready
$ kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg status
$ tailscale ping edge-b                     # 공개 예시는 합성 endpoint 사용
$ kubectl top nodes                         # 5개 정상, 일시 공백은 재조회 이력과 함께 기록

마무리

이번 점검에서 확인한 결과다.

  • RKE2 노드 5대가 한 클러스터에 합류했고 모두 Ready다.
  • control plane 3대의 embedded etcd가 voter 3개로 구성돼 있다.
  • Cilium agent 5개, operator, Hubble Relay/UI가 준비 상태이며 kube-proxy replacement와 VXLAN datapath가 동작한다.
  • 온프레미스 worker는 Tailscale subnet routing을 통해 OCI 사설망의 control plane에 도달한다.
  • 장애 대응 과정과 남은 관측 공백을 재현 가능한 CLI로 설명할 수 있다.

아래 항목은 아직 검증하지 않았다.

  • 장애 주입으로 측정하지 않은 RTO와 RPO
  • 반복 부하 테스트로 검증하지 않은 성능 향상
  • Cilium VXLAN 전체 구간의 암호화
  • 모든 장애 시나리오에서의 무중단

노드와 구성 요소의 현재 상태는 확인했다. 다음에는 장애를 하나씩 주입하고 복구 과정을 시간순으로 기록해, 문서에 남은 미측정 항목에 실측값을 채울 계획이다.


하이브리드 Kubernetes 구축기 6/6 · ← 이전 글


SEARCH JOURNAL

기록 검색

글 제목, 요약, 태그에서 찾습니다.

검색을 준비하고 있어요.

검색어를 입력해 주세요.

    Ctrl/⌘ K 검색 Esc 닫기 결과 이동