[Kubernetes] Cluster: RKE2를 이용해 클러스터 구성하기 - 1.1. 서버 노드 설치

14 분 소요

서종호(가시다)님의 On-Premise K8s Hands-on Study 7주차 학습 내용을 기반으로 합니다.


TL;DR

이번 글의 목표는 RKE2 서버 노드를 설치하고 기본 상태를 확인하는 것이다.

  • 환경: Vagrant + VirtualBox, Rocky Linux 9 기반 VM 2대
  • 설치: get.rke2.io 스크립트를 통한 RPM 방식 설치
  • 설정: config.yaml, HelmChartConfig로 Canal CNI 및 애드온 설정
  • 확인: 프로세스 트리, 디렉터리 구조, static pod manifest, 보안 설정


들어가며

이전 글에서 RKE2가 무엇이고 어떤 아키텍처로 동작하는지 살펴봤다. supervisor가 containerd와 kubelet을 spawn하고, static pod로 컨트롤 플레인을, Helm Controller로 애드온을 관리하는 이중 구조가 핵심이었다.

이번 글에서는 실제로 RKE2를 설치하고 클러스터를 구성해 본다. Kubernetes The Hard Way나 kubeadm과 달리, 단일 바이너리 하나를 설치하고 서비스를 시작하는 것만으로 클러스터가 만들어진다. 설치 자체는 단순하지만, 그 결과로 어떤 파일과 프로세스가 생기는지를 표준 Kubernetes 프로비저닝과 비교해 본다.


환경 구성

실습 환경

Vagrant + VirtualBox로 VM 두 대를 준비한다. 이번 글에서는 서버 노드(node1)에 집중하고, 에이전트 노드(node2) 합류는 다음 글에서 다룬다.

구분
OS Rocky Linux 9 (bento/rockylinux-9, 버전 202510.26.0)
서버 노드 IP 192.168.10.11 (node1)
에이전트 노드 IP 192.168.10.12 (node2)
CPU / 메모리 4코어 / 4GiB

Vagrantfile

BOX_IMAGE = "bento/rockylinux-9"
BOX_VERSION = "202510.26.0"
N = 2

Vagrant.configure("2") do |config|
  (1..N).each do |i|
    config.vm.define "week07-k8s-node#{i}" do |subconfig|
      subconfig.vm.box = BOX_IMAGE
      subconfig.vm.box_version = BOX_VERSION
      subconfig.vm.provider "virtualbox" do |vb|
        vb.customize ["modifyvm", :id, "--groups", "/RKE2-Lab"]
        vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
        vb.name = "week07-k8s-node#{i}"
        vb.cpus = 4
        vb.memory = 4096
        vb.linked_clone = true
      end
      subconfig.vm.host_name = "week07-k8s-node#{i}"
      subconfig.vm.network "private_network", ip: "192.168.10.1#{i}"
      subconfig.vm.network "forwarded_port", guest: 22, host: "6000#{i}", auto_correct: true, id: "ssh"
      subconfig.vm.synced_folder "./", "/vagrant", disabled: true
      subconfig.vm.provision "shell", path: "init_cfg.sh", args: [N]
    end
  end
end

init_cfg.sh

VM 프로비저닝 시 실행되는 초기화 스크립트다. Kubernetes 클러스터 노드에 필요한 공통 설정들을 처리한다.

#!/usr/bin/env bash

echo ">>>> Initial Config Start <<<<"

echo "[TASK 1] Change Timezone and Enable NTP"
timedatectl set-local-rtc 0
timedatectl set-timezone Asia/Seoul

echo "[TASK 2] Disable firewalld and selinux"
systemctl disable --now firewalld >/dev/null 2>&1
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config

echo "[TASK 3] Disable and turn off SWAP & Delete swap partitions"
swapoff -a
sed -i '/swap/d' /etc/fstab
sfdisk --delete /dev/sda 2 >/dev/null 2>&1
partprobe /dev/sda >/dev/null 2>&1

echo "[TASK 4] Config kernel & module"
cat << EOF > /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
modprobe overlay >/dev/null 2>&1
modprobe br_netfilter >/dev/null 2>&1

cat << EOF >/etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sysctl --system >/dev/null 2>&1

echo "[TASK 5] Setting Local DNS Using Hosts file"
sed -i '/^127\.0\.\(1\|2\)\.1/d' /etc/hosts
for (( i=1; i<=$1; i++ )); do echo "192.168.10.1$i k8s-node$i" >> /etc/hosts; done

echo "[TASK 6] Install Helm"
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | DESIRED_VERSION=v3.20.0 bash >/dev/null 2>&1

echo "[TASK 7] Setting SSHD"
cat << EOF >> /etc/ssh/sshd_config
PermitRootLogin yes
PasswordAuthentication yes
EOF
systemctl restart sshd >/dev/null 2>&1

echo "[TASK 8] Install packages"
dnf install -y conntrack python3-pip git >/dev/null 2>&1

echo "[TASK 9] NetworkManager to ignore calico/flannel related network interfaces"
# https://docs.rke2.io/known_issues#networkmanager
cat << EOF > /etc/NetworkManager/conf.d/k8s.conf
[keyfile]
unmanaged-devices=interface-name:flannel*;interface-name:cali*;interface-name:tunl*;interface-name:vxlan.calico;interface-name:vxlan-v6.calico;interface-name:wireguard.cali;interface-name:wg-v6.cali
EOF
systemctl reload NetworkManager

echo "[TASK 10] Install K9s"
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
wget -P /tmp https://github.com/derailed/k9s/releases/latest/download/k9s_linux_${CLI_ARCH}.tar.gz >/dev/null 2>&1
tar -xzf /tmp/k9s_linux_${CLI_ARCH}.tar.gz -C /tmp
chown root:root /tmp/k9s
mv /tmp/k9s /usr/local/bin/
chmod +x /usr/local/bin/k9s

echo "[TASK 11] ETC"
echo "sudo su -" >> /home/vagrant/.bashrc

echo ">>>> Initial Config End <<<<"

TASK 9: NetworkManager CNI 인터페이스 예외 처리

RKE2 Known Issues 문서에서 권장하는 설정이다.

NetworkManager manipulates the routing table for interfaces in the default network namespace where many CNIs, including RKE2’s default, create veth pairs for connections to containers.

NetworkManager가 기본 네트워크 네임스페이스의 라우팅 테이블을 조작하는데, Canal/Flannel/Calico가 컨테이너 연결을 위해 생성하는 veth pair도 그 대상에 포함된다. 이 설정이 없으면 CNI가 만든 가상 인터페이스에 간섭이 생겨 Pod 네트워킹이 불안정해질 수 있다.

kubeadm이나 Kubespray 실습에서는 이 설정이 필요하지 않았다. CNI를 직접 설치하면서 필요한 경우 처리했다. RKE2는 Canal을 기본 CNI로 사용하고 Helm Controller로 자동 배포하기 때문에, 사전에 NetworkManager 예외 처리를 해두는 것이다.

VM 프로비저닝

mkdir k8s-rke2
cd k8s-rke2

curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-rke2/Vagrantfile
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-rke2/init_cfg.sh

vagrant up
vagrant status
==> week07-k8s-node1: [TASK 1] Change Timezone and Enable NTP
==> week07-k8s-node1: [TASK 2] Disable firewalld and selinux
==> week07-k8s-node1: [TASK 3] Disable and turn off SWAP & Delete swap partitions
==> week07-k8s-node1: [TASK 4] Config kernel & module
==> week07-k8s-node1: [TASK 5] Setting Local DNS Using Hosts file
==> week07-k8s-node1: [TASK 6] Install Helm
==> week07-k8s-node1: [TASK 7] Setting SSHD
==> week07-k8s-node1: [TASK 8] Install packages
==> week07-k8s-node1: [TASK 9] NetworkManager to ignore calico/flannel related network interfaces
==> week07-k8s-node1: [TASK 10] Install K9s
==> week07-k8s-node1: [TASK 11] ETC
==> week07-k8s-node1: >>>> Initial Config End <<<<


RKE2 서버 노드 설치

설치 스크립트

RKE2는 get.rke2.io 설치 스크립트를 제공한다. 환경 변수로 채널, 버전, 설치 방식을 제어할 수 있다.

설치 과정에서 사용할 수 있는 주요 환경 변수는 다음과 같다.

변수 설명 기본값
INSTALL_RKE2_CHANNEL 릴리즈 채널 stable
INSTALL_RKE2_VERSION 특정 버전 고정 (채널 우회) -
INSTALL_RKE2_TYPE systemd 서비스 유형 server
INSTALL_RKE2_METHOD 설치 방식 RPM 기반은 rpm, 그 외는 tar

INSTALL_RKE2_CHANNEL

RKE2 다운로드 URL을 가져오는 데 사용할 채널이다. RKE2 채널 구조는 아래와 같다.

채널 성격 대상
stable 커뮤니티 검증 완료, Rancher 호환 보장 운영 환경
latest 최신 기능, 검증 미완료 테스트/개발 환경
testing QA 단계 내부 테스트
v1.34 특정 마이너 버전의 최신 패치 마이너 버전 고정 운영

스크립트 내부의 get_release_version() 함수는 채널 URL(https://update.rke2.io/v1-release/channels/{채널명})에 HTTP 요청을 보내고, 리다이렉트된 최종 URL에서 버전 문자열을 추출하는 방식으로 동작한다.

Rocky Linux 9는 RPM 기반 시스템이므로 INSTALL_RKE2_METHOD가 자동으로 rpm으로 설정된다. 스크립트가 /etc/yum.repos.d/rancher-rke2.repo를 생성하고 yum install로 패키지를 설치하는 방식이다.

INSTALL_RKE2_CHANNEL + INSTALL_RKE2_VERSION 동시 사용

두 변수를 동시에 지정하면 역할이 분리된다. INSTALL_RKE2_CHANNEL은 repo URL을 구성하는 데만 사용되고, INSTALL_RKE2_VERSION은 설치할 패키지 버전을 고정한다. 스크립트 내부 로직을 보면:

# CHANNEL로 repo 구성 (v*.* 패턴이면 get_release_version() 호출 자체를 건너뜀)
case "${INSTALL_RKE2_CHANNEL}" in
    v*.*)
        rke2_majmin=$(echo "${INSTALL_RKE2_CHANNEL}" | sed -E -e "s/^v([0-9]+\.[0-9]+).*/\1/")
        rke2_rpm_channel="stable"  # 채널 suffix가 없으면 stable로 fallback
        ;;
    *)
        get_release_version  # VERSION 미지정 시에만 채널에서 버전 조회
        ;;
esac

# VERSION으로 설치할 패키지 버전 고정
if [ -z "${INSTALL_RKE2_VERSION}" ]; then
    yum install -y "rke2-server"              # 최신 버전
else
    rke2_rpm_version=$(echo "${INSTALL_RKE2_VERSION}" | sed -E -e "s/[\+-]/~/g" | sed -E -e "s/v(.*)/\1/")
    yum install -y "rke2-server-${rke2_rpm_version}"  # 버전 고정
fi

즉, INSTALL_RKE2_CHANNEL=v1.33 INSTALL_RKE2_VERSION=v1.33.8+rke2r1처럼 쓰면 v1.33 repo를 구성하되 정확히 해당 버전의 패키지를 설치한다. 채널만 지정하면 해당 채널의 최신 패치 버전이 자동 선택된다.

설치 스크립트 전문 전문은 [get.rke2.io](https://get.rke2.io/)에서 확인할 수 있다. 주요 환경 변수 주석 일부를 발췌하면: ```bash # Environment variables: # # - INSTALL_RKE2_CHANNEL # Channel to use for fetching rke2 download URL. # Defaults to 'stable'. # # - INSTALL_RKE2_VERSION # Version of rke2 to download. # # - INSTALL_RKE2_TYPE # Type of rke2 service. Either 'server' or 'agent'. Default is 'server'. # # - INSTALL_RKE2_METHOD # The installation method to use. Default is on RPM-based systems 'rpm', # all else 'tar'. ```

설치

서버 노드에 접속해 RKE2를 설치한다.

curl -sfL https://get.rke2.io --output install.sh
chmod +x install.sh
INSTALL_RKE2_CHANNEL=v1.33 ./install.sh
[INFO]  using stable RPM repositories
[INFO]  using 1.33 series from channel stable
...
Installing:
 rke2-server             aarch64  1.33.8~rke2r1-0.el9  rancher-rke2-1.33-stable  8.3 k
Installing dependencies:
 rke2-common             aarch64  1.33.8~rke2r1-0.el9  rancher-rke2-1.33-stable   25 M
 rke2-selinux            noarch   0.22-1.el9            rancher-rke2-common-stable 22 k
...
Installed:
  rke2-common-1.33.8~rke2r1-0.el9.aarch64  rke2-selinux-0.22-1.el9.noarch  rke2-server-1.33.8~rke2r1-0.el9.aarch64

RPM이 3개 설치된다. rke2-common이 핵심 바이너리를 포함하고, rke2-server는 서버 서비스 관련 파일, rke2-selinux는 SELinux 정책 모듈이다.

설치 스크립트가 repo 등록까지 함께 처리한다. dnf config-manager --add-repo 같은 작업을 별도로 할 필요가 없다.

dnf repolist
# rancher-rke2-1.33-stable   Rancher RKE2 1.33 (v1.33)
# rancher-rke2-common-stable Rancher RKE2 Common (v1.33)

cat /etc/yum.repos.d/rancher-rke2.repo
[rancher-rke2-common-stable]
name=Rancher RKE2 Common (v1.33)
baseurl=https://rpm.rancher.io/rke2/stable/common/centos/9/noarch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://rpm.rancher.io/public.key

[rancher-rke2-1.33-stable]
name=Rancher RKE2 1.33 (v1.33)
baseurl=https://rpm.rancher.io/rke2/stable/1.33/centos/9/aarch64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://rpm.rancher.io/public.key

설치 직후 디렉터리 상태를 보면, 아직 RKE2가 한 번도 실행되지 않았으므로 기본 골격만 생성된 상태다.

tree /etc/rancher
# /etc/rancher
# └── rke2
#
# 1 directory, 0 files

tree /var/lib/rancher -L 3
# /var/lib/rancher
# └── rke2
#     ├── agent
#     │   ├── containerd
#     │   └── logs
#     ├── data
#     └── server

두 경로의 역할이 명확하게 분리된다:

  • /etc/rancher/rke2/ — 사용자가 직접 작성·수정하는 설정 디렉터리
  • /var/lib/rancher/rke2/ — RKE2가 런타임에 생성·관리하는 데이터 디렉터리

리눅스 FHS(Filesystem Hierarchy Standard)의 /etc = 설정, /var/lib = 가변 상태 데이터 관례를 그대로 따른다. kubeadm은 설정과 데이터 모두 /etc/kubernetes/ 아래에 두는 반면, RKE2는 이 두 경로를 명확히 분리한다.


RKE2 설정

서비스 시작 전에 설정 파일을 먼저 작성한다.

config.yaml

기본 설정 경로는 /etc/rancher/rke2/config.yaml이다.

cat << EOF > /etc/rancher/rke2/config.yaml
write-kubeconfig-mode: "0644"

debug: true

cni: canal

# 두 번째 네트워크 인터페이스로 통신하도록 설정
bind-address: 192.168.10.11
advertise-address: 192.168.10.11
node-ip: 192.168.10.11

# cloud-controller 기능 비활성화
disable-cloud-controller: true

# 실습 환경에서 불필요한 기능 비활성화
disable:
  - servicelb
  - rke2-coredns-autoscaler
  - rke2-ingress-nginx
  - rke2-snapshot-controller
  - rke2-snapshot-controller-crd
  - rke2-snapshot-validation-webhook
EOF

bind-address, advertise-address, node-ip를 명시적으로 설정한 이유는 Vagrant VM의 네트워크 구조 때문이다. VirtualBox VM에는 NAT용 첫 번째 NIC(enp0s3, 10.0.2.15)와 호스트 전용 네트워크용 두 번째 NIC(enp0s8, 192.168.10.11)가 있다. 설정하지 않으면 RKE2가 첫 번째 인터페이스 IP를 사용하게 되어, 노드 간 통신이 정상적으로 이루어지지 않는다. kubeadm 실습(Kubeadm - 1. 기본 구성)에서도 동일한 이유로 --apiserver-advertise-address를 지정했었다.

disable 목록에 있는 항목들은 Content Bootstrap 시 data/charts/에서 server/manifests/로 복사되지 않는다. 즉, Helm Controller의 배포 대상에서 제외된다.

HelmChartConfig

Helm Controller가 배포하는 애드온의 Helm chart 값을 오버라이드하기 위해 HelmChartConfig 리소스를 작성한다.

이 파일들을 server/manifests/에 미리 작성해 두면, 서비스 시작 시 Content Bootstrap 단계에서 runtime 이미지로부터 추출된 rke2-canal.yaml (HelmChart)과 함께 Helm Controller가 감지한다. HelmChart와 HelmChartConfig가 같은 이름(rke2-canal)을 공유할 때, HelmChartConfig의 값이 HelmChart의 기본값을 오버라이드하는 방식으로 동작한다.

mkdir -p /var/lib/rancher/rke2/server/manifests/

# Canal: flannel이 바라볼 인터페이스 지정
cat << EOF > /var/lib/rancher/rke2/server/manifests/rke2-canal-config.yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: rke2-canal
  namespace: kube-system
spec:
  valuesContent: |-
    flannel:
      iface: "enp0s9"
EOF

# CoreDNS: autoscaler 비활성화
cat << EOF > /var/lib/rancher/rke2/server/manifests/rke2-coredns-config.yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: rke2-coredns
  namespace: kube-system
spec:
  valuesContent: |-
    autoscaler:
      enabled: false
EOF

Canal의 iface: "enp0s9" 설정은 Flannel이 VXLAN overlay를 어떤 인터페이스에 올릴지를 지정한다. 이 역시 두 번째 NIC를 사용하도록 맞추는 설정이다.


서비스 시작

# 별도 터미널에서 모니터링
watch -d pstree -a
journalctl -u rke2-server -f

# RKE2 시작
systemctl enable --now rke2-server.service
systemctl status rke2-server --no-pager

RKE2가 뜨는 순서를 프로세스 트리로 보면 이전 글에서 설명한 라이프사이클이 그대로 보인다.

rke2-start

# 1단계: rke2 supervisor만 보임
rke2

# 2단계: containerd spawn
rke2
└── containerd

# 3단계: kubelet spawn
rke2
├── containerd
└── kubelet

# 4단계: containerd-shim들이 나타남 (static pod 기동)
rke2
├── containerd
├── kubelet
containerd-shim-runc-v2  ← etcd
containerd-shim-runc-v2  ← apiserver
containerd-shim-runc-v2  ← kcm
containerd-shim-runc-v2  ← scheduler
...


참고: containerd-shim

4단계에서 containerd-shim-runc-v2가 rke2 트리 밖에 별도로 나타나는 것이 눈에 띈다. 이를 제대로 이해하려면 containerd-shim이 무엇인지, 왜 분리되는지를 먼저 알아야 한다.

double fork + setsid는 RKE2 고유의 동작이 아니다. containerd를 런타임으로 쓰는 모든 Kubernetes 환경(kubeadm, kubespray 포함)에서 동일하게 적용된다. 여기서는 기존에 다룬 적이 없었기 때문에, 이후 실습 출력 결과를 해석하기 위한 배경 지식 수준으로 간단하게 다룬다.


컨테이너 1개당 1개씩 생성되는 중간 프로세스다. containerd가 컨테이너를 만들 때 직접 runc를 호출하지 않고, shim을 먼저 spawn한 뒤 shim이 runc를 호출해서 컨테이너를 생성한다. runc는 컨테이너 프로세스를 시작한 후 종료되고, shim만 남아서 컨테이너의 stdin/stdout/stderr 스트림 관리, exit status 수집 등을 담당한다.

shim을 두는 핵심 이유는 containerd와 컨테이너의 생명주기 분리다. containerd가 재시작되거나 업그레이드되어도 shim이 컨테이너를 계속 유지해준다.

double fork

containerd는 shim을 spawn할 때 fork를 두 번 하는 방식으로 프로세스 트리에서 분리한다. fork를 한 번만 하면 shim은 containerd의 직접 자식으로 남아 생명주기가 결합된다.

# 일반 fork (1회)
containerd (부모)
└── shim (자식)
    → containerd가 shim의 wait()를 담당 → 생명주기 결합
    → containerd가 죽으면 shim이 고아가 되어 시그널 전파 위험

fork를 두 번 하면 중간 프로세스가 즉시 종료되면서 shim의 부모가 사라지고, 커널이 shim을 PID 1의 자식으로 reparent한다.

# double fork
containerd (부모)
└── 중간 프로세스 (자식)  ← 1차 fork
    └── shim (손자)       ← 2차 fork
        ↓
    중간 프로세스 즉시 exit()
        ↓
    shim의 부모가 사라짐 → orphan → 커널이 PID 1로 reparent

setsid

double fork만으로는 프로세스 트리(부모-자식 관계)만 분리된다. 리눅스에서 프로세스는 부모-자식 관계 외에도 세션프로세스 그룹으로 묶여 있다. 세션 리더가 죽으면 해당 세션에 속한 모든 프로세스 그룹에 SIGHUP이 전파되는데, double fork만 한 경우 shim은 PID 1의 자식이 됐지만 여전히 containerd와 같은 세션에 속해 있다.

# double fork만 한 경우
세션 S1
├── containerd           ← 세션 멤버
└── shim (PID 1 자식)    ← 여전히 같은 세션 S1 멤버
    → 세션 리더가 죽으면 SIGHUP 받을 수 있음

setsid()는 POSIX 시스템 콜로, 호출한 프로세스를 새로운 세션의 리더로 만든다. 새 세션 생성, 새 프로세스 그룹 생성, 제어 터미널 분리를 한 번에 처리한다. shim이 setsid()를 호출하면 containerd의 세션과 완전히 독립된 새 세션이 생긴다.

# double fork + setsid 조합
세션 S1
└── containerd

세션 S2 (새로 생성)     ← setsid() 결과
└── shim (세션 리더)
    → S1에서 무슨 일이 일어나도 영향 없음

double fork + setsid

두 기법의 조합으로 shim은 containerd로부터 프로세스 트리, 세션, 시그널 그룹이 모두 분리된다.

기법 분리하는 것 없으면 생기는 문제
double fork 부모-자식 프로세스 관계 containerd가 shim의 wait() 책임을 가짐, 좀비 프로세스 관리 결합
setsid() 세션, 프로세스 그룹, 제어 터미널 세션 리더 종료 시 SIGHUP 전파, 터미널 시그널 영향

containerd가 재시작되거나 업그레이드되어도 shim(과 그 하위 컨테이너)은 영향을 받지 않는다. pstree에서 shim들이 rke2 트리 밖에 PID 1의 자식으로 나타나는 것이 이 메커니즘의 결과다.


RKE2에서 진짜 특별한 점은 shim이 분리된다는 사실 자체가 아니라, systemd unit 파일에 KillMode=process + Delegate=yes를 명시적으로 설정했다는 것이다. rke2, containerd, kubelet, shim이 모두 같은 cgroup 아래에 묶여 있으면서도, systemd가 서비스를 중지할 때 Main PID(rke2)만 건드리고 나머지는 rke2가 직접 정리하도록 위임한 구조다.


결과 확인

systemctl status

서비스가 정상적으로 떴는지, cgroup 구성이 어떻게 되는지 확인한다.

systemctl status rke2-server.service
● rke2-server.service - Rancher Kubernetes Engine v2 (server)
     Loaded: loaded (/usr/lib/systemd/system/rke2-server.service; enabled; preset: disabled)
     Active: active (running) since Wed 2026-02-18 21:02:06 KST; 7min ago
   Main PID: 6591 (rke2)
      Tasks: 138
     Memory: 2.3G
     CGroup: /system.slice/rke2-server.service
             ├─6591 "/usr/bin/rke2 server"
             ├─6612 containerd -c /var/lib/rancher/rke2/agent/etc/containerd/config.toml
             ├─6690 kubelet --volume-plugin-dir=/var/lib/kubelet/volumeplugins ...
             ├─6740 /var/lib/rancher/rke2/data/.../bin/containerd-shim-runc-v2 ...
             ├─6761 /var/lib/rancher/rke2/data/.../bin/containerd-shim-runc-v2 ...
             ...

CGroup 항목을 보면 rke2, containerd, kubelet, containerd-shim이 모두 rke2-server.service 아래에 묶여 있다.

뒤이어 살펴 볼 프로세스 트리상으로는 shim이 rke2와 분리되어 있지만, cgroup 관점에서는 같은 유닛 소속이다. unit 파일의 Delegate=yes 덕분인데, 이 설정이 kubelet이 Pod마다 cgroup을 직접 생성·관리할 수 있는 근거이기도 하다.

pstree

프로세스 트리에서 실제로 어떻게 보이는지 확인한다.

pstree -a | grep 'rke2$' -A10
  |-rke2
  |   |-containerd -c /var/lib/rancher/rke2/agent/etc/containerd/config.toml
  |   |   `-12*[{containerd}]
  |   |-kubelet --volume-plugin-dir=/var/lib/kubelet/volumeplugins ...
  |   |   `-11*[{kubelet}]
  |   `-12*[{rke2}]

rke2 트리 안에는 supervisor → containerd, kubelet만 보인다.

pstree -a | grep 'containerd-shim' -A3
  |-containerd-shim -namespace k8s.io ...
  |   |-etcd --config-file=/var/lib/rancher/rke2/server/db/etcd/config
  |   |-pause
  |-containerd-shim -namespace k8s.io ...
  |   |-kube-apiserver --admission-control-config-file=...
  |   |-pause
  |-containerd-shim -namespace k8s.io ...
  |   |-kube-controller ...
  |   |-pause
  |-containerd-shim -namespace k8s.io ...
  |   |-kube-scheduler ...
  |   |-pause
  |-containerd-shim -namespace k8s.io ...
  |   |-flanneld ...
  |   |-runsvdir ...
  |   |   |-runsv felix
  |   |   |   `-calico-node -felix
  |-containerd-shim -namespace k8s.io ...
  |   |-coredns ...

containerd-shim들은 rke2 트리 밖, PID 1의 자식으로 나타난다. double fork + setsid의 결과다.

shim 수가 많은 것도 자연스러운 결과다. 컨테이너 1개당 shim 1개가 생성된다. 서버 노드에는 static pod(etcd, apiserver, kcm, scheduler, kube-proxy) + Helm으로 배포된 애드온(canal 2개 컨테이너, coredns, metrics-server)까지 실행 중이니 shim이 8~10개 보이는 게 정상이다. 이후 kubeconfig 설정 후 kubectl get pod -A로 실행 중인 컨테이너 수와 비교해보면 shim 수와 거의 일치하는 것을 확인할 수 있다.


kubeconfig 설정 및 편의성 설정

# kubeconfig 복사
mkdir ~/.kube
cp /etc/rancher/rke2/rke2.yaml ~/.kube/config

# 바이너리 심볼릭 링크 (PATH 수정 없이 표준 위치에서 사용)
ln -s /var/lib/rancher/rke2/bin/containerd /usr/local/bin/containerd
ln -s /var/lib/rancher/rke2/bin/kubectl /usr/local/bin/kubectl
ln -s /var/lib/rancher/rke2/bin/crictl /usr/local/bin/crictl
ln -s /var/lib/rancher/rke2/bin/runc /usr/local/bin/runc
ln -s /var/lib/rancher/rke2/bin/ctr /usr/local/bin/ctr
ln -s /var/lib/rancher/rke2/agent/etc/crictl.yaml /etc/crictl.yaml

# kubectl 편의성 설정
source <(kubectl completion bash)
alias k=kubectl
complete -F __start_kubectl k
echo 'source <(kubectl completion bash)' >> /etc/profile
echo 'alias k=kubectl' >> /etc/profile
echo 'complete -F __start_kubectl k' >> /etc/profile

RKE2는 바이너리를 /var/lib/rancher/rke2/bin/에 두고 시스템 PATH에는 노출하지 않는다. Content Bootstrap에서 추출된 바이너리가 RKE2 전용 경로에만 존재하기 때문이다. 심볼릭 링크로 /usr/local/bin/에 연결해서 편하게 사용할 수 있다.

kubeadm 환경에서는 kubectl이 패키지로 직접 /usr/bin/kubectl에 설치되어 PATH에서 바로 사용할 수 있었다. RKE2에서는 이 점이 다르다.

클러스터 기본 상태 확인

kubectl cluster-info -v=6
# Kubernetes control plane is running at https://192.168.10.11:6443

kubectl get node -o wide
# NAME               STATUS   ROLES                       AGE   VERSION          INTERNAL-IP     CONTAINER-RUNTIME
# week07-k8s-node1   Ready    control-plane,etcd,master   53m   v1.33.8+rke2r1   192.168.10.11   containerd://2.1.5-k3s1

컨테이너 런타임이 containerd://2.1.5-k3s1로 표시된다. K3s에서 포크한 containerd라는 뜻이다. 공식 upstream containerd와 동일한 기능이지만, K3s/RKE2에 맞게 빌드된 버전이다.

kubectl get pod -A
# NAMESPACE     NAME                                         READY   STATUS      RESTARTS   AGE
# kube-system   etcd-week07-k8s-node1                        1/1     Running     0          53m
# kube-system   helm-install-rke2-canal-4q56n                0/1     Completed   0          53m
# kube-system   helm-install-rke2-coredns-l9r66              0/1     Completed   0          53m
# kube-system   helm-install-rke2-metrics-server-9q57h       0/1     Completed   0          53m
# kube-system   helm-install-rke2-runtimeclasses-8qzkc       0/1     Completed   0          53m
# kube-system   kube-apiserver-week07-k8s-node1              1/1     Running     0          53m
# kube-system   kube-controller-manager-week07-k8s-node1     1/1     Running     0          53m
# kube-system   kube-proxy-week07-k8s-node1                  1/1     Running     0          36m
# kube-system   kube-scheduler-week07-k8s-node1              1/1     Running     0          53m
# kube-system   rke2-canal-mqjm6                             2/2     Running     0          53m
# kube-system   rke2-coredns-rke2-coredns-559595db99-tsxnc   1/1     Running     0          53m
# kube-system   rke2-metrics-server-fdcdf575d-kqg72          1/1     Running     0          52m

helm list -A
# NAME                NAMESPACE   REVISION  CHART                               APP VERSION
# rke2-canal          kube-system 1         rke2-canal-v3.31.3-build2026020600  v3.31.3
# rke2-coredns        kube-system 1         rke2-coredns-1.45.201               1.13.1
# rke2-metrics-server kube-system 1         rke2-metrics-server-3.13.007        0.8.0
# rke2-runtimeclasses kube-system 1         rke2-runtimeclasses-0.1.000         0.1.0

helm-install-rke2-* Pod들이 Completed 상태다. Helm Controller가 각 HelmChart를 배포하기 위해 Job으로 실행한 Pod들이다. 설치가 완료되면 Completed로 남는다.

kubeadm에서는 kube-proxy가 DaemonSet으로 배포된다. RKE2에서는 kube-proxy-week07-k8s-node1이 static pod로 실행된다. kubeadm과의 차이점이다.


결과

RKE2 서버 노드 설치를 완료했다. 설치 과정 자체는 스크립트 실행 → 설정 파일 작성 → 서비스 시작으로 단순하지만, 내부에서 일어나는 일은 표준 Kubernetes 프로비저닝과 꽤 다르다.

  • 설치 스크립트 한 번으로 RPM 설치 + repo 등록까지 처리된다.
  • 서비스 시작 시 Content Bootstrap → goroutine 기반 컨트롤 플레인 초기화 → Helm Controller 애드온 배포가 순서대로 일어난다.
  • 바이너리는 PATH에 노출되지 않고 RKE2 전용 경로에만 존재한다.

다음 글에서는 설치로 만들어진 디렉터리 구조와 설정 파일을 상세히 확인하고, static pod manifest와 보안 설정을 표준 Kubernetes와 비교해 본다.


참고



hit count

댓글남기기