들어가며
Kubernetes 환경에서는 설치 후 정상 기동을 확인하는 단계와 운영 관점에서 안정적으로 관리하는 단계 사이에 요구사항과 절차 차이가 큽니다. RKE2는 보안과 규정 준수 요구사항을 기본 설계에 반영하고, 운영자가 선택해야 하는 변수를 줄여 일관된 배포 구성을 제공하는 것을 목표로 합니다. 또한 컨트롤 플레인 구성요소를 정적 Pod 형태로 배치하고 기본 런타임을 containerd로 고정하여 구성 복잡도를 낮추고 재현성과 안정성을 높이도록 설계되어 있습니다.
실습은 두 가지 관점으로 구성되어 있습니다.
- RKE2 관점: 부트스트랩 구조, 런타임 이미지 기반 콘텐츠 구성, 정적 Pod 배치, Helm Controller 기반 애드온 배포, 주요 디렉터리와 산출물을 설치 이후 단계까지 추적합니다.
- Cluster API 관점: 단일 클러스터 운영을 넘어 여러 클러스터의 생성, 확장, 업그레이드, 폐기를 Kubernetes의 CRD와 컨트롤러 패턴으로 표준화하는 방식을 실습으로 확인합니다.
전체 흐름 살펴보기
RKE2 구성 원칙과 주요 특징
- 보안 및 컴플라이언스 기본값
- CIS Benchmark 준수 관점의 기본 구성 옵션
- FIPS 지원
- 빌드 단계에서 취약점 점검 도구 사용 등
- 구성요소 패키징 방식
- 런타임 이미지에 바이너리, 차트, 매니페스트가 포함됩니다.
- 실행 시점에 콘텐츠를 추출하여 /var/lib/rancher/rke2/data/<DATA_KEY>/... 경로에 배치합니다.
- 정적 Pod 기반 컨트롤 플레인
- etcd, kube-apiserver, kube-scheduler, kube-controller-manager, kube-proxy의 정적 Pod 매니페스트가 생성됩니다.
- 매니페스트는 /var/lib/rancher/rke2/agent/pod-manifests/에 위치하며 kubelet이 감시하여 실행합니다.
- Helm Controller 내장
- /var/lib/rancher/rke2/server/manifests를 감시합니다.
- HelmChart, HelmChartConfig 기반으로 필수 애드온을 자동 설치하고 상태를 유지합니다.
RKE2 동작 흐름 정리
실습에서 확인한 RKE2의 동작 흐름은 다음과 같습니다.
- 콘텐츠 준비 단계
- rke2-runtime 이미지에서 바이너리, 차트, 매니페스트를 추출합니다.
- 컴포넌트 이미지는 로컬 tar 파일을 우선 사용하며, 없을 경우 레지스트리에서 가져옵니다.
- 로컬 tar 경로 예시: /var/lib/rancher/rke2/agent/images/*.tar
- 서버 초기화 단계
- containerd와 kubelet 준비 후 etcd를 기동합니다.
- kube-apiserver를 기동한 뒤 controller-manager, scheduler, proxy 순으로 정적 Pod 정의를 생성하고 실행합니다.
- 조인용 supervisor 포트 9345를 통해 노드 연결을 수용합니다.
- apiserver 준비 이후 helm-controller가 기동됩니다.
- 에이전트 초기화 단계
- containerd와 kubelet을 실행 및 감시하며, 정적 Pod 실행이 가능한 상태로 수렴합니다.
- 서버 노드에서는 manifests 기반 애드온이 클러스터 리소스로 적용됩니다.
- 상주 프로세스 단계
- 종료 신호 수신 또는 런타임 종료 시점까지 프로세스가 유지됩니다.
RKE2 실습 범위
- 환경 준비
- Rocky Linux 9 VM 2대(Vagrant)
- 커널 모듈 및 파라미터 적용, SWAP 비활성화, 방화벽 및 SELinux 조정, NetworkManager 예외 처리 등 기본 준비 작업을 스크립트로 정리합니다.
- 서버 설치 및 구성
- 채널 기반 설치로 버전 라인을 적용합니다.
- config.yaml에서 CNI(canal), bind, advertise, node-ip, 불필요 애드온 비활성화를 설정합니다.
- HelmChartConfig로 canal 인터페이스 고정, CoreDNS autoscaler 비활성화를 적용합니다.
- 디렉터리 및 산출물 확인
- /var/lib/rancher/rke2/{server,agent,data} 구조를 확인합니다.
- server token, TLS, DB, manifests를 점검합니다.
- agent의 containerd, kubelet 설정과 crictl endpoint 소켓 경로를 확인합니다.
- 노드 라이프사이클 검증
- 서버의 token 및 9345 접근을 확인한 뒤 워커에서 server와 token을 설정하여 조인합니다.
- drain, 노드 삭제, 제거 후 재조인 절차로 운영 흐름을 확인합니다.
- 샘플 애플리케이션 배포
- NodePort로 접근을 확인하고, 다중 노드에서 분산 동작을 점검합니다.
운영 절차: 인증서와 업그레이드
- 인증서 점검 및 교체
- rke2 certificate check로 만료 여부와 잔여 기간을 확인합니다.
- rke2 certificate rotate로 교체를 수행하며, 서버를 우선 적용한 뒤 에이전트를 순차 적용합니다.
- 수동 업그레이드
- 설치 스크립트의 채널을 변경하여 서버를 업그레이드한 뒤 워커에 동일하게 적용합니다.
- 자동 업그레이드 system-upgrade-controller
- server용 Plan과 agent용 Plan을 분리하여 순차적으로 cordon 및 업그레이드를 수행합니다.
- latest 채널은 중간 버전을 건너뛸 수 있으므로 단계적 업그레이드가 필요할 경우 version 고정 또는 release line 채널 사용을 권장합니다.
고가용성 구성: TCP 로드밸런서와 다중 컨트롤 플레인
- 기본 원칙
- 6443(Kubernetes API)와 9345(RKE2 supervisor)는 TLS 기반이므로 L7이 아닌 TCP 로드밸런서 구성이 적합합니다.
- 구성 예시
- HAProxy로 전단을 구성합니다.
- 컨트롤 플레인 3대(홀수 권장)와 워커 노드 확장 구조로 구성합니다.
- 인증서 교체와 업그레이드는 컨트롤 플레인과 워커 모두 순차적으로 수행하는 운영 절차를 적용합니다.
Cluster API 개요
- 역할 정의
- 관리 클러스터: CAPI Core와 Provider 컨트롤러가 설치되어 워크로드 클러스터를 생성하고 관리합니다.
- 워크로드 클러스터: 선언된 리소스에 따라 프로비저닝되며 애플리케이션이 배치되는 대상입니다.
- 리소스 모델
- Cluster, Machine, MachineDeployment, MachineSet, KubeadmControlPlane 등의 리소스로 수명주기를 분해하여 관리합니다.
- 변경 처리 방식
- 스펙 변경은 교체 기반 롤링 방식이 기본이며, 필요한 경우 확장 지점을 통해 예외를 처리할 수 있습니다.
CAPI 실습 범위
- 관리 클러스터 준비
- kind로 관리 클러스터를 구성합니다.
- CAPD 실습을 위해 Docker 소켓 마운트가 필요합니다.
- clusterctl init --infrastructure docker로 CAPI core, kubeadm bootstrap 및 control-plane, CAPD, cert-manager를 설치합니다.
- Provider, CRD, 기능 게이트를 확인합니다.
- 워크로드 클러스터 생성 및 확인
- clusterctl generate cluster --flavor development로 ClusterClass와 Topology가 포함된 YAML을 생성합니다.
- 적용 후 CAPD가 노드 컨테이너와 LB 컨테이너를 생성하고 kubeadm이 init 및 join을 수행합니다.
- 초기 NotReady는 일반적인 흐름이며, CNI 설치 이후 Ready로 전환되는지 확인합니다.
- Managed Topology 동작 확인
- ClusterClass 템플릿을 직접 참조하지 않고, 변수와 패치가 반영된 클러스터 전용 템플릿 복사본이 생성되어 참조되는지 확인합니다.
- 컨트롤 플레인 엔드포인트 확인
- controlPlaneEndpoint가 특정 노드가 아니라 LB 컨테이너 IP로 설정되는지 확인합니다.
- haproxy 설정에서 apiserver 백엔드 분산 구성을 확인합니다.
- kubeadm 산출물 검증
- controlPlaneEndpoint, dnsDomain, pod 및 service subnet, 인증서 SAN 등의 결과물을 워크로드 클러스터 내부에서 확인합니다.
RKE2 소개
RKE2는 Rancher에서 개발한 엔터프라이즈 Kubernetes 배포판으로, 미국 연방 정부 부문의 보안 및 규정 준수를 고려한 설계를 특징으로 합니다.
- 운영자의 개입을 최소화하면서 클러스터가 CIS Kubernetes Benchmark v1.7 또는 v1.8을 통과할 수 있도록 기본 설정 및 구성 옵션을 제공합니다.
- FIPS 140-2 규정 준수를 지원합니다.
- 빌드 파이프라인에서 trivy를 사용하여 구성 요소의 CVE를 정기적으로 검사합니다.
- RKE2는 컨트롤 플레인 구성 요소를 kubelet이 관리하는 정적 Pod로 실행합니다. 내장 컨테이너 런타임은 containerd입니다.
- 이 배포판은 독립 실행형으로 실행되거나 Rancher에 통합되어 실행될 수 있습니다.
아키텍처
- RKE2는 경량 Kubernetes 배포판인 K3s를 개발/유지하며 얻은 경험을 바탕으로, K3s의 사용 편의성을 유지하면서도 엔터프라이즈 환경에 적합하도록 구성한 배포판입니다.
- RKE2는 Kubernetes 클러스터에 참여할 모든 노드에 설치 및 구성해야 하는 단일 바이너리 파일입니다.
- 시작되면 RKE2가 각 노드의 역할에 맞는 에이전트를 부트스트랩하고 관리하면서, 네트워크에서 필요한 콘텐츠를 가져오는 방식으로 동작합니다.
RKE2 구성요소
K3s 기반 구성요소
- Helm Controller : GitOps 스타일 경량 배포기
RKE2 클러스터가 올라올 때 필수 Helm 차트를 자동으로 설치·유지하는 내장 배포 컨트롤러입니다.- manifests 디렉토리 감시 방식
/var/lib/rancher/rke2/server/manifests 디렉토리에 (HelmChart) YAML 파일만 두면 Helm Controller에 의해 자동으로 Helm chart가 (다운로드)/설치/업데이트됩니다.
- manifests 디렉토리 감시 방식
tree /var/lib/rancher/rke2/server/manifests
├── rke2-canal-config.yaml
├── rke2-canal.yaml
├── rke2-coredns-config.yaml
├── rke2-coredns.yaml
├── rke2-metrics-server.yaml
└── rke2-runtimeclasses.yaml
- 용도 비교
Helm Controller는 부트스트랩(부팅 시 필수 애드온 자동 설치) 목적, Argo CD는 GitOps(애플리케이션 배포) 목적에 가깝습니다.
Kubernetes 기본 구성요소
- API Server, Controller Manager, Scheduler
- Proxy, Kubelet
기타 구성요소
- etcd
- runc
- containerd / cri
- CNI: Canal( Calico & Flannel ), Cilium, Calico, Flannel, 추가로 Multus 지원
- Canal : 네트워크 연결은 Flannel, 보안 정책은 Calico가 담당하는 하이브리드 CNI.
- canal DaemonSet 파드 내 컨테이너 : flannel(vxlan overlay 담당), calico-node(network policy, iptables)
kubectl describe pod -n kube-system -l k8s-app=canal | grep Image: | uniq
Image: rancher/hardened-calico:v3.31.3-build20260119
Image: rancher/hardened-flannel:v0.28.0-build20260119
- CoreDNS
- Ingress NGINX Controller 및/또는 Traefik
- Metrics Server
- Helm
Process Lifecycle
RKE2는 크게 Content Bootstrap → Initialize Server → Initialize Agent → Daemon Process 흐름으로 이해할 수 있습니다.
Content Boostrap
- RKE2 런타임 이미지로부터 sources, binaries, manifests를 추출하여 서버 노드와 에이전트 노드를 모두 실행합니다.
- rke2-runtime 컨테이너 내부에는 바이너리와 HelmChart가 포함되어 있습니다.
crictl images
IMAGE TAG IMAGE ID SIZE
docker.io/rancher/rke2-runtime v1.34.3-rke2r3 30afde048693a 91.6MB
tree
├── 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
RKE2 런타임 이미지는 /var/lib/rancher/rke2/agent/images/*.tar에서 스캔하며(태그는 rke2 --version 출력), 이를 찾을 수 없는 경우 DockerHub에서 찾습니다.
- /var/lib/rancher/rke2/agent/images/에 runtime-image.txt 파일 확인
tree /var/lib/rancher/rke2/agent/images/
├── etcd-image.txt
├── kube-apiserver-image.txt
├── kube-controller-manager-image.txt
├── kube-proxy-image.txt
├── kube-scheduler-image.txt
└── runtime-image.txt
cat /var/lib/rancher/rke2/agent/images/runtime-image.txt
index.docker.io/rancher/rke2-runtime:v1.34.3-rke2r3
- 그런 다음 RKE2는 런타임 이미지에서 /bin/을 추출하여 /var/lib/rancher/rke2/data/${RKE2_DATA_KEY}/bin으로 평탄화합니다.
- ${RKE2_DATA_KEY}는 이미지를 식별(버전)하는 고유 문자열을 나타냅니다.
tree /var/lib/rancher/rke2/data/
└── v1.34.3-rke2r3-5b8349de68df
├── 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
- RKE2가 예상대로 작동하려면 런타임 이미지는 최소한 다음을 제공해야 합니다.
- containerd (CRI)
- containerd-shim (shims wrap runc tasks and do not stop when containerd does)
- containerd-shim-runc-v1
- containerd-shim-runc-v2
- kubelet (쿠버네티스 노드 에이전트)
- runc (OCI 런타임)
- 런타임 이미지에는 다음과 같은 운영 도구도 포함되어 있습니다.
- ctr (저수준 유지보수 및 점검)
- crictl (낮은 수준의 CRI 유지 관리 및 점검)
- kubectl (쿠버네티스 클러스터 유지 관리 및 점검)
- socat (포트 포워딩에 필요함)
- 바이너리 파일 압축 해제가 완료되면 RKE2는 이미지에서 차트를 추출하여 /var/lib/rancher/rke2/server/manifests 디렉터리에 저장합니다.
tree /var/lib/rancher/rke2/server/manifests
├── rke2-canal-config.yaml
├── rke2-canal.yaml
├── rke2-coredns-config.yaml
├── rke2-coredns.yaml
├── rke2-metrics-server.yaml
└── rke2-runtimeclasses.yaml
Initialize Server
임베디드 K3s 엔진 서버에는 특수 에이전트 프로세스가 포함되어 있어, 노드 컨테이너 런타임이 시작될 때까지 다음 시작이 연기됩니다.
- /var/lib/rancher/rke2/agent/pod-manifests/ static pod 확인
ls -ltr /var/lib/rancher/rke2/agent/pod-manifests/
total 32
-rw-r--r--. 1 root root 3279 Feb 14 16:32 etcd.yaml
-rw-r--r--. 1 root root 2325 Feb 14 16:32 kube-proxy.yaml
-rw-r--r--. 1 root root 9337 Feb 14 16:33 kube-apiserver.yaml
-rw-r--r--. 1 root root 3724 Feb 14 16:33 kube-scheduler.yaml
-rw-r--r--. 1 root root 6325 Feb 14 16:33 kube-controller-manager.yaml
Prepare Components
- kube-apiserver
- kube-apiserver 이미지가 아직 존재하지 않는 경우, goroutine을 회전시켜 etcd를 기다린 다음 /var/lib/rancher/rke2/agent/pod-manifests/에 정적 포드 정의를 작성합니다.
- kube-controller-manager
- kube-controller-manager 이미지가 아직 존재하지 않는 경우, goroutine을 회전시켜 kube-apiserver를 기다린 다음 /var/lib/rancher/rke2/agent/pod-manifests/에 정적 포드 정의를 작성합니다.
- kube-scheduler
- kube-scheduler 이미지가 아직 없는 경우, goroutine을 회전시켜 kube-apiserver를 기다린 다음 /var/lib/rancher/rke2/agent/pod-manifests/에 정적 포드 정의를 작성합니다.
Start Cluster
다른 클러스터 서버/에이전트가 들어올 수 있도록 goroutine에서 HTTP server를 띄워 listen 하고, 이후 클러스터 initialize/join 흐름으로 넘어갑니다.
- etcd
- 이미 존재하지 않는 경우 etcd 이미지를 가져와서 goroutine을 회전시켜 kubelet을 기다린 다음 /var/lib/rancher/rke2/agent/pod-manifests/에 정적 포드 정의를 작성합니다.
- helm-controller
- kube-apiserver가 준비될 때까지 기다린 후 goroutine을 회전시켜 내장 helm-controller를 시작합니다.
rke2-server 프로세스 및 서비스 확인
#
pstree -al
├─rke2
│ ├─containerd -c /var/lib/rancher/rke2/agent/etc/containerd/config.toml
│ │ └─12*[{containerd}]
│ ├─kubelet --volume-plugin-dir=/var/lib/kubelet/volumeplugins --file-check-frequency=5s --sync-frequency=30s --config-dir=/var/lib/rancher/rke2/agent/etc/kubelet.conf.d --containerd=/run/k3s/containerd/containerd.sock --hostname-override=k8s-node1 --kubeconfig=/var/lib/rancher/rke2/agent/kubelet.kubeconfig --node-ip=192.168.10.11 --node-labels= --read-only-port=0
│ │ └─14*[{kubelet}]
│ └─12*[{rke2}]*
systemctl status rke2-server.service --no-pager
● rke2-server.service - Rancher Kubernetes Engine v2 (server)
Loaded: loaded (/usr/lib/systemd/system/rke2-server.service; enabled; preset: disabled)
Active: active (running) since Sat 2026-02-14 16:33:49 KST; 1h 37min ago
Docs: https://github.com/rancher/rke2#readme
Process: 6346 ExecStartPre=/sbin/modprobe br_netfilter (code=exited, status=0/SUCCESS)
Process: 6347 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS)
Main PID: 6348 (rke2)
Tasks: 146
Memory: 1.1G
CPU: 5min 15.217s
CGroup: /system.slice/rke2-server.service
├─6348 "/usr/bin/rke2 server"
├─6367 containerd -c /var/lib/rancher/rke2/agent/etc/containerd/config.toml
├─6426 kubelet --volume-plugin-dir=/var/lib/kubelet/volumeplugins --file-check-frequency=5s --sync-frequency…
├─6483 /var/lib/rancher/rke2/data/v1.34.3-rke2r3-5b8349de68df/bin/containerd-shim-runc-v2 -namespace k8s.io …
├─6484 /var/lib/rancher/rke2/data/v1.34.3-rke2r3-5b8349de68df/bin/containerd-shim-runc-v2 -namespace k8s.io …
...
systemd unit 파일 확인
cat /usr/lib/systemd/system/rke2-server.service
[Unit]
Description=Rancher Kubernetes Engine v2 (server)
Documentation=https://github.com/rancher/rke2#readme
Wants=network-online.target
After=network-online.target
Conflicts=rke2-agent.service
[Install]
WantedBy=multi-user.target
[Service]
Type=notify
EnvironmentFile=-/etc/default/%N
EnvironmentFile=-/etc/sysconfig/%N
EnvironmentFile=-/usr/lib/systemd/system/%N.env
KillMode=process # "나만 죽이고, 내가 띄운 애들까지는 systemd가 건드리지 마" - RKE2, containerd 같은 애들은 내부적으로 여러 자식 프로세스 shim, runc, container 프로세스를 관리함. 물론 완전 무중단은 아니지만, 충격 최소화
Delegate=yes # "cgroup(리소스 제어)는 내가 직접 관리할게" - cgroup(리눅스 리소스 그룹) 관리를 systemd가 아니라 자기 자신이 직접 하도록 위임(delegate) 하겠다는 의미.
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
TimeoutStartSec=0
Restart=always
RestartSec=5s
ExecStartPre=-/sbin/modprobe br_netfilter
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/bin/rke2 server
ExecStopPost=-/bin/sh -c "systemd-cgls /system.slice/%n | grep -Eo '[0-9]+ (containerd|kubelet)' | awk '{print $1}' | xargs -r kill"
Initialize Agent
The agent process entry point. 서버 프로세스의 경우 임베디드 K3s 엔진이 이를 직접 호출합니다.
Container Runtime
- containerd : containerd 프로세스를 생성하고 종료를 기다립니다. containerd가 종료되면 rke2 프로세스도 종료됩니다.
Node Agent
- kubelet : Spawn and supervise the kubelet process.
- kubelet이 종료되면 rke2가 재시작을 시도합니다. kubelet이 작동하면 사용 가능한 모든 정적 포드가 시작됩니다.
- 서버의 경우, etcd와 kube-apiserver가 연속적으로 시작되어 정적 포드를 통해 시작된 나머지 구성 요소들이 kube-apiserver에 연결되어 처리를 시작할 수 있게 됩니다.
Server Charts
- 서버 노드에서는 helm-controller가 /var/lib/rancher/rke2/server/manifests에 있는 차트를 클러스터에 적용할 수 있습니다.
- rke2*-canal.yaml 또는 rke2-cilium.yaml 또는 rke2-calico.yaml 또는 rke2-flannel.yaml 또는 rke2-multus.yaml (daemonset, bootstrap)
- rke2-coredns.yaml (deployment, bootstrap)
- rke2-ingress-nginx.yaml 및/또는 rke2-traefik.yaml, rke2-traefik-crd.yaml (deployment)
- rke2-metrics-server.yaml (deployment)
- rke2-runtimeclasses.yaml (deployment)
- rke2-snapshot-controller-crd.yaml, rke2-snapshot-controller.yaml, rke2-snapshot-validation-webhook.yaml (deployment)
Daemon Process
- 이제 RKE2 프로세스는 SIGTERM 또는 SIGKILL을 받거나 컨테이너 프로세스가 종료될 때까지 무기한으로 실행됩니다.
RKE2 실습
실습 환경 배포
실습은 Rocky Linux 9 기반 VM 2대로 구성했습니다. k8s-node1은 서버(컨트롤 플레인), k8s-node2는 에이전트(워커)로 사용합니다.
Vagrantfile
# Base Image https://portal.cloud.hashicorp.com/vagrant/discover/bento/rockylinux-9
BOX_IMAGE = "bento/rockylinux-9" # "bento/rockylinux-10.0"
BOX_VERSION = "202510.26.0"
N = 2 # max number of Node
Vagrant.configure("2") do |config|
# Nodes
(1..N).each do |i|
config.vm.define "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 = "k8s-node#{i}"
vb.cpus = 4
vb.memory = 4096
vb.linked_clone = true
end
subconfig.vm.host_name = "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
노드 공통 초기화 스크립트입니다. 타임존 설정, 방화벽/SELinux, SWAP 해제, 커널 모듈/파라미터, hosts 기반 로컬 DNS, Helm/K9s 설치, 그리고 CNI 인터페이스 관련 NetworkManager 설정까지 한 번에 처리합니다.
#!/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
#vxlan
EOF
modprobe overlay >/dev/null 2>&1
modprobe br_netfilter >/dev/null 2>&1
#modprobe vxlan >/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"
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 11] 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 12] ETC"
echo "sudo su -" >> /home/vagrant/.bashrc
echo ">>>> Initial Config End <<<<"
실행
아래 순서로 디렉터리 준비 → 파일 다운로드 → 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
RKE2 설치 (서버 노드)
서버 노드는 k8s-node1에서 진행했습니다. 설치는 제공되는 설치 스크립트를 그대로 사용했고, 설치 후 config.yaml과 HelmChartConfig로 기본 애드온 구성을 조정한 다음 rke2-server를 기동했습니다.
설치 스크립트에 적용 가능한 변수
설치 스크립트는 환경변수로 동작을 제어할 수 있습니다. 실습에서는 채널/타입 위주로 사용했습니다.
- INSTALL_RKE2_VERSION : GitHub에서 다운로드할 RKE2 버전
- INSTALL_RKE2_TYPE : 생성할 systemd 서비스의 유형 (server/agent)
- INSTALL_RKE2_CHANNEL_URL : 다운로드 URL을 가져오기 위한 채널 URL
- INSTALL_RKE2_CHANNEL : 사용할 채널 (stable, latest, testing, 또는 v1.xx)
- INSTALL_RKE2_METHOD : 설치 방법 (rpm 또는 tar)
설치 실행 및 설치 결과 확인
# 설치 스크립트 다운로드/실행 준비
curl -sfL https://get.rke2.io --output install.sh
chmod +x install.sh
# 설치 (채널 지정)
INSTALL_RKE2_CHANNEL=v1.33 ./install.sh
설치 후 버전/리포지토리/명령 구성을 확인했습니다.
# rke2 버전 확인
rke2 --version
# repo 추가 확인
dnf repolist
tree /etc/yum.repos.d/
cat /etc/yum.repos.d/rancher-rke2.repo
# 디렉터리 생성 확인
tree /etc/rancher/
tree /var/lib/rancher/
# rke2 명령 확인
rke2 --h
예시 출력은 아래 형태로 확인했습니다.
Installed:
rke2-common-1.33.7~rke2r3-0.el9.aarch64 rke2-selinux-0.22-1.el9.noarch rke2-server-1.33.7~rke2r3-0.el9.aarch64
rke2 version v1.33.7+rke2r3 (7e4fd1a82edf497cab91c220144619bbad659cf4)
go version go1.24.11 X:boringcrypto
rancher-rke2-1.33-stable Rancher RKE2 1.33 (v1.33)
rancher-rke2-common-stable Rancher RKE2 Common (v1.33)
[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
server Run management server
agent Run node agent
RKE2 서버 설정 파일 작성
CNI는 canal을 사용했고, 서버의 bind/advertise 주소와 node-ip도 명시했습니다. 또한 실습에서는 필요 없는 일부 구성요소는 disable로 제외했습니다.
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
disable-cloud-controller: true
disable:
- servicelb
- rke2-coredns-autoscaler
- rke2-ingress-nginx
- rke2-snapshot-controller
- rke2-snapshot-controller-crd
- rke2-snapshot-validation-webhook
EOF
cat /etc/rancher/rke2/config.yaml
canal / coredns HelmChartConfig 적용
- canal에서는 flannel 인터페이스를 enp0s9로 고정했습니다.
- coredns autoscaler는 미설치되도록 values로 비활성화했습니다.
mkdir -p /var/lib/rancher/rke2/server/manifests/
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
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
기동 및 kubeconfig 설정
기동 과정은 모니터링을 켜 두고 확인했습니다.
# 모니터링 (신규 터미널)
watch -d pstree -a
journalctl -u rke2-server -f
# RKE2 시작
systemctl enable --now rke2-server.service
systemctl status rke2-server --no-pager
# 프로세스 확인
pstree -a | grep -v color | grep 'rke2$' -A5
pstree -a | grep -v color | grep 'containerd-shim ' -A2
# 자격증명 파일 복사
mkdir ~/.kube
ls -l /etc/rancher/rke2/rke2.yaml
cp /etc/rancher/rke2/rke2.yaml ~/.kube/config
# /etc/rancher 디렉터리 확인
tree /etc/rancher/
cat /etc/rancher/node/password
cat /etc/rancher/rke2/config.yaml
cat /etc/rancher/rke2/rke2-pss.yaml
바이너리 확인 및 심볼릭 링크 구성
RKE2가 내려주는 바이너리 경로는 /var/lib/rancher/rke2/bin/이고, 표준 경로에서 쓰기 위해 심볼릭 링크로 노출했습니다.
tree /var/lib/rancher/rke2/bin/
# 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
runc --version
containerd --version
kubectl version
# 편의성 설정
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
k9s
설치 확인
kubectl cluster-info -v=6
kubectl get node -owide
helm list -A
kubectl get pod -A
예시 출력은 아래 형태로 확인했습니다.
Kubernetes control plane is running at https://192.168.10.11:6443
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-node1 Ready control-plane,etcd 15m v1.34.3+rke2r3 192.168.10.11 <none> Rocky Linux 9.6 (Blue Onyx) 5.14.0-570.52.1.el9_6.aarch64 containerd://2.1.5-k3s1
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
rke2-canal kube-system 1 2026-02-09 14:30:45.002896818 +0000 UTC deployed rke2-canal-v3.31.3-build2026011900 v3.31.3
rke2-coredns kube-system 1 2026-02-09 14:30:44.989504642 +0000 UTC deployed rke2-coredns-1.45.008 1.13.1
rke2-metrics-server kube-system 1 2026-02-09 14:31:13.07011618 +0000 UTC deployed rke2-metrics-server-3.13.006 0.8.0
rke2-runtimeclasses kube-system 1 2026-02-09 14:31:17.059719859 +0000 UTC deployed rke2-runtimeclasses-0.1.000 0.1.0
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system etcd-k8s-node1 1/1 Running 0 2m31s
kube-system helm-install-rke2-canal-8ck96 0/1 Completed 0 2m42s
kube-system helm-install-rke2-coredns-mhw4x 0/1 Completed 0 2m42s
kube-system helm-install-rke2-metrics-server-rhqcp 0/1 Completed 0 2m42s
kube-system helm-install-rke2-runtimeclasses-7m69w 0/1 Completed 0 2m42s
kube-system kube-apiserver-k8s-node1 1/1 Running 0 2m31s
kube-system kube-controller-manager-k8s-node1 1/1 Running 0 2m31s
kube-system kube-proxy-k8s-node1 1/1 Running 0 2m31s
kube-system kube-scheduler-k8s-node1 1/1 Running 0 2m31s
kube-system rke2-canal-dkw2n 2/2 Running 0 2m22s
kube-system rke2-coredns-rke2-coredns-784bcb7f4d-tpt2d 1/1 Running 0 2m22s
kube-system rke2-metrics-server-7b59bd8854-m5w2c 1/1 Running 0 114s
설치 후 디렉터리/리소스 확인
RKE2는 /var/lib/rancher/rke2 아래에 실행에 필요한 바이너리/차트/인증서/토큰/DB 등을 구조적으로 내려줍니다. 서버 노드에서는 server 디렉터리까지 포함해서 확인했고, 이후 워커 노드에서는 agent 중심으로 확인했습니다.
/var/lib/rancher/rke2 기본 구조
tree /var/lib/rancher/rke2 -L 1
├── agent
├── bin -> /var/lib/rancher/rke2/data/v1.34.3-rke2r3-5b8349de68df/bin
├── data
└── server
server 디렉터리 (토큰/차트/인증서)
tree /var/lib/rancher/rke2/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
토큰은 워커(에이전트) 조인 시 사용하므로 아래처럼 확인했습니다.
ls -l /var/lib/rancher/rke2/server/
cat /var/lib/rancher/rke2/server/node-token
cat /var/lib/rancher/rke2/server/token
Helm controller가 처리하는 manifests는 values(set 포함)까지 들어 있는 형태라서, 실제로 파일을 열어 구성 값을 확인할 수 있었습니다.
cat /var/lib/rancher/rke2/server/manifests/rke2-coredns.yaml
Helm controller 관련 CRD/리소스도 같이 확인했습니다.
kubectl get crd | grep -E 'helm|addon'
kubectl get helmcharts.helm.cattle.io -n kube-system -owide
kubectl get job -n kube-system
kubectl get helmchartconfigs -n kube-system
kubectl describe helmchartconfigs -n kube-system rke2-canal
kubectl get addons.k3s.cattle.io -n kube-system
예시 출력은 아래 형태였습니다.
addons.k3s.cattle.io
helmchartconfigs.helm.cattle.io
helmcharts.helm.cattle.io
NAME REPO CHART VERSION TARGETNAMESPACE BOOTSTRAP FAILED JOB
rke2-canal true False helm-install-rke2-canal
rke2-coredns true False helm-install-rke2-coredns
rke2-metrics-server False helm-install-rke2-metrics-server
rke2-runtimeclasses False helm-install-rke2-runtimeclasses
NAME STATUS COMPLETIONS DURATION AGE
helm-install-rke2-canal Complete 1/1 21s 118m
helm-install-rke2-coredns Complete 1/1 20s 118m
helm-install-rke2-metrics-server Complete 1/1 52s 118m
helm-install-rke2-runtimeclasses Complete 1/1 51s 118m
NAME AGE
rke2-canal 119m
rke2-coredns 119m
Spec:
Failure Policy: reinstall
Values Content: flannel:
iface: "enp0s9"
NAME SOURCE CHECKSUM
rke2-canal /var/lib/rancher/rke2/server/manifests/rke2-canal.yaml 2de126b0695f5c424eba2b8e7c45c67a757edac6ecccce7a6141228e6c2fd852
rke2-canal-config /var/lib/rancher/rke2/server/manifests/rke2-canal-config.yaml 122823431e18d9f23621508c24bff9aff307f46b4d6d9208bbbbf23311db058e
rke2-coredns /var/lib/rancher/rke2/server/manifests/rke2-coredns.yaml 523c74e141f7722f3721ce5b3b6556241199bf3aed9b781a5cbfeebd2fe595f4
rke2-coredns-config /var/lib/rancher/rke2/server/manifests/rke2-coredns-config.yaml 9c8e2bbb7603c69c233c9343c516d3d302fd59b758e2e7084c7ab08b5bfba0e4
rke2-metrics-server /var/lib/rancher/rke2/server/manifests/rke2-metrics-server.yaml 4fe80fe31c6cc3d5f1cb7cd19f4d3181f1c810751ed995fa1ca5d9a560c436f8
rke2-runtimeclasses /var/lib/rancher/rke2/server/manifests/rke2-runtimeclasses.yaml 7915375038cf0683ea2ee558d01adb9a478d1c51ec3f0a508b9082cd4f0a7145
인증서는 server/tls 아래에서 확인했고, 예시로 apiserver 인증서의 SAN/기간도 확인했습니다.
cat /var/lib/rancher/rke2/server/tls/server-ca.crt | openssl x509 -text -noout
cat /var/lib/rancher/rke2/server/tls/serving-kube-apiserver.crt | openssl x509 -text -noout
예시 일부
Issuer: CN=rke2-server-ca@1770533252
Validity
Not Before: Feb 8 06:47:32 2026 GMT
Not After : Feb 8 06:47:32 2027 GMT
Subject: CN=kube-apiserver
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Subject Alternative Name:
DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:localhost, DNS: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:192.168.10.11, IP Address:10.43.0.1
data / agent 디렉터리 확인
tree /var/lib/rancher/rke2/data/
tree /var/lib/rancher/rke2/data/ -L 2
cat /var/lib/rancher/rke2/data/v1.34.3-rke2r3-5b8349de68df/charts/rke2-coredns.yaml
tree /var/lib/rancher/rke2/agent/ | more
tree /var/lib/rancher/rke2/agent/ -L 3
예시 구조
├── containerd
│ ├── bin
│ ├── containerd.log
│ ├── io.containerd.content.v1.content
│ │ ├── blobs
│ │ └── ingest
│ └── tmpmounts
├── etc
│ ├── containerd
│ │ └── config.toml
│ ├── crictl.yaml
│ └── kubelet.conf.d
│ └── 00-rke2-defaults.conf
├── images
│ ├── cloud-controller-manager-image.txt
│ ├── etcd-image.txt
│ ├── kube-apiserver-image.txt
│ ├── kube-controller-manager-image.txt
│ ├── kube-proxy-image.txt
│ ├── kube-scheduler-image.txt
│ └── runtime-image.txt
├── logs
│ └── kubelet.log
├── pod-manifests
│ ├── cloud-controller-manager.yaml
│ ├── etcd.yaml
│ ├── kube-apiserver.yaml
│ ├── kube-controller-manager.yaml
│ ├── kube-proxy.yaml
│ └── kube-scheduler.yaml
...
crictl 설정은 RKE2 기준으로 k3s 경로가 포함된 sock을 바라보는 것도 같이 확인했습니다.
crictl ps
cat /var/lib/rancher/rke2/agent/etc/crictl.yaml
ln -s /var/lib/rancher/rke2/agent/etc/crictl.yaml /etc/crictl.yaml
crictl ps
crictl images
예시
runtime-endpoint: unix:///run/k3s/containerd/containerd.sock
containerd 설정은 RKE2가 생성한 파일이므로 직접 수정은 권장되지 않는다고 명시되어 있었고, registry 설정 경로도 같이 확인했습니다.
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"
...
[plugins.'io.containerd.cri.v1.images'.registry]
config_path = "/var/lib/rancher/rke2/agent/etc/containerd/certs.d"
certs.d 경로는 초기 상태에서는 없음을 확인했습니다.
ls -l /var/lib/rancher/rke2/agent/etc/containerd/certs.d
ls: cannot access '/var/lib/rancher/rke2/agent/etc/containerd/certs.d': No such file or directory
이미지 목록 텍스트 파일도 확인했습니다.
grep -H '' /var/lib/rancher/rke2/agent/images/*
crictl images
/var/lib/rancher/rke2/agent/images/etcd-image.txt:index.docker.io/rancher/hardened-etcd:v3.6.7-k3s1-build20260126
/var/lib/rancher/rke2/agent/images/kube-apiserver-image.txt:index.docker.io/rancher/hardened-kubernetes:v1.34.3-rke2r3-build20260127
/var/lib/rancher/rke2/agent/images/kube-controller-manager-image.txt:index.docker.io/rancher/hardened-kubernetes:v1.34.3-rke2r3-build20260127
/var/lib/rancher/rke2/agent/images/kube-proxy-image.txt:index.docker.io/rancher/hardened-kubernetes:v1.34.3-rke2r3-build20260127
/var/lib/rancher/rke2/agent/images/kube-scheduler-image.txt:index.docker.io/rancher/hardened-kubernetes:v1.34.3-rke2r3-build20260127
/var/lib/rancher/rke2/agent/images/runtime-image.txt:index.docker.io/rancher/rke2-runtime:v1.34.3-rke2r3
kubelet 기본 설정도 확인했습니다.
cat /var/lib/rancher/rke2/agent/etc/kubelet.conf.d/00-rke2-defaults.conf
tail -f /var/lib/rancher/rke2/agent/logs/kubelet.log
address: 192.168.10.11
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
anonymous:
enabled: false
...
cgroupDriver: systemd
...
containerRuntimeEndpoint: unix:///run/k3s/containerd/containerd.sock
...
failSwapOn: false
nodeStatusUpdateFrequency: 10s
serializeImagePulls: false
staticPodPath: /var/lib/rancher/rke2/agent/pod-manifests
...
static pod manifests 확인 (서버 노드)
/var/lib/rancher/rke2/agent/pod-manifests에는 컨트롤 플레인 구성요소들이 정적 파드 형태로 배치됩니다.
tree /var/lib/rancher/rke2/agent/pod-manifests
├── etcd.yaml
├── kube-apiserver.yaml
├── kube-controller-manager.yaml
├── kube-proxy.yaml
└── kube-scheduler.yaml
apiserver/etcd/scheduler/controller-manager/proxy를 각각 확인했습니다.
kubectl describe pod -n kube-system kube-apiserver-k8s-node1
cat /var/lib/rancher/rke2/agent/pod-manifests/kube-apiserver.yaml
cat /var/lib/rancher/rke2/server/cred/encryption-config.json | jq
kubectl describe pod -n kube-system etcd-k8s-node1
cat /var/lib/rancher/rke2/agent/pod-manifests/etcd.yaml
find / -name etcdctl 2>/dev/null
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 version
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
kubectl describe pod -n kube-system kube-scheduler-k8s-node1
cat /var/lib/rancher/rke2/agent/pod-manifests/kube-scheduler.yaml
cat /var/lib/rancher/rke2/agent/pod-manifests/kube-controller-manager.yaml
cat /var/lib/rancher/rke2/agent/pod-manifests/kube-proxy.yaml
# kube-apiserver.yaml 일부
- --anonymous-auth=false
- --api-audiences=https://kubernetes.default.svc.cluster.local,rke2
- --egress-selector-config-file=/var/lib/rancher/rke2/server/etc/egress-selector-config.yaml
- --encryption-provider-config=/var/lib/rancher/rke2/server/cred/encryption-config.json
- --encryption-provider-config-automatic-reload=true
- --tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
env:
- name: FILE_HASH
value: 6fa5251708b3bc2cb032195cc31ae26e8550da13563ddacc90d723fa807594ab
- name: NO_PROXY
value: .svc,.cluster.local,10.42.0.0/16,10.43.0.0/16
image: index.docker.io/rancher/hardened-kubernetes:v1.34.3-rke2r3-build20260127
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
# encryption-config.json 일부
{
"kind": "EncryptionConfiguration",
"apiVersion": "apiserver.config.k8s.io/v1",
"resources": [
{
"resources": [
"secrets"
],
"providers": [
{
"aescbc": {
"keys": [
{
"name": "aescbckey",
"secret": "OmLRJEFMVSmN4f4wtLTkPG12nOor+uAn0iHBLMZxX8U="
}
]
}
},
{
"identity": {}
}
]
}
]
}
# etcd.yaml 일부 (hostPath 타입 분리)
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
# etcdctl 예시
etcdctl version: 3.5.26
API version: 3.5
+------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| https://127.0.0.1:2379 | 6571fb7574e87dba | 3.5.26 | 7.0 MB | true | false | 2 | 3322 | 3322 | |
+------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
# kube-scheduler.yaml 일부
- --permit-port-sharing=true
...
image: index.docker.io/rancher/hardened-kubernetes:v1.34.3-rke2r3-build20260127
# kube-controller-manager.yaml 일부
- --permit-port-sharing=true
- --flex-volume-plugin-dir=/var/lib/kubelet/volumeplugins
- --terminated-pod-gc-threshold=1000
...
- --tls-cert-file=/var/lib/rancher/rke2/server/tls/kube-controller-manager/kube-controller-manager.crt
- --tls-private-key-file=/var/lib/rancher/rke2/server/tls/kube-controller-manager/kube-controller-manager.key
# kube-proxy.yaml 일부
- --cluster-cidr=10.42.0.0/16
- --conntrack-max-per-core=0
- --conntrack-tcp-timeout-close-wait=0s
- --conntrack-tcp-timeout-established=0s
- --healthz-bind-address=127.0.0.1
- --hostname-override=k8s-node1
- --kubeconfig=/var/lib/rancher/rke2/agent/kubeproxy.kubeconfig
- --proxy-mode=iptables
노드 관리 (워커 추가/삭제/재추가)
워커 노드 추가
먼저 서버 노드에서 조인 토큰과 조인 포트를 확인했습니다.
[k8s-node1]
cat /var/lib/rancher/rke2/server/node-token
ss -tnlp | grep 9345
watch -d 'kubectl get node; echo; kubectl get pod -n kube-system'
K104ac00ae75a23d9725104217c2d690145218b822ddb69590d333c99332ba26cc9::server:306b362200f9cb1bd9ccb9f783c1ad6c
LISTEN 0 4096 192.168.10.11:9345 0.0.0.0:* users:(("rke2",pid=6348,fd=6))
워커 노드(k8s-node2)에서는 에이전트 타입으로 설치하고, /etc/rancher/rke2/config.yaml에 server/token을 설정한 뒤 rke2-agent를 기동했습니다.
[k8s-node2]
curl -sfL https://get.rke2.io | INSTALL_RKE2_TYPE="agent" INSTALL_RKE2_CHANNEL=v1.33 sh -
TOKEN=K104ac00ae75a23d9725104217c2d690145218b822ddb69590d333c99332ba26cc9::server:306b362200f9cb1bd9ccb9f783c1ad6c
mkdir -p /etc/rancher/rke2/
cat << EOF > /etc/rancher/rke2/config.yaml
server: https://192.168.10.11:9345
token: $TOKEN
EOF
cat /etc/rancher/rke2/config.yaml
systemctl enable --now rke2-agent.service
journalctl -u rke2-agent -f
워커 노드 조인 확인
서버 노드에서 노드/파드가 추가되는 것을 확인했습니다.
[k8s-node1]
kubectl get node -owide
kubectl get pod -n kube-system -owide | grep k8s-node2
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-node1 Ready control-plane,etcd,master 4m7s v1.33.7+rke2r3 192.168.10.11 <none> Rocky Linux 9.6 (Blue Onyx) 5.14.0-570.52.1.el9_6.aarch64 containerd://2.1.5-k3s1
k8s-node2 Ready <none> 19s v1.34.3+rke2r3 192.168.10.12 <none> Rocky Linux 9.6 (Blue Onyx) 5.14.0-570.52.1.el9_6.aarch64 containerd://2.1.5-k3s1
kube-proxy-k8s-node2 1/1 Running 0 5m11s 192.168.10.12 k8s-node2
rke2-canal-74gwp 2/2 Running 0 5m11s 192.168.10.12 k8s-node2
워커 노드에서도 rke2-agent가 containerd/kubelet을 포함해 동작 중인지 확인했습니다.
[k8s-node2]
tree /var/lib/rancher/rke2 -L 1
systemctl status rke2-agent.service --no-pager
cat /usr/lib/systemd/system/rke2-agent.service
pstree -al
├── agent
├── bin -> /var/lib/rancher/rke2/data/v1.34.3-rke2r3-5b8349de68df/bin
├── data
└── server
├─rke2
│ ├─containerd -c /var/lib/rancher/rke2/agent/etc/containerd/config.toml
│ ├─kubelet ... --containerd=/run/k3s/containerd/containerd.sock --hostname-override=k8s-node2 ...
│ └─...
워커 노드에서도 표준 경로에서 사용할 수 있게 심볼릭 링크를 동일하게 구성했습니다.
ln -s /var/lib/rancher/rke2/bin/containerd /usr/local/bin/containerd
ln -s /var/lib/rancher/rke2/bin/crictl /usr/local/bin/crictl
ln -s /var/lib/rancher/rke2/agent/etc/crictl.yaml /etc/crictl.yaml
crictl ps
crictl images
워커 노드의 /etc/rancher 및 agent 디렉터리도 확인했습니다.
tree /etc/rancher/
tree /var/lib/rancher/rke2/agent/ -L 3
또한 워커 노드 기준으로 “클러스터 조인용(9345)”과 “API Server(6443)” 주소가 각각 어떤 파일에 기록되는지 확인했습니다.
cat /var/lib/rancher/rke2/agent/etc/rke2-agent-load-balancer.json | jq
cat /var/lib/rancher/rke2/agent/etc/rke2-api-server-agent-load-balancer.json | jq
"ServerURL": "https://192.168.10.11:9345",
"ServerAddresses": [
"192.168.10.11:9345"
]
"ServerURL": "https://192.168.10.11:6443",
"ServerAddresses": [
"192.168.10.11:6443"
]
워커 노드 삭제 후 재추가
워커 노드 삭제는 먼저 drain으로 파드를 안전하게 이동시키고, 이후 노드를 클러스터에서 제거했습니다.
[k8s-node1]
kubectl drain k8s-node2 --ignore-daemonsets --delete-emptydir-data
kubectl delete node k8s-node2
워커 노드에서는 에이전트 서비스를 중지한 다음, uninstall 스크립트로 정리했습니다.
[k8s-node2]
systemctl stop rke2-agent
ls -l /usr/bin/rke2*
cat /usr/bin/rke2-uninstall.sh
rke2-uninstall.sh
tree /etc/rancher
tree /var/lib/rancher
재추가는 agent 설치 → config.yaml 작성 → 서비스 기동 순서로 진행했습니다.
[k8s-node2]
curl -sfL https://get.rke2.io | INSTALL_RKE2_TYPE="agent" INSTALL_RKE2_CHANNEL=v1.33 sh -
TOKEN=K104ac00ae75a23d9725104217c2d690145218b822ddb69590d333c99332ba26cc9::server:306b362200f9cb1bd9ccb9f783c1ad6c
mkdir -p /etc/rancher/rke2/
cat << EOF > /etc/rancher/rke2/config.yaml
server: https://192.168.10.11:9345
token: $TOKEN
EOF
cat /etc/rancher/rke2/config.yaml
systemctl enable --now rke2-agent.service
journalctl -u rke2-agent -f
샘플 애플리케이션 배포
기본 동작 확인을 위해 간단한 Deployment/Service(NodePort)를 배포했습니다.
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: webpod
spec:
replicas: 2
selector:
matchLabels:
app: webpod
template:
metadata:
labels:
app: webpod
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- sample-app
topologyKey: "kubernetes.io/hostname"
containers:
- name: webpod
image: traefik/whoami
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: webpod
labels:
app: webpod
spec:
selector:
app: webpod
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30000
type: NodePort
EOF
배포 후 리소스 상태를 확인했습니다.
kubectl get deploy,pod,svc,ep -owide
호스트 PC에서 NodePort로 반복 호출하면서 응답(Hostname)이 정상적으로 오는지 확인했습니다. (호출 IP는 노드 구성에 따라 변경될 수 있습니다.)
while true; do curl -s http://192.168.10.12:30000 | grep Hostname; date; sleep 1; done
업그레이드 (인증서 / 수동 업그레이드 / 자동 업그레이드)
(이미지/도전과제는 제외하고, 실습에서 실행한 흐름과 명령 위주로 정리했습니다.)
인증서 확인 및 수동 갱신
인증서 확인은 rke2 certificate check로 진행했습니다.
rke2 certificate --help
[k8s-node1]
rke2 certificate check --output table
[k8s-node2]
rke2 certificate check --output table
[k8s-node1]
FILENAME SUBJECT USAGES EXPIRES RESIDUAL TIME STATUS
-------- ------- ------ ------- ------------- ------
client-kube-apiserver.crt system:apiserver ClientAuth Feb 09, 2027 14:29 UTC 1 year OK
client-kube-apiserver.crt rke2-client-ca@1770647369 CertSign Feb 07, 2036 14:29 UTC 10 years OK
serving-kube-apiserver.crt kube-apiserver ServerAuth Feb 09, 2027 14:29 UTC 1 year OK
serving-kube-apiserver.crt rke2-server-ca@1770647369 CertSign Feb 07, 2036 14:29 UTC 10 years OK
...
client-supervisor.crt system:rke2-supervisor ClientAuth Feb 09, 2027 14:29 UTC 1 year OK
client-supervisor.crt rke2-client-ca@1770647369 CertSign Feb 07, 2036 14:29 UTC 10 years OK
[k8s-node2]
FILENAME SUBJECT USAGES EXPIRES RESIDUAL TIME STATUS
-------- ------- ------ ------- ------------- ------
client-kubelet.crt system:node:k8s-node2 ClientAuth Feb 14, 2027 14:21 UTC 1 year OK
client-kubelet.crt rke2-client-ca@1771054377 CertSign Feb 12, 2036 07:32 UTC 10 years OK
serving-kubelet.crt k8s-node2 ServerAuth Feb 14, 2027 14:21 UTC 1 year OK
serving-kubelet.crt rke2-server-ca@1771054377 CertSign Feb 12, 2036 07:32 UTC 10 years OK
...
client-kube-proxy.crt system:kube-proxy ClientAuth Feb 14, 2027 14:21 UTC 1 year OK
client-kube-proxy.crt rke2-client-ca@1771054377 CertSign Feb 12, 2036 07:32 UTC 10 years OK
수동 교체는 rotate로 진행했고, 서버 노드에서 먼저 수행했습니다.
[k8s-node1]
systemctl stop rke2-server
rke2 certificate rotate
rke2 certificate check --output table
systemctl start rke2-server
rke2 certificate check --output table
diff /etc/rancher/rke2/rke2.yaml ~/.kube/config
yes | cp /etc/rancher/rke2/rke2.yaml ~/.kube/config ; echo
kubectl cluster-info
INFO[0000] Server detected, rotating agent and server certificates
INFO[0000] Rotating dynamic listener certificate
INFO[0000] Rotating certificates for admin
INFO[0000] Rotating certificates for controller-manager
INFO[0000] Rotating certificates for kube-proxy
INFO[0000] Rotating certificates for kubelet
INFO[0000] Rotating certificates for rke2-controller
INFO[0000] Rotating certificates for api-server
INFO[0000] Rotating certificates for auth-proxy
INFO[0000] Rotating certificates for cloud-controller
INFO[0000] Rotating certificates for etcd
INFO[0000] Rotating certificates for scheduler
INFO[0000] Rotating certificates for supervisor
INFO[0000] Successfully backed up certificates to /var/lib/rancher/rke2/server/tls-1770651290, please restart rke2 server or agent to rotate certificates
수동 업그레이드 v1.33 → v1.34 (서버 먼저, 이후 에이전트)
업그레이드 중 모니터링은 아래처럼 확인했습니다.
while true; do curl -s http://192.168.10.12:30000 | grep Hostname; date; sleep 1; done
watch -d "kubectl get pod -n kube-system -owide --sort-by=.metadata.creationTimestamp | tac"
watch -d "kubectl get node"
watch -d 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 \
member list --write-out=table
서버 노드에서 버전/채널을 확인 후, 채널을 지정해서 업그레이드했습니다.
kubectl get node
rke2 --version
curl -s https://update.rke2.io/v1-release/channels | jq .data
curl -sfL https://get.rke2.io | INSTALL_RKE2_CHANNEL=v1.34 sh -
업그레이드 후 확인
rke2 --version
kubectl get pod -n kube-system --sort-by=.metadata.creationTimestamp | tac
dnf repolist
cat /etc/yum.repos.d/rancher-rke2.repo
cat /etc/yum.repos.d/rancher-rke2.repo | grep -iE 'name|baseurl'
kubectl get pods -n kube-system -o custom-columns="POD_NAME:.metadata.name,IMAGES:.spec.containers[*].image"
kubectl get pods -n kube-system \
-o custom-columns=\
POD:.metadata.name,\
CONTAINERS:.spec.containers[*].name,\
IMAGES:.spec.containers[*].image
systemctl restart rke2-server
kubectl get node -owide
rke2 version v1.33.7+rke2r3 (7e4fd1a82edf497cab91c220144619bbad659cf4)
{
"id": "stable",
"type": "channel",
"links": { "self": "https://update.rke2.io/v1-release/channels/stable" },
"name": "stable",
"latest": "v1.34.3+rke2r3"
},
{
"id": "latest",
"type": "channel",
"links": { "self": "https://update.rke2.io/v1-release/channels/latest" },
"name": "latest",
"latest": "v1.35.0+rke2r3",
"latestRegexp": ".*",
"excludeRegexp": "(^[^+]+-|v1\\.25\\.5\\+rke2r1|v1\\.26\\.0\\+rke2r1)"
},
{
"id": "v1.34",
"type": "channel",
"links": { "self": "https://update.rke2.io/v1-release/channels/v1.34" },
"name": "v1.34",
"latest": "v1.34.3+rke2r3",
"latestRegexp": "v1\\.34\\..*",
"excludeRegexp": "^[^+]+-"
}
Running transaction
Preparing : 1/1
Upgrading : rke2-common-1.34.3~rke2r3-0.el9.aarch64 1/4
Upgrading : rke2-server-1.34.3~rke2r3-0.el9.aarch64 2/4
Running scriptlet: rke2-server-1.34.3~rke2r3-0.el9.aarch64 2/4
Running scriptlet: rke2-server-1.33.7~rke2r3-0.el9.aarch64 3/4
Cleanup : rke2-server-1.33.7~rke2r3-0.el9.aarch64 3/4
...
rke2 version v1.34.3+rke2r3 (7598946e0086a9131564ccbb3c142b3fa54516ad)
go version go1.24.11 X:boringcrypto
helm-install-rke2-canal-jv2ml 0/1 Completed 0 2m41s
helm-install-rke2-coredns-tf6np 0/1 Completed 0 2m41s
helm-install-rke2-metrics-server-2pb6j 0/1 Completed 0 2m41s
helm-install-rke2-runtimeclasses-cslch 0/1 Completed 0 2m41s
kube-controller-manager-k8s-node1 1/1 Running 0 2m58s
kube-scheduler-k8s-node1 1/1 Running 0 2m58s
kube-proxy-k8s-node1 1/1 Running 0 3m52s
etcd-k8s-node1 1/1 Running 0 3m52s
kube-apiserver-k8s-node1 1/1 Running 1 (3m52s ago) 3m52s
...
rancher-rke2-1.34-stable Rancher RKE2 1.34 (v1.34)
rancher-rke2-common-stable Rancher RKE2 Common (v1.34)
name=Rancher RKE2 Common (v1.34)
baseurl=https://rpm.rancher.io/rke2/stable/common/centos/9/noarch
name=Rancher RKE2 1.34 (v1.34)
baseurl=https://rpm.rancher.io/rke2/stable/1.34/centos/9/aarch64
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-node1 Ready control-plane,etcd,master 18m v1.34.3+rke2r3 192.168.10.11 <none> Rocky Linux 9.6 (Blue Onyx) 5.14.0-570.52.1.el9_6.aarch64 containerd://2.1.5-k3s1
k8s-node2 Ready <none> 7m23s v1.33.7+rke2r3 192.168.10.12 <none> Rocky Linux 9.6 (Blue Onyx) 5.14.0-570.52.1.el9_6.aarch64 containerd://2.1.5-k3s1
워커(에이전트) 업그레이드는 워커 노드에서 채널을 지정해 진행했습니다.
[k8s-node2]
rke2 --version
curl -sfL https://get.rke2.io | INSTALL_RKE2_TYPE=agent INSTALL_RKE2_CHANNEL=v1.34 sh -
rke2 --version
dnf repolist
systemctl restart rke2-agent
서버에서 최종 확인
[k8s-node1]
diff /etc/rancher/rke2/rke2.yaml ~/.kube/config
kubectl get node -owide
kubectl get pod -n kube-system --sort-by=.metadata.creationTimestamp | tac
kubectl get pods -n kube-system -o custom-columns="POD_NAME:.metadata.name,IMAGES:.spec.containers[*].image"
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-node1 Ready control-plane,etcd,master 22m v1.34.3+rke2r3 192.168.10.11 <none> Rocky Linux 9.6 (Blue Onyx) 5.14.0-570.52.1.el9_6.aarch64 containerd://2.1.5-k3s1
k8s-node2 Ready <none> 11m v1.34.3+rke2r3 192.168.10.12 <none> Rocky Linux 9.6 (Blue Onyx) 5.14.0-570.52.1.el9_6.aarch64 containerd://2.1.5-k3s1
kube-proxy-k8s-node2 1/1 Running 0 84s
kube-proxy-k8s-node1 1/1 Running 0 7m54s
...
자동 업그레이드 v1.34 → v1.35 (system-upgrade-controller)
모니터링은 아래처럼 준비했습니다.
while true; do curl -s http://192.168.10.12:30000 | grep Hostname; date; sleep 1; done
watch -d 'kubectl -n system-upgrade get plans -o wide; echo ; kubectl -n system-upgrade get jobs,pods'
watch -d "kubectl get node"
watch -d "kubectl get pod -n kube-system -owide --sort-by=.metadata.creationTimestamp | tac"
system-upgrade-controller 설치
kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/latest/download/crd.yaml -f https://github.com/rancher/system-upgrade-controller/releases/latest/download/system-upgrade-controller.yaml
설치 확인
kubectl get deploy,pod,cm -n system-upgrade
kubectl get crd | grep upgrade
kubectl logs -n system-upgrade -l app.kubernetes.io/name=system-upgrade-controller -f
customresourcedefinition.apiextensions.k8s.io/plans.upgrade.cattle.io created
namespace/system-upgrade created
serviceaccount/system-upgrade created
...
deployment.apps/system-upgrade-controller created
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/system-upgrade-controller 1/1 1 1 12m
pod/system-upgrade-controller-6f9f9b8cf4-46n82 1/1 Running 0 12m
configmap/default-controller-env 10 12m
plans.upgrade.cattle.io 2026-02-09T18:08:08Z
Plan 적용
cat << EOF | kubectl apply -f -
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: server-plan
namespace: system-upgrade
spec:
concurrency: 1
cordon: true
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: In
values:
- "true"
serviceAccountName: system-upgrade
upgrade:
image: rancher/rke2-upgrade
channel: https://update.rke2.io/v1-release/channels/latest
---
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: agent-plan
namespace: system-upgrade
spec:
concurrency: 1
cordon: true
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
prepare:
args:
- prepare
- server-plan
image: rancher/rke2-upgrade
serviceAccountName: system-upgrade
upgrade:
image: rancher/rke2-upgrade
channel: https://update.rke2.io/v1-release/channels/latest
EOF
진행/완료 확인
kubectl get node -owide
kubectl -n system-upgrade get plans -o wide
kubectl -n system-upgrade get jobs
kubectl get pod -n system-upgrade -owide
kubectl describe pod -n system-upgrade |grep ^Volumes: -A4
kubectl logs -n system-upgrade -l app.kubernetes.io/name=system-upgrade-controller
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-node1 Ready control-plane,etcd,master 108m v1.35.0+rke2r3 192.168.10.11 <none> Rocky Linux 9.6 (Blue Onyx) 5.14.0-570.52.1.el9_6.aarch64 containerd://2.1.5-k3s1
k8s-node2 Ready <none> 97m v1.35.0+rke2r3 192.168.10.12 <none> Rocky Linux 9.6 (Blue Onyx) 5.14.0-570.52.1.el9_6.aarch64 containerd://2.1.5-k3s1
NAME IMAGE CHANNEL VERSION COMPLETE MESSAGE APPLYING
agent-plan rancher/rke2-upgrade https://update.rke2.io/v1-release/channels/latest True
server-plan rancher/rke2-upgrade https://update.rke2.io/v1-release/channels/latest True
NAME STATUS COMPLETIONS DURATION AGE
apply-agent-plan-on-k8s-node2-with-db1bffd09b601fca4c7c06-7dc30 Complete 1/1 2m5s 4m16s
apply-server-plan-on-k8s-node1-with-db1bffd09b601fca4c7c0-28ad1 Complete 1/1 55s 4m16s
Volumes:
host-root:
Type: HostPath (bare host directory volume)
Path: /
HostPathType: Directory
object="system-upgrade/server-plan" type="Normal" reason="Resolved" message="Resolved latest version from Spec.Channel: v1.35.0-rke2r3"
object="system-upgrade/server-plan" type="Normal" reason="SyncJob" message="Jobs synced for version v1.35.0-rke2r3 on Nodes k8s-node1. Hash: db1bffd09b601fca4c7c067d987c4d368f9237f8219289438d8678e8"
object="system-upgrade/server-plan" type="Normal" reason="JobComplete" message="Job completed on Node k8s-node1"
object="system-upgrade/server-plan" type="Normal" reason="Complete" message="Jobs complete for version v1.35.0-rke2r3. Hash: db1bffd09b601fca4c7c067d987c4d368f9237f8219289438d8678e8"
RKE2 HA 구성
[macOS] LoadBalancer(HAProxy) 구성 (TCP 6443/9345)
Kubernetes API(6443) / RKE2 Supervisor(9345)는 TLS 기반이므로 TCP 모드 사용이 핵심
설치/실행
brew install haproxy
# 버전 확인
haproxy -v
# 서비스 실행(로그인 시 자동 시작)
brew services start haproxy
설정 파일 예시
Apple Silicon 기준 기본 경로는 보통 /opt/homebrew/etc/haproxy.cfg (Intel은 /usr/local/...일 수 있음)
sudo vi /opt/homebrew/etc/haproxy.cfg
global
maxconn 2000
defaults
log global
mode tcp
option tcplog
timeout connect 10s
timeout client 1m
timeout server 1m
# [1] Kubernetes API Server (6443)
frontend k8s-api
bind *:6443
default_backend k8s-api-backend
backend k8s-api-backend
balance roundrobin
server k8s-node1 192.168.10.11:6443 check
server k8s-node2 192.168.10.12:6443 check
server k8s-node3 192.168.10.13:6443 check
# [2] RKE2 Supervisor (9345) - 노드 조인용
frontend rke2-supervisor
bind *:9345
default_backend rke2-supervisor-backend
backend rke2-supervisor-backend
balance roundrobin
server k8s-node1 192.168.10.11:9345 check
server k8s-node2 192.168.10.12:9345 check
server k8s-node3 192.168.10.13:9345 check
# [3] HAProxy 상태 모니터링 (선택)
listen stats
bind *:9000
mode http
stats enable
stats uri /stats
stats refresh 10s
설정 검증/상태 확인
haproxy -c -f /opt/homebrew/etc/haproxy.cfg
brew services restart haproxy
# 리스닝 확인
netstat -an | egrep '6443|9345|9000'
# 모니터링 UI
open http://127.0.0.1:9000/stats
중지
brew services stop haproxy
brew services list | grep haproxy
ps aux | grep haproxy | grep -v grep
CT 3대 + WK 2대 (HA Control Plane) 구성
- 공식 문서: https://docs.rke2.io/install/ha
- 권장: Server(컨트롤플레인/etcd) 홀수(3/5)
공통(모든 노드) - v1.33 설치 + LB 도메인 매핑
# [all] /etc/hosts (각자 LB IP로 변경)
echo "192.168.10.1 k8s-api.admin.com admin" >> /etc/hosts
# [all] rke2 server 설치 (채널 v1.33)
curl -sfL https://get.rke2.io | INSTALL_RKE2_CHANNEL=v1.33 sh -
[k8s-node1] 첫 번째 컨트롤 플레인 노드
RKE2 설정
cat << 'EOF' > /etc/rancher/rke2/config.yaml
cluster-init: true
tls-san:
- 192.168.10.1 # 각자 LB IP
- k8s-api.admin.com
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
disable-cloud-controller: true
disable:
- servicelb
- rke2-coredns-autoscaler
- rke2-ingress-nginx
- rke2-snapshot-controller
- rke2-snapshot-controller-crd
- rke2-snapshot-validation-webhook
EOF
cat /etc/rancher/rke2/config.yaml
Canal / CoreDNS HelmChartConfig
mkdir -p /var/lib/rancher/rke2/server/manifests/
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
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
서비스 시작 + kubeconfig 설정
systemctl enable --now rke2-server
mkdir -p ~/.kube
cp /etc/rancher/rke2/rke2.yaml ~/.kube/config
ln -sf /var/lib/rancher/rke2/bin/kubectl /usr/local/bin/kubectl
ln -sf /var/lib/rancher/rke2/bin/crictl /usr/local/bin/crictl
ln -sf /var/lib/rancher/rke2/agent/etc/crictl.yaml /etc/crictl.yaml
# kubeconfig의 서버 주소를 LB(6443)로 변경
sed -i 's/192.168.10.11/192.168.10.1/g' /root/.kube/config
kubectl cluster-info
[k8s-node2/3] 나머지 컨트롤 플레인 노드 조인
node-token 확인(노드1)
cat /var/lib/rancher/rke2/server/node-token
노드2 설정/기동
TOKEN='<노드1에서 가져온 node-token>'
cat << EOF > /etc/rancher/rke2/config.yaml
token: ${TOKEN}
server: https://192.168.10.1:9345 # 각자 LB IP
tls-san:
- 192.168.10.1
- k8s-api.admin.com
write-kubeconfig-mode: "0644"
debug: true
cni: canal
bind-address: 192.168.10.12
advertise-address: 192.168.10.12
node-ip: 192.168.10.12
disable-cloud-controller: true
disable:
- servicelb
- rke2-coredns-autoscaler
- rke2-ingress-nginx
- rke2-snapshot-controller
- rke2-snapshot-controller-crd
- rke2-snapshot-validation-webhook
EOF
systemctl enable --now rke2-server
mkdir -p ~/.kube
cp /etc/rancher/rke2/rke2.yaml ~/.kube/config
sed -i 's/192.168.10.12/192.168.10.1/g' /root/.kube/config
ln -sf /var/lib/rancher/rke2/bin/kubectl /usr/local/bin/kubectl
ln -sf /var/lib/rancher/rke2/bin/crictl /usr/local/bin/crictl
ln -sf /var/lib/rancher/rke2/agent/etc/crictl.yaml /etc/crictl.yaml
kubectl cluster-info
노드3도 동일(주소만 192.168.10.13로)
bind-address / advertise-address / node-ip만 192.168.10.13으로 맞춰서 동일 진행
상태 확인 (nodes/pods/etcd)
kubectl get node -o wide
kubectl get pod -A
kubectl describe pod -n kube-system -l component=kube-apiserver | grep -E '^Name:|advertise|etcd-servers'
node-token 확인(노드1)
cat /var/lib/rancher/rke2/server/node-token
노드2 설정/기동
TOKEN='<노드1에서 가져온 node-token>'
cat << EOF > /etc/rancher/rke2/config.yaml
token: ${TOKEN}
server: https://192.168.10.1:9345 # 각자 LB IP
tls-san:
- 192.168.10.1
- k8s-api.admin.com
write-kubeconfig-mode: "0644"
debug: true
cni: canal
bind-address: 192.168.10.12
advertise-address: 192.168.10.12
node-ip: 192.168.10.12
disable-cloud-controller: true
disable:
- servicelb
- rke2-coredns-autoscaler
- rke2-ingress-nginx
- rke2-snapshot-controller
- rke2-snapshot-controller-crd
- rke2-snapshot-validation-webhook
EOF
systemctl enable --now rke2-server
mkdir -p ~/.kube
cp /etc/rancher/rke2/rke2.yaml ~/.kube/config
sed -i 's/192.168.10.12/192.168.10.1/g' /root/.kube/config
ln -sf /var/lib/rancher/rke2/bin/kubectl /usr/local/bin/kubectl
ln -sf /var/lib/rancher/rke2/bin/crictl /usr/local/bin/crictl
ln -sf /var/lib/rancher/rke2/agent/etc/crictl.yaml /etc/crictl.yaml
kubectl cluster-info
노드3도 동일(주소만 192.168.10.13로)
bind-address / advertise-address / node-ip만 192.168.10.13으로 맞춰서 동일 진행
상태 확인 (nodes/pods/etcd)
kubectl get node -o wide
kubectl get pod -A
kubectl describe pod -n kube-system -l component=kube-apiserver | grep -E '^Name:|advertise|etcd-servers'
etcdctl (권장: “find로 실제 위치 확인”)
find / -name etcdctl 2>/dev/null
# 위치 확인 후 심볼릭 링크(환경마다 snapshot 번호가 달라질 수 있어 find 결과 기반으로 적용)
ln -sf <찾은_etcdctl_경로> /usr/local/bin/etcdctl
etcdctl version
kube-ops-view 배포 + 이미지 사전 pull
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
# NodePort 30000
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 \
--set service.main.type=NodePort,service.main.ports.http.nodePort=30000 \
--set env.TZ="Asia/Seoul" --namespace kube-system \
--set image.repository="abihf/kube-ops-view" --set image.tag="latest"
kubectl get deploy,pod,svc,ep -n kube-system -l app.kubernetes.io/instance=kube-ops-view
# 접속(노드 IP 어느 쪽이든 가능)
open "http://192.168.10.11:30000/#scale=1.5"
open "http://192.168.10.11:30000/#scale=2"
이미지 사전 pull(air-gap 대비)
crictl pull docker.io/abihf/kube-ops-view
crictl images | grep kube-ops-view
샘플 애플리케이션 배포
cat << 'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: webpod
spec:
replicas: 3
selector:
matchLabels:
app: webpod
template:
metadata:
labels:
app: webpod
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- webpod
topologyKey: "kubernetes.io/hostname"
containers:
- name: webpod
image: traefik/whoami
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: webpod
labels:
app: webpod
spec:
selector:
app: webpod
type: NodePort
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30001
EOF
kubectl get deploy,pod,svc,ep -o wide
호출 테스트(노드 IP는 아무거나 가능)
while true; do
curl -s http://192.168.10.11:30001 | grep Hostname
date
sleep 1
done
수동 업그레이드 v1.33 → v1.34 (Installation Script)
모니터링
# 샘플 앱 연속 호출
while true; do curl -s http://192.168.10.11:30001 | grep Hostname; date; sleep 1; done
# 시스템 파드 흐름
watch -d "kubectl get pod -n kube-system -o wide --sort-by=.metadata.creationTimestamp | tac"
watch -d "kubectl get node"
업그레이드(노드별)
# 각 서버 노드에서 실행
curl -sfL https://get.rke2.io | INSTALL_RKE2_CHANNEL=v1.34 sh -
# 필요 시(환경에 따라 helm-install 계열 pod가 꼬이면) rke2 재시작
systemctl restart rke2-server
kubectl get node -o wide
kubectl get pod -n kube-system -o wide --sort-by=.metadata.creationTimestamp | tac
수동 인증서 갱신(rotate)
권장 순서 예시: k8s-node3 → k8s-node2 → k8s-node1 (순차)
rke2 certificate check --output table
systemctl stop rke2-server
rke2 certificate rotate
systemctl start rke2-server
rke2 certificate check --output table
(etcd 리더 변경 확인은 기존 etcdctl endpoint status로 확인)
자동 업그레이드 v1.34 → v1.35 (system-upgrade-controller)
문서: https://docs.rke2.io/upgrades/automated
Automated Upgrades | RKE2
Overview
docs.rke2.io
설치
kubectl apply -f \
https://github.com/rancher/system-upgrade-controller/releases/latest/download/crd.yaml \
-f https://github.com/rancher/system-upgrade-controller/releases/latest/download/system-upgrade-controller.yaml
kubectl get deploy,pod,cm -n system-upgrade
kubectl get crd | grep upgrade
kubectl logs -n system-upgrade -l app.kubernetes.io/name=system-upgrade-controller -f
Plan 작성(서버 노드 순차 1대씩)
cat << 'EOF' | kubectl apply -f -
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: server-plan
namespace: system-upgrade
spec:
concurrency: 1
cordon: true
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: In
values:
- "true"
serviceAccountName: system-upgrade
upgrade:
image: rancher/rke2-upgrade
channel: https://update.rke2.io/v1-release/channels/latest
EOF
kubectl -n system-upgrade get plans -o wide
kubectl -n system-upgrade get jobs
kubectl -n system-upgrade get pods -o wide
kubectl get node -o wide
“자동 순차 업그레이드가 버전을 건너뜀” 정리
원인 요약:
- channel: .../latest는 “최신”을 바로 잡아오므로 중간 버전을 건너뛸 수 있음
- “v1.32 → v1.33 → v1.34 → v1.35”처럼 단계별로 강제하려면:
- version:을 명시하거나
- 해당 단계 전용 channel(또는 release line)을 사용해야 함
버전 고정 예시
spec:
version: v1.35.0+rke2r3
Cluster API 소개
앞에서 RKE2로 “클러스터를 설치/운영하는 내부 동작”을 한 번 끝까지 따라가 봤다면, 이제는 관점을 “클러스터를 여러 개 만들고(또는 교체하고), 업그레이드하고, 폐기하는 라이프사이클 자동화”로 확장할 차례입니다.
Cluster API(CAPI)는 이 영역을 Kubernetes CRD/컨트롤러 패턴으로 표준화한 프로젝트이며, “K8S를 K8S로 배포/관리한다”는 접근을 제공합니다.
Cluster API 핵심 아이디어 (Management / Workload)
- Management 클러스터
- Cluster API(Core) + Provider(Infra/Bootstrap/ControlPlane) 컨트롤러들이 설치되어 “클러스터를 만드는 쪽(제어/관리)” 역할을 수행하는 Kubernetes
- Workload 클러스터
- Management 클러스터의 선언( CRD/YAML )을 기반으로 실제로 프로비저닝되는 “애플리케이션이 올라가는” Kubernetes
- 요약
- 운영자가 “클러스터 생성/확장/업그레이드/삭제” 같은 수명주기 작업을 선언적으로 요청하면,
- 컨트롤러가 이를 해석해서 인프라 생성 → 노드 부트스트랩 → 제어평면 구성 → 워커 확장/교체 흐름을 자동으로 수행합니다.
- 주의
- 자동화(오퍼레이터)가 강력한 만큼, 장애/교체/업그레이드 시 어떤 리소스가 어떤 순서로 움직이는지 모델을 이해해야 운영 리스크를 줄일 수 있습니다.
Cluster API가 해결하려는 문제
- Kubernetes는 “정상 동작”을 위해 올바르게 맞춰야 할 구성 요소가 많고, 설치/운영이 복잡합니다.
- kubeadm 같은 도구는 “부트스트랩(초기 설치)”을 표준화하는 데 큰 역할을 했지만,
- 클러스터를 장기적으로 운영/확장/업그레이드/삭제하는 문제는 별도 영역입니다.
- 프로덕션에서는 반복적으로 아래 질문을 마주치게 됩니다.
- 여러 인프라(AWS/Azure/vSphere/온프렘 등)에서 일관된 방식으로 머신/로드밸런서/네트워크 등을 만들려면?
- 업그레이드/삭제까지 포함한 클러스터 수명주기 관리를 어떻게 자동화할까?
- 이 과정을 확장해 다수 클러스터를 어떻게 관리할까?
- Cluster API는 이 격차를 “Kubernetes 스타일 API(선언적 CRD) + 컨트롤러”로 메우는 방식입니다.
Concepts (Core 리소스 모델)
Cluster
- “Cluster”는 Cluster API에 의해 라이프사이클이 관리되는 Kubernetes 클러스터(= 워크로드 클러스터)를 나타내는 맞춤형 리소스입니다.
- 공통 속성(예: 네트워크 CIDR 등)은 Cluster spec에 정의하고,
- 인프라/제어평면별 상세 구현은 infrastructureRef, controlPlaneRef로 분리합니다.
apiVersion: cluster.x-k8s.io/v1beta2
kind: **Cluster**
metadata:
**name**: my-cluster
**spec**:
**clusterNetwork**:
pods:
cidrBlocks:
- 192.168.0.0/16
**infrastructureRef**:
apiGroup: infrastructure.cluster.x-k8s.io
kind: VSphereCluster
name: my-cluster-infrastructure
**controlPlaneRef**:
apiGroup: controlplane.cluster.x-k8s.io
kind: KubeadmControlPlane
name: my-control-plane
Machine
- “Machine”은 Kubernetes 노드(대개 VM/서버)를 호스팅하는 인프라에 대한 선언적 사양을 제공하는 맞춤형 리소스입니다.
- provider별 상세 내용은 infrastructureRef, bootstrap.configRef로 분리됩니다.
apiVersion: cluster.x-k8s.io/v1beta2
kind: **Machine**
metadata:
name: my-machine
**spec:**
**clusterName**: my-cluster
**version**: v1.35.0
**infrastructureRef:**
apiGroup: infrastructure.cluster.x-k8s.io
kind: VSphereMachineTemplate
name: my-machine-infrastructure
**bootstrap:**
configRef:
apiGroup: bootstrap.cluster.x-k8s.io
kind: KubeadmConfigTemplate
name: my-bootstrap-config
**status**:
nodeRef:
name: the-node-running-on-my-machine
- Machine 객체가 생성되면 provider 컨트롤러가 호스트를 프로비저닝하고 노드로 조인시키며,
- Machine 객체가 삭제되면 기본 인프라와 노드가 정리됩니다.
- 단일 Machine을 직접 다루기보다는, 아래 상위 리소스로 “그룹”을 관리하는 것이 일반적입니다.
- ControlPlane 리소스(예: KubeadmControlPlane)
- MachineDeployment / MachineSet / MachinePool
Providers (Core + 구현체 구조)
Cluster API는 “표준 인터페이스(Core)”와 “구현체(Provider)”가 분리된 구조입니다.
- Infrastructure provider
- VM/네트워크/LB/VPC 등 인프라 리소스 프로비저닝 담당
- 예: AWS, Azure, vSphere, Metal3, Harvester 등
- Control plane provider
- Kubernetes 제어 평면을 어떤 방식으로 제공/관리할지 담당
- 예:
- Self-provisioned(머신 기반, kubeadm 정적 파드 등)
- Pod-based(외부 호스팅 클러스터 필요)
- External/Managed(GKE/AKS/EKS처럼 외부 시스템이 제어평면 제공)
- Bootstrap provider
- 머신을 “노드”로 전환하기 위한 초기화 데이터(주로 cloud-init 계열)를 생성
- 인증서 생성, 제어평면 초기화 게이트, 조인 절차 등에 필요한 bootstrap data를 생성하고,
- Infrastructure provider가 해당 bootstrap data를 사용해 머신을 노드로 부트스트랩합니다.
Machine 관리 리소스 (Deployment 계열과 유사한 계층)
graph TD
%% 노드 정의
MD[MachineDeployment]
MS[MachineSet]
M[Machine]
Node[Infrastructure Provider Resource<br/>ex: AWSMachine, AzureMachine]
Instance[Actual VM/Cloud Instance]
%% 관계 정의
MD -- "manages" --> MS
MS -- "manages (Replicas)" --> M
M -- "references" --> Node
Node -- "provisions" --> Instance
%% 설명 스타일링
style MD fill:#f9f,stroke:#333,stroke-width:2px
style MS fill:#bbf,stroke:#333,stroke-width:2px
style M fill:#dfd,stroke:#333,stroke-width:2px
style Instance fill:#fff,stroke:#333,stroke-dasharray: 5 5
- MachineDeployment
- Machines/MachineSets에 대한 선언적 업데이트 제공
- Kubernetes Deployment처럼 “스펙 변경 → 롤아웃(교체)”로 원하는 상태를 맞춤
- MachineSet
- ReplicaSet처럼 “주어진 시점에 안정적인 머신 수 유지”
- 보통 직접 다루기보다 MachineDeployment의 하위 메커니즘으로 사용
- MachinePool
- 특정 Infrastructure provider에서 “풀/그룹” 개념을 지원할 때 사용하는 머신 그룹 리소스
- MachineHealthCheck
- 노드가 missing/unhealthy로 간주되는 조건을 정의
- 일정 시간 이상 불건전하면 remediation을 수행하는데, 일반적으로 “교체(replace)”로 처리
- MachineSet에 의해 소유된 경우에만 교체가 안정적으로 수행되도록 제한하는 패턴이 일반적
업데이트 전략: In-place update vs Replace, 그리고 chained upgrades
- Machine Immutability(기본 관점)
- Machine은 생성 후(라벨/주석/상태 제외) “스펙을 바꾸는 방식으로” 업데이트하기보다,
- 변경이 필요하면 교체(Replace) 하는 것이 기본 전략입니다.
- 그래서 MachineDeployment 같은 상위 리소스를 통한 롤링 교체가 선호됩니다.
- In-place update
- 특정 조건 하에서 “교체가 아닌 제자리 업데이트”를 지원하는 확장 포인트가 도입되면서,
- 원하는 상태 달성에 사용할 수 있는 전략(도구 세트)이 확장되는 흐름입니다.
- chained upgrades(연쇄 업그레이드)
- 사용자는 “최종 목표 버전”만 지정하고,
- 컨트롤러가 내부적으로 단계(중간 마이너)를 계산/실행하며,
- 제어평면/워커를 엄격한 순서로 업그레이드해 원하는 상태로 수렴시키는 접근입니다.
- 워커는 버전 스큐 정책 등 조건을 만족하면 일부 중간 단계를 건너뛸 수 있습니다.
Cluster API 실습
앞에서 RKE2로 “클러스터를 설치/운영하는 내부 동작(프로세스/디렉터리/정적 파드)”을 한 번 끝까지 따라갔다면, 이번 장은 관점을 바꿔 클러스터 수명주기(생성/확장/업그레이드/삭제)를 선언적으로 자동화하는 흐름을 손으로 밟아보는 실습입니다.
핵심은 관리 클러스터(Management) 위에 Cluster API 컨트롤러(프로바이더)를 올리고, 그 컨트롤러가 워크로드 클러스터(Workload) 를 “인프라(Docker) + 부트스트랩(kubeadm)”로 만들어내는 과정을 관찰하는 것입니다.
아래 내용은 공유해준 실습 흐름/코드/출력을 그대로 유지하면서, 각 단계에서 “왜 필요한지 / 무엇을 확인하는지 / 어디서 막히기 쉬운지”만 덧붙였습니다.
Cluster API 실습
목표 : kind k8s 로 관리 클러스터 배포 후, 도커 데몬을 활용하여 워크로드 클러스터를 배포 ⇒ 윈도우 WSL2 환경(Docker)에서 실습 해보기
- 빠른 설치 2가지 방법
- ClusterCTL : CLI 도구로 Cluster API 관리 클러스터의 수명 주기 관리
→ 실습에서 사용하는 방식입니다. “관리 클러스터에 프로바이더 설치/업데이트/검증”을 빠르게 끝낼 수 있고, CAPD(Docker provider) 같은 로컬 실습도 자연스럽게 이어집니다.- clusterctl 명령줄 인터페이스는 Cluster API를 사용하여 간단하고 빠른 시작을 할 수 있도록 특별히 설계되었습니다.
- 이 인터페이스는 공급자 구성 요소를 정의하는 YAML 파일을 자동으로 가져 오고 설치합니다.
- 이 문서에는 공급자 관리에 대한 모범 사례 세트가 포함되어 있어 사용자가 잘못된 구성을 방지하거나 업그레이드와 같은 2일차 운영을 관리하는 데 도움이 됩니다.
- Cluster API Operator
→ GitOps/선언적 운영에 더 익숙한 팀에 맞는 선택지입니다. clusterctl을 감싸는 형태라 개념은 동일하되 “설치/업데이트를 CR로” 하게 해줍니다.- Cluster API Operator는 clusterctl을 기반으로 구축된 Kubernetes Operator로, 클러스터 관리자가 선언적 접근 방식을 사용하여 관리 클러스터 내의 Cluster API 공급자 생명주기를 관리할 수 있도록 지원합니다.
- Cluster API 배포 및 관리 경험을 개선하여 일상적인 작업을 더 쉽게 처리하고 GitOps를 활용한 워크플로 자동화를 구현하는 것을 목표로 합니다
- ClusterCTL : CLI 도구로 Cluster API 관리 클러스터의 수명 주기 관리
- 관리용 Management K8S 클러스터 설치 및 구성
여기서 가장 중요한 포인트는 “Cluster API는 기존 Kubernetes 클러스터가 필요하다”는 점입니다. 그 클러스터에 프로바이더 구성요소를 설치해 “관리 클러스터”로 전환합니다.- 클러스터 API를 사용하려면 kubectl을 통해 접근 가능한 기존 Kubernetes 클러스터가 필요합니다.
- 설치 과정에서 Kubernetes 클러스터는 클러스터 API 공급자 구성 요소를 설치하여 관리 클러스터 로 변환되므로 , 애플리케이션 워크로드와 분리하여 관리하는 것이 좋습니다.
- 일반적으로 임시 로컬 부트스트랩 클러스터를 생성한 다음, 이를 사용하여 선택한 인프라 공급자 에게 대상 관리 클러스터를 프로비저닝하는 것이 관례입니다.
→ 실습에서는 “부트스트랩=kind”로 시작하고, “인프라=CAPD(Docker)”로 워크로드 클러스터를 만듭니다.
- Management Cluster: v1.31.x ~ v1.35.x
- Workload Cluster: v1.29.x ~ v1.35.x
kind k8s에 관리용 Management 클러스터 설치 init - Docs
- 특히 WSL2/로컬 환경에서 흔히 막히는 지점은 Docker socket 공유(/var/run/docker.sock) 입니다.
워크로드 클러스터의 노드가 “도커 컨테이너”로 뜨기 때문에, 컨테이너 안에서 또 다른 컨테이너를 만들 수 있도록 socket을 마운트해 두는 것이 핵심입니다. - 이 단계는 “실습용 관리 클러스터(=kind)”를 준비하고, 그 위에 clusterctl init으로 CAPI 코어 + 프로바이더(CAPD/Kubeadm) + cert-manager를 설치하는 과정입니다.
# 작업 디렉터리 생성
mkdir capi-docker && cd capi-docker
# (TS)
**docker context ls**
*NAME DESCRIPTION DOCKER ENDPOINT ERROR
default Current DOCKER_HOST based configuration unix://**/var/run/docker.sock**
orbstack * OrbStack unix://**/Users/gasida/.orbstack/run/docker.sock***
**# 중요! 혹시 미삭제 컨테이너 있는지 확인**
**docker ps -a**
# k8s 설치
kind create cluster --name myk8s --image kindest/node:**v1.35.0** --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
**extraMounts:
- hostPath: /Users/gasida/.orbstack/run/docker.sock** *# 대분분 이 값 사용: /var/run/docker.sock*
**containerPath: /var/run/docker.sock**
extraPortMappings:
- containerPort: 30000 # sample
hostPort: 30000
- containerPort: 30001 # *kube-ops-view*
hostPort: 30001
EOF
# (옵션) kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 \
--set service.main.type=NodePort,service.main.ports.http.nodePort=30001 \
--set env.TZ="Asia/Seoul" --namespace kube-system
open "http://127.0.0.1:30001/#scale=1.5"
# Install clusterctl
## mac 사용자
brew install clusterctl
## Windows WSL2 사용자 (Linux amd64)
curl -L https://github.com/kubernetes-sigs/cluster-api/releases/download/**v1.12.2**/clusterctl-linux-amd64 -o clusterctl
sudo install -o root -g root -m 0755 clusterctl /usr/local/bin/clusterctl
## 버전 확인
clusterctl version -o json | jq
# [Docker 프로바이더] Initialize the management cluster : 현재 k8s 를 관리 클러스터로 변환
## Docker 프로바이더는 프로덕션 환경에 사용하도록 설계되지 않았으며 개발 환경 전용
## ClusterTopology관리형 토폴로지 및 ClusterClass 지원을 활성화하는 데 필요한 기능은 다음과 같이 활성화
## https://cluster-api.sigs.k8s.io/tasks/experimental-features/experimental-features
export CLUSTER_TOPOLOGY=true # Enable the experimental Cluster topology feature
**clusterctl init --infrastructure docker** # Initialize the management cluster
*Fetching providers
Installing cert-manager version="v1.19.1"
Waiting for cert-manager to be available...
...*
# 확인
## 관련 crd 설치
**kubectl get crd**
**kubectl get crd | grep x-k8s**
*clusterclasses.cluster.x-k8s.io 2026-02-10T14:53:47Z
clusterresourcesetbindings.addons.cluster.x-k8s.io 2026-02-10T14:53:47Z
clusterresourcesets.addons.cluster.x-k8s.io 2026-02-10T14:53:47Z
clusters.cluster.x-k8s.io 2026-02-10T14:53:47Z
devclusters.infrastructure.cluster.x-k8s.io 2026-02-10T14:53:48Z
devclustertemplates.infrastructure.cluster.x-k8s.io 2026-02-10T14:53:48Z
devmachines.infrastructure.cluster.x-k8s.io 2026-02-10T14:53:48Z
devmachinetemplates.infrastructure.cluster.x-k8s.io 2026-02-10T14:53:48Z
dockerclusters.infrastructure.cluster.x-k8s.io 2026-02-10T14:53:48Z
dockerclustertemplates.infrastructure.cluster.x-k8s.io 2026-02-10T14:53:48Z
dockermachinepools.infrastructure.cluster.x-k8s.io 2026-02-10T14:53:48Z
dockermachinepooltemplates.infrastructure.cluster.x-k8s.io 2026-02-10T14:53:48Z
dockermachines.infrastructure.cluster.x-k8s.io 2026-02-10T14:53:48Z
dockermachinetemplates.infrastructure.cluster.x-k8s.io 2026-02-10T14:53:48Z
extensionconfigs.runtime.cluster.x-k8s.io 2026-02-10T14:53:47Z
ipaddressclaims.ipam.cluster.x-k8s.io 2026-02-10T14:53:47Z
ipaddresses.ipam.cluster.x-k8s.io 2026-02-10T14:53:47Z
kubeadmconfigs.bootstrap.cluster.x-k8s.io 2026-02-10T14:53:48Z
kubeadmconfigtemplates.bootstrap.cluster.x-k8s.io 2026-02-10T14:53:48Z
kubeadmcontrolplanes.controlplane.cluster.x-k8s.io 2026-02-10T14:53:48Z
kubeadmcontrolplanetemplates.controlplane.cluster.x-k8s.io 2026-02-10T14:53:48Z
machinedeployments.cluster.x-k8s.io 2026-02-10T14:53:47Z
machinedrainrules.cluster.x-k8s.io 2026-02-10T14:53:47Z
machinehealthchecks.cluster.x-k8s.io 2026-02-10T14:53:47Z
machinepools.cluster.x-k8s.io 2026-02-10T14:53:47Z
machines.cluster.x-k8s.io 2026-02-10T14:53:47Z
machinesets.cluster.x-k8s.io 2026-02-10T14:53:47Z
providers.clusterctl.cluster.x-k8s.io 2026-02-10T14:53:15Z*
## capd-system, capi-(kueadm-X/Y, system), cert-manager 네임스페이스가 생성
**kubectl get pod -A**
*NAMESPACE NAME READY STATUS RESTARTS AGE
capd-system capd-controller-manager-7c9d67ffdf-7npsd 1/1 Running 0 3m10s
capi-kubeadm-bootstrap-system capi-kubeadm-bootstrap-controller-manager-bd5f89bbd-9c9ng 1/1 Running 0 3m10s
capi-kubeadm-control-plane-system capi-kubeadm-control-plane-controller-manager-55c48d9b5-bckj5 1/1 Running 0 3m10s
capi-system capi-controller-manager-6cc7b949c4-rmd7h 1/1 Running 0 3m11s
cert-manager cert-manager-598d877b78-9lkmd 1/1 Running 0 3m37s
cert-manager cert-manager-cainjector-6b5777d564-7mfzz 1/1 Running 0 3m37s
cert-manager cert-manager-webhook-5d9fc6b4ff-slscg 1/1 Running 0 3m37s*
...
관리용 Management K8S 클러스터 정보 확인
여기서는 “관리 클러스터에 어떤 프로바이더가 설치되었는지”를 구조적으로 확인합니다.
특히 실습의 큰 줄기는 다음 한 줄로 요약됩니다.
- CAPI(Core) 가 “Cluster/Machine 등 CRD 수렴”을 오케스트레이션
- CAPD(Infrastructure) 가 “도커 컨테이너(노드)를 실제로 생성”
- Kubeadm(Bootstrap/ControlPlane) 이 “노드 init/join, 컨트롤플레인 구성/업그레이드”를 수행
graph TD
%% 프로바이더 노드
Core[Core Provider<br/><b>Cluster API</b>]
Infra[Infrastructure Provider<br/><b>CAPD : Docker</b>]
Boot[Bootstrap Provider<br/><b>Kubeadm</b>]
CP[Control Plane Provider<br/><b>Kubeadm</b>]
%% 클러스터 생성 흐름
Core -->|1. 정의 및 조정| CP
CP -->|2. 설정 요청| Boot
CP -->|3. 인프라 요청| Infra
subgraph "Target Cluster (생성되는 클러스터)"
Node[Docker Container<br/>as K8s Node]
end
Infra -->|실제 리소스 생성| Node
Boot -->|Cloud-init / Config 데이터 전달| Node
%% 스타일링
style Core fill:#f96,stroke:#333
style Infra fill:#3cf,stroke:#333
style Boot fill:#9f9,stroke:#333
style CP fill:#f9f,stroke:#333
아래 출력에서 “실제로 어떤 네임스페이스/디플로이먼트가 떴는지, feature-gates가 어떻게 들어갔는지, providers CR로 타입별 설치가 끝났는지”를 확인합니다.
## capd-system, capi-(kueadm-X/Y, system), cert-manager 네임스페이스가 생성
**kubectl get pod -A**
*NAMESPACE NAME READY STATUS RESTARTS AGE
capd-system capd-controller-manager-7c9d67ffdf-7npsd 1/1 Running 0 3m10s
capi-kubeadm-bootstrap-system capi-kubeadm-bootstrap-controller-manager-bd5f89bbd-9c9ng 1/1 Running 0 3m10s
capi-kubeadm-control-plane-system capi-kubeadm-control-plane-controller-manager-55c48d9b5-bckj5 1/1 Running 0 3m10s
capi-system capi-controller-manager-6cc7b949c4-rmd7h 1/1 Running 0 3m11s
cert-manager cert-manager-598d877b78-9lkmd 1/1 Running 0 3m37s
cert-manager cert-manager-cainjector-6b5777d564-7mfzz 1/1 Running 0 3m37s
cert-manager cert-manager-webhook-5d9fc6b4ff-slscg 1/1 Running 0 3m37s*
...
## Enabling Experimental Features 정보 확인 : ClusterTopology=true , InPlaceUpdates=false 등
**kubectl describe -n capi-system deployment.apps/capi-controller-manager | grep feature-gates**
*--feature-gates=MachinePool=true,**ClusterTopology=true**,RuntimeSDK=false,MachineSetPreflightChecks=true,MachineWaitForVolumeDetachConsiderVolumeAttachments=true,PriorityQueue=false,ReconcilerRateLimiting=false,**InPlaceUpdates=false**,MachineTaintPropagation=false*
# 프로바이더 (타입별)확인 : CAPI 구성요소가 설치된 상태
**kubectl get providers.clusterctl.cluster.x-k8s.io -A**
*NAMESPACE NAME AGE TYPE PROVIDER VERSION
capd-system infrastructure-docker 5m30s InfrastructureProvider docker v1.12.2
capi-system cluster-api 5m31s CoreProvider cluster-api v1.12.2
capi-kubeadm-bootstrap-system bootstrap-kubeadm 5m31s BootstrapProvider kubeadm v1.12.2
capi-kubeadm-control-plane-system control-plane-kubeadm 5m31s ControlPlaneProvider kubeadm v1.12.2*
# CAPI의 핵심 컨트롤러 집합 : Cluster / MachineDeployment / MachineSet / Machine CRD 관리, 전체 reconcile orchestration 담당
****kubectl get providers -n capi-system cluster-api -o yaml**
*providerName: **cluster-api**
type: **CoreProvider**
version: v1.12.2*
# 노드를 Kubernetes로 부팅시키는 역할 : cloud-init user-data 생성, kubeadm join/init config 생성
****kubectl get providers -n capi-kubeadm-bootstrap-system bootstrap-kubeadm -o yaml**
*providerName: **kubeadm**
type: **BootstrapProvider**
version: v1.12.2*
# Control Plane 전용 Machine 관리 : KubeadmControlPlane 리소스 관리, Control Plane 노드 스케일링, etcd 포함 업그레이드 관리
**kubectl get providers -n capi-kubeadm-control-plane-system control-plane-kubeadm -o yaml**
*providerName: **kubeadm**
type: **ControlPlaneProvider**
version: v1.12.2*
# 실제 인프라 리소스 생성 담당 : 실제 Docker 컨테이너를 VM처럼 생성, Dev/Test 용도 (CAPD)
****kubectl get providers -n capd-system infrastructure-docker -o yaml**
*providerName: **docker**
type: **InfrastructureProvider**
version: v1.12.2*
cert-manger 확인
Cluster API는 webhook/CRD 등에서 인증서가 필요하고, 실습에서는 clusterctl init이 cert-manager를 함께 설치합니다.
아래에서는 “cert-manager 자체 리소스 + 각 프로바이더 네임스페이스의 selfsigned issuer + certificate/certificaterequest”가 정상인지 확인합니다.
#
**kubectl get crd | grep cert**
*certificaterequests.cert-manager.io 2026-02-10T14:53:20Z
certificates.cert-manager.io 2026-02-10T14:53:20Z
challenges.acme.cert-manager.io 2026-02-10T14:53:20Z
clusterissuers.cert-manager.io 2026-02-10T14:53:20Z
issuers.cert-manager.io 2026-02-10T14:53:20Z
orders.acme.cert-manager.io 2026-02-10T14:53:20Z*
**kubectl get deploy,pod,svc,ep,cm,secret,sa -n cert-manager**
*NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/cert-manager 1/1 1 1 14m
deployment.apps/cert-manager-cainjector 1/1 1 1 14m
deployment.apps/cert-manager-webhook 1/1 1 1 14m
NAME READY STATUS RESTARTS AGE
pod/cert-manager-598d877b78-9lkmd 1/1 Running 0 14m
pod/cert-manager-cainjector-6b5777d564-7mfzz 1/1 Running 0 14m
pod/cert-manager-webhook-5d9fc6b4ff-slscg 1/1 Running 0 14m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/cert-manager ClusterIP 10.96.250.194 <none> 9402/TCP 14m
service/cert-manager-cainjector ClusterIP 10.96.184.41 <none> 9402/TCP 14m
service/cert-manager-webhook ClusterIP 10.96.218.248 <none> 443/TCP,9402/TCP 14m
NAME ENDPOINTS AGE
endpoints/cert-manager 10.244.0.7:9402 14m
endpoints/cert-manager-cainjector 10.244.0.6:9402 14m
endpoints/cert-manager-webhook 10.244.0.8:10250,10.244.0.8:9402 14m
NAME DATA AGE
configmap/kube-root-ca.crt 1 14m
NAME TYPE DATA AGE
secret/cert-manager-webhook-ca Opaque 3 14m
NAME AGE
serviceaccount/cert-manager 14m
serviceaccount/cert-manager-cainjector 14m
serviceaccount/cert-manager-webhook 14m
serviceaccount/default 14m*
#
**kubectl get issuers.cert-manager.io -A**
*NAMESPACE NAME READY AGE
capd-system capd-selfsigned-issuer True 14m
capi-system capi-selfsigned-issuer True 14m
capi-kubeadm-bootstrap-system capi-kubeadm-bootstrap-selfsigned-issuer True 14m
capi-kubeadm-control-plane-system capi-kubeadm-control-plane-selfsigned-issuer True 14m*
**kubectl get certificaterequests.cert-manager.io -A -owide**
*NAMESPACE NAME APPROVED DENIED READY ISSUER REQUESTER STATUS AGE
capd-system capd-serving-cert-1 True True capd-selfsigned-issuer system:serviceaccount:cert-manager:cert-manager Certificate fetched from issuer successfully 15m
capi-system capi-serving-cert-1 True True capi-selfsigned-issuer system:serviceaccount:cert-manager:cert-manager Certificate fetched from issuer successfully 15m
capi-kubeadm-bootstrap-system capi-kubeadm-bootstrap-serving-cert-1 True True capi-kubeadm-bootstrap-selfsigned-issuer system:serviceaccount:cert-manager:cert-manager Certificate fetched from issuer successfully 15m
capi-kubeadm-control-plane-system capi-kubeadm-control-plane-serving-cert-1 True True capi-kubeadm-control-plane-selfsigned-issuer system:serviceaccount:cert-manager:cert-manager Certificate fetched from issuer successfully 15m*
**kubectl get certificates.cert-manager.io -A -owide**
*NAMESPACE NAME READY SECRET ISSUER STATUS AGE
capd-system capd-serving-cert True capd-webhook-service-cert capd-selfsigned-issuer Certificate is up to date and has not expired 16m
capi-system capi-serving-cert True capi-webhook-service-cert capi-selfsigned-issuer Certificate is up to date and has not expired 16m
capi-kubeadm-bootstrap-system capi-kubeadm-bootstrap-serving-cert True capi-kubeadm-bootstrap-webhook-service-cert capi-kubeadm-bootstrap-selfsigned-issuer Certificate is up to date and has not expired 16m
capi-kubeadm-control-plane-system capi-kubeadm-control-plane-serving-cert True capi-kubeadm-control-plane-webhook-service-cert capi-kubeadm-control-plane-selfsigned-issuer Certificate is up to date and has not expired 16m*
첫 번째 워크로드 Workload 클러스터 생성 및 확인
여기서부터 “관리 클러스터가 실제 워크로드 클러스터를 만들어내는” 구간입니다.
실습에서 보는 주요 이벤트 흐름은 보통 아래 순서로 진행됩니다.
- Cluster(Topology 포함) 생성
- 컨트롤러가 ClusterClass/Template을 기반으로 “클러스터 전용 템플릿 복사본” 생성
- CAPD가 도커 컨테이너를 “노드”로 띄움 (+ LB 컨테이너 포함)
- kubeadm이 init/join 진행
- CNI 미설치 상태에서는 노드가 NotReady → CNI 설치 후 Ready
- 모니터링
모니터링 커맨드는 “docker 컨테이너 생성/삭제”와 “Cluster API 오브젝트 상태”를 같이 보도록 구성되어 있습니다.
# 모니터링
**watch -d "docker ps ; echo ; clusterctl describe cluster capi-quickstart"
watch -d kubectl get cluster -o wide** # or kubectl get cluster -w
**watch -d kubectl get machines** # watch -d "docker ps ; echo ; kubectl get machines"
**watch -d kubectl get pod -A**
첫 번째 워크로드 Workload 클러스터 생성
핵심은 clusterctl generate cluster ... --flavor development가 “관리형 토폴로지(ClusterClass)” 기반 YAML을 생성해준다는 점입니다.
- capi-quickstart.yaml
아래 YAML은 설계도(ClusterClass + Templates) + 실제 인스턴스(Cluster topology) 가 한 파일에 같이 들어있는 형태입니다.
apiVersion: cluster.x-k8s.io/v1beta2
kind: ClusterClass
metadata:
name: quick-start
namespace: default
spec:
controlPlane:
healthCheck:
checks:
unhealthyMachineConditions:
- status: Unknown
timeoutSeconds: 300
type: NodeReady
- status: "False"
timeoutSeconds: 300
type: NodeReady
unhealthyNodeConditions:
- status: Unknown
timeoutSeconds: 300
type: Ready
- status: "False"
timeoutSeconds: 300
type: Ready
machineInfrastructure:
templateRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachineTemplate
name: quick-start-control-plane
templateRef:
apiVersion: controlplane.cluster.x-k8s.io/v1beta2
kind: KubeadmControlPlaneTemplate
name: quick-start-control-plane
infrastructure:
templateRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerClusterTemplate
name: quick-start-cluster
patches:
- definitions:
- jsonPatches:
- op: add
path: /spec/template/spec/kubeadmConfigSpec/clusterConfiguration/imageRepository
valueFrom:
variable: imageRepository
selector:
apiVersion: controlplane.cluster.x-k8s.io/v1beta2
kind: KubeadmControlPlaneTemplate
matchResources:
controlPlane: true
description: Sets the imageRepository used for the KubeadmControlPlane.
enabledIf: '{{ ne .imageRepository "" }}'
name: imageRepository
- definitions:
- jsonPatches:
- op: add
path: /spec/template/spec/kubeadmConfigSpec/clusterConfiguration/etcd
valueFrom:
template: |
local:
imageTag: {{ .etcdImageTag }}
selector:
apiVersion: controlplane.cluster.x-k8s.io/v1beta2
kind: KubeadmControlPlaneTemplate
matchResources:
controlPlane: true
description: Sets tag to use for the etcd image in the KubeadmControlPlane.
enabledIf: '{{ ne .etcdImageTag "" }}'
name: etcdImageTag
- definitions:
- jsonPatches:
- op: add
path: /spec/template/spec/kubeadmConfigSpec/clusterConfiguration/dns
valueFrom:
template: |
imageTag: {{ .coreDNSImageTag }}
selector:
apiVersion: controlplane.cluster.x-k8s.io/v1beta2
kind: KubeadmControlPlaneTemplate
matchResources:
controlPlane: true
description: Sets tag to use for the etcd image in the KubeadmControlPlane.
enabledIf: '{{ ne .coreDNSImageTag "" }}'
name: coreDNSImageTag
- definitions:
- jsonPatches:
- op: add
path: /spec/template/spec/customImage
valueFrom:
template: |
kindest/node:{{ .builtin.machineDeployment.version | replace "+" "_" }}
selector:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachineTemplate
matchResources:
machineDeploymentClass:
names:
- default-worker
- jsonPatches:
- op: add
path: /spec/template/spec/template/customImage
valueFrom:
template: |
kindest/node:{{ .builtin.machinePool.version | replace "+" "_" }}
selector:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachinePoolTemplate
matchResources:
machinePoolClass:
names:
- default-worker
- jsonPatches:
- op: add
path: /spec/template/spec/customImage
valueFrom:
template: |
kindest/node:{{ .builtin.controlPlane.version | replace "+" "_" }}
selector:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachineTemplate
matchResources:
controlPlane: true
description: Sets the container image that is used for running dockerMachines
for the controlPlane and default-worker machineDeployments.
name: customImage
- definitions:
- jsonPatches:
- op: add
path: /spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs
value:
- name: admission-control-config-file
value: /etc/kubernetes/kube-apiserver-admission-pss.yaml
- op: add
path: /spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraVolumes
value:
- hostPath: /etc/kubernetes/kube-apiserver-admission-pss.yaml
mountPath: /etc/kubernetes/kube-apiserver-admission-pss.yaml
name: admission-pss
pathType: File
readOnly: true
- op: add
path: /spec/template/spec/kubeadmConfigSpec/files
valueFrom:
template: |
- content: |
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1{{ if semverCompare "< v1.25-0" .builtin.controlPlane.version }}beta1{{ end }}
kind: PodSecurityConfiguration
defaults:
enforce: "{{ .podSecurityStandard.enforce }}"
enforce-version: "latest"
audit: "{{ .podSecurityStandard.audit }}"
audit-version: "latest"
warn: "{{ .podSecurityStandard.warn }}"
warn-version: "latest"
exemptions:
usernames: []
runtimeClasses: []
namespaces: [kube-system]
path: /etc/kubernetes/kube-apiserver-admission-pss.yaml
selector:
apiVersion: controlplane.cluster.x-k8s.io/v1beta2
kind: KubeadmControlPlaneTemplate
matchResources:
controlPlane: true
description: Adds an admission configuration for PodSecurity to the kube-apiserver.
enabledIf: '{{ .podSecurityStandard.enabled }}'
name: podSecurityStandard
variables:
- name: imageRepository
required: true
schema:
openAPIV3Schema:
default: ""
description: imageRepository sets the container registry to pull images from.
If empty, nothing will be set and the from of kubeadm will be used.
example: registry.k8s.io
type: string
- name: etcdImageTag
required: true
schema:
openAPIV3Schema:
default: ""
description: etcdImageTag sets the tag for the etcd image.
example: 3.5.3-0
type: string
- name: coreDNSImageTag
required: true
schema:
openAPIV3Schema:
default: ""
description: coreDNSImageTag sets the tag for the coreDNS image.
example: v1.8.5
type: string
- name: podSecurityStandard
required: false
schema:
openAPIV3Schema:
properties:
audit:
default: restricted
description: audit sets the level for the audit PodSecurityConfiguration
mode. One of privileged, baseline, restricted.
type: string
enabled:
default: true
description: enabled enables the patches to enable Pod Security Standard
via AdmissionConfiguration.
type: boolean
enforce:
default: baseline
description: enforce sets the level for the enforce PodSecurityConfiguration
mode. One of privileged, baseline, restricted.
type: string
warn:
default: restricted
description: warn sets the level for the warn PodSecurityConfiguration
mode. One of privileged, baseline, restricted.
type: string
type: object
workers:
machineDeployments:
- bootstrap:
templateRef:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta2
kind: KubeadmConfigTemplate
name: quick-start-default-worker-bootstraptemplate
class: default-worker
healthCheck:
checks:
unhealthyMachineConditions:
- status: Unknown
timeoutSeconds: 300
type: NodeReady
- status: "False"
timeoutSeconds: 300
type: NodeReady
unhealthyNodeConditions:
- status: Unknown
timeoutSeconds: 300
type: Ready
- status: "False"
timeoutSeconds: 300
type: Ready
infrastructure:
templateRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachineTemplate
name: quick-start-default-worker-machinetemplate
machinePools:
- bootstrap:
templateRef:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta2
kind: KubeadmConfigTemplate
name: quick-start-default-worker-bootstraptemplate
class: default-worker
infrastructure:
templateRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachinePoolTemplate
name: quick-start-default-worker-machinepooltemplate
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerClusterTemplate
metadata:
name: quick-start-cluster
namespace: default
spec:
template:
spec: {}
---
apiVersion: controlplane.cluster.x-k8s.io/v1beta2
kind: KubeadmControlPlaneTemplate
metadata:
name: quick-start-control-plane
namespace: default
spec:
template:
spec:
kubeadmConfigSpec:
clusterConfiguration:
apiServer:
certSANs:
- localhost
- 127.0.0.1
- 0.0.0.0
- host.docker.internal
initConfiguration:
nodeRegistration:
kubeletExtraArgs:
- name: eviction-hard
value: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%
joinConfiguration:
nodeRegistration:
kubeletExtraArgs:
- name: eviction-hard
value: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachineTemplate
metadata:
name: quick-start-control-plane
namespace: default
spec:
template:
spec:
extraMounts:
- containerPath: /var/run/docker.sock
hostPath: /var/run/docker.sock
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachineTemplate
metadata:
name: quick-start-default-worker-machinetemplate
namespace: default
spec:
template:
spec:
extraMounts:
- containerPath: /var/run/docker.sock
hostPath: /var/run/docker.sock
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: DockerMachinePoolTemplate
metadata:
name: quick-start-default-worker-machinepooltemplate
namespace: default
spec:
template:
spec:
template: {}
---
apiVersion: bootstrap.cluster.x-k8s.io/v1beta2
kind: KubeadmConfigTemplate
metadata:
name: quick-start-default-worker-bootstraptemplate
namespace: default
spec:
template:
spec:
joinConfiguration:
nodeRegistration:
kubeletExtraArgs:
- name: eviction-hard
value: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%
---
apiVersion: cluster.x-k8s.io/v1beta2
kind: Cluster
metadata:
name: capi-quickstart
namespace: default
spec:
clusterNetwork:
pods:
cidrBlocks:
- 10.10.0.0/16
serviceDomain: myk8s-1.local
services:
cidrBlocks:
- 10.20.0.0/16
topology:
classRef:
name: quick-start
controlPlane:
replicas: 3
variables:
- name: imageRepository
value: ""
- name: etcdImageTag
value: ""
- name: coreDNSImageTag
value: ""
- name: podSecurityStandard
value:
audit: restricted
enabled: false
enforce: baseline
warn: restricted
version: v1.34.3
workers:
machineDeployments:
- class: default-worker
name: md-0
replicas: 3
아래 다이어그램은 “Cluster(인스턴스) ↔ ClusterClass(설계도) ↔ 템플릿(부품)”의 관계를 직관적으로 보여줍니다.
```mermaid
graph RL
subgraph "Cluster (실제 인스턴스)"
C[Cluster: capi-quickstart]
end
subgraph "ClusterClass (설계도)"
CC[ClusterClass: quick-start]
Patches[Patches: PodSecurity, Images]
end
subgraph "Infrastructure & ControlPlane Templates (부품)"
DCT[DockerClusterTemplate]
KCPT[KubeadmControlPlaneTemplate]
DMT[DockerMachineTemplate]
KCT[KubeadmConfigTemplate]
end
C -- "uses" --> CC
CC -- "selects & modifies" --> DCT
CC -- "selects & modifies" --> KCPT
CC -- "selects & modifies" --> DMT
CC -- "selects & modifies" --> KCT
CC -.-> Patches
```
- **Control Plane 정의:** `KubeadmControlPlaneTemplate`을 사용하여 쿠버네티스 컨트롤 플레인을 어떻게 만들지 정의합니다.
- **Infrastructure 정의:** `DockerMachineTemplate`을 사용하여 실제 노드가 Docker 컨테이너(CAPD)로 생성됨을 정의합니다.
- **Patches (가장 중요한 부분):** `ClusterClass`는 매우 유연합니다. `patches` 섹션은 사용자가 입력한 변수(`variables`)에 따라 YAML 구조를 실시간으로 변경합니다.
- `imageRepository`: 컨테이너 이미지를 받아올 저장소를 변경합니다.
- `etcdImageTag`, `coreDNSImageTag`: 특정 컴포넌트의 버전을 바꿉니다.
- PodSecurityStandard: 사용자가 설정한 값에 따라 API 서버의 Admission Configuration에 Pod Security 정책을 자동으로 삽입합니다.
이어지는 커맨드는 “변수 → YAML 생성 → 적용”으로 이어지는 가장 표준적인 워크플로입니다.
```bash
# 첫 번째 워크로드 구성을 위한 환경 변수 설정 : 필요에 맞게 수정.
## The list of service CIDR, default ["10.128.0.0/12"]
export SERVICE_CIDR=["10.20.0.0/16"]
## The list of pod CIDR, default ["192.168.0.0/16"]
export POD_CIDR=["10.10.0.0/16"]
## The service domain, default "cluster.local"
export SERVICE_DOMAIN="myk8s-1.local"
## PSS Disable
export POD_SECURITY_STANDARD_ENABLED="false"
# Generating the cluster configuration : 워크로드 클러스터 생성 --dry-run
## https://cluster-api.sigs.k8s.io/clusterctl/commands/generate-cluster
clusterctl generate cluster **capi-quickstart** --flavor **development** \
--kubernetes-version **v1.34.3** \
--**control-plane**-machine-count=**3** \
--**worker**-machine-count=**3** \
> capi-quickstart.yaml
# Cluster, Machines, Machine Deployments 등과 같은 Cluster API 객체의 미리 정의된 목록이 포함된 YAML 파일이 생성됨.
**open capi-quickstart.yaml**
**cat capi-quickstart.yaml | grep -E '^apiVersion:|^kind:'**
*apiVersion: cluster.x-k8s.io/v1beta2
kind: **ClusterClass** # ClusterClass 정의 (설계도)
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: **DockerClusterTemplate**
apiVersion: controlplane.cluster.x-k8s.io/v1beta2
kind: **KubeadmControlPlaneTemplate**
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: **DockerMachineTemplate**
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: **DockerMachineTemplate**
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: **DockerMachinePoolTemplate**
apiVersion: bootstrap.cluster.x-k8s.io/v1beta2
kind: **KubeadmConfigTemplate**
apiVersion: cluster.x-k8s.io/v1beta2
kind: **Cluster** # Cluster (Topology 기반 실제 클러스터)*
# Apply the workload cluster
**kubectl apply -f capi-quickstart.yaml**
*clusterclass.cluster.x-k8s.io/quick-start created
dockerclustertemplate.infrastructure.cluster.x-k8s.io/quick-start-cluster created
kubeadmcontrolplanetemplate.controlplane.cluster.x-k8s.io/quick-start-control-plane created
dockermachinetemplate.infrastructure.cluster.x-k8s.io/quick-start-control-plane created
dockermachinetemplate.infrastructure.cluster.x-k8s.io/quick-start-default-worker-machinetemplate created
dockermachinepooltemplate.infrastructure.cluster.x-k8s.io/quick-start-default-worker-machinepooltemplate created
kubeadmconfigtemplate.bootstrap.cluster.x-k8s.io/quick-start-default-worker-bootstraptemplate created
cluster.cluster.x-k8s.io/capi-quickstart created*
```
생성 확인 & kubeconfig 자격 증명 & CNI 플러그인 설치 후 확인
여기서 “노드 NotReady → CNI 설치 후 Ready”는 실습에서 가장 흔히 만나는 정상 패턴입니다.
(초기에는 Pod 네트워크가 없으니 Ready가 안 뜨는 것이 자연스럽습니다.)
# Accessing the workload cluster
## The cluster will now start provisioning. You can check status with
**kubectl get cluster -o wide**
*NAME CLUSTERCLASS AVAILABLE CP DESIRED CP CURRENT CP READY CP AVAILABLE CP UP-TO-DATE W DESIRED W CURRENT W READY W AVAILABLE W UP-TO-DATE PAUSED PHASE AGE VERSION
capi-quickstart quick-start False 3 3 0 0 3 3 3 0 0 3 False Provisioned 3m38s v1.34.3*
## You can also get an “at glance” view of the cluster and its resources by running:
**clusterctl describe cluster capi-quickstart**
...
# docker 프로바이더에 의해서, 워크로드 클러스터에 머신이 컨테이너로 기동
**docker ps**
*CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d7622e825f15 kindest/node:v1.34.3 "/usr/local/bin/entr…" 2 minutes ago Up 2 minutes 127.0.0.1:32772->6443/tcp capi-quickstart-hvt8l-plmp7
803b5917463f kindest/node:v1.34.3 "/usr/local/bin/entr…" 3 minutes ago Up 3 minutes 127.0.0.1:32771->6443/tcp capi-quickstart-hvt8l-j6tpk
051bcc807657 kindest/node:v1.34.3 "/usr/local/bin/entr…" 3 minutes ago Up 3 minutes capi-quickstart-md-0-64l6n-9hjzs-lcjv4
46b27b6eb154 kindest/node:v1.34.3 "/usr/local/bin/entr…" 3 minutes ago Up 3 minutes capi-quickstart-md-0-64l6n-9hjzs-8rz7j
a5271457c0d6 kindest/node:v1.34.3 "/usr/local/bin/entr…" 3 minutes ago Up 3 minutes capi-quickstart-md-0-64l6n-9hjzs-qnd5z
1607db8af33f kindest/node:v1.34.3 "/usr/local/bin/entr…" 3 minutes ago Up 3 minutes 127.0.0.1:32770->6443/tcp capi-quickstart-hvt8l-lpp75
6c1f5141492c kindest/haproxy:v20230606-42a2262b "haproxy -W -db -f /…" 3 minutes ago Up 3 minutes 0.0.0.0:32768->6443/tcp, 0.0.0.0:32769->8404/tcp capi-quickstart-lb
2f4d7bd4d2ad kindest/node:v1.35.0 "/usr/local/bin/entr…" 22 minutes ago Up 22 minutes 0.0.0.0:30000-30001->30000-30001/tcp, 127.0.0.1:50800->6443/tcp myk8s-control-plane*
# 컨트롤 플레인 역할 컨테이너 로그 1대 확인 해보기
**docker logs -f capi-quickstart-sn62k-pr4kp**
*...
Welcome to Debian GNU/Linux 12 (bookworm)!
Queued start job for default target graphical.target.
[ OK ] Created slice kubelet.slic… used to run Kubernetes / Kubelet.
[ OK ] Created slice system-modpr…lice - Slice /system/modprobe.
...*
# 워크로드 클러스터 자격증명
**clusterctl get kubeconfig capi-quickstart > capi-quickstart.kubeconfig**
kubectl --kubeconfig=capi-quickstart.kubeconfig get nodes -owide
# 노드 NotReady 상태 해결 : CNI 플러그인 설치 하자!
**kubectl --kubeconfig=capi-quickstart.kubeconfig apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml**
**kubectl --kubeconfig=capi-quickstart.kubeconfig get nodes -owide**
*NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
capi-quickstart-md-0-lt794-jfjvx-9wrql Ready <none> 113s v1.35.0 192.168.97.6 <none> Debian GNU/Linux 12 (bookworm) 6.17.4-orbstack-00308-g195e9689a04f containerd://2.2.0
capi-quickstart-md-0-lt794-jfjvx-f4pr4 Ready <none> 113s v1.35.0 192.168.97.5 <none> Debian GNU/Linux 12 (bookworm) 6.17.4-orbstack-00308-g195e9689a04f containerd://2.2.0
capi-quickstart-md-0-lt794-jfjvx-rjfz9 Ready <none> 113s v1.35.0 192.168.97.7 <none> Debian GNU/Linux 12 (bookworm) 6.17.4-orbstack-00308-g195e9689a04f containerd://2.2.0
capi-quickstart-sn62k-6n9sd Ready control-plane 2m14s v1.35.0 192.168.97.4 <none> Debian GNU/Linux 12 (bookworm) 6.17.4-orbstack-00308-g195e9689a04f containerd://2.2.0
capi-quickstart-sn62k-pr4kp Ready control-plane 46s v1.35.0 192.168.97.9 <none> Debian GNU/Linux 12 (bookworm) 6.17.4-orbstack-00308-g195e9689a04f containerd://2.2.0
capi-quickstart-sn62k-xhmtw Ready control-plane 75s v1.35.0 192.168.97.8 <none> Debian GNU/Linux 12 (bookworm) 6.17.4-orbstack-00308-g195e9689a04f containerd://2.2.0*
# 워크로드 클러스터 상태 : 성공!
**clusterctl describe cluster capi-quickstart**
*NAME REPLICAS AVAILABLE READY UP TO DATE STATUS REASON SINCE MESSAGE
Cluster/capi-quickstart 6/6 6 6 6 True Available 2m2s
├─ClusterInfrastructure - DockerCluster/capi-quickstart-wcb2g True Ready 3m19s
├─ControlPlane - KubeadmControlPlane/capi-quickstart-sn62k 3/3 3 3 3 True Available 2m2s
│ └─3 Machines... 3 3 3 True Ready 2m13s See capi-quickstart-sn62k-6n9sd, capi-quickstart-sn62k-pr4kp, ...
└─Workers
└─MachineDeployment/capi-quickstart-md-0-lt794 3/3 3 3 3 True Available 2m13s
└─3 Machines... 3 3 3 True Ready 2m16s See capi-quickstart-md-0-lt794-jfjvx-9wrql, capi-quickstart-md-0-lt794-jfjvx-f4pr4, ...*
첫 번째 워크로드 클러스터 관련 Cluster API CRD 확인
이 섹션은 “실제로 어떤 CRD들이 어떤 관계로 묶여서 노드가 만들어졌는지”를 해부하는 파트입니다.
특히 Managed Topology를 쓰면, ClusterClass에 정의된 템플릿(설계도)을 “그대로 참조”하는 게 아니라, 클러스터 전용 복사본을 자동 생성해서 그 복사본을 MachineDeployment 등이 참조하게 됩니다.
아래에서 quick-start-*(설계도)와 capi-quickstart-*(실제 사용) 템플릿이 공존하는 이유가 바로 이것입니다.
graph RL
subgraph "Cluster (실제 인스턴스)"
C[Cluster: capi-quickstart]
end
subgraph "ClusterClass (설계도)"
CC[ClusterClass: quick-start]
Patches[Patches: PodSecurity, Images]
end
subgraph "Infrastructure & ControlPlane Templates (부품)"
DCT[DockerClusterTemplate]
KCPT[KubeadmControlPlaneTemplate]
DMT[DockerMachineTemplate]
KCT[KubeadmConfigTemplate]
end
C -- "uses" --> CC
CC -- "selects & modifies" --> DCT
CC -- "selects & modifies" --> KCPT
CC -- "selects & modifies" --> DMT
CC -- "selects & modifies" --> KCT
CC -.-> Patches
ClusterClass, Cluster, KubeadmControlPlane, kubeadmconfigtemplate
graph TD
subgraph "ClusterClass (Template Space)"
Origin[quick-start-default-worker-bootstraptemplate<br/><i>'원본 설계도'</i>]
end
subgraph "Cluster: capi-quickstart (Runtime Space)"
Generated[capi-quickstart-md-0-ln5zl<br/><i>'실제 적용된 템플릿'</i>]
MD[MachineDeployment: md-0]
MS[MachineSet]
M[Machine]
end
%% 관계
Origin -- "Cloned & Patched by CAPI Controller" --> Generated
MD -- "references" --> Generated
MD -- "manages" --> MS
MS -- "manages" --> M
# clusterclasses 확인 : 클러스터의 기본 템플릿 정보 제공, 예) 아래 처럼 etcd 컨테이너 이미지 tag 3.5.3-0
**kubectl get clusterclasses quick-start -o yaml**
*...
**patches:**
- definitions:
- jsonPatches:
...
- name: **etcdImageTag**
required: true
schema:
openAPIV3Schema:
default: ""
description: etcdImageTag sets the tag for the etcd image.
example: **3.5.3-0**
type: string
...*
**kubectl get clusterclasses -owide**
*NAME PAUSED VARIABLES READY AGE
quick-start False True 10m*
# cluster 확인 : 아래 Varibles 중 Name에 값이 없는 경우, clusterclasses 템플릿에 variables 값이 반영됨
**kubectl get cluster -owide**
*NAME CLUSTERCLASS AVAILABLE CP DESIRED CP CURRENT CP READY CP AVAILABLE CP UP-TO-DATE W DESIRED W CURRENT W READY W AVAILABLE W UP-TO-DATE PAUSED PHASE AGE VERSION
capi-quickstart quick-start True 3 3 3 3 3 3 3 3 3 3 False Provisioned 4m37s v1.35.0*
**kubectl describe cluster capi-quickstart**
*...
**Spec:**
**Cluster Network**:
Pods:
Cidr Blocks:
10.10.0.0/16
Service Domain: myk8s-1.local
Services:
Cidr Blocks:
10.20.0.0/16
**Control Plane Endpoint**:
Host: 192.168.97.3
Port: 6443
...
**Topology**:
Class Ref:
Name: quick-start
**Control Plane:
Replicas: 3**
**Variables**:
Name: imageRepository
Value:
Name: etcdImageTag
Value:
Name: coreDNSImageTag
Value:
Name: podSecurityStandard
Value:
Audit: restricted
Enabled: false
Enforce: baseline
Warn: restricted
**Version**: v1.34.3
**Workers:**
Machine Deployments:
Class: default-worker
Name: md-0
Replicas: 3*
# 워크로드 클러스터에서 etcd 이미지 태그 정보 확인
**kubectl --kubeconfig=capi-quickstart.kubeconfig get pod -n kube-system -l component=etcd -o yaml | grep image: | uniq**
*image: registry.k8s.io/**etcd:3.6.5-0***
# To verify the first control plane is up:
**kubectl get kubeadmcontrolplane -owide**
*NAME CLUSTER AVAILABLE DESIRED CURRENT READY AVAILABLE UP-TO-DATE PAUSED INITIALIZED AGE VERSION
capi-quickstart-j9fdm capi-quickstart True 3 3 3 3 3 False true 48m v1.34.3*
## kubeadm 에 의해 생성된 컨트롤 플레인 정보
**kubectl get kubeadmcontrolplane -o yaml**
*...
spec:
**kubeadmConfigSpec**:
clusterConfiguration:
**apiServer**:
**certSANs**:
- localhost
- 127.0.0.1
- **0.0.0.0**
- host.docker.internal
**initConfiguration**:
nodeRegistration:
kubeletExtraArgs:
- name: eviction-hard
value: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%
**joinConfiguration**:
nodeRegistration:
kubeletExtraArgs:
- name: eviction-hard
value: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%
**machineTemplate**:
metadata:
labels:
cluster.x-k8s.io/cluster-name: capi-quickstart
topology.cluster.x-k8s.io/owned: ""
**spec:**
infrastructureRef:
apiGroup: infrastructure.cluster.x-k8s.io
kind: **DockerMachineTemplate**
name: capi-quickstart-tdwjt
**replicas: 3**
rollout:
strategy:
rollingUpdate:
maxSurge: 1
type: RollingUpdate
**version: v1.34.3**
status:
availableReplicas: 3
...*
# kubeadmconfigtemplate 확인
**kubectl get kubeadmconfigtemplate**
*NAME CLUSTERCLASS **CLUSTER** AGE
capi-quickstart-md-0-ln5zl **capi-quickstar**t 37m # 실제 Cluster 생성 시 자동으로 만들어진 복사본
quick-start-default-worker-bootstraptemplate quick-start 37m # ClusterClass에 속함, 실제 클러스터에 직접 사용되지 않음, '설계도용 템플릿'*
## kubeadm 작업 시 템플릿 : 아래는 워커 노드 join 시, kubelet args 설정
kubectl get kubeadmconfigtemplate -l cluster.x-k8s.io/cluster-name=capi-quickstart *# 실제 Cluster 생성 시 자동으로 만들어진 복사본*
**kubectl get kubeadmconfigtemplate -l cluster.x-k8s.io/cluster-name=capi-quickstart -o yaml** # 아래 spec 내용과 동일
*...
spec:
template:
spec:
joinConfiguration:
nodeRegistration:
kubeletExtraArgs:
- name: eviction-hard
value: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%*
**kubectl get kubeadmconfigtemplate quick-start-default-worker-bootstraptemplate -o yaml** *# ClusterClass에 속함, 실제 클러스터에 직접 사용되지 않음, '설계도용 템플릿'*
*...
spec:
template:
spec:
**joinConfiguration:**
**nodeRegistration**:
kubeletExtraArgs:
- name: **eviction-hard**
value: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%*
이 표는 “설계도(원본) vs 실제 클러스터 전용 복사본”을 구분하는데 가장 직관적입니다.
| 이름 | 역할 | 유형 |
| quick-start-control-plane | ClusterClass 원본 CP 템플릿 | 설계도 |
| quick-start-default-worker-machinetemplate | ClusterClass 원본 Worker 템플릿 | 설계도 |
| capi-quickstart-tdwjt | CP용 복사본 | 실제 사용 |
| capi-quickstart-md-0-xgl7l | Worker용 복사본 | 실제 사용 |
# docker**cluster**template 확인
kubectl get dockerclustertemplate
**kubectl describe dockerclustertemplate**
*...
Spec:
Template:
Spec:
**Control Plane Endpoint:
Port: 6443**
Load Balancer:*
# docker**machine**template 확인
**kubectl get dockermachinetemplate**
*NAME AGE
capi-quickstart-md-0-xgl7l 16m # Worker(MachineDeployment md-0) 전용 복사본
capi-quickstart-tdwjt 16m # ControlPlane용 클러스터 전용 복사본
quick-start-control-plane 16m # ClusterClass에서 정의한 원본 템플릿 : 설계도, ClusterClass가 참조
quick-start-default-worker-machinetemplate 16m # ClusterClass에 정의된 Worker용 원본 템플릿 : 설계도, 실제 사용 시 복제됨*
## dockermachinetemplate : 도커 프로바이더에 의해 배포된 노드(실제로는 컨테이너) 템플릿 - Spec(마운트 정보 등) 등 설정
## quick-start-control-plane, quick-start-default-worker-machinetemplate 의 Spec, Capacity 동일
**kubectl describe dockermachinetemplate quick-start-control-plane
kubectl describe dockermachinetemplate quick-start-default-worker-machinetemplate**
*...
Spec:
**Template**:
Spec:
**Extra Mounts**:
Container Path: /var/run/docker.sock
Host Path: /var/run/docker.sock
Status:
**Capacity**:
Cpu: 8
Memory: 11092404Ki*
## (참고) 아래 2개는 cluster 에 의해서 생성
**kubectl get dockermachinetemplate -l cluster.x-k8s.io/cluster-name=capi-quickstart**
*NAME AGE
capi-quickstart-md-0-xgl7l 32m
capi-quickstart-tdwjt 32m*
**kubectl describe cluster capi-quickstart | grep DockerMachineTemplate**
*Normal TopologyCreate 33m topology/cluster-controller Created DockerMachineTemplate "default/capi-quickstart-tdwjt"
Normal TopologyCreate 33m topology/cluster-controller Created DockerMachineTemplate "default/capi-quickstart-md-0-xgl7l"*
**kubectl get dockermachinetemplate -l cluster.x-k8s.io/cluster-name=capi-quickstart -o yaml**
*...
spec:
template:
**spec**:
**customImage**: kindest/node:**v1.34.3**
**extraMount**s:
- containerPath: /var/run/docker.sock
hostPath: /var/run/docker.sock
status:
**capacity**:
cpu: "8"
memory: 11092404Ki*
# (참고) docker**machinepool**template 확인 : 특별한 설정은 없음
kubectl get dockermachinepooltemplate
**kubectl get dockermachinepooltemplate quick-start-default-worker-machinepooltemplate -o yaml**
*...
Spec:
Template:
Spec:
Template:*
- 왜 두 개가 존재하나요? (Managed Topology) : Cluster API의 Managed Topology 엔진은 다음과 같은 단계를 거칩니다.
- 설계도 참조: 사용자가 Cluster를 만들 때 ClusterClass(quick-start)를 참조합니다.
- 변수 계산: Cluster에 정의된 변수(Variable)와 패치(Patch)를 원본 템플릿에 적용합니다.
- 실제 리소스 생성: 계산된 결과를 바탕으로, 특정 클러스터(capi-quickstart)만을 위한 고유한 템플릿 리소스를 자동으로 생성합니다.
- 이때 생성된 리소스 이름이 capi-quickstart-md-0-ln5zl 같은 형식이 됩니다.
- 이 리소스는 capi-quickstart 클러스터의 MachineDeployment에 의해 직접 참조됩니다.
- machinedeployments → machinesets → machines , machinehealthchecks
“스케일/롤아웃/헬스체크”가 어떤 오브젝트 단위로 수행되는지 확인하는 파트입니다.- 워커는 MachineDeployment → MachineSet → Machine 계층으로 관리
- 컨트롤플레인은 KubeadmControlPlane이 별도로 관리(즉, 워커처럼 MachineDeployment로 보지 않음)
# machinedeployments 확인 : 컨트롤플레인 노드(머신들)에 machinedeployments와 같은 동작은 KubeadmControlPlane 에서 처리함.
**k get machinedeployments**
*NAME CLUSTER AVAILABLE DESIRED CURRENT READY AVAILABLE UP-TO-DATE PHASE AGE VERSION
capi-quickstart-md-0-z4t5t capi-quickstart True 3 3 3 3 3 Running 2m37s v1.34.3*
**k get machinedeployments -o yaml**
*...
spec:
clusterName: capi-quickstart
replicas: 3
rollout:
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
selector:
matchLabels:
cluster.x-k8s.io/cluster-name: capi-quickstart
topology.cluster.x-k8s.io/deployment-name: md-0
topology.cluster.x-k8s.io/owned: ""
template:
metadata:
labels:
cluster.x-k8s.io/cluster-name: capi-quickstart
topology.cluster.x-k8s.io/deployment-name: md-0
topology.cluster.x-k8s.io/owned: ""
**spec:
bootstrap:
configRef:**
apiGroup: bootstrap.cluster.x-k8s.io
kind: KubeadmConfigTemplate
name: capi-quickstart-md-0-25dll
clusterName: capi-quickstart
**infrastructureRef:**
apiGroup: infrastructure.cluster.x-k8s.io
kind: DockerMachineTemplate
name: capi-quickstart-md-0-ms2hr
version: v1.34.3*
# machinesets 확인
**k get machinesets**
*NAME CLUSTER DESIRED CURRENT READY AVAILABLE UP-TO-DATE AGE VERSION
capi-quickstart-md-0-z4t5t-crp4c capi-quickstart 3 3 3 3 3 4m30s v1.34.3*
**k get machinesets -o yaml**
*...
spec:
clusterName: capi-quickstart
**replicas: 3**
selector:
matchLabels:
cluster.x-k8s.io/cluster-name: capi-quickstart
machine-template-hash: 1586322356-crp4c
topology.cluster.x-k8s.io/deployment-name: **md-0**
topology.cluster.x-k8s.io/owned: ""
template:
metadata:
labels:
cluster.x-k8s.io/cluster-name: capi-quickstart
machine-template-hash: 1586322356-crp4c
topology.cluster.x-k8s.io/deployment-name: **md-0**
topology.cluster.x-k8s.io/owned: ""
**spec:
bootstrap:**
configRef:
apiGroup: bootstrap.cluster.x-k8s.io
kind: KubeadmConfigTemplate
name: capi-quickstart-md-0-25dll
clusterName: capi-quickstart
**infrastructureRef**:
apiGroup: infrastructure.cluster.x-k8s.io
kind: DockerMachineTemplate
name: capi-quickstart-md-0-ms2hr
**version**: v1.34.3*
# machines 확인 : 개별 머신에 대한 정보
**k get machines -o wide**
*NAME CLUSTER NODE NAME PROVIDER ID READY AVAILABLE UP-TO-DATE INTERNAL-IP EXTERNAL-IP OS-IMAGE PAUSED PHASE AGE VERSION
capi-quickstart-md-0-z4t5t-crp4c-2dltp capi-quickstart capi-quickstart-md-0-z4t5t-crp4c-2dltp docker:////capi-quickstart-md-0-z4t5t-crp4c-2dltp True True True 192.168.97.5 192.168.97.5 Debian GNU/Linux 12 (bookworm) False Running 9m7s v1.34.3
capi-quickstart-md-0-z4t5t-crp4c-cq52h capi-quickstart capi-quickstart-md-0-z4t5t-crp4c-cq52h docker:////capi-quickstart-md-0-z4t5t-crp4c-cq52h True True True 192.168.97.7 192.168.97.7 Debian GNU/Linux 12 (bookworm) False Running 9m7s v1.34.3
capi-quickstart-md-0-z4t5t-crp4c-pqlg8 capi-quickstart capi-quickstart-md-0-z4t5t-crp4c-pqlg8 docker:////capi-quickstart-md-0-z4t5t-crp4c-pqlg8 True True True 192.168.97.6 192.168.97.6 Debian GNU/Linux 12 (bookworm) False Running 9m7s v1.34.3
capi-quickstart-z2v4j-f6btm capi-quickstart capi-quickstart-z2v4j-f6btm docker:////capi-quickstart-z2v4j-f6btm True True True 192.168.97.4 192.168.97.4 Debian GNU/Linux 12 (bookworm) False Running 9m21s v1.34.3
capi-quickstart-z2v4j-kzztz capi-quickstart capi-quickstart-z2v4j-kzztz docker:////capi-quickstart-z2v4j-kzztz True True True 192.168.97.8 192.168.97.8 Debian GNU/Linux 12 (bookworm) False Running 8m45s v1.34.3
capi-quickstart-z2v4j-pqn8l capi-quickstart capi-quickstart-z2v4j-pqn8l docker:////capi-quickstart-z2v4j-pqn8l True True True 192.168.97.9 192.168.97.9 Debian GNU/Linux 12 (bookworm) False Running 8m9s v1.34.3*
## controlplane 머신 정보
**k get machines -l cluster.x-k8s.io/control-plane -o yaml**
*...
spec:
**bootstrap:**
configRef:
apiGroup: bootstrap.cluster.x-k8s.io
kind: KubeadmConfig
name: capi-quickstart-z2v4j-f6btm
**dataSecretName**: capi-quickstart-z2v4j-f6btm
**clusterName**: capi-quickstart
**deletion**:
nodeDeletionTimeoutSeconds: 10
**infrastructureRef:**
apiGroup: infrastructure.cluster.x-k8s.io
kind: DockerMachine
name: capi-quickstart-z2v4j-f6btm
**providerID**: docker:////capi-quickstart-z2v4j-f6btm
**readinessGates**:
- conditionType: APIServerPodHealthy
- conditionType: ControllerManagerPodHealthy
- conditionType: SchedulerPodHealthy
- conditionType: EtcdPodHealthy
- conditionType: EtcdMemberHealthy
**version:** v1.34.3*
# machinehealthchecks 확인 : 머신(집합) 수준 헬스체크 상태
k get machinehealthchecks -o yaml
**k get machinehealthchecks -owide**
*NAME CLUSTER REPLICAS HEALTHY PAUSED AGE
capi-quickstart-md-0-z4t5t capi-quickstart 3 3 False 13m
capi-quickstart-z2v4j capi-quickstart 3 3 False 13m*
첫 번째 워크로드 클러스터 노드 정보 확인 & alias 설정(k8s1) & kube-ops-view 설치 & 샘플 app 배포
이 단계는 “워크로드 클러스터에 접속해서(별도 kubeconfig) 일반적인 운영 작업이 가능한지”를 확인합니다.
- 관리 클러스터의 kubectl은 “CAPI 리소스”를 다루는 용도
- 워크로드 kubeconfig로는 “그 클러스터 내부 리소스(pods/nodes/etc)”를 다루는 용도
# alias 설정 : **k8s1**
kubectl --kubeconfig=capi-quickstart.kubeconfig **cluster-info
alias k8s1='**kubectl --kubeconfig=capi-quickstart.kubeconfig'
**k8s1 cluster-info**
*Kubernetes control plane is running at https://192.168.97.3:6443*
# kube-ops-view 설치
**helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 \
--set env.TZ="Asia/Seoul" --namespace kube-system --kubeconfig=capi-quickstart.kubeconfig**
helm list --kubeconfig=capi-quickstart.kubeconfig -n kube-system
k8s1 get svc,ep -n kube-system kube-ops-view
## kube-ops-view 포트 포워딩 설정
**k8s1 -n kube-system port-forward svc/kube-ops-view 8080:8080 &**
open "http://127.0.0.1:8080/#scale=1.5" # 웹 접속
# 호스트에서 컨테이너 정보 확인 : LB 컨테이너 1대, CT 컨테이너 3대, WK 컨테이너 3대, 마지막 1대는 최초 kind로 구성한 관리형 k8s
**docker ps**
*CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[**Controlplane** node] fdf81c4e850a kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour 127.0.0.1:32775->6443/tcp capi-quickstart-j9fdm-6zg8v
**[Controlplane** node] 6fd07a6a4028 kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour 127.0.0.1:32774->6443/tcp capi-quickstart-j9fdm-27w2s
[**Controlplan**e node] 439466b27b34 kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour capi-quickstart-md-0-p7lv8-t7r9t-nhfpb
[**Worker** node] 6f700f8ee747 kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour capi-quickstart-md-0-p7lv8-t7r9t-rmcls
[**Worker** node] 12b85d4f4bab kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour capi-quickstart-md-0-p7lv8-t7r9t-t5ds2
**[Worker** node] a5507e114f4b kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour 127.0.0.1:32773->6443/tcp capi-quickstart-j9fdm-ggm9z
[**CTRL Loadbalancer**] d3e002a0c9bc kindest/haproxy:v20230606-42a2262b "haproxy -W -db -f /…" About an hour ago Up About an hour 0.0.0.0:32770->6443/tcp, 0.0.0.0:32771->8404/tcp capi-quickstart-lb
5f01ea7d7bd5 kindest/node:v1.35.0 "/usr/local/bin/entr…" 2 hours ago Up 2 hours 0.0.0.0:30000-30001->30000-30001/tcp, 127.0.0.1:54601->6443/tcp myk8s-control-plane*
샘플 앱은 “노드가 여러 대일 때 파드가 분산되는지/서비스가 동작하는지”를 확인하는 목적입니다.
# 샘플 애플리케이션 배포
**cat << EOF | kubectl** --kubeconfig=capi-quickstart.kubeconfig **apply -f -**
apiVersion: apps/v1
kind: Deployment
metadata:
name: **webpod**
spec:
**replicas: 3**
selector:
matchLabels:
app: **webpod**
template:
metadata:
labels:
app: **webpod**
spec:
**affinity**:
**** **podAntiAffinity**:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- sample-app
**topologyKey**: "kubernetes.io/hostname"
containers:
- name: **webpod**
image: **traefik/whoami**
ports:
- containerPort: **80**
---
apiVersion: v1
kind: Service
metadata:
name: **webpod**
labels:
app: **webpod**
spec:
selector:
app: **webpod**
ports:
- protocol: TCP
port: **80**
targetPort: **80
nodePort: 30003**
type: **NodePort
EOF**
# 확인 : alias k8s1='kubectl --kubeconfig=capi-quickstart.kubeconfig'
**k8s1 get deploy,pod,svc,ep -owide**
# 반복 호출 : 최초 배포한 kind k8s 에 컨트롤 플레인 노드에서 서비스의 NodePort 로 호출
docker ps
CT1=capi-quickstart-hvt8l-hb4hb
docker exec -it myk8s-control-plane curl -s $CT1:30003
**while true; do** docker exec -it myk8s-control-plane curl -s $CT1:30003 **| grep Hostname; date; sleep 1; done**
첫 번째 워크로드 클러스터에 컨트롤 플레인 노드 3대 k8s apiserver 에 대한 LB(HAProxy) 확인
CAPD 환경에서 control plane endpoint는 대개 HAProxy 컨테이너가 앞단에서 받아서 백엔드 API 서버(각 control-plane 노드의 6443)로 분산합니다.
그래서 cluster-info의 endpoint IP가 “특정 control-plane 노드”가 아니라 “LB 컨테이너 IP”로 보이는 것이 정상입니다.
# 호스트에서 컨테이너 정보 확인 : LB 컨테이너 1대, CT 컨테이너 3대, WK 컨테이너 3대, 마지막 1대는 최초 kind로 구성한 관리형 k8s
**docker ps**
*CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[**Controlplane** node] fdf81c4e850a kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour 127.0.0.1:32775->6443/tcp capi-quickstart-j9fdm-6zg8v
**[Controlplane** node] 6fd07a6a4028 kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour 127.0.0.1:32774->6443/tcp capi-quickstart-j9fdm-27w2s
[**Worker** node] 439466b27b34 kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour capi-quickstart-md-0-p7lv8-t7r9t-nhfpb
[**Worker** node] 6f700f8ee747 kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour capi-quickstart-md-0-p7lv8-t7r9t-rmcls
[**Worker** node] 12b85d4f4bab kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour capi-quickstart-md-0-p7lv8-t7r9t-t5ds2
[**Controlplane** node] a5507e114f4b kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour 127.0.0.1:32773->6443/tcp capi-quickstart-j9fdm-ggm9z
[CTRL **Loadbalancer**] d3e002a0c9bc kindest/haproxy:v20230606-42a2262b "haproxy -W -db -f /…" About an hour ago Up About an hour 0.0.0.0:32770->6443/tcp, 0.0.0.0:32771->8404/tcp capi-quickstart-lb
[kind k8s] 5f01ea7d7bd5 kindest/node:v1.35.0 "/usr/local/bin/entr…" 2 hours ago Up 2 hours 0.0.0.0:30000-30001->30000-30001/tcp, 127.0.0.1:54601->6443/tcp myk8s-control-plane*
# LB 컨테이너
**docker inspect capi-quickstart-lb | jq**
*...
"Entrypoint": [
"haproxy",
"-W",
"-db",
"-f",
"/usr/local/etc/haproxy/haproxy.cfg"
"Ports": {
"6443/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "**32770**" # 호스트PC에서 curl -sk **https**://127.0.0.1:**32770/version**
}
],
"8404/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "**32771**" # 호스트PC 웹에서 open **http**://127.0.0.1:32771**/stats***
*...
"IPAddress": "**192.168.97.3**", # 현재 **k8s apiserver 엔드포인**트는 192.168.97.3 IP이고 해당 IP는 HAProxy 컨테이너의 IP.*
# LB에 APIserver 포트로 호출
**curl -sk https://127.0.0.1:32770/version | jq**
*{
"major": "1",
"minor": "34",*
...
# 현재 k8s apiserver 엔드포인트는 192.168.97.3 IP이고 해당 IP는 HAProxy 컨테이너의 IP.
**k8s1 cluster-info**
*Kubernetes control plane is running at **https://192.168.97.3:**6443*
# 컨테이너 내부에 설정 파일을 호스트에 복사하여 가져오기
**docker cp capi-quickstart-lb:/usr/local/etc/haproxy/haproxy.cfg .**
# 설정 파일 확인
**cat haproxy.cfg**
-------------------------------------------
# generated by kind
global
log /dev/log local0
log /dev/log local1 notice
daemon
# limit memory usage to approximately 18 MB
# (see https://github.com/kubernetes-sigs/kind/pull/3115)
**maxconn 100000**
**resolvers docker**
nameserver dns 127.0.0.11:53
defaults
log global
**mode tcp**
option dontlognull
# TODO: tune these
timeout connect 5000
timeout client 50000
timeout server 50000
# allow to boot despite dns don't resolve backends
default-server init-addr none
**frontend stats**
mode http
**bind *:8404**
stats enable
stats **uri /stats**
stats refresh 1s
stats admin if TRUE
**frontend control-plane**
**bind *:6443**
default_backend kube-apiservers
**backend kube-apiservers**
option httpchk GET /healthz
**server capi-quickstart-j9fdm-27w2s 192.168.97.8:6443** weight 100 check check-ssl verify none resolvers docker resolve-prefer ipv4
**server capi-quickstart-j9fdm-6zg8v 192.168.97.9:6443** weight 100 check check-ssl verify none resolvers docker resolve-prefer ipv4
**server capi-quickstart-j9fdm-ggm9z 192.168.97.4:6443** weight 100 check check-ssl verify none resolvers docker resolve-prefer ipv4
-------------------------------------------
# 로그 확인
**docker logs -f capi-quickstart-lb**
*[WARNING] 041/141425 (90) : Server kube-apiservers/capi-quickstart-j9fdm-6zg8v is UP, reason: Layer7 check passed, code: 200, check duration: 5ms. 3 active and 0 backup servers online. 0 sessions requeued, 0 total in queue.
...*
컨트롤 플레인 노드 정보 확인 : kubeadm-config configmap
여기서 보는 kubeadm-config는 워크로드 클러스터 내부에 “kubeadm이 클러스터를 어떤 설정으로 부트스트랩했는지”가 담긴 핵심입니다.
특히 controlPlaneEndpoint가 LB(HAProxy)로 잡혀 있고, dnsDomain / podSubnet / serviceSubnet이 Cluster spec과 맞게 들어갔는지 확인할 수 있습니다.
# ct node 확인
k8s1 describe node -l node-role.kubernetes.io/control-plane
**k8s1 get node -owide -l node-role.kubernetes.io/control-plane**
*NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
capi-quickstart-j9fdm-27w2s Ready control-plane 96m v1.34.3 192.168.97.8 <none> Debian GNU/Linux 12 (bookworm) 6.17.4-orbstack-00308-g195e9689a04f containerd://2.2.0
capi-quickstart-j9fdm-6zg8v Ready control-plane 96m v1.34.3 192.168.97.9 <none> Debian GNU/Linux 12 (bookworm) 6.17.4-orbstack-00308-g195e9689a04f containerd://2.2.0
capi-quickstart-j9fdm-ggm9z Ready control-plane 97m v1.34.3 192.168.97.4 <none> Debian GNU/Linux 12 (bookworm) 6.17.4-orbstack-00308-g195e9689a04f containerd://2.2.0*
# kube-system 네임스페이스에 주요 파드 확인
**k8s1 get pod -n kube-system**
*NAME READY STATUS RESTARTS AGE
etcd-capi-quickstart-j9fdm-27w2s 1/1 Running 0 98m
etcd-capi-quickstart-j9fdm-6zg8v 1/1 Running 0 98m
etcd-capi-quickstart-j9fdm-ggm9z 1/1 Running 0 99m
kube-apiserver-capi-quickstart-j9fdm-27w2s 1/1 Running 0 98m
kube-apiserver-capi-quickstart-j9fdm-6zg8v 1/1 Running 0 98m
kube-apiserver-capi-quickstart-j9fdm-ggm9z 1/1 Running 0 99m
kube-controller-manager-capi-quickstart-j9fdm-27w2s 1/1 Running 0 98m
kube-proxy-mn8fg 1/1 Running 0 99m
kube-scheduler-capi-quickstart-j9fdm-6zg8v 1/1 Running 0 98m
...(생략)*
#
**k8s1 get cm,secret,csr -n kube-system**
*NAME DATA AGE
configmap/kubeadm-config 1 101m
configmap/kubelet-config 1 101m
configmap/kube-proxy 2 101m
...*
# kubeadm-config configmap 확인
**k8s1 get cm -n kube-system kubeadm-config -o yaml**
*data:
ClusterConfiguration: |
**apiServer:
certSANs:**
- localhost
- 127.0.0.1
- **0.0.0.0**
- host.docker.internal
apiVersion: kubeadm.k8s.io/v1beta4
caCertificateValidityPeriod: 87600h0m0s
certificateValidityPeriod: 8760h0m0s
**certificatesDir: /etc/kubernetes/pki**
**clusterName: capi-quickstart**
controlPlaneEndpoint: 192.168.97.3:6443
controllerManager: {}
dns: {}
encryptionAlgorithm: RSA-2048
**etcd:
local:
dataDir: /var/lib/etcd**
**featureGates:
ControlPlaneKubeletLocalMode: true**
imageRepository: registry.k8s.io
kind: ClusterConfiguration
kubernetesVersion: v1.34.3
networking:
**dnsDomain: myk8s-1.local**
podSubnet: 10.10.0.0/16
serviceSubnet: 10.20.0.0/16
...*
# featureGates: ControlPlaneKubeletLocalMode: true 의미
## 목적 : 컨트롤 플레인 초기화/재기동 시에 'API 서버 살아있어야 kubelet이 컨트롤플레인 파드를 관리하는' 닭-달걀 문제를 완화
## 기본 kubeadm 동작 : kubelet ──(API Server 필요)──> control plane pod 관리
## feature gate 사용 : kubelet ──(로컬 파일만 보고)──> control plane pod 기동
# 컨트롤 플레인 역할 컨테이너 이름 변수 지정
docker ps
**CTR1=capi-quickstart-j9fdm-27w2s**
docker exec -i $CTR1 sh -c "apt update ; apt install tree psmisc -y"
docker exec -i $CTR1 pstree -a
docker exec -i $CTR1 crictl ps
docker exec -i $CTR1 kubeadm certs check-expiration
docker exec -i $CTR1 tree /etc/kubernetes
docker exec -i $CTR1 tree /etc/kubernetes/pki
docker exec -i $CTR1 cat /etc/kubernetes/pki/ca.crt | openssl x509 -text -noout
docker exec -i $CTR1 tree /etc/kubernetes/manifests
docker exec -i $CTR1 tree /var/lib/etcd
# apiserver 인증서 **SAN**에 **IP 0.0.0.0** 등록 확인
**docker exec -i $CTR1 cat /etc/kubernetes/pki/apiserver.crt | openssl x509 -text -noout**
*X509v3 Subject Alternative Name:
DNS:capi-quickstart-j9fdm-27w2s, DNS:host.docker.internal, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.myk8s-1.local, DNS:localhost, IP Address:10.20.0.1, IP Address:192.168.97.8, IP Address:192.168.97.3, IP Address:127.0.0.1, **IP Address:0.0.0.0***
# search 도메인 확인
**k8s1 exec -it -n kube-system deploy/kube-ops-view -- cat /etc/resolv.conf**
*search kube-system.svc.**myk8s-1.local** svc.**myk8s-1.local** **myk8s-1.local**
nameserver 10.20.0.10
options ndots:5*
# kubelet-config
**k8s1 get cm -n kube-system kubelet-config -o yaml**
*...
cgroupDriver: systemd
clusterDNS:
- 10.20.0.10
**clusterDomain: myk8s-1.local**
...
staticPodPath: /etc/kubernetes/manifests*
컨트롤 플레인 노드 apiserver, scheduler, kcm, etcd 정보 확인
여기서는 control plane 구성요소가 “정적 파드(manifests) + 로컬 엔드포인트(127.0.0.1) + etcd peer/client URL”로 어떻게 맞물리는지 확인합니다.
또한 리더 선출(leases) 상태를 보면 scheduler/kcm이 어느 노드에서 리더인지도 바로 확인됩니다.
# lease 확인
**k8s1 get lease -n kube-system**
*NAME HOLDER AGE
apiserver-44rjrhdw6sckznxix4juycogvu apiserver-44rjrhdw6sckznxix4juycogvu_47d8d31c-82ab-4a90-883a-3ea42b0cfbac 142m
apiserver-e4xlorhq7a4dk46q43brkzi754 apiserver-e4xlorhq7a4dk46q43brkzi754_658997f4-39e1-4e0a-8ec1-d200b3cfd5be 141m
apiserver-wr3gad63nsg3iq3kowasc6j4nm apiserver-wr3gad63nsg3iq3kowasc6j4nm_2aaf0a47-d6a1-42a7-b9ab-faa436544486 143m
kube-controller-manager capi-quickstart-j9fdm-ggm9z_110208d1-b7c8-4c86-8ec1-17fecbcd985a 143m
kube-scheduler capi-quickstart-j9fdm-ggm9z_3013a56f-d279-4b39-9251-82fc9eb91d7c 143m*
# 컨트롤 플레인 역할 컨테이너 이름 변수 지정
docker ps
**CTR1=capi-quickstart-j9fdm-27w2s**
# kube-apiserver 확인
k8s1 get pod -n kube-system -l component=kube-apiserver
**k8s1 describe pod -n kube-system -l component=kube-apiserver**
*--etcd-servers=https://127.0.0.1:2379
--service-account-issuer=https://kubernetes.default.svc.**myk8s-1.local**
--service-cluster-ip-range=10.20.0.0/16*
# kube-scheduler 확인
k8s1 get pod -n kube-system -l component=kube-scheduler
k8s1 describe pod -n kube-system -l component=kube-scheduler
**docker exec -i $CTR1 cat /etc/kubernetes/scheduler.conf | grep server**
*server: https://192.168.97.8:6443* # server 엔드포인트가 LB(HAProxy)가 아니고, 자신의 CT노드 IP
# kube-controller-manager 확인
k8s1 get pod -n kube-system -l component=kube-controller-manager
**k8s1 describe pod -n kube-system -l component=kube-controller-manager**
*--bind-address=127.0.0.1
--cluster-cidr=10.10.0.0/16
--cluster-name=capi-quickstart
--service-cluster-ip-range=10.20.0.0/16*
# etcd 확인
k8s1 get pod -n kube-system -l component=etcd
**k8s1 describe pod -n kube-system -l component=etcd**
*--advertise-client-urls=https://192.168.97.4:2379
--data-dir=/var/lib/etcd
--**feature-gates=InitialCorruptCheck=true**
--initial-advertise-peer-urls=https://192.168.97.4:2380
--**initial-cluster**=capi-quickstart-j9fdm-ggm9z=https://192.168.97.4:2380
--key-file=/etc/kubernetes/pki/etcd/server.key
--**listen-client-urls=https://127.0.0.1:2379**,https://192.168.97.4:2379
--**listen-metrics-urls=http://127.0.0.1:2381**
--listen-peer-urls=https://192.168.97.4:2380
--**name=capi-quickstart-j9fdm-ggm9z***
# 컨트롤 플레인 역할 컨테이너 Shell 진입 후 etcdctl 확인
docker exec -it $CTR1 bash
-------------------------------------
#
**find / -name etcdctl**
*/run/containerd/io.containerd.runtime.v2.task/k8s.io/dc6d875c747442a06c64cd753f424ce9ab0a035a6f7d849804bbc8f3b470858c/rootfs/usr/local/bin/etcdctl
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/59/fs/usr/local/bin/etcdctl*
**ln -s** */var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/59/fs/usr/local/bin/etcdctl* **/usr/local/bin/**
**etcdctl version**
*etcdctl version: 3.6.5
API version: 3.6*
# etcdctl 환경변수 설정
**export ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt
export ETCDCTL_CERT=/etc/kubernetes/pki/etcd/server.crt
export ETCDCTL_KEY=/etc/kubernetes/pki/etcd/server.key
export ETCDCTL_ENDPOINTS=https://127.0.0.1:2379** # listen-client-urls
# 확인
**etcdctl endpoint status -w table**
*+------------------------+------------------+---------+-----------------+---------+--------+-----------------------+-------+-----------+------------+-----------+------------+--------------------+--------+--------------------------+-------------------+
| ENDPOINT | ID | VERSION | STORAGE VERSION | DB SIZE | IN USE | PERCENTAGE NOT IN USE | QUOTA | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS | DOWNGRADE TARGET VERSION | DOWNGRADE ENABLED |
+------------------------+------------------+---------+-----------------+---------+--------+-----------------------+-------+-----------+------------+-----------+------------+--------------------+--------+--------------------------+-------------------+
| https://127.0.0.1:2379 | 2dcfb333ca0ac2ee | 3.6.5 | 3.6.0 | 6.2 MB | 1.9 MB | 69% | 0 B | false | false | 2 | 28253 | 28253 | | | false |
+------------------------+------------------+---------+-----------------+---------+--------+-----------------------+-------+-----------+------------+-----------+------------+--------------------+--------+--------------------------+-------------------+*
**etcdctl member list -w table**
*+------------------+---------+-----------------------------+---------------------------+---------------------------+------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
+------------------+---------+-----------------------------+---------------------------+---------------------------+------------+
| 2dcfb333ca0ac2ee | started | capi-quickstart-j9fdm-27w2s | https://192.168.97.8:2380 | https://192.168.97.8:2379 | false |
| 83a694a295cb4f15 | started | capi-quickstart-j9fdm-6zg8v | https://192.168.97.9:2380 | https://192.168.97.9:2379 | false |
| e4fa9627a6238971 | started | **capi-quickstart-j9fdm-ggm9z** | https://192.168.97.4:2380 | https://192.168.97.4:2379 | false |
+------------------+---------+-----------------------------+---------------------------+---------------------------+------------+*
# 빠져나오기
**exit**
-------------------------------------
인증서 정보 확인 - Docs
이 파트는 “인증서가 관리 클러스터의 Secret으로 관리되고, 각 노드 부트스트랩 데이터(cloud-init)에 심어져서 워크로드 클러스터가 올라간다”는 연결고리를 보여줍니다.
즉, 관리 클러스터가 워크로드 클러스터의 PKI/Join 토큰/구성 데이터를 ‘생성해서 전달’하고 있다는 증거입니다.
# kind 관리클러스터에서 에서 인증서 정보가 담긴 secret 확인
**k get secret -l cluster.x-k8s.io/cluster-name=capi-quickstart**
*NAME TYPE DATA AGE
capi-quickstart-ca cluster.x-k8s.io/secret 2 3h7m
capi-quickstart-etcd cluster.x-k8s.io/secret 2 3h7m
**capi-quickstart-j9fdm-27w2s** cluster.x-k8s.io/secret 2 3h6m
capi-quickstart-j9fdm-6zg8v cluster.x-k8s.io/secret 2 3h5m
capi-quickstart-j9fdm-ggm9z cluster.x-k8s.io/secret 2 3h7m
**capi-quickstart-kubeconfig** cluster.x-k8s.io/secret 1 3h7m
capi-quickstart-md-0-p7lv8-t7r9t-nhfpb cluster.x-k8s.io/secret 2 3h6m
capi-quickstart-md-0-p7lv8-t7r9t-rmcls cluster.x-k8s.io/secret 2 3h6m
capi-quickstart-md-0-p7lv8-t7r9t-t5ds2 cluster.x-k8s.io/secret 2 3h6m
capi-quickstart-proxy cluster.x-k8s.io/secret 2 3h7m
capi-quickstart-sa cluster.x-k8s.io/secret 2 3h7m*
# 샘플로 시크릿 확인
**k get secret capi-quickstart-kubeconfig -o jsonpath='{.data.value}' | base64 -d**
...
**k get secret capi-quickstart-j9fdm-27w2s -o jsonpath='{.data.value}' | base64 -d**
*## template: jinja
#cloud-config
write_files:
- path: **/etc/kubernetes/pki/ca.crt**
owner: root:root
permissions: '0640'
content: |
-----BEGIN CERTIFICATE-----
MIIC6jCCAdKgAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
cm5ldGVzMB4XDTI2MDIxMTE0MDc0N1oXDTM2MDIwOTE0MTI0N1owFTETMBEGA1UE
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMAj
TBg3y1Jxt6dbmhzZRifW8lGhZK15qxWSg0QgOsxXByCjgUDkAMSmvnEj4+wl7dTW
1Nc6DPugaXn3AGzxGUNnBfCSjLbTVymw+LoNI4DPc4kl9m7R3d2Ju0NHAeWrIeDb
Khf538jIa2I4C1q94ND/kQA6/m5yRJSimP3q3uz0XeM+PE1+dcC4+2q63v3E4h5M
Mh34ZeIRnPbU6m7O8G/wFA7zT18waZuuOWLucZz/BYZpccjkyAc5xkNRAYDTQ4em
wFJSbEmTnCip4+QfCJGvOIHnd07aAR2S8MwKFcyDTNaTnAijUzWuTQUk6CDvA+qN
sDgm7+Jxdn5E3In06pkCAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgKkMBIGA1UdEwEB
/wQIMAYBAf8CAQAwHQYDVR0OBBYEFH/MTvmCIYIiFqX5DID/PbxKBaDiMA0GCSqG
SIb3DQEBCwUAA4IBAQBhve6GnsR/bEsxqQbexW2AzElY8n4TnzI85rogQB3ukS5r
9ZB6on19MndZMnPbQDUb+2A82PV/A1QWKYgjK9/RjvUppKl5ruM9xiK5huFSnvgs
omVMAgAMnpZHBYRnVpc6QpAwVz+vedMlHx2Zrwxi8cquJIOIAkaAjQUfQJUsF/XW
zrYdgaY2It/cIJ6wJehWWWbveVBImw31m2hx02XWDk70rF03vl6Jvb7gg0uEjX78
rbkM+VH637cdbhT9+Y1QhNvTgGap9BFOrZCKIuJ2oCdZngpPFp7ilyI9W+6u+Lng
pGJSfIkiGw/buODhO9aW63pmLI09fU49/XBgXJi1
-----END CERTIFICATE-----
- path: /etc/kubernetes/pki/ca.key
owner: root:root
permissions: '0600'
content: |
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAwCNMGDfLUnG3p1uaHNlGJ9byUaFkrXmrFZKDRCA6zFcHIKOB
QOQAxKa+cSPj7CXt1NbU1zoM+6BpefcAbPEZQ2cF8JKMttNXKbD4ug0jgM9ziSX2
btHd3Ym7Q0cB5ash4NsqF/nfyMhrYjgLWr3g0P+RADr+bnJElKKY/ere7PRd4z48
TX51wLj7arre/cTiHkwyHfhl4hGc9tTqbs7wb/AUDvNPXzBpm645Yu5xnP8Fhmlx
yOTIBznGQ1EBgNNDh6bAUlJsSZOcKKnj5B8Ika84ged3TtoBHZLwzAoVzINM1pOc
CKNTNa5NBSToIO8D6o2wOCbv4nF2fkTcifTqmQIDAQABAoH/cDQsu/fZRMwj9BgK
Z856qclcuU8G/EeRIYfuIFqx+6LXBrh1Qu/jgvPdQzUyZTXBLgpHQWklK80By7fz
B6vElRgU3+i1RA8nV7GBjyUdFpwPXIhO4WitqDJGqYNOGFFX93gFnPrPNPHVxAX2
m4dHQsX8Z6YL73PLmQFbVu1SQg1ulIKm5u8xPoGXjxUsX6KJ/8FEKZOz37eH2mu7
xVd0yaZ2UVC7+9cG58PNewxGBsAvMkAY+SREBXT0tWz1D7kkW1o5gB9JPBmqqhlC
UIxki8S83oci5htdjPBya2bNxNcAbdiqz7cD8TqX+AgtfjincJi67815EmooujX0
LD5ZAoGBAMm/guO9Zg8Eg9QGV7tjV0MZmQCmdT9LFi07eijVF+g+McJyeNPjJUjZ
Hrd2I4UXSjxu7qH9Zehfe3ijX/RiLww++TDZ1VJYOnt5OVKPsSidjT1FL0lUAnmG
qLcwzpHZWtTNC36hw/91roFxo5daru1WG105d2B7XbGxElz671RlAoGBAPPONj1Q
lvSOfVqm3uN2y/ONL74F6Gl031h+pfPJiGi41rhooccsSnerzMv+LBgPgsQLmtf5
UfFbKV2b3RMinQShux60ryAuVQP6VHHMoAjjlssOB8ukzoPlKZZRAOmLUD8Rf2NW
zoAAprCyHOP+7n+db3blzNNBmPA5TLrVv1glAoGBAKHAKfDtqSXhONCFRVG4E/54
R3N+AgL88IrTZN3X+5L4Smzb2oQHWZ1OdpIg5dmPErXhOvIld88Wvqe6VPRaw93N
n3zLKX0bv8e/KXZIPoGRz+uPzJm+AT3t+NbnCrdkzK5QtyaQC9SFRaQCuGZhDl66
6rWTnWJ9hmEw1sg1aC0BAoGBAOXytI2YGQiH7As4IDkBrtMDcugSwmXUaWMZ8IPC
rPm4fCxp40vrpkDAtOQh0ozO9FLbbywMZxDxHk6/1v9ZQidAMzB+0j3T9TNPQ54h
lT9NCMhzbz7PLkiQN20i8W1UjFcvtaqIETQBaTZ2h3Ey3NdYMe0+SLVnxUutf4Uo
XNRRAoGAK/+G+mG2DtTYuLZY/DYrn6a6iVa5WQJzvdKIOs4JZXVQQx2xGi68pC+u
Ywmxswntx4PsAHmEV8odFed74eer7daJTLI5mAISeR9/dqv9NT7o+2x9Z0v9lGQW
mCB/I70hN78ZevLxlx7iNdTxXZmOGEsMPcBkJ9C14WNYc9B1cf8=
-----END RSA PRIVATE KEY-----
...
path: **/run/kubeadm/kubeadm-join-config.yaml**
owner: root:root
permissions: '0640'
content: |
apiVersion: kubeadm.k8s.io/v1beta4
controlPlane:
localAPIEndpoint: {}
**discovery:
bootstrapToken:**
apiServerEndpoint: 192.168.97.3:6443
caCertHashes:
- sha256:045aeb6d64f09db6043f269ab972d1cf3898173fe3251520714748cc0b9061f1
token: n44tp7.jdwzblvaqldkyvuw
**kind: JoinConfiguration**
nodeRegistration:
kubeletExtraArgs:
- name: eviction-hard
value: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%
taints: null
- path: /run/cluster-api/placeholder
owner: root:root
permissions: '0640'
content: "This placeholder file is used to create the /run/cluster-api sub directory in a way that is compatible with both Linux and Windows (mkdir -p /run/cluster-api does not work with Windows)"
**runcmd:
- kubeadm join --config /run/kubeadm/kubeadm-join-config.yaml && echo success > /run/cluster-api/bootstrap-success.complete***
첫 번째 워크로드 클러스터에 워커 노드 3대 확인
워커 노드 관점에서는 “kubelet이 어디로 붙는지(=LB), containerd 설정이 어떤 형태인지”를 보면, CAPD 환경이 실제로 VM과 유사한 형태로 동작하고 있음을 확인할 수 있습니다.
# 호스트에서 컨테이너 정보 확인 : LB 컨테이너 1대, CT 컨테이너 3대, WK 컨테이너 3대, 마지막 1대는 최초 kind로 구성한 관리형 k8s
**docker ps**
*CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[**Controlplane** node] fdf81c4e850a kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour 127.0.0.1:32775->6443/tcp capi-quickstart-j9fdm-6zg8v
**[Controlplane** node] 6fd07a6a4028 kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour 127.0.0.1:32774->6443/tcp capi-quickstart-j9fdm-27w2s
[**Worker** node] 439466b27b34 kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour capi-quickstart-md-0-p7lv8-t7r9t-nhfpb
[**Worker** node] 6f700f8ee747 kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour capi-quickstart-md-0-p7lv8-t7r9t-rmcls
[**Worker** node] 12b85d4f4bab kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour capi-quickstart-md-0-p7lv8-t7r9t-t5ds2
[**Controlplane** node] a5507e114f4b kindest/node:v1.34.3 "/usr/local/bin/entr…" About an hour ago Up About an hour 127.0.0.1:32773->6443/tcp capi-quickstart-j9fdm-ggm9z
[CTRL **Loadbalancer**] d3e002a0c9bc kindest/haproxy:v20230606-42a2262b "haproxy -W -db -f /…" About an hour ago Up About an hour 0.0.0.0:32770->6443/tcp, 0.0.0.0:32771->8404/tcp capi-quickstart-lb
[kind k8s] 5f01ea7d7bd5 kindest/node:v1.35.0 "/usr/local/bin/entr…" 2 hours ago Up 2 hours 0.0.0.0:30000-30001->30000-30001/tcp, 127.0.0.1:54601->6443/tcp myk8s-control-plane*
# 워커 역할 컨테이너 이름 변수 지정
docker ps
WK1=*capi-quickstart-md-0-p7lv8-t7r9t-nhfpb*
****docker exec -i $WK1 sh -c "apt update ; apt install tree psmisc -y"
docker exec -i $WK1 pstree -a
docker exec -i $WK1 crictl ps
docker exec -i $WK1 tree /etc/kubernetes
docker exec -i $WK1 tree /var/lib/kubelet
# kubelet(client) -> [HAProxy] -> apiserver 파드들 호출 확인
**docker exec -i $WK1 cat /etc/kubernetes/kubelet.conf | grep server**
*server: https://**192.168.97.3**:6443*
# args 조사해두자
**docker exec -i $WK1 cat /var/lib/kubelet/kubeadm-flags.env**
*KUBELET_KUBEADM_ARGS="--cgroup-root=/kubelet --eviction-hard=nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% --fail-swap-on=false --image-gc-high-threshold=100 --pod-infra-container-image=registry.k8s.io/pause:3.10.1 --register-with-taints=node.cluster.x-k8s.io/uninitialized:NoSchedule --runtime-cgroups=/system.slice/containerd.service"*
**docker exec -i $WK1 cat /var/lib/kubelet/instance-config.yaml**
*containerRuntimeEndpoint: "unix:///var/run/containerd/containerd.sock"*
docker exec -i $WK1 systemctl status kubelet --no-pager
docker exec -i $WK1 cat /var/lib/kubelet/config.yaml
****docker exec -i $WK1 systemctl status containerd --no-pager
docker exec -i $WK1 cat /etc/containerd/cri-base.json | jq
**docker exec -i $WK1 containerd --version**
*containerd github.com/containerd/containerd/v2 v2.2.0*
**docker exec -i $WK1 cat /etc/containerd/config.toml**
# explicitly use v2 config format
**version = 2** # containerd 가 2.x 인데, config 는 아직 v2 사용 중
[proxy_plugins]
# fuse-overlayfs is used for rootless
[proxy_plugins."fuse-overlayfs"]
type = "snapshot"
address = "/run/containerd-fuse-overlayfs.sock"
[plugins."io.containerd.grpc.v1.cri".containerd]
# save disk space when using a single snapshotter
discard_unpacked_layers = true
# explicitly use default snapshotter so we can sed it in entrypoint
**snapshotter = "overlayfs"**
# explicit default here, as we're configuring it below
**default_runtime_name = "runc"**
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
# set default runtime handler to v2, which has a per-pod shim
runtime_type = "io.containerd.runc.v2"
# Generated by "ctr oci spec" and modified at base container to mount poduct_uuid
**base_runtime_spec = "/etc/containerd/cri-base.json"**
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
# use systemd cgroup by default
**SystemdCgroup = true**
# Setup a runtime with the magic name ("test-handler") used for Kubernetes
# runtime class tests ...
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.test-handler]
# same settings as runc
runtime_type = "io.containerd.runc.v2"
**base_runtime_spec = "/etc/containerd/cri-base.json"**
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.test-handler.options]
**SystemdCgroup = true**
[plugins."io.containerd.grpc.v1.cri"]
# use fixed sandbox image
**sandbox_image = "registry.k8s.io/pause:3.10"**
# allow hugepages controller to be missing
# see https://github.com/containerd/cri/pull/1501
tolerate_missing_hugepages_controller = true
# restrict_oom_score_adj needs to be true when running inside UserNS (rootless)
restrict_oom_score_adj = false
이 파트는 “Managed Topology(ClusterClass 기반) 환경에서 운영자가 무엇을 건드려야 하고, 무엇을 건드리면 원복되는지”를 체감하는 구간입니다.
결론부터 말하면, KubeadmControlPlane / MachineDeployment를 직접 만지기보다 Cluster.spec.topology를 패치하는 것이 정석입니다.
워크로드 클러스터 업그레이드
모니터링
kube-ops-view # k8s1 -n kube-system port-forward svc/kube-ops-view 8080:8080 &
haproxy web stats
kubectl --kubeconfig=capi-quickstart.kubeconfig get node -w
watch -d kubectl --kubeconfig=capi-quickstart.kubeconfig get node
**watch -d "docker ps ; echo ; clusterctl describe cluster capi-quickstart"**
업그레이드 수행
# 업그레이드 실행 : ClusterClass / Topology 사용 중이므로 KubeadmControlPlane, MachineDeployment 를 직접 수정 안됨.
## 컨트롤 플레인 노드 1대씩 완료 -> 워커 노드
kubectl get cluster
**kubectl patch cluster capi-quickstart --type merge -p '{"spec":{"topology":{"version":"v1.35.0"}}}'**
## (참고) kube-ops-view 포트 포워딩 exit 될 경우 다시 실행
**k8s1 -n kube-system port-forward svc/kube-ops-view 8080:8080 &**
# 확인 : 맨 아래 노드는 제거 중
**k8s1 get node**
*NAME STATUS ROLES AGE VERSION
capi-quickstart-74kjs-ncktx Ready control-plane 3m44s **v1.35.0**
capi-quickstart-74kjs-npnjr Ready control-plane 2m41s v1.35.0
capi-quickstart-74kjs-tdd8d Ready control-plane 2m5s v1.35.0
capi-quickstart-md-0-npmnz-x7b2z-892h8 Ready <none> 101s **v1.35.0**
capi-quickstart-md-0-npmnz-x7b2z-fszrs Ready <none> 56s v1.35.0
capi-quickstart-md-0-npmnz-x7b2z-lksqj Ready <none> 13s v1.35.0
capi-quickstart-md-0-npmnz-x9jx6-qx8rf Ready,SchedulingDisabled <none> 7m48s v1.34.3*
# 신규 버전의 머신(컨테이너)이 생성됨을 확인
**docker ps**
*CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9a79e524c198 kindest/node:v1.35.0 "/usr/local/bin/entr…" About a minute ago Up About a minute capi-quickstart-md-0-npmnz-x7b2z-lksqj
f5658e9d5385 kindest/node:v1.35.0 "/usr/local/bin/entr…" 2 minutes ago Up 2 minutes capi-quickstart-md-0-npmnz-x7b2z-fszrs
1a71ef5e651a kindest/node:v1.35.0 "/usr/local/bin/entr…" 2 minutes ago Up 2 minutes capi-quickstart-md-0-npmnz-x7b2z-892h8
3baae4af04d4 kindest/node:v1.35.0 "/usr/local/bin/entr…" 3 minutes ago Up 3 minutes 127.0.0.1:32778->6443/tcp capi-quickstart-74kjs-tdd8d
5e85d57bd948 kindest/node:v1.35.0 "/usr/local/bin/entr…" 4 minutes ago Up 4 minutes 127.0.0.1:32777->6443/tcp capi-quickstart-74kjs-npnjr
e7742cd044b3 kindest/node:v1.35.0 "/usr/local/bin/entr…" 5 minutes ago Up 5 minutes 127.0.0.1:32776->6443/tcp capi-quickstart-74kjs-ncktx
c125003ec5d7 kindest/haproxy:v20230606-42a2262b "haproxy -W -db -f /…" 9 minutes ago Up 9 minutes 0.0.0.0:32770->6443/tcp, 0.0.0.0:32771->8404/tcp capi-quickstart-lb
6419a7cf30ce kindest/node:v1.35.0 "/usr/local/bin/entr…" 11 minutes ago Up 11 minutes 0.0.0.0:30000-30001->30000-30001/tcp, 127.0.0.1:51597->6443/tcp myk8s-control-plane*
LB 확인
업그레이드 중/후에 HAProxy 백엔드 목록이 새 컨트롤플레인 노드들로 갱신되는지 확인합니다.
# 컨테이너 내부에 설정 파일을 호스트에 복사하여 가져오기
**docker cp capi-quickstart-lb:/usr/local/etc/haproxy/haproxy.cfg .**
# 설정 파일 확인 : 아래 백엔드 서버에 목록 업데이트 확인!
**cat haproxy.cfg**
...
*backend kube-apiservers
option httpchk GET /healthz
server capi-quickstart-hvt8l-fwxnz 192.168.97.10:6443 weight 100 check check-ssl verify none resolvers docker resolve-prefer ipv4
server capi-quickstart-hvt8l-gjztc 192.168.97.9:6443 weight 100 check check-ssl verify none resolvers docker resolve-prefer ipv4
server capi-quickstart-hvt8l-hb4hb 192.168.97.4:6443 weight 100 check check-ssl verify none resolvers docker resolve-prefer ipv4
server capi-quickstart-hvt8l-hznkv 192.168.97.8:6443 weight 100 check check-ssl verify none resolvers docker resolve-prefer ipv4*
워크로드 클러스터 노드(워커) 확장 → 축소
컨트롤 플레인 노드 확장 기능 지원은 없음 - Docs
이 섹션에서 얻는 핵심 교훈은 다음입니다.
- “Managed Topology 사용 중”이면 MachineDeployment를 직접 scale해도 다시 원복될 수 있음
- 정석은 Cluster.spec.topology.workers 를 패치해서 desired state를 바꾸는 것
kube-ops-view # k8s1 -n kube-system port-forward svc/kube-ops-view 8080:8080 &
haproxy web stats
kubectl --kubeconfig=capi-quickstart.kubeconfig get node -w
watch -d kubectl --kubeconfig=capi-quickstart.kubeconfig get node
watch -d "docker ps ; echo ; clusterctl describe cluster capi-quickstart"
**watch -d "docker ps ; echo ; kubectl get machines"**
# 업그레이드 실행 : ClusterClass / Topology 사용 중이므로 MachineDeployment를 직접 수정 안됨.
~~# MachineSet 및 MachineDeployment 수정해도 다시 원복됨 ex) kubectl scale machinedeployment foo --replicas=5.~~
kubectl get cluster
**kubectl patch cluster capi-quickstart --type merge -p '{
"spec": {
"topology": {
"workers": {
"machineDeployments": [
{
"class": "default-worker",
"name": "md-0",
"replicas": 5
}
]
}
}
}
}'**
# 확인
**kubectl get cluster**
*NAME CLUSTERCLASS AVAILABLE CP DESIRED CP AVAILABLE CP UP-TO-DATE **W DESIRED W AVAILABLE W UP-TO-DATE** PHASE AGE VERSION
capi-quickstart quick-start True 3 3 3 **5 5 5** Provisioned 21m v1.35.0*
**k8s1 get node**
*NAME STATUS ROLES AGE VERSION
capi-quickstart-74kjs-ncktx Ready control-plane 17m v1.35.0
capi-quickstart-74kjs-npnjr Ready control-plane 16m v1.35.0
capi-quickstart-74kjs-tdd8d Ready control-plane 15m v1.35.0
capi-quickstart-md-0-npmnz-x7b2z-4wnvf Ready <none> 2m37s v1.35.0
capi-quickstart-md-0-npmnz-x7b2z-892h8 Ready <none> 15m v1.35.0
capi-quickstart-md-0-npmnz-x7b2z-fszrs Ready <none> 14m v1.35.0
capi-quickstart-md-0-npmnz-x7b2z-lksqj Ready <none> 14m v1.35.0
capi-quickstart-md-0-npmnz-x7b2z-sq265 Ready <none> 2m37s v1.35.0*
# 다시 3대로 축소
**kubectl patch cluster capi-quickstart --type merge -p '{
"spec": {
"topology": {
"workers": {
"machineDeployments": [
{
"class": "default-worker",
"name": "md-0",
"replicas": 3
}
]
}
}
}
}'**
kubectl get cluster
Add a MachineDeployment : 머신디플로이먼트 자체 추가 - Docs
“새로운 워커 그룹을 추가”하는 시나리오입니다. 운영 관점에서는 “워크로드 성격별로 서로 다른 MachineDeployment”를 두는 형태(예: batch 전용/웹 전용)를 떠올리면 이해가 빠릅니다.
# 현재 워크로드 클러스터에 워커노드 머신디플로이먼트 확인
**kubectl get cluster capi-quickstart -o jsonpath='{.spec.topology.workers}' | jq**
*{
"machineDeployments": [
{
**"class": "default-worker",
"name": "md-0",
"replicas": 3**
}
]
}*
# second-deployment 이름의 머신디플로이먼트 추가
**kubectl patch cluster capi-quickstart --type json --patch '[{"op": "add", "path": "/spec/topology/workers/machineDeployments/-", "value": {"name": "second-deployment", "replicas": 1, "class": "default-worker"} }]'**
# 확인
**kubectl --kubeconfig=capi-quickstart.kubeconfig get nodes -owide**
**kubectl get cluster capi-quickstart -o jsonpath='{.spec.topology.workers}' | jq**
*{
"machineDeployments": [
{
"class": "default-worker",
"name": "md-0",
"replicas": 3
},
{
**"class": "default-worker",
"name": "second-deployment",
"replicas": 1**
}
]
}*
워크로드 클러스터 컨트롤 플레인 노드 Scale : - Docs
컨트롤플레인도 결국 “Cluster topology에서 replicas를 조정”하는 방식으로 확장합니다.
(앞의 워커 확장과 동일하게 “원하는 상태는 Cluster에 선언”하고, 컨트롤러가 실제 머신 증설/조인을 수행합니다.)
# 현재 워크로드 클러스터에 컨트롤플레인 정보 확인
**kubectl get cluster capi-quickstart -o jsonpath='{.spec.topology.controlPlane}' | jq**
*{
**"replicas": 3**
}*
# 기존 3대에서 -> 5대로 업데이트
**kubectl patch cluster capi-quickstart --type json --patch '[{"op": "replace", "path": "/spec/topology/controlPlane/replicas", "value": 5}]'**
# 확인
**kubectl --kubeconfig=capi-quickstart.kubeconfig get nodes -owide**
**kubectl get cluster capi-quickstart -o jsonpath='{.spec.topology.controlPlane}' | jq**
*{
"replicas": 5
}*
아래 실패 메시지는 “연쇄 업그레이드(중간 마이너 버전 자동 단계)”를 쓰려면 추가 설정/기능 활성화가 필요하다는 힌트를 줍니다.
(즉, 단순히 topology version을 크게 점프시키는 것이 항상 허용되는 것은 아닙니다.)
# 1.33.7 워크로드 클러스터 생성
clusterctl generate cluster **capi-quickstart** --flavor **development** \
--kubernetes-version **v1.33.7** \
--**control-plane**-machine-count=**3** \
--**worker**-machine-count=**3** | **kubectl apply -f -**
# calico cni 플러그인 설치
clusterctl get kubeconfig capi-quickstart > capi-quickstart.kubeconfig
**kubectl --kubeconfig=capi-quickstart.kubeconfig apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml**
kubectl --kubeconfig=capi-quickstart.kubeconfig get nodes -owide
# kube-ops-view 설치
**helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 \
--set env.TZ="Asia/Seoul" --namespace kube-system --kubeconfig=capi-quickstart.kubeconfig**
**kubectl --kubeconfig=capi-quickstart.kubeconfig -n kube-system port-forward svc/kube-ops-view 8080:8080 &**
open "http://127.0.0.1:8080/#scale=1.5" # 웹 접속
# 모니터링
kube-ops-view # kubectl --kubeconfig=capi-quickstart.kubeconfig -n kube-system port-forward svc/kube-ops-view 8080:8080 &
haproxy web stats
kubectl --kubeconfig=capi-quickstart.kubeconfig get node -w
# 업그레이드 실행 : ***실패! -> 아마도 자동 연쇄 업그레이드를 위한 설정이 필요한 것으로 보임.***
kubectl get cluster
**kubectl patch cluster capi-quickstart --type merge -p '{"spec":{"topology":{"version":"v1.35.0"}}}'**
***The Cluster "capi-quickstart" is invalid: spec.topology.version: Invalid value: "v1.35.0": version cannot be increased from "1.33.7" to "1.35.0"***
삭제
마지막 정리는 “Cluster API 관점으로 리소스를 지우면, 도커 컨테이너(노드)까지 정리되는지”를 확인하는 단계입니다.
- 첫 번째 워크로드 클러스터(머신 포함) 삭제 kubectl delete cluster capi-quickstart && docker ps
- kind 삭제 kind delete cluster --name myk8s
마무리하며
본 문서의 목적은 절차를 나열하는 것이 아니라, 설치 이후 운영자가 확인해야 하는 구조와 산출물을 기준으로 RKE2와 Cluster API의 동작을 검증하는 데 있습니다.
- RKE2에서는 런타임 이미지 기반 부트스트랩, /var/lib/rancher/rke2 중심의 디렉터리 구조, 정적 Pod 기반 컨트롤 플레인, Helm Controller 기반 애드온 적용, 인증서 교체, 업그레이드, 고가용성 구성을 하나의 흐름으로 정리하였습니다.
- Cluster API에서는 CRD 선언에 따라 컨트롤러가 인프라와 부트스트랩, 제어 평면을 조합하여 목표 상태로 수렴하는 모델을 확인하였고, Managed Topology의 템플릿 처리 방식과 Machine 계층 및 ControlPlane 리소스의 역할 분리를 실습 결과로 검증하였습니다.
추가 확장 과제는 다음과 같습니다.
- RKE2: 프라이빗 레지스트리 및 미러 구성, 에어갭 운영, etcd 백업 및 복구, CIS 프로파일 적용 및 검증 자동화, Rancher 연동 운영
- CAPI: vSphere, AWS, Azure, Metal3 등 인프라 프로바이더 확장, GitOps 기반 템플릿 관리, 업그레이드 정책과 채널 전략 표준화
운영 리스크는 기능 부족보다 리소스가 생성되고 수렴되는 순서와 의존관계를 명확히 이해하지 못할 때 증가합니다. 본 문서는 해당 모델을 RKE2와 CAPI 관점에서 문서화하고 검증 가능한 형태로 정리하였습니다.