[하이브리드 Kubernetes 구축기 1] 2024년, 홈랩에서 개인 클라우드 설계를 시작하다
집에서 쓰던 서버와 OCI를 함께 관리하려고 하이브리드 Kubernetes를 설계했다. 비용, CPU 아키텍처, 네트워크 경계와 etcd 배치를 어떻게 나눴는지 정리했다.
하이브리드 Kubernetes 구축기 1/6 · 다음 글 →
기록 시기: 2024년
2024년에는 집에서 쓰던 서버와 OCI의 저비용 ARM 인스턴스를 따로 관리하고 있었다. 개인 프로젝트가 늘어나니 배포 방법과 네트워크 경로도 제각각이 됐다. 홈랩은 계산 자원이 넉넉한 대신 가정용 회선 안에 있었고, OCI는 외부 서비스를 올리기 편한 대신 자원을 아껴 써야 했다. 이 둘을 함께 관리할 방법부터 고민했다.
각 환경의 장점을 살리면서 배포와 관리를 한곳에서 하고 싶었다. 사설망은 그대로 유지하고, 두 환경 사이에 필요한 통신만 연결하기로 했다.
이 글에는 그때 정한 설계 기준을 남겼다. RKE2 Control Plane을 실제로 구축하고 안정화한 과정은 2025년 기록인 2편에서 이어진다. 아래 5노드 표와 구성도는 이후 확장한 현재 구성을 설계 당시의 기준과 비교한 자료이며, 다섯 노드가 모두 2024년에 존재했다는 뜻은 아니다.
공개 글에서는 RKE2 토큰, 공인 NAT endpoint, 실제 bootstrap DNS 이름과 Tailnet 도메인을 노출하지 않는다. 이후 글에 나오는 .example 도메인과 임의 식별자는 공개 설명용 예시이며 실제 운영 값이 아니다.홈랩과 OCI를 함께 써야 했던 이유
서버마다 접속해 수작업으로 배포하는 일을 줄이고 싶었다. 함께 해결해야 할 조건은 다음과 같았다.
- Control Plane 한 대에 모든 상태가 집중되지 않도록 할 것
- OCI의 저비용 ARM64 자원과 온프레미스 AMD64 자원을 함께 사용할 것
- OCI 서버의 Kubernetes 통신을 불필요하게 인터넷이나 VPN으로 우회시키지 않을 것
- 온프레미스 공유기 내부의 서버를 공인 포트 포워딩 없이 클러스터에 참여시킬 것
- 운영자가 어디에 있든 동일한 이름으로 각 노드에 접속할 수 있을 것
- Pod 네트워크는 물리 위치와 CPU 아키텍처 차이를 넘어 하나의 클러스터처럼 동작할 것
이 요구사항을 기준으로 Control Plane과 etcd는 OCI에 3대로 분산하고, Worker는 OCI와 온프레미스에 각각 배치했다.
시리즈의 시간축은 다음처럼 구분한다.
| 구간 | 이 글에서 다루는 내용 |
|---|---|
| 2024년 | 홈랩·OCI 자원 정리, 네트워크 경계와 역할 배치 설계, RKE2 선택 |
| 2025년 | 3개 RKE2 server와 embedded etcd 구축·안정화 |
| 현재 구조 대조 | 설계가 확장된 5노드 구성과 실제 경로를 읽기 전용으로 재검증 |
당시 로그가 없는 부분은 날짜나 수치를 새로 만들지 않았다. 아래 CLI 예시는 2024년에 고려했던 항목을 현재 노드에서 다시 확인한 결과다.
설계가 도달한 현재 5개 노드
설계 이후 구축과 확장을 거친 현재 노드 구성이다. 2024년 당시의 장비 목록과는 구분해서 봐야 한다.
| 노드 | 위치 | 역할 | Node Internal-IP | Tailscale IP | 아키텍처 / 자원 |
|---|---|---|---|---|---|
core-a |
OCI | Control Plane + etcd | 10.0.0.3 |
100.123.123.1 |
ARM64 / 2 vCPU / 12 GB |
core-b |
OCI | Control Plane + etcd | 10.0.0.2 |
100.123.123.2 |
ARM64 / 2 vCPU / 12 GB |
core-c |
OCI | Control Plane + etcd | 10.0.1.3 |
100.123.123.3 |
ARM64 / 2 vCPU / 12 GB |
edge-a |
OCI | Worker | 10.0.1.2 |
100.123.123.4 |
ARM64 / 2 vCPU / 12 GB |
edge-b |
On-Prem | Worker | 192.168.0.188 |
100.123.123.5 |
AMD64 / 24 Core / 12 GB |
현재 Kubernetes는 v1.33.5+rke2r1, Cilium은 1.18.1을 사용한다. 이 버전과 상태는 2024년 당시 값이 아니라 현재 구성 검증 결과이며, 실제 RKE2 상태 확인은 다음 편에서 다룬다.
먼저 장비마다 CPU 아키텍처가 다르다는 점을 확인했다. 현재 노드에서 다시 조회해도 OCI는 ARM64, 온프레미스는 AMD64로 나뉜다.
$ for node in core-a core-b core-c edge-a edge-b; do
printf '%-8s ' "$node"
ssh user@"$node" 'printf "arch=%s cpu=%s mem=%s\n" \
"$(uname -m)" \
"$(nproc)" \
"$(free -g | awk '\''/^Mem:/ {print $2 "GiB"}'\'')"'
done
core-a arch=aarch64 cpu=2 mem=11GiB
core-b arch=aarch64 cpu=2 mem=11GiB
core-c arch=aarch64 cpu=2 mem=11GiB
edge-a arch=aarch64 cpu=2 mem=11GiB
edge-b arch=x86_64 cpu=24 mem=11GiB
메모리 출력은 운영체제가 GiB 단위로 내림해 표시한 값이므로 표의 할당 사양인 12 GB와 달라 보일 수 있다. 더 중요한 문제는 아키텍처다. 이후 Kubernetes에서 사용할 컨테이너 이미지는 linux/arm64와 linux/amd64를 모두 지원해야 하고, 그렇지 않으면 nodeSelector나 affinity로 실행 노드를 제한해야 한다는 요구사항이 여기서 나왔다.
전체 아키텍처
논리 구조를 단순화하면 다음과 같다.
flowchart LR
ADMIN[관리 PC] -. Tailscale SSH .-> TS[Tailscale Mesh]
subgraph OCI[OCI]
subgraph NET_A[사설망 10.0.0.0/24]
CA[core-a<br/>10.0.0.3<br/>Control Plane + etcd]
CB[core-b<br/>10.0.0.2<br/>Control Plane + etcd]
end
subgraph NET_B[사설망 10.0.1.0/24]
CC[core-c<br/>10.0.1.3<br/>Control Plane + etcd]
EA[edge-a<br/>10.0.1.2<br/>Worker]
end
NET_A <-->|OCI 사설 연결 / LPG| NET_B
end
subgraph HOME[On-Premises]
EB[edge-b<br/>192.168.0.188<br/>Worker]
end
TS -. 관리 경로 .-> CA
TS -. 관리 경로 .-> CB
TS -. 관리 경로 .-> CC
TS -. 관리 경로 .-> EA
TS -. 관리 경로 .-> EB
EB == Tailscale WireGuard<br/>Subnet Routing ==> CB
EB == Tailscale WireGuard<br/>Subnet Routing ==> CC
CILIUM[Cilium VXLAN<br/>Pod Network] --- CA
CILIUM --- CB
CILIUM --- CC
CILIUM --- EA
CILIUM --- EB
이 다이어그램에서 실선과 점선을 구분해야 한다. OCI의 core-a, core-b, core-c, edge-a가 Kubernetes 트래픽을 주고받을 때는 OCI 사설망을 사용한다. 모든 노드에 Tailscale을 설치했지만, OCI 내부의 Kubernetes 트래픽까지 Tailscale로 보내지는 않았다.
온프레미스의 edge-b만 OCI 사설 대역에 직접 연결되어 있지 않다. 그래서 edge-b가 10.0.0.0/24와 10.0.1.0/24에 접근할 때는 Tailscale의 subnet route를 사용한다. 현재 선택된 경로는 각각 core-b, core-c가 광고하는 더 구체적인 /24 경로다.
$ ip route get 10.0.0.2
10.0.0.2 dev tailscale0 table 52 src 100.123.123.5
$ ip route get 10.0.1.3
10.0.1.3 dev tailscale0 table 52 src 100.123.123.5
반면 OCI 노드에서 다른 OCI 사설 대역으로 가는 경로는 일반 NIC를 사용한다.
$ ip route get 10.0.1.3
10.0.1.3 via 10.0.0.1 dev enp0s6 src 10.0.0.3
OCI 내부에서는 사설망을 기본 경로로 쓰고, 온프레미스에서 그 사설망에 접근하는 구간만 Tailscale subnet routing으로 연결했다.
왜 RKE2를 선택했나
kubeadm으로 구성 요소를 직접 설치하는 방법도 고려했다. 이후 업그레이드와 운영을 반복할 때 관리할 항목을 줄이고 싶어서, 2024년 설계 단계에서 RKE2를 후보로 정했다.
RKE2를 선택한 이유는 다음과 같다.
- Control Plane 구성 요소와 containerd를 일관된 방식으로 배포할 수 있다.
- server와 agent 역할이 명확해 노드별 운영 절차를 단순화할 수 있다.
- embedded etcd를 이용해 별도 etcd 배포판을 조립하지 않아도 된다.
- Cilium 같은 CNI를 RKE2 설정으로 선택하고 함께 관리할 수 있다.
- 보안 강화와 FIPS 사용을 위한 선택지를 제공한다.
다만 FIPS는 별도 확인이 필요했다. RKE2의 FIPS 지원 문서에 나온 기능을 사용한다고 클러스터 전체가 FIPS 준수 상태가 되지는 않는다. 공식 문서에서 번들 CNI 중 FIPS 준수용으로 다시 빌드하는 것은 Canal이다. 현재 구성은 Cilium을 쓰며 전체 구성의 준수 여부는 검증하지 않았다.
실제 RKE2 server/agent 서비스 구성과 버전 검증은 2025년 구축 기록인 다음 편으로 넘긴다.
왜 3개의 Control Plane을 OCI에 두었나
설계 단계에서는 core-a, core-b, core-c가 Control Plane과 etcd 역할을 함께 수행하도록 정했다. etcd는 합의에 과반수의 멤버가 필요하므로 3개 멤버에서는 2개가 quorum이다. 구조적으로는 한 멤버의 장애를 허용할 수 있다.
온프레미스의 edge-b에 etcd를 배치하지 않은 이유는 네트워크 경계를 합의 경로에 넣고 싶지 않았기 때문이다. 가정용 회선이나 공유기, Tailscale 경로에 문제가 생기더라도 Control Plane의 etcd quorum은 OCI 내부에서 유지되는 편이 운영상 단순하다. edge-b가 큰 CPU 자원을 제공하더라도, compute 자원과 control-plane 상태 저장소의 배치는 별개의 문제로 봤다.
한 멤버 장애를 허용하는 구성까지 설계했지만, 실제 장애 전환 시간은 아직 측정하지 않았다. 노드를 중단했을 때 API 재연결, 진행 중인 요청, 워크로드 복구에 걸리는 시간은 장애 주입 시험이 필요하다. 다음 글에서는 2025년에 구축한 RKE2와 3-member etcd의 현재 상태를 확인한다.
Tailscale은 두 가지 역할만 맡긴다
이 구성에서 Tailscale의 역할은 크게 두 가지다.
1. 관리용 SSH
각 노드는 Tailscale MagicDNS 이름으로 접속할 수 있고 Tailscale SSH가 활성화되어 있다.
$ for node in core-a core-b core-c edge-a edge-b; do
printf '%-8s ' "$node"
ssh user@"$node" 'tailscale ip -4'
done
core-a 100.123.123.1
core-b 100.123.123.2
core-c 100.123.123.3
edge-a 100.123.123.4
edge-b 100.123.123.5
실제 Tailnet suffix와 ACL 정책은 공개하지 않았다. 외부에 22번 포트를 열지 않고, Tailnet 정책 안에서 같은 호스트 이름으로 접속해 관리했다.
2. 온프레미스와 OCI 사설망 사이의 underlay
edge-b는 Control Plane이 사용하는 10.0.0.0/24, 10.0.1.0/24에 Tailscale subnet route로 접근한다. 이 경로 위로 RKE2 supervisor/API 트래픽과 edge-b 구간의 Cilium VXLAN 패킷이 이동한다.
즉 edge-b의 Pod 패킷은 개념적으로 다음처럼 캡슐화된다.
Pod packet
-> Cilium VXLAN
-> node underlay packet
-> Tailscale WireGuard
-> Internet
-> OCI subnet router
오버레이가 중첩되므로 MTU를 무시하면 큰 패킷에서 단편화나 연결 이상을 만나기 쉽다. 현재 Cilium과 Tailscale 인터페이스 MTU는 1280으로 맞췄고, 원격 Pod 경로는 캡슐화 오버헤드를 반영해 더 작은 값을 사용한다. 이 부분은 Cilium 데이터 경로를 다루는 글에서 별도로 확인할 예정이다.
2024년 설계에서 확정한 원칙
2024년에는 아래 배치 원칙과 검증 항목을 정했다.
- Control Plane과 etcd quorum은 OCI 내부에 둔다.
- OCI 노드 사이의 Kubernetes underlay는 OCI 사설망을 사용한다.
- 온프레미스와 OCI 경계, 그리고 관리 SSH에는 Tailscale을 사용한다.
- Kubernetes Node IP는 각 환경의 사설 IP를 사용하고 Tailscale IP와 역할을 분리한다.
- ARM64와 AMD64가 섞인다는 사실을 이미지 빌드와 스케줄링 정책에 반영한다.
- Pod 네트워크 오버레이와 Tailscale이 중첩되는 구간에서는 MTU를 별도로 검증한다.
5개 노드의 Ready, RKE2 server/agent 역할, embedded etcd membership 같은 실제 구현 상태는 2025년 구축 이후의 현재 검증 값이므로 2편부터 다룬다.
이 설계와 현재 상태 조회로 확인하지 못한 항목도 남아 있다.
- 부하 테스트를 하지 않아 성능 향상 폭은 확인하지 못했다.
- 장애 주입을 하지 않아 무중단 HA 여부는 확인하지 못했다.
- 현재 Cilium 자체 암호화는 비활성 상태다. 전체 통신의 암호화 범위를 별도로 확인해야 한다.
- RKE2를 사용하지만 클러스터 전체의 FIPS 준수 여부는 검증하지 않았다.
마무리
OCI 내부 통신은 사설망에 두고, 온프레미스와 연결하는 구간에 Tailscale을 사용하기로 했다. etcd는 OCI 안에 배치하고 홈랩의 계산 자원은 Worker로 사용했다. 이후 4대의 ARM64 노드와 1대의 AMD64 노드를 구성할 때도 이 기준을 유지했다.
다음 편에서는 core-a, core-b, core-c의 3-member embedded etcd 구성을 확인하고, quorum이 의미하는 범위와 RKE2의 DNS bootstrap endpoint가 실제로 어떤 역할을 하는지 CLI 출력과 함께 살펴본다.
하이브리드 Kubernetes 구축기 1/6 · 다음 글 →