[Kubernetes] Cluster: RKE2를 이용해 클러스터 구성하기 - 1.2. 서버 노드 설치 결과 확인
서종호(가시다)님의 On-Premise K8s Hands-on Study 7주차 학습 내용을 기반으로 합니다.
TL;DR
이번 글의 목표는 RKE2 서버 노드에서 생성된 데이터를 상세히 확인하는 것이다.
- 디렉터리:
/var/lib/rancher/rke2/의 agent, data, server 구조 - Content Bootstrap: runtime 이미지에서 추출된 바이너리와 Helm chart
- Static pod manifest: kubeadm과 다른 경로, 보안 플래그 차이
- 보안 설정: 암호화, mTLS liveness probe, TLS cipher suite 등 RKE2 기본 적용 항목
- Helm Controller: HelmChart, HelmChartConfig CRD 동작 확인
들어가며
이전 글에서 RKE2 서버 노드를 설치하고 서비스를 시작했다. 이번 글에서는 그 결과로 만들어진 파일들을 하나씩 살펴본다.
RKE2의 아키텍처를 이해하는 데 가장 좋은 방법은 실제 파일을 직접 확인하는 것이다. 특히 표준 Kubernetes(kubeadm)와 비교하면 RKE2가 어떤 부분을 다르게 처리하는지 명확하게 드러난다. “hardened by default”라는 RKE2의 보안 철학이 실제 설정 파일에 어떻게 반영되어 있는지 확인한다.
디렉터리 구조
전체 구조
서비스가 정상적으로 기동된 후 /var/lib/rancher/rke2/ 최상위 구조를 보자.
tree /var/lib/rancher/rke2 -L 1
/var/lib/rancher/rke2
├── agent
├── bin -> /var/lib/rancher/rke2/data/v1.33.8-rke2r1-1b2872361ec5/bin
├── data
└── server
각 디렉터리의 역할은 아래와 같다.
| 디렉터리 | 역할 | 존재 노드 |
|---|---|---|
| agent/ | 노드 런타임 데이터 (containerd, kubelet, static pod) | 모든 노드 |
| bin/ | 현재 활성 버전 바이너리 symlink → data/ | 모든 노드 |
| data/ | runtime 이미지 추출 원본 (바이너리 + Helm chart) | 모든 노드 |
| server/ | 컨트롤 플레인 전용 (인증서, etcd, helm manifests, 토큰) | 서버 노드만 |
bin/이 symlink라는 점이 흥미롭다. 실체는 data/${RKE2_DATA_KEY}/bin/을 가리키고, bin/은 편의 경로다. 업그레이드 시 새 버전의 바이너리가 data/ 아래에 새 디렉터리로 추출되고, bin/ symlink만 새 버전을 가리키도록 전환된다. 기존 버전 디렉터리는 그대로 남는다.
data/
Content Bootstrap 단계에서 rke2-runtime 컨테이너 이미지로부터 추출된 원본 아티팩트가 저장된다.
tree /var/lib/rancher/rke2/data
/var/lib/rancher/rke2/data
└── v1.33.8-rke2r1-1b2872361ec5
├── bin
│ ├── containerd
│ ├── containerd-shim-runc-v2
│ ├── crictl
│ ├── ctr
│ ├── kubectl
│ ├── kubelet
│ └── runc
└── charts
├── harvester-cloud-provider.yaml
├── harvester-csi-driver.yaml
├── rancher-vsphere-cpi.yaml
├── rancher-vsphere-csi.yaml
├── rke2-calico-crd.yaml
├── rke2-calico.yaml
├── rke2-canal.yaml
├── rke2-cilium.yaml
├── rke2-coredns.yaml
├── rke2-flannel.yaml
├── rke2-ingress-nginx.yaml
├── rke2-metrics-server.yaml
├── rke2-multus.yaml
├── rke2-runtimeclasses.yaml
├── rke2-snapshot-controller-crd.yaml
├── rke2-snapshot-controller.yaml
├── rke2-snapshot-validation-webhook.yaml
├── rke2-traefik-crd.yaml
└── rke2-traefik.yaml
디렉터리 이름 v1.33.8-rke2r1-1b2872361ec5가 RKE2_DATA_KEY다. {버전태그}-{runtime 이미지 참조의 SHA256 해시 앞 12자리} 형식이다. 이 해시가 붙는 이유는 업그레이드 시 버전별 아티팩트를 격리하기 위해서다.
charts/에는 19개의 HelmChart YAML이 있다. RKE2가 지원하는 모든 CNI 옵션(canal, calico, cilium, flannel), ingress, snapshot controller, traefik 등이 포함된다. config.yaml의 disable 목록과 CNI 선택에 따라 이 중 일부만 server/manifests/로 복사된다.
어떤 runtime 이미지에서 추출됐는지는 다음으로 확인한다:
cat /var/lib/rancher/rke2/agent/images/runtime-image.txt
# index.docker.io/rancher/rke2-runtime:v1.33.8-rke2r1
# containerd에 import된 이미지로도 확인
crictl images | grep rke2-runtime
# docker.io/rancher/rke2-runtime v1.33.8-rke2r1 35592a070625a 91.3MB
즉, 위에서 본 data/ 하위의 바이너리와 차트는 모두 rke2-runtime 이미지 하나에 담겨 있고, Content Bootstrap 단계에서 이 이미지를 pull한 뒤 호스트로 추출된 것이다.
server/
서버 노드에만 존재하는 컨트롤 플레인 운영 데이터다.
tree /var/lib/rancher/rke2/server/ -L 1
├── agent-token -> /var/lib/rancher/rke2/server/token
├── cred
├── db
├── etc
├── manifests
├── node-token -> /var/lib/rancher/rke2/server/token
├── tls
└── token
cred/: kubeconfig + 암호화 설정
tree /var/lib/rancher/rke2/server/cred
/var/lib/rancher/rke2/server/cred
├── admin.kubeconfig
├── api-server.kubeconfig
├── cloud-controller.kubeconfig
├── controller.kubeconfig
├── encryption-config.json
├── encryption-state.json
├── ipsec.psk
├── passwd
├── scheduler.kubeconfig
└── supervisor.kubeconfig
kubeadm으로 프로비저닝한 클러스터에서는 컴포넌트별 kubeconfig가 /etc/kubernetes/ 아래에 위치했다. RKE2에서는 /var/lib/rancher/rke2/server/cred/에 모여 있다.
admin.kubeconfig: 클러스터 관리자용 (→/etc/rancher/rke2/rke2.yaml로 복사되는 원본)controller.kubeconfig,scheduler.kubeconfig: 각 컴포넌트용encryption-config.json: etcd encryption at rest 설정 (RKE2 기본 활성화)supervisor.kubeconfig: K3s 엔진 내부 supervisor용 (K3s 계승 아키텍처 흔적)
RKE2가 보안을 중시해 encryption-at-rest를 기본 적용한다고 했다. 관련해 encryption-config.json의 내용을 살펴 보자.
{
"kind": "EncryptionConfiguration",
"apiVersion": "apiserver.config.k8s.io/v1",
"resources": [
{
"resources": ["secrets"],
"providers": [
{
"aescbc": {
"keys": [
{
"name": "aescbckey",
"secret": "OmLRJEFMVSmN4f4wtLTkPG12nOor+uAn0iHBLMZxX8U="
}
]
}
},
{
"identity": {}
}
]
}
]
}
Kubernetes Secret을 AES-CBC로 암호화한다. identity provider는 fallback이다. aescbc로 복호화 시도 → 실패 시 identity(평문)로 읽기 시도하는 순서다. kubeadm에서는 이 설정을 수동으로 작성하고 --encryption-provider-config 플래그로 지정해야 한다. RKE2는 서비스 시작 시 자동으로 생성하고 적용한다.
db/: etcd 데이터
kubeadm 환경에서 etcd 데이터는 /var/lib/etcd/에 위치했다. RKE2에서는 /var/lib/rancher/rke2/server/db/etcd/다. 경로가 다를 뿐 etcd 데이터 구조 자체는 동일하다.
tree /var/lib/rancher/rke2/server/db
/var/lib/rancher/rke2/server/db
├── etcd
│ ├── config
│ ├── member
│ │ ├── snap
│ │ │ └── db
│ │ └── wal
│ │ ├── 0000000000000000-0000000000000000.wal
│ │ └── 0.tmp
│ └── name
└── snapshots
etcd/member/snap/db: etcd BoltDB 데이터 파일 (모든 클러스터 상태)etcd/member/wal/: Write-Ahead Log (WAL), 트랜잭션 로그snapshots/:rke2 etcd-snapshot명령으로 관리하는 스냅샷
manifests: Helm Controller 배포용
tree /var/lib/rancher/rke2/server/manifests/
/var/lib/rancher/rke2/server/manifests/
├── rke2-canal-config.yaml ← HelmChartConfig (오버라이드 목적으로 작성)
├── rke2-canal.yaml ← HelmChart (data/charts/ 에서 자동 복사)
├── rke2-coredns-config.yaml ← HelmChartConfig (오버라이드 목적으로 작성)
├── rke2-coredns.yaml ← HelmChart (자동 복사)
├── rke2-metrics-server.yaml ← HelmChart (자동 복사)
└── rke2-runtimeclasses.yaml ← HelmChart (자동 복사)
data/charts/에는 19개가 있지만, disable 목록의 항목과 선택하지 않은 CNI를 제외한 6개만 여기에 복사됐다. 사전에 작성해 둔 HelmChartConfig 파일 2개도 같이 있다.
rke2-coredns.yaml의 내용을 보면:
cat /var/lib/rancher/rke2/server/manifests/rke2-coredns.yaml
apiVersion: helm.cattle.io/v1
kind: HelmChart
...
set:
global.clusterCIDR: 10.42.0.0/16
global.clusterCIDRv4: 10.42.0.0/16
kind: HelmChart로, Helm Controller가 감시하는 CRD 리소스다. 같은 이름의 HelmChartConfig를 두면, 그 안에 적은 Helm 차트 values(예: replica 수, 리소스 한도, set으로 넘기던 값들)가 해당 HelmChart 배포 시 기본값을 오버라이드한다.
tls: 전체 PKI
쿠버네티스 PKI·인증서 구조는 앞선 Kubernetes 클러스터 학습 과정에서도 여러 차례 다뤘던 내용이다. RKE2에서는 그 PKI가 전부 여기 server/tls/에 모여 있고, 역할·구성은 kubeadm의 /etc/kubernetes/pki 등과 비슷하다.
tree /var/lib/rancher/rke2/server/tls
├── client-admin.crt
├── client-admin.key
├── client-ca.crt
├── client-ca.key
├── ...
├── etcd
│ ├── client.crt
│ ├── peer-ca.crt
│ ├── peer-ca.key
│ ├── server-ca.crt
│ ├── server-ca.key
│ └── ...
├── server-ca.crt
├── server-ca.key
├── serving-kube-apiserver.crt
├── serving-kube-apiserver.key
├── service.key
└── ...
| 분류 | 주요 파일 | 용도 |
|---|---|---|
| Server CA | server-ca.crt/key |
API 서버 서빙 인증서 발급 CA |
| Client CA | client-ca.crt/key |
클라이언트 인증서 발급 CA |
| API 서버 서빙 | serving-kube-apiserver.crt/key |
API 서버 TLS |
| 클라이언트 인증서 | client-admin.*, client-controller.*, … |
컴포넌트별 mTLS 클라이언트 인증 |
| etcd PKI | etcd/server-ca.*, etcd/peer-ca.*, … |
etcd 별도 CA 체계 |
| 서비스 어카운트 키 | service.key, service.current.key |
SA 토큰 서명/검증 |
CA 인증서를 확인해 보면:
cat /var/lib/rancher/rke2/server/tls/server-ca.crt | openssl x509 -text -noout
Certificate:
Data:
Issuer: CN=rke2-server-ca@1771416044
Validity
Not Before: Feb 18 12:00:44 2026 GMT
Not After : Feb 16 12:00:44 2036 GMT ← 10년 유효
Subject: CN=rke2-server-ca@1771416044
Public Key Algorithm: id-ecPublicKey ← ECDSA P-256
자체 서명 CA 유효기간이 10년이다. kubeadm도 10년으로 생성한다. 다만 RKE2는 이 CA가 자동 갱신되지 않으므로, 필요 시 수동으로 로테이션해야 한다.
API 서버 서빙 인증서를 살펴 보자.
cat /var/lib/rancher/rke2/server/tls/serving-kube-apiserver.crt | openssl x509 -text -noout
...
X509v3 Subject Alternative Name:
DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc,
DNS:kubernetes.default.svc.cluster.local, DNS:localhost,
DNS:week07-k8s-node1,
IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1,
IP Address:192.168.10.11, IP Address:10.43.0.1
Validity
Not Before: Feb 18 12:00:44 2026 GMT
Not After : Feb 18 12:00:44 2027 GMT ← 1년 유효, 만료 120일 전 자동 갱신
서빙 인증서 유효기간은 1년이고, 만료 120일 전에 자동 갱신된다. kubeadm은 만료 90일 전에 kubeadm certs renew를 수동 실행하거나 자동화해야 했다. RKE2는 기본으로 자동 갱신된다.
인증서에서 주의 깊게 살펴 봐야 하는 부분은, 역시나 SAN이다.
| 구분 | SAN 예시 | 용도 |
|---|---|---|
| 클러스터 내부 접근 | kubernetes.default.svc, kubernetes.default.svc.cluster.local, ClusterIP 10.43.0.1 |
파드·컨트롤러가 API 서버를 부를 때 쓰는 이름·IP가 모두 들어 있어 TLS 검증 통과 |
| 노드/외부 접근 | localhost, 노드 호스트명(week07-k8s-node1), 노드 IP(192.168.10.11) |
노드에서 kubectl 사용 시, 또는 외부에서 노드 IP로 API 호출 시 같은 인증서로 서빙 |
token/: 에이전트 join 토큰
cat /var/lib/rancher/rke2/server/node-token
K1037f1b1f84d631265adcff239308d8b19ae073480250e9fcded6330c97452ad8d::server:158adc28471c9fd7122146cb86bfb5a5
node-token과 agent-token은 모두 token 파일의 symlink다. 세 이름이 같은 파일을 가리킨다. K3s 호환(node-token), 의미적 명확성(agent-token), 실제 파일(token)이 역할 분리되어 있다. 에이전트 노드 합류 시 이 토큰을 사용한다.
kubeadm에서는 kubeadm token create로 bootstrap token을 생성하고 24시간 후 만료됐다. RKE2의 서버 토큰은 만료되지 않는 permanent token이고, 별도로 생성할 필요 없이 서비스 시작 시 자동 생성된다.
agent/
서버 노드와 에이전트 노드 모두에 존재한다. 서버 노드에도 에이전트 기능이 내장되어 있기 때문이다.
tree /var/lib/rancher/rke2/agent/ -L 2
/var/lib/rancher/rke2/agent/
├── client-ca.crt
├── client-kubelet.crt, .key
├── client-kube-proxy.crt, .key
├── client-rke2-controller.crt, .key
├── containerd/ ← containerd root 디렉터리
├── etc/
│ ├── containerd/
│ │ └── config.toml ← containerd 설정 (자동 생성)
│ ├── crictl.yaml
│ └── kubelet.conf.d/
│ └── 00-rke2-defaults.conf ← kubelet 설정
├── images/ ← 컴포넌트 이미지 참조
├── kubelet.kubeconfig
├── kubeproxy.kubeconfig
├── logs/
│ └── kubelet.log
├── pod-manifests/ ← kubelet이 watch하는 static pod 경로
│ ├── etcd.yaml
│ ├── kube-apiserver.yaml
│ ├── kube-controller-manager.yaml
│ ├── kube-proxy.yaml
│ └── kube-scheduler.yaml
├── server-ca.crt
├── serving-kubelet.crt, .key
└── rke2controller.kubeconfig
containerd 설정
cat /var/lib/rancher/rke2/agent/etc/containerd/config.toml
# File generated by rke2. DO NOT EDIT. Use config.toml.tmpl instead.
version = 3
root = "/var/lib/rancher/rke2/agent/containerd"
state = "/run/k3s/containerd"
[grpc]
address = "/run/k3s/containerd/containerd.sock"
...
[plugins.'io.containerd.cri.v1.runtime']
enable_selinux = true
enable_unprivileged_ports = true
enable_unprivileged_icmp = true
...
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
SystemdCgroup = true
state = "/run/k3s/containerd", address = "/run/k3s/containerd/containerd.sock" — /run/k3s/ 경로가 보인다. K3s에서 계승한 아키텍처의 흔적이다. 실제로 K3s가 설치된 게 아니라, RKE2가 임베딩한 K3s 엔진이 이 경로 네이밍을 그대로 사용하는 것이다.
kubeadm 환경에서는 containerd를 별도 패키지로 설치하고 /etc/containerd/config.toml을 직접 작성했다. RKE2에서는 RKE2가 이 파일을 자동으로 생성하고 관리한다. 파일 상단에 “DO NOT EDIT” 주석이 있는 이유다. 직접 수정하면 재시작 시 덮어써진다. 수정이 필요하면 config.toml.tmpl 방식을 사용해야 한다.
containerd 설정 변경: config.toml.tmpl
RKE2는 재시작 시 같은 디렉터리에 .tmpl 파일이 있으면 Go의 text/template 엔진으로 렌더링해서 config.toml을 생성한다(공식 문서). containerd 2.0 기준으로는 config-v3.toml.tmpl을 사용한다.
권장 방식은 기존 config.toml을 통째로 복사하는 게 아니라, ``로 기본 설정을 그대로 상속한 뒤 추가 설정만 덧붙이는 것이다.
# /var/lib/rancher/rke2/agent/etc/containerd/config-v3.toml.tmpl
# 추가하고 싶은 설정만 작성
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.'custom']
runtime_type = "io.containerd.runc.v2"
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.'custom'.options]
BinaryName = "/usr/bin/custom-container-runtime"
SystemdCgroup = true
기존 config.toml을 복사해서 수정하는 방식은 공식 문서에서 명시적으로 비권장한다. RKE2 업그레이드 시 기본 템플릿이 바뀌면 복사본이 구버전 설정으로 고정되어 문제가 생길 수 있다.
아래와 같은 절차로 설정을 변경해야 한다.
# 1. tmpl 파일 작성
vi /var/lib/rancher/rke2/agent/etc/containerd/config-v3.toml.tmpl
# 2. RKE2 재시작 — 재시작 시 tmpl을 렌더링해서 config.toml을 새로 생성
systemctl restart rke2-server
# 3. 결과 확인
cat /var/lib/rancher/rke2/agent/etc/containerd/config.toml
crictl 설정
cat /var/lib/rancher/rke2/agent/etc/crictl.yaml
runtime-endpoint: unix:///run/k3s/containerd/containerd.sock
crictl이 K3s 소켓 경로를 바라보고 있다. kubeadm 환경에서는 /run/containerd/containerd.sock이었다.
kubelet 설정
cat /var/lib/rancher/rke2/agent/etc/kubelet.conf.d/00-rke2-defaults.conf
address: 192.168.10.11
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
anonymous:
enabled: false ← anonymous 인증 비활성화 (CIS Benchmark 요건)
webhook:
enabled: true
x509:
clientCAFile: /var/lib/rancher/rke2/agent/client-ca.crt
authorization:
mode: Webhook
cgroupDriver: systemd
clusterDNS:
- 10.43.0.10
clusterDomain: cluster.local
containerRuntimeEndpoint: unix:///run/k3s/containerd/containerd.sock
failSwapOn: false
staticPodPath: /var/lib/rancher/rke2/agent/pod-manifests
tlsCertFile: /var/lib/rancher/rke2/agent/serving-kubelet.crt
tlsPrivateKeyFile: /var/lib/rancher/rke2/agent/serving-kubelet.key
...
authentication.anonymous.enabled: false가 눈에 띈다. kubelet의 anonymous 인증을 비활성화한 것으로, CIS Kubernetes Benchmark 요건이다. kubeadm에서는 이 설정을 수동으로 해야 했다.
staticPodPath가 /var/lib/rancher/rke2/agent/pod-manifests로 설정되어 있다. kubelet이 이 경로를 감시하면서 static pod를 기동하는 구조다.
Static Pod Manifest 확인
pod-manifests/
kubelet이 기동하는 static pod manifest가 위치해 있다.
tree /var/lib/rancher/rke2/agent/pod-manifests
├── etcd.yaml
├── kube-apiserver.yaml
├── kube-controller-manager.yaml
├── kube-proxy.yaml
└── kube-scheduler.yaml
kubeadm에서는 /etc/kubernetes/manifests/에 etcd, kube-apiserver, kube-controller-manager, kube-scheduler 4개가 있고, kube-proxy는 DaemonSet으로 배포됐다. RKE2에서는 kube-proxy도 static pod로 관리된다. 경로도 다르다.
kube-apiserver
cat /var/lib/rancher/rke2/agent/pod-manifests/kube-apiserver.yaml
...
spec:
containers:
- args:
- --admission-control-config-file=/etc/rancher/rke2/rke2-pss.yaml ← Pod Security Standards
- --anonymous-auth=false ← anonymous 인증 비활성화
- --api-audiences=https://kubernetes.default.svc.cluster.local,rke2 ← SA 토큰 audience 제한
- --authorization-mode=Node,RBAC
- --encryption-provider-config=/var/lib/rancher/rke2/server/cred/encryption-config.json ← etcd 암호화
- --encryption-provider-config-automatic-reload=true
- --profiling=false ← 프로파일링 비활성화
- --tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,... ← 강화 TLS cipher suite
...
image: index.docker.io/rancher/hardened-kubernetes:v1.33.8-rke2r1-build20260210
livenessProbe:
exec:
command:
- kubectl
- get
- --server=https://localhost:6443/
- --client-certificate=/var/lib/rancher/rke2/server/tls/client-kube-apiserver.crt
- --client-key=/var/lib/rancher/rke2/server/tls/client-kube-apiserver.key
- --certificate-authority=/var/lib/rancher/rke2/server/tls/server-ca.crt
- --raw=/livez
kubeadm 설정과 비교해서 눈에 띄는 보안 설정들이다:
| 설정 / 항목 | 설명 |
|---|---|
--anonymous-auth=false |
kubeadm은 기본 true. RKE2는 anonymous 인증을 기본 차단 |
--api-audiences |
서비스 어카운트 토큰을 특정 audience로 제한해 의도치 않은 재사용 방지. kubeadm 기본에는 없음 |
--encryption-provider-config |
etcd의 Secret을 AES-CBC로 암호화. kubeadm은 별도 설정 없으면 평문 저장 |
--profiling=false |
API 서버 /debug/pprof 비활성화. 운영에서 성능 데이터 노출 방지 권장 |
--tls-cipher-suites |
취약 cipher 제외, ECDHE + AES-GCM/ChaCha20만 허용. Go 기본이 아닌 강화 목록 |
| liveness probe mTLS | API 서버가 클라이언트 인증서로 /livez 조회. kubeadm은 HTTP 헬스체크, RKE2는 probe도 mTLS |
이미지 rancher/hardened-kubernetes |
표준 kube-apiserver 아님. Trivy CVE 스캔 + CIS Benchmark 적용 Rancher 빌드 |
etcd
cat /var/lib/rancher/rke2/agent/pod-manifests/etcd.yaml
...
spec:
containers:
- args:
- --config-file=/var/lib/rancher/rke2/server/db/etcd/config
command:
- etcd
image: index.docker.io/rancher/hardened-etcd:v3.5.26-k3s1-build20260126
volumes:
- hostPath:
path: /var/lib/rancher/rke2/server/db/etcd
type: DirectoryOrCreate
name: dir0
- hostPath:
path: /var/lib/rancher/rke2/server/tls/etcd/server-client.crt
type: File ← 파일 단위 마운트
name: file0
- hostPath:
path: /var/lib/rancher/rke2/server/tls/etcd/server-client.key
type: File ← 파일 단위 마운트
name: file1
볼륨 마운트가 type: File이다. kubeadm에서는 etcd 인증서 디렉터리 전체를 type: Directory로 마운트했다. RKE2는 필요한 파일만 개별적으로 마운트하는 방식으로 attack surface를 최소화한다.
etcdctl로 상태를 확인하려면 etcd 컨테이너 내부의 바이너리를 사용해야 한다:
find / -name etcdctl 2>/dev/null
# /var/lib/rancher/rke2/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs/usr/local/bin/etcdctl
# 심볼릭 링크로 노출
ln -s /var/lib/rancher/rke2/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs/usr/local/bin/etcdctl /usr/local/bin/etcdctl
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/client.crt \
--key=/var/lib/rancher/rke2/server/tls/etcd/client.key \
endpoint status --write-out=table
# ENDPOINT ID VERSION DB SIZE IS LEADER RAFT TERM RAFT INDEX
# https://127.0.0.1:2379 6571fb7574e87dba 3.6.7 12 MB true 2 34013
kubeadm 환경에서는 etcdctl이 /usr/local/bin/etcdctl에 직접 설치되거나, etcd Pod에 exec해서 사용했다. RKE2에서는 etcd 컨테이너 이미지의 overlayfs 스냅샷에서 바이너리를 꺼내야 한다.
kube-scheduler
cat /var/lib/rancher/rke2/agent/pod-manifests/kube-scheduler.yaml
...
spec:
containers:
- args:
- --permit-port-sharing=true ← 멀티 NIC / 특수 네트워크 구성에서 포트 제약 완화
- --authentication-kubeconfig=/var/lib/rancher/rke2/server/cred/scheduler.kubeconfig
- --authorization-kubeconfig=/var/lib/rancher/rke2/server/cred/scheduler.kubeconfig
- --bind-address=127.0.0.1
- --profiling=false
- --secure-port=10259
- --tls-cert-file=/var/lib/rancher/rke2/server/tls/kube-scheduler/kube-scheduler.crt
- --tls-private-key-file=/var/lib/rancher/rke2/server/tls/kube-scheduler/kube-scheduler.key
--bind-address=127.0.0.1로 localhost만 바인딩한다. kubeadm 기본값은 0.0.0.0이어서 모든 인터페이스에서 접근 가능하다. RKE2는 불필요한 노출을 최소화하는 방향이다.
kube-controller-manager
cat /var/lib/rancher/rke2/agent/pod-manifests/kube-controller-manager.yaml
...
spec:
containers:
- args:
- --permit-port-sharing=true
- --flex-volume-plugin-dir=/var/lib/kubelet/volumeplugins
- --terminated-pod-gc-threshold=1000 ← 완료된 Pod가 1000개 이상이면 자동 GC
- --bind-address=127.0.0.1 ← localhost만 바인딩
- --profiling=false
- --cluster-signing-kube-apiserver-client-cert-file=.../client-ca.nochain.crt
- --cluster-signing-kubelet-client-cert-file=.../client-ca.nochain.crt
...
- --use-service-account-credentials=true
--terminated-pod-gc-threshold=1000: 완료된 Pod가 1000개 이상 쌓이면 자동 정리한다. kubeadm 기본값은 12500이다. RKE2가 더 적극적으로 정리하도록 설정했다.
--cluster-signing-*-cert-file에 nochain.crt 파일이 사용된다. 중간 CA 체인 없이 루트 CA만 포함한 인증서다. 이 설정을 통해 컨트롤러 매니저가 클러스터 내 인증서 서명 요청(CSR)에 서명하는 역할을 수행한다.
kube-proxy
cat /var/lib/rancher/rke2/agent/pod-manifests/kube-proxy.yaml
...
spec:
containers:
- args:
- --cluster-cidr=10.42.0.0/16
- --conntrack-max-per-core=0 ← conntrack 최대값 제한 없음
- --conntrack-tcp-timeout-close-wait=0s ← CLOSE_WAIT timeout 무제한
- --conntrack-tcp-timeout-established=0s ← ESTABLISHED timeout 무제한
- --healthz-bind-address=127.0.0.1
- --hostname-override=k8s-node1
- --kubeconfig=/var/lib/rancher/rke2/agent/kubeproxy.kubeconfig
- --proxy-mode=iptables
--conntrack-* 튜닝이 눈에 띈다. 대규모 연결 환경에서 NAT 테이블 부족이나 세션 끊김을 방지하기 위한 설정이다. 프로덕션 환경을 고려한 기본값이다.
kubeadm에서 kube-proxy는 DaemonSet으로 배포됐다. RKE2에서는 static pod로 관리된다. 관리 방식은 다르지만 kube-proxy 자체의 기능은 동일하다.
Helm Controller 확인
CRD 확인
kubectl get crd | grep -E 'helm|addon'
# addons.k3s.cattle.io 2026-02-18T12:02:03Z
# helmchartconfigs.helm.cattle.io 2026-02-18T12:02:03Z
# helmcharts.helm.cattle.io 2026-02-18T12:02:03Z
CRD 이름에 k3s.cattle.io, helm.cattle.io가 붙어 있다. K3s 프로젝트의 Helm Controller를 그대로 가져다 쓰기 때문이다. cattle.io는 Rancher Labs(현 SUSE)의 도메인이다.
HelmChart와 HelmChartConfig
kubectl get helmcharts.helm.cattle.io -n kube-system -o wide
# NAME BOOTSTRAP FAILED JOB
# rke2-canal true False helm-install-rke2-canal
# rke2-coredns true False helm-install-rke2-coredns
# rke2-metrics-server false False helm-install-rke2-metrics-server
# rke2-runtimeclasses false False helm-install-rke2-runtimeclasses
kubectl get helmchartconfigs -n kube-system
# NAME AGE
# rke2-canal 73m
# rke2-coredns 73m
BOOTSTRAP: true인 항목(canal, coredns)은 클러스터 기능에 필수적인 컴포넌트다. 클러스터가 정상적으로 동작하기 위해 다른 것보다 먼저 배포된다.
HelmChartConfig에 설정한 내용이 실제로 반영됐는지 확인한다:
kubectl describe helmchartconfigs -n kube-system rke2-canal
...
Spec:
Values Content: flannel:
iface: "enp0s9"
사전에 작성한 flannel.iface: "enp0s9" 설정이 반영되어 있다.
helm-install Job
kubectl get job -n kube-system
# NAME STATUS COMPLETIONS DURATION AGE
# helm-install-rke2-canal Complete 1/1 13s 73m
# helm-install-rke2-coredns Complete 1/1 13s 73m
# helm-install-rke2-metrics-server Complete 1/1 52s 73m
# helm-install-rke2-runtimeclasses Complete 1/1 50s 73m
각 HelmChart마다 Job이 하나씩 실행됐다. helm install을 실행하는 Job Pod가 배포되고, 완료 후 Completed 상태로 남는다. kubectl get pod -A에서 보이던 helm-install-rke2-* Pod들이 이 Job의 결과물이다.
보안 이미지 확인
crictl images
# IMAGE TAG SIZE
# docker.io/rancher/hardened-calico v3.31.3-build20260206 217MB
# docker.io/rancher/hardened-coredns v1.14.1-build20260206 27.2MB
# docker.io/rancher/hardened-etcd v3.5.26-k3s1-build20260126 17.1MB
# docker.io/rancher/hardened-flannel v0.28.1-build20260206 19.8MB
# docker.io/rancher/hardened-k8s-metrics-server v0.8.1-build20260206 19.4MB
# docker.io/rancher/hardened-kubernetes v1.33.8-rke2r1-build20260210 187MB
# docker.io/rancher/klipper-helm v0.9.14-build20260210 59MB
# docker.io/rancher/mirrored-pause 3.6 253kB
# docker.io/rancher/rke2-runtime v1.33.8-rke2r1 91.3MB
모든 이미지에 hardened- 접두사가 붙어 있다. 표준 upstream 이미지(k8s.gcr.io/etcd, k8s.gcr.io/kube-apiserver 등)가 아니라 Rancher에서 직접 빌드한 이미지다.
kubeadm이나 Kubespray 실습에서는 registry.k8s.io/kube-apiserver:v1.xx.x 같은 표준 이미지를 사용했다. RKE2에서는 이 이미지들이 모두 rancher/hardened-* 버전으로 교체된다.
rancher/klipper-helm은 Helm Controller가 helm install을 실행하는 Job Pod에 사용하는 이미지다. Helm 바이너리를 포함한 경량 이미지다.
Canal DaemonSet의 이미지를 보면:
kubectl describe pod -n kube-system -l k8s-app=canal | grep Image: | uniq
Image: rancher/hardened-calico:v3.31.3-build20260206
Image: rancher/hardened-flannel:v0.28.1-build20260206
Canal Pod에 calico와 flannel 컨테이너가 같이 있다. 이전 글에서 설명한 것처럼, 네트워크 연결은 flannel, 네트워크 정책은 calico가 담당하는 하이브리드 구조다.
/etc/rancher 디렉터리 확인
tree /etc/rancher
/etc/rancher
├── node
│ └── password
└── rke2
├── config.yaml
├── rke2-pss.yaml
└── rke2.yaml
/etc/rancher/node/password는 노드 인증에 사용하는 password 파일이다. 클러스터 내에서 kubelet이 API 서버에 노드 등록 시 사용한다.
rke2-pss.yaml은 Pod Security Standards 설정이다:
cat /etc/rancher/rke2/rke2-pss.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1beta1
kind: PodSecurityConfiguration
defaults:
enforce: "privileged" ← 기본 모드: privileged (제한 없음)
enforce-version: "latest"
exemptions:
usernames: []
runtimeClasses: []
namespaces: []
profile: "cis" 설정을 추가하면 restricted 정책이 클러스터 전체에 적용된다. 기본 설정은 privileged이므로 Pod에 대한 별도 제한이 없다.
K3s 계승 흔적 정리
RKE2를 사용하다 보면 K3s 네이밍이 여러 곳에서 보인다. 이건 버그나 미완성이 아니라 의도된 아키텍처다.
| 위치 | K3s 흔적 | 이유 |
|---|---|---|
containerd socket 경로 |
/run/k3s/containerd/containerd.sock |
K3s 엔진 임베딩 |
kubelet config containerRuntimeEndpoint |
/run/k3s/containerd/containerd.sock |
동일 |
| Helm Controller CRD | helm.cattle.io/v1, k3s.cattle.io/v1 |
K3s의 Helm Controller 재사용 |
| Addon CRD | addons.k3s.cattle.io |
동일 |
| containerd 버전 | containerd github.com/k3s-io/containerd v2.1.5-k3s1 |
K3s 포크 빌드 |
RKE2의 공식 아키텍처 자체가 K3s 엔진 위에 보안 하드닝을 얹은 구조이므로, K3s의 네이밍과 경로가 내부적으로 그대로 남아 있는 것이다.
결과
RKE2 서버 노드에서 생성된 파일들을 전체적으로 살펴봤다. 표준 Kubernetes(kubeadm)와의 주요 차이를 아래와 같이 정리할 수 있다.
| 항목 | kubeadm | RKE2 |
|---|---|---|
| 컴포넌트 이미지 | registry.k8s.io/kube-* |
rancher/hardened-* |
| static pod 경로 | /etc/kubernetes/manifests/ |
/var/lib/rancher/rke2/agent/pod-manifests/ |
| kube-proxy 관리 방식 | DaemonSet | static pod |
| etcd 데이터 경로 | /var/lib/etcd/ |
/var/lib/rancher/rke2/server/db/etcd/ |
| etcd 암호화 | 수동 설정 필요 | 기본 활성화 |
| anonymous-auth | 기본 허용 | 기본 차단 |
| TLS cipher suite | Go 기본값 | 명시적 강화 목록 |
| liveness probe | HTTP 헬스체크 | mTLS 인증 |
| 인증서 갱신 | 수동 또는 별도 자동화 | 만료 120일 전 자동 갱신 |
| 에이전트 join 토큰 | 24시간 만료 bootstrap token | 영구 서버 토큰 |
| containerd 설정 | 수동 작성 | 자동 생성 (tmpl로 오버라이드) |
다음 글에서는 에이전트 노드를 클러스터에 합류시켜 멀티 노드 클러스터를 완성한다.
댓글남기기