3·19강 · 노드 조인 · CNI · CRI¶
영상: 3강 내 클러스터 만들기 (kubeadm init/join · CNI) · 19강 노드 안을 열어 보기 (crictl · kubelet · CRI 설치) · 이어서 풀 채점 세트:
03-cluster-setup
실습 목표¶
- 노드 준비(swap · 커널 모듈 · sysctl · containerd cgroup · apt hold)가 실제로 되어 있는지 명령으로 확인한다.
kubeadm init이 어떤 단계(phase) 를 거쳐 어떤 파일을 남겼는지 찾아본다.- 조인 토큰과 discovery 해시가 무엇을 증명하는지 이해하고, 워커를 빼고 다시 넣어 본다.
- kubectl 없이 crictl 로 컨테이너 · 파드 · 로그 · 이미지를 본다.
- CNI 설정과 바이너리가 어디에 있는지, CNI 가 없으면 노드와 파드가 어떻게 되는지 확인한다.
- kubelet 이 어떤 파일들로 설정되는지(systemd 드롭인 · config.yaml · flags.env)와 로그 보는 법을 익힌다.
- 문서의 CNI 매니페스트를 내려받아 cidr 를 클러스터 값으로 고친다.
- CRI 패키지(cri-dockerd)를 dpkg 로 설치 · enable --now · sysctl 영구 설정까지 해 본다.
0) 준비¶
이 페이지는 kubectl 보다 노드 안에서 치는 명령이 많습니다. 터미널을 세 개 열어 두면 편합니다.
| 터미널 | 접속 | 주로 하는 일 |
|---|---|---|
| ① | control-plane (192.168.56.10) |
kubectl · kubeadm · crictl |
| ② | worker-1 (192.168.56.11) |
5) 빼고 다시 넣기 |
| ③ | worker-2 (192.168.56.12) |
6) crictl · 7) CNI 실험 · 9) CRI 패키지 |
# 내 노트북(호스트)에서 — 사용자 이름은 VM 을 만들 때 정한 것으로
ssh <사용자>@192.168.56.10
ssh <사용자>@192.168.56.11
ssh <사용자>@192.168.56.12
모든 명령 블록 첫 줄에 어느 노드에서 치는지 주석을 달아 두었습니다. 프롬프트를 보고 치세요.
# control-plane 에서
kubectl create namespace guide-cluster
mkdir -p ~/guide-cluster
kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP CONTAINER-RUNTIME
control-plane Ready control-plane 3d v1.32.x 192.168.56.10 containerd://1.7.x
worker-1 Ready <none> 3d v1.32.x 192.168.56.11 containerd://1.7.x
worker-2 Ready <none> 3d v1.32.x 192.168.56.12 containerd://1.7.x
5) 와 7) 은 노드를 일부러 망가뜨립니다
시작 전에 VirtualBox 에서 세 VM 모두의 스냅샷을 찍어 두세요(04번 영상의 clean-cluster).
한 대만 되돌리면 클러스터 상태가 어긋나므로, 되돌릴 때도 세 대를 함께 되돌립니다.
시험 팁 — 노드에 들어가는 법
시험 지문에 ssh <노드> 가 주어집니다. 들어가면 프롬프트가 바뀌는지 먼저 보고, root 권한이 필요하면 sudo -i 로 전환합니다.
작업이 끝나면 반드시 exit 로 나옵니다. 노드 안에서는 kubectl 이 안 되거나 다른 클러스터를 가리킬 수 있습니다.
1) 노드 준비 — "설치했다" 가 아니라 "되어 있다" 를 확인한다¶
kubeadm 은 노드 준비를 대신 해 주지 않습니다. swap · 커널 · 런타임 · 패키지 네 가지가 어긋나면 init 이나 join 이 실패하거나, 더 나쁘게는 성공한 뒤에 이상하게 동작합니다. 세 노드 모두에서 같은 확인을 합니다.
# 모든 노드에서 (control-plane · worker-1 · worker-2)
swapon --show # 아무것도 안 나와야 한다
lsmod | grep -E '^(overlay|br_netfilter)'
sysctl net.bridge.bridge-nf-call-iptables net.ipv4.ip_forward
grep -n 'SystemdCgroup' /etc/containerd/config.toml
apt-mark showhold
systemctl is-active containerd kubelet
← swapon: 빈 출력 = swap 꺼짐
br_netfilter 32768 0
overlay 212992 12
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
139: SystemdCgroup = true
kubeadm
kubectl
kubelet
active
active
| 확인 항목 | 틀리면 생기는 일 |
|---|---|
| swap 꺼짐 | kubelet 이 뜨지 않는다 (preflight 에서 [ERROR Swap]) |
br_netfilter · ip_forward |
파드끼리 · 노드 밖으로 통신이 안 된다 |
SystemdCgroup = true |
kubelet 과 런타임의 cgroup 드라이버가 달라 파드가 불안정하거나 ContainerCreating 에서 멈춘다 |
apt-mark hold |
apt upgrade 한 번에 kubelet 버전이 멋대로 올라간다 |
관찰 포인트: modprobe 와 sysctl -w 는 재부팅하면 사라집니다. 영구 설정은 파일에 있어야 합니다.
시험 팁 — \"영구적으로(persistently)\" 가 붙으면 파일이다
| 지금만 | 재부팅 뒤에도 |
|---|---|
sudo modprobe br_netfilter |
/etc/modules-load.d/<이름>.conf 에 모듈 이름 한 줄 |
sudo sysctl -w net.ipv4.ip_forward=1 |
/etc/sysctl.d/<이름>.conf 에 쓰고 sudo sysctl --system |
sudo systemctl start <서비스> |
sudo systemctl enable --now <서비스> |
.deb 파일을 주고 설치 후 서비스로 띄우라는 문제도 나옵니다: sudo dpkg -i <파일>.deb → sudo systemctl enable --now <서비스> → systemctl status <서비스>. 9) 에서 cri-dockerd 로 직접 해 봅니다.
대조 실험 — preflight 는 무엇을 검사하나¶
이미 클러스터가 있는 control-plane 에서 preflight 단계만 돌려 보면, kubeadm 이 무엇을 검사하는지 목록으로 보입니다. 검사만 하고 아무것도 바꾸지 않습니다.
[preflight] Running pre-flight checks
error execution phase preflight: [preflight] Some fatal errors occurred:
[ERROR Port-6443]: Port 6443 is in use
[ERROR Port-10259]: Port 10259 is in use
[ERROR Port-10257]: Port 10257 is in use
[ERROR FileAvailable--etc-kubernetes-manifests-kube-apiserver.yaml]: /etc/kubernetes/manifests/kube-apiserver.yaml already exists
...
[ERROR Port-10250]: Port 10250 is in use
[ERROR Port-2379]: Port 2379 is in use
[ERROR DirAvailable--var-lib-etcd]: /var/lib/etcd is not empty
[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`
관찰 포인트: 에러 목록이 곧 init 이 만들 것들의 목록입니다. 6443(API 서버) · 10259(스케줄러) · 10257(컨트롤러 매니저) · 10250(kubelet) · 2379/2380(etcd) 포트와 매니페스트 파일, etcd 데이터 디렉터리.
2) kubeadm init 이 남긴 것 — 단계(phase)와 파일¶
kubeadm init 은 한 덩어리 명령처럼 보이지만 실제로는 정해진 단계를 차례로 실행합니다. 단계 목록은 도움말에 있습니다.
The "init" command executes the following phases:
preflight Run pre-flight checks
certs Certificate generation
/ca Generate the self-signed Kubernetes CA ...
/apiserver ...
...
kubeconfig Generate all kubeconfig files necessary to establish the control plane and the admin kubeconfig file
...
etcd Generate static Pod manifest file for local etcd
control-plane Generate all static Pod manifest files necessary to establish the control plane
kubelet-start Write kubelet settings and (re)start the kubelet
wait-control-plane Wait for the control plane to start
upload-config Upload the kubeadm and kubelet configuration to a ConfigMap
upload-certs Upload certificates to kubeadm-certs
mark-control-plane Mark a node as a control-plane
bootstrap-token Generates bootstrap tokens used to join a node to a cluster
kubelet-finalize Updates settings relevant to the kubelet after TLS bootstrap
addon Install required addons for passing conformance tests
...
flowchart LR
A[preflight<br/>검사] --> B[certs<br/>/etc/kubernetes/pki]
B --> C[kubeconfig<br/>/etc/kubernetes/*.conf]
C --> D[etcd · control-plane<br/>/etc/kubernetes/manifests]
D --> E[kubelet-start<br/>/var/lib/kubelet]
E --> F[upload-config<br/>kubeadm-config CM]
F --> G[bootstrap-token<br/>조인 토큰]
G --> H[addon<br/>CoreDNS · kube-proxy]
각 단계의 결과물을 직접 찾아봅니다.
# control-plane 에서
sudo ls /etc/kubernetes/ /etc/kubernetes/pki/ /etc/kubernetes/pki/etcd/ /etc/kubernetes/manifests/
/etc/kubernetes/:
admin.conf controller-manager.conf kubelet.conf manifests pki scheduler.conf super-admin.conf
/etc/kubernetes/pki/:
apiserver.crt apiserver-etcd-client.crt apiserver-kubelet-client.crt ca.crt ca.key etcd
front-proxy-ca.crt front-proxy-client.crt sa.key sa.pub ...
/etc/kubernetes/pki/etcd/:
ca.crt ca.key healthcheck-client.crt healthcheck-client.key peer.crt peer.key server.crt server.key
/etc/kubernetes/manifests/:
etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml
| 단계 | 남긴 것 | 이 가이드에서 다시 쓰는 곳 |
|---|---|---|
| certs | /etc/kubernetes/pki/ (CA 10년 · 나머지 1년) |
20강 · 인증서 |
| kubeconfig | admin.conf · super-admin.conf · kubelet.conf … |
3) |
| etcd · control-plane | /etc/kubernetes/manifests/*.yaml (스태틱 파드) |
19강 · static pod |
| kubelet-start | /var/lib/kubelet/config.yaml · kubeadm-flags.env |
8) |
| upload-config | kube-system/kubeadm-config ConfigMap |
아래 |
| bootstrap-token | kube-system/bootstrap-token-* Secret |
4) |
init 때 준 옵션은 어디에 남나¶
--pod-network-cidr 같은 옵션은 ConfigMap 으로 클러스터 안에 남습니다. 업그레이드 때 kubeadm 이 이것을 읽습니다.
# control-plane 에서
kubectl -n kube-system get cm kubeadm-config -o jsonpath='{.data.ClusterConfiguration}' \
| grep -E 'kubernetesVersion|podSubnet|serviceSubnet|controlPlaneEndpoint|dataDir'
kubernetesVersion: v1.32.x
dataDir: /var/lib/etcd
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
podSubnet 은 10.244.0.0/16 인데 파드 IP 는 10.0.x.x
cilium install 기본 모드는 Cilium 자체 IPAM(cluster-pool, 10.0.0.0/8)으로 파드 IP 를 나눠 줍니다. kubeadm 에 준 --pod-network-cidr 은 기록만 되고 Cilium 은 쓰지 않습니다. 자세한 배경은 3강 · 내 클러스터 만들기의 8) 을 보세요.
기본값과 비교해 보기
kubeadm config print init-defaults 는 아무 옵션도 안 줬을 때의 설정을 YAML 로 보여 줍니다. 클러스터를 바꾸지 않습니다.
advertiseAddress: 1.2.3.4 처럼 자리표시 값이 보입니다. 그래서 VirtualBox 에서는 --apiserver-advertise-address 를 꼭 줘야 했습니다.
단계 하나만 다시 돌릴 수 있다
kubeadm init phase <단계> 로 단계 하나만 실행할 수 있습니다. 예: 컨트롤플레인을 하나 더 붙이려고 인증서를 다시 올릴 때
sudo kubeadm init phase upload-certs --upload-certs. 시험에서 자주 쓰지는 않지만 "init 은 단계의 묶음" 이라는 감각은 트러블슈팅에 도움이 됩니다.
3) kubeconfig — admin.conf 와 super-admin.conf¶
init 이 끝나면 admin.conf 를 ~/.kube/config 로 복사했습니다. 두 파일이 같은지, 그리고 1.29 부터 생긴 super-admin.conf 와 무엇이 다른지 봅니다.
# control-plane 에서
sudo diff -q /etc/kubernetes/admin.conf ~/.kube/config && echo "같은 파일"
kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}{"\n"}{.users[0].name}{"\n"}'
클라이언트 인증서의 그룹(O) 이 권한을 정합니다.
# control-plane 에서
for f in admin super-admin; do
echo "== $f.conf"
sudo grep client-certificate-data /etc/kubernetes/$f.conf | awk '{print $2}' | base64 -d \
| openssl x509 -noout -subject
done
== admin.conf
subject=O = kubeadm:cluster-admins, CN = kubernetes-admin
== super-admin.conf
subject=O = system:masters, CN = kubernetes-super-admin
| 파일 | 그룹 | 성격 |
|---|---|---|
admin.conf |
kubeadm:cluster-admins |
RBAC 로 cluster-admin 을 받은 그룹 — RBAC 를 지우면 권한도 사라진다 |
super-admin.conf |
system:masters |
RBAC 를 건너뛰는 비상용 — 평소에는 쓰지 않는다 |
관찰 포인트: 일상 작업은 admin.conf 로, RBAC 를 망가뜨려 잠겼을 때만 super-admin.conf 를 씁니다. 권한 구조는 15강 · RBAC 에서 이어집니다.
4) 조인 토큰 — 무엇이 무엇을 증명하나¶
join 명령에는 두 가지 값이 들어 있습니다.
| 값 | 증명하는 것 | 방향 |
|---|---|---|
--token abcdef.0123456789abcdef |
"나는 이 클러스터에 들어올 자격이 있는 노드다" | 노드 → 클러스터 |
--discovery-token-ca-cert-hash sha256:… |
"내가 접속한 API 서버는 진짜 이 클러스터다" (CA 공개키 고정) | 클러스터 → 노드 |
토큰 목록과 만료¶
init 때 만든 토큰은 24시간 뒤 만료되어 사라집니다. 새로 만듭니다. 이번에는 짧게, 설명을 붙여서 만들어 봅니다.
# control-plane 에서
sudo kubeadm token create --ttl 2h --description "guide-cluster 실습" --print-join-command
sudo kubeadm token list
kubeadm join 192.168.56.10:6443 --token 7k2m9x.q4w8e1r5t3y6u0i2 --discovery-token-ca-cert-hash sha256:5f1c...e9a2
TOKEN TTL EXPIRES USAGES DESCRIPTION EXTRA GROUPS
7k2m9x.q4w8e1r5t3y6u0i2 1h 2026-09-29T08:12:41Z authentication,signing guide-cluster 실습 system:bootstrappers:kubeadm:default-node-token
토큰은 kube-system 의 Secret 으로 저장됩니다. 토큰 앞부분(. 앞)이 Secret 이름에 들어갑니다.
# control-plane 에서
kubectl -n kube-system get secrets | grep bootstrap-token
kubectl -n kube-system get secret bootstrap-token-7k2m9x -o jsonpath='{.data.expiration}' | base64 -d; echo
discovery 해시를 직접 계산하기¶
해시는 CA 인증서의 공개키를 SHA-256 한 값입니다. 쿠버네티스 문서의 명령 그대로 계산해서, 위 join 명령의 값과 같은지 봅니다.
# control-plane 에서
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt \
| openssl rsa -pubin -outform der 2>/dev/null \
| openssl dgst -sha256 -hex | sed 's/^.* //'
관찰 포인트: 토큰은 몇 번을 새로 만들어도 해시는 항상 같습니다. CA 가 바뀌지 않는 한 클러스터의 "지문" 이기 때문입니다.
시험 팁
"노드를 조인하라" 가 나오면 init 출력을 찾지 말고 control-plane 에서 kubeadm token create --print-join-command 로 완성된 명령을 새로 뽑습니다.
그 줄을 복사해 워커에서 sudo 를 붙여 실행하면 끝입니다. 문서 페이지는 Creating a cluster with kubeadm 과 kubeadm join.
5) 워커를 빼고 다시 넣기 — drain → delete → reset → join¶
worker-1 을 클러스터에서 실제로 뺍니다
- 0) 의 VirtualBox 스냅샷이 있는지 먼저 확인하세요.
- worker-1 의 파드는 모두 다른 노드로 옮겨집니다. 한 대만 남는 동안 자원이 모자라 Pending 이 생길 수 있습니다.
kubeadm reset은 되돌리는 명령이 없습니다. 이 절을 시작했으면 끝의 join 까지 진행하세요.
sequenceDiagram
participant CP as control-plane
participant W as worker-1
CP->>CP: kubectl drain worker-1
CP->>CP: kubectl delete node worker-1
W->>W: sudo kubeadm reset -f
W->>W: sudo rm -rf /etc/cni/net.d
CP->>CP: kubeadm token create --print-join-command
W->>CP: sudo kubeadm join ... (TLS 부트스트랩)
CP-->>W: kubelet 클라이언트 인증서 발급
Note over W: NotReady → CNI 파드 뜬 뒤 Ready
① 비우고 지우기¶
# control-plane 에서
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data
kubectl delete node worker-1
kubectl get nodes
node/worker-1 cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/cilium-8xk2p, kube-system/kube-proxy-m2n8c, ...
evicting pod kube-system/coredns-668d6bf9bc-7xk2p
...
node/worker-1 drained
node "worker-1" deleted
NAME STATUS ROLES AGE VERSION
control-plane Ready control-plane 3d v1.32.x
worker-2 Ready <none> 3d v1.32.x
drain 플래그의 의미는 20강 · drain 과 19강 · drain 의 걸림돌 에서 자세히 봅니다.
② 노드 쪽 흔적 지우기¶
노드 오브젝트를 지워도 worker-1 의 kubelet 은 아직 옛 인증서로 돌고 있습니다. 노드 쪽을 초기화합니다.
[preflight] Running pre-flight checks
...
[reset] Stopping the kubelet service
[reset] Unmounting mounted directories in "/var/lib/kubelet"
[reset] Deleting contents of directories: [/etc/kubernetes/manifests /var/lib/kubelet /etc/kubernetes/pki]
[reset] Deleting files: [/etc/kubernetes/admin.conf /etc/kubernetes/super-admin.conf /etc/kubernetes/kubelet.conf /etc/kubernetes/bootstrap-kubelet.conf ...]
The reset process does not clean CNI configuration. To do so, you must remove /etc/cni/net.d
The reset process does not reset or clean up iptables rules or IPVS tables.
...
출력이 알려 주는 대로 CNI 설정을 지웁니다. (CNI 바이너리 /opt/cni/bin 은 지우지 않습니다.)
③ 다시 조인¶
# worker-1 에서 — 위 출력 한 줄을 그대로, 앞에 sudo 를 붙여서
sudo kubeadm join 192.168.56.10:6443 --token <토큰> --discovery-token-ca-cert-hash sha256:<해시>
[preflight] Running pre-flight checks
[preflight] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap
...
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
NAME STATUS ROLES AGE VERSION INTERNAL-IP
worker-1 NotReady <none> 5s v1.32.x 192.168.56.11 ← CNI 에이전트가 아직 안 떴다
worker-1 Ready <none> 40s v1.32.x 192.168.56.11 ← cilium 파드가 뜨고 CNI 설정을 다시 썼다
관찰 포인트: 조인 직후 잠깐 NotReady 인 이유는 CNI 가 그 노드에 아직 없기 때문입니다. Cilium DaemonSet 이 새 노드에 파드를 띄우면서 /etc/cni/net.d 를 다시 채웁니다.
INTERNAL-IP 가 10.0.2.15 로 나오면
VirtualBox 는 NAT 어댑터(10.0.2.x)가 기본 경로라서, kubelet 이 그 주소를 노드 IP 로 고를 수 있습니다.
이 과정의 클러스터는 /etc/hosts 에 192.168.56.11 worker-1 이 있어서 kubelet 이 hostname 을 해석해 192.168.56.x 를 고릅니다(3강 1-5). hosts 가 맞는데도 10.0.2.15 가 나오면 /etc/default/kubelet 에 KUBELET_EXTRA_ARGS=--node-ip=... 를 넣습니다. kubeadm reset 은 이 파일을 지우지 않습니다. 8) 에서 이 파일을 확인합니다.
노드 오브젝트를 안 지우고 reset → join 하면?
같은 이름으로 다시 등록되므로 대부분 동작합니다. 하지만 옛 노드의 레이블 · taint · 상태가 섞여 헷갈리기 쉬워서, 실무에서는 kubectl delete node 를 먼저 합니다.
6) crictl — kubectl 이 안 될 때의 창구¶
kubectl 은 API 서버에 묻는 도구이고, crictl 은 그 노드의 컨테이너 런타임(containerd)에 직접 묻는 도구입니다. API 서버가 죽어도 crictl 은 됩니다.
런타임 엔드포인트 설정¶
이 파일이 없으면 crictl 이 여러 소켓을 차례로 찔러 보며 경고를 냅니다.
WARN[0000] runtime connect using default endpoints: [unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock]. As the default settings are now deprecated, you should set the endpoint instead.
없다면 한 번 만들어 둡니다.
파드 · 컨테이너 · 이미지¶
# control-plane 에서
sudo crictl pods --namespace kube-system | head -5
sudo crictl ps --name kube-scheduler
sudo crictl images | head -5
POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME
9c8d7e6f5a4b1 3 hours ago Ready kube-scheduler-control-plane kube-system 2 (default)
1a2b3c4d5e6f7 3 hours ago Ready etcd-control-plane kube-system 2 (default)
...
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD
3f1c2a9b7d10e a389e107f4ff1 3 hours ago Running kube-scheduler 3 9c8d7e6f5a4b1 kube-scheduler-control-plane
IMAGE TAG IMAGE ID SIZE
quay.io/cilium/cilium v1.x.y 9a8b7c6d5e4f3 ...
registry.k8s.io/coredns/coredns v1.11.x c69fa2e9cbf5f 18.6MB
registry.k8s.io/etcd 3.5.x-0 2e96e5913fc06 ...
...
| crictl | 비슷한 kubectl | 차이 |
|---|---|---|
crictl pods |
kubectl get pods |
이 노드의 파드 샌드박스만 |
crictl ps / ps -a |
kubectl get pods 의 컨테이너 |
컨테이너 단위 · -a 는 종료된 것까지 |
crictl logs <ID> |
kubectl logs |
API 서버 없이 읽는다 |
crictl inspect <ID> |
kubectl describe |
런타임 수준 정보(PID · 마운트 · 이미지 다이제스트) |
crictl images |
— | 이 노드에 받아 둔 이미지 |
로그 읽기와 파일로 남기기¶
# control-plane 에서
ID=$(sudo crictl ps -q --name kube-scheduler | head -1)
sudo crictl logs --tail 5 "$ID"
sudo crictl logs "$ID" > ~/guide-cluster/scheduler.log 2>&1
wc -l ~/guide-cluster/scheduler.log
I0929 03:02:11.418263 1 serving.go:386] Generated self-signed cert in-memory
I0929 03:02:12.071934 1 server.go:167] "Starting Kubernetes Scheduler" version="v1.32.x"
I0929 03:02:12.176112 1 leaderelection.go:257] attempting to acquire leader lease kube-system/kube-scheduler...
...
48 /home/<사용자>/guide-cluster/scheduler.log
2>&1 을 빼면 파일이 비어 있을 수 있습니다
컨테이너 로그 중 stderr 로 나온 줄은 crictl 도 stderr 로 내보냅니다. 쿠버네티스 구성 요소는 대부분 stderr 에 로그를 씁니다.
> 파일 만 하면 화면에는 보이는데 파일은 비는 일이 생깁니다.
관찰 포인트: 로그의 실체는 노드의 파일입니다. crictl 과 kubectl 은 이 파일을 읽어 줄 뿐입니다.
# control-plane 에서
sudo ls /var/log/pods/ | grep scheduler
sudo ls -l /var/log/containers/ | grep scheduler
kube-system_kube-scheduler-control-plane_4b9e2c...
lrwxrwxrwx 1 root root ... kube-scheduler-control-plane_kube-system_kube-scheduler-3f1c2a9b....log -> /var/log/pods/kube-system_kube-scheduler-control-plane_.../kube-scheduler/3.log
inspect 로 런타임 정보 보기¶
# control-plane 에서
sudo crictl inspect "$ID" | grep -m1 '"pid"'
sudo crictl inspect "$ID" | grep -m1 '"image"'
대조 실험 — 컨테이너를 crictl 로 멈추면?¶
worker-2 에 파드를 하나 고정해서 띄웁니다.
# control-plane 에서
kubectl run pinned -n guide-cluster --image=nginx:1.27 \
--overrides='{"spec":{"nodeName":"worker-2"}}'
kubectl get pod pinned -n guide-cluster -o wide
# worker-2 에서
sudo crictl ps --name pinned
sudo crictl stop $(sudo crictl ps -q --name pinned)
sleep 10; sudo crictl ps -a --name pinned
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD
b7e1... 9bea9f2796e2 2 minutes ago Running pinned 0 4c2d... pinned
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD
e0a9... 9bea9f2796e2 5 seconds ago Running pinned 1 4c2d... pinned ← 새 컨테이너
b7e1... 9bea9f2796e2 2 minutes ago Exited pinned 0 4c2d... pinned
관찰 포인트: crictl 로 멈춰도 kubelet 이 원하는 상태(파드 스펙)를 보고 다시 띄웁니다. POD ID 는 그대로이고 ATTEMPT 와 RESTARTS 만 1 올라갔습니다. 파드를 없애는 것은 여전히 kubelet(또는 API)의 일입니다.
시험 팁
- crictl 은 노드 안에서, sudo 로 칩니다.
crictl ps -q --name <이름>으로 ID 만 뽑으면 한 줄로 이어 쓰기 좋습니다. - 문서 페이지: Debugging Kubernetes nodes with crictl.
7) CNI — 설정은 어디에, 없으면 어떻게 되나¶
CNI 는 두 조각입니다. 설정 파일(어떤 플러그인을 쓸지)과 바이너리(실제로 인터페이스를 만드는 실행 파일). 컨테이너 런타임이 파드를 만들 때 설정을 읽고 바이너리를 실행합니다.
05-cilium.conflist
{
"cniVersion": "0.3.1",
"name": "cilium",
"plugins": [
{
"type": "cilium-cni",
...
}
]
}
bandwidth bridge cilium-cni dhcp firewall host-device host-local loopback macvlan portmap ptp ...
| 위치 | 내용 | 누가 채우나 |
|---|---|---|
/etc/cni/net.d/ |
설정 (사전순으로 첫 파일이 쓰인다) | CNI 에이전트(cilium DaemonSet)가 노드마다 씀 |
/opt/cni/bin/ |
플러그인 실행 파일 | kubernetes-cni 패키지 + CNI 에이전트 |
| containerd 설정 | 위 두 경로를 가리킨다 | /etc/containerd/config.toml 의 [plugins."io.containerd.grpc.v1.cri".cni] |
어떤 CNI 를 쓰는지 알아내는 세 가지 방법¶
# control-plane 에서
kubectl get pods -n kube-system -o wide | grep -Ei 'cilium|calico|flannel|weave'
kubectl get ds -n kube-system
ls /etc/cni/net.d/
cilium-8xk2p 1/1 Running ... worker-1
cilium-envoy-... 1/1 Running ... worker-1
cilium-operator-6c9d... 1/1 Running ... worker-2
...
NAME DESIRED CURRENT READY ...
cilium 3 3 3
cilium-envoy 3 3 3
kube-proxy 3 3 3
설치 방식까지 보려면:
CNI 설치 명령은 CNI 마다 다르다
| 방식 | 예 |
|---|---|
| 매니페스트 한 장 | kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml |
| Helm 차트 | helm repo add cilium https://helm.cilium.io/ → helm install cilium cilium/cilium --version 1.x.y -n kube-system |
| 전용 CLI | cilium install --version 1.x.y |
이 클러스터에는 이미 CNI 가 있으니 실행하지는 마세요. 시험에서는 문서 링크나 주어진 URL 을 보고 이 중 한 형태로 설치합니다.
--pod-network-cidr 을 요구하는 CNI(Flannel · Calico 기본 설정)는 init 때 준 값과 맞아야 합니다.
문서의 매니페스트가 바로 안 될 때 — 내려받아 cidr 맞추기¶
시험에서는 "문서대로 kubectl create -f <URL> 했더니 안 된다" 가 그대로 문제로 나옵니다. Calico 의 operator 방식이 대표적입니다. 문서는 두 줄을 칩니다.
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/<버전>/manifests/tigera-operator.yaml
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/<버전>/manifests/custom-resources.yaml
두 번째 파일(custom-resources.yaml)을 URL 그대로 쓰면 안 되는 이유가 두 가지입니다.
| 이유 | 증상 |
|---|---|
IP 풀 cidr 가 문서 기본값 192.168.0.0/16 이다 |
클러스터의 파드 CIDR 와 달라 파드 IP 가 엉뚱한 대역에서 나오거나 노드 간 라우팅이 어긋난다 |
Installation 은 operator 가 만드는 CRD 종류다 |
operator 를 먼저 안 깔았으면 no matches for kind "Installation" 으로 실패한다 |
그래서 파일로 내려받아 cidr 를 클러스터 값으로 고친 뒤 적용합니다. 여기서는 이미 Cilium 이 있으므로 고치기까지만 하고 적용은 하지 않습니다.
① 클러스터의 파드 CIDR 찾기¶
# control-plane 에서
kubectl -n kube-system get cm kubeadm-config -o jsonpath='{.data.ClusterConfiguration}' | grep podSubnet
kubectl get nodes -o custom-columns=NAME:.metadata.name,PODCIDR:.spec.podCIDR
podSubnet: 10.244.0.0/16
NAME PODCIDR
control-plane 10.244.0.0/24
worker-1 10.244.1.0/24
worker-2 10.244.2.0/24
| 어디서 | 보이는 값 | 쓰임 |
|---|---|---|
kubeadm-config 의 podSubnet |
클러스터 전체 대역 (--pod-network-cidr) |
CNI 매니페스트의 cidr 에 넣는 값 |
노드의 spec.podCIDR |
그 노드에 떼어 준 /24 조각 | 전체 대역을 짐작하는 단서 — 이 값을 그대로 넣으면 안 된다 |
이 클러스터의 파드 IP 는 10.0.x.x 인데요?
2) 에서 본 것처럼 Cilium 기본 모드는 자체 IPAM 을 써서 podSubnet 과 다른 대역을 씁니다. 하지만 "클러스터의 파드 CIDR" 를 묻는 문제의 답은 kubeadm 에 기록된 값(podSubnet)입니다. kubectl get pods -o wide 의 IP 를 보고 고르지 마세요.
② 내려받아서 고치기¶
버전만 다를 뿐 파일 구조는 같습니다. 이 페이지에서는 v3.30.3 을 받아 봅니다.
# control-plane 에서
cd ~/guide-cluster
curl -fsSLo calico-cr.yaml https://raw.githubusercontent.com/projectcalico/calico/v3.30.3/manifests/custom-resources.yaml
grep -n -B2 -A3 'cidr' calico-cr.yaml
11- - name: default-ipv4-ippool
12- blockSize: 26
13: cidr: 192.168.0.0/16
14- encapsulation: VXLANCrossSubnet
15- natOutgoing: Enabled
16- nodeSelector: all()
vi 로 고쳐도 되고, 값이 한 군데뿐이면 sed 가 빠릅니다. 값은 손으로 치지 말고 클러스터에서 읽은 것을 넣습니다.
# control-plane 에서
POD_CIDR=$(kubectl -n kube-system get cm kubeadm-config -o jsonpath='{.data.ClusterConfiguration}' | awk '/podSubnet:/{print $2}')
echo "$POD_CIDR"
sed -i "s#cidr: 192.168.0.0/16#cidr: ${POD_CIDR}#" calico-cr.yaml
grep -n 'cidr' calico-cr.yaml
③ 고친 파일이 YAML 로 멀쩡한지 확인¶
kubectl create --dry-run=client 는 이 파일에 쓸 수 없습니다. Installation CRD 가 없어서 dry-run 도 같은 이유로 실패합니다. YAML 문법만 보려면 파이썬으로 읽어 봅니다.
# control-plane 에서
python3 -c 'import yaml,sys; [print(d["kind"], d.get("spec",{}).get("calicoNetwork",{}).get("ipPools",[{}])[0].get("cidr","")) for d in yaml.safe_load_all(open(sys.argv[1])) if d]' calico-cr.yaml
들여쓰기를 망가뜨렸다면 yaml.scanner.ScannerError 가 나옵니다. 특히 vi 에서 탭이 들어가면 YAML 이 깨집니다.
④ 적용 명령 — URL 이 아니라 고친 파일¶
# 실제 클러스터를 새로 만들 때라면 (여기서는 실행하지 않는다)
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.30.3/manifests/tigera-operator.yaml
kubectl create -f ~/guide-cluster/calico-cr.yaml
대조 실험 — operator 없이 custom-resources 를 적용하면¶
이 실험은 아무것도 만들지 않고 실패하므로 안전합니다. 문서의 두 번째 줄만 먼저 치면 어떻게 되는지 봅니다.
error: resource mapping not found for name: "default" namespace: "" from "/home/<사용자>/guide-cluster/calico-cr.yaml": no matches for kind "Installation" in version "operator.tigera.io/v1"
ensure CRDs are installed first
... ← APIServer · Goldmane · Whisker 도 같은 오류
관찰 포인트: "문서 명령이 실패한다" 의 원인은 대개 순서(CRD/operator 먼저)이거나 값(cidr)입니다. 오류 메시지의 ensure CRDs are installed first 가 순서 문제라는 힌트입니다.
시험 팁 — URL 을 바로 create 하지 말고 받아서 본다
curl -LO <URL>또는wget <URL>로 받아grep -n cidr부터 봅니다.- cidr 는
kubeadm-config의 podSubnet 으로 맞춥니다. 노드의/24조각을 넣지 않습니다. - 적용은
kubectl create -f <고친 파일>. operator 매니페스트가 따로 있으면 그것을 먼저 적용합니다.
대조 실험 — worker-2 에서 CNI 설정을 치우면¶
worker-2 의 네트워크를 잠깐 끊습니다
반드시 같은 절 안에서 파일을 되돌리세요. 되돌리지 않으면 worker-2 에 새 파드가 뜨지 않습니다.
# control-plane 에서 — 30초쯤 뒤
kubectl get nodes worker-2
kubectl describe node worker-2 | grep -A1 'Ready '
NAME STATUS ROLES AGE VERSION
worker-2 NotReady <none> 3d v1.32.x
Ready False ... KubeletNotReady container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized
이 상태에서 worker-2 에 파드를 고정해 띄워 봅니다.
# control-plane 에서
kubectl run no-cni -n guide-cluster --image=nginx:1.27 \
--overrides='{"spec":{"nodeName":"worker-2"}}'
kubectl get pod no-cni -n guide-cluster
kubectl describe pod no-cni -n guide-cluster | tail -3
NAME READY STATUS RESTARTS AGE
no-cni 0/1 ContainerCreating 0 20s
Warning NetworkNotReady ... network is not ready: container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:Network plugin returns error: cni plugin not initialized
파일이 저절로 다시 생겼다면
CNI 에이전트(Cilium)는 자기 설정 파일을 관리합니다. 버전에 따라 지워진 파일을 곧바로 다시 써서 노드가 NotReady 까지 가지 않을 수 있습니다.
ls /etc/cni/net.d/ 에 파일이 돌아와 있다면 그것이 관찰 포인트입니다 — CNI 설정은 사람이 아니라 에이전트가 채운다.
이 경우 아래 되돌리기에서 백업 파일(/root/05-cilium.conflist.guide)만 지우면 됩니다.
관찰 포인트: 이미 돌던 파드(pinned)는 그대로 Running 입니다. CNI 는 파드 샌드박스를 만들 때만 쓰이기 때문입니다. 새 파드만 ContainerCreating 에 걸립니다.
스케줄러를 거치는 파드였다면 NotReady 노드의 taint 때문에 아예 배치되지 않고 다른 노드로 갔을 것입니다. nodeName 은 스케줄러를 건너뛰어서 이 증상을 볼 수 있었습니다.
되돌립니다.
# worker-2 에서
sudo mv -n /root/05-cilium.conflist.guide /etc/cni/net.d/05-cilium.conflist # -n: 이미 있으면 덮지 않는다
sudo rm -f /root/05-cilium.conflist.guide
ls /etc/cni/net.d/
# control-plane 에서
kubectl get nodes worker-2 -w # Ready 로 돌아올 때까지
kubectl get pod no-cni -n guide-cluster
시험 팁 — NotReady 를 보면 describe 의 메시지부터
| 메시지 | 방향 |
|---|---|
NetworkPluginNotReady · cni plugin not initialized |
CNI 가 없거나 설정이 없다 → CNI 설치 또는 /etc/cni/net.d 확인 |
Kubelet stopped posting node status |
kubelet 이 멈췄다 → 노드에 들어가 systemctl status kubelet (19강) |
8) kubelet — 설정 파일 세 겹과 로그¶
kubelet 은 파드가 아니라 systemd 서비스입니다. 설정이 여러 파일에 나뉘어 있어서, 고칠 곳을 잘못 찾기 쉽습니다.
# /usr/lib/systemd/system/kubelet.service
[Unit]
Description=kubelet: The Kubernetes Node Agent
...
[Service]
ExecStart=/usr/bin/kubelet
Restart=always
...
# /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf
[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
EnvironmentFile=-/etc/default/kubelet
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS
드롭인(.service.d/10-kubeadm.conf)이 본 유닛의 ExecStart 를 비우고 다시 씁니다. 실제 인자는 네 군데에서 모입니다.
| 파일 | 누가 쓰나 | 담긴 것 |
|---|---|---|
10-kubeadm.conf (드롭인) |
kubelet 패키지 | kubeconfig 경로 · --config 경로 |
/var/lib/kubelet/config.yaml |
kubeadm (init/join/upgrade) | cgroupDriver · clusterDNS · staticPodPath · 인증서 경로 |
/var/lib/kubelet/kubeadm-flags.env |
kubeadm | KUBELET_KUBEADM_ARGS (런타임 소켓 등) |
/etc/default/kubelet |
사람 | KUBELET_EXTRA_ARGS (예: --node-ip) — kubeadm 이 건드리지 않는다 |
# 아무 노드에서
sudo grep -E 'cgroupDriver|clusterDNS|staticPodPath|clientCAFile|rotateCertificates' -A1 /var/lib/kubelet/config.yaml
cat /var/lib/kubelet/kubeadm-flags.env
cat /etc/default/kubelet 2>/dev/null
clientCAFile: /etc/kubernetes/pki/ca.crt
cgroupDriver: systemd
clusterDNS:
- 10.96.0.10
rotateCertificates: true
staticPodPath: /etc/kubernetes/manifests
KUBELET_KUBEADM_ARGS="--container-runtime-endpoint=unix:///var/run/containerd/containerd.sock ..."
KUBELET_EXTRA_ARGS=
이 과정의 클러스터는 hosts 파일로 노드 IP 가 정해지므로 KUBELET_EXTRA_ARGS 가 비어 있거나 파일이 없는 것이 정상입니다. 10.0.2.15 문제를 고친 노드라면 --node-ip=192.168.56.x 가 들어 있습니다.
실행 중인 프로세스 인자로도 같은 것을 확인할 수 있습니다.
--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf
--kubeconfig=/etc/kubernetes/kubelet.conf
--config=/var/lib/kubelet/config.yaml
--container-runtime-endpoint=unix:///var/run/containerd/containerd.sock
...
고친 파일에 따라 다시 읽히는 방법이 다르다
- 유닛 · 드롭인 파일(
/usr/lib/systemd/...,/etc/systemd/...)을 고쳤다 →sudo systemctl daemon-reload먼저, 그다음restart kubelet config.yaml·kubeadm-flags.env·/etc/default/kubelet을 고쳤다 →sudo systemctl restart kubelet
daemon-reload 를 빠뜨리면 고쳤는데도 옛 설정으로 뜹니다.
로그는 journalctl¶
# 아무 노드에서
sudo journalctl -u kubelet -n 20 --no-pager
sudo journalctl -u kubelet --since "10 min ago" --no-pager | grep -iE 'error|fail' | tail -5
Sep 29 06:12:41 worker-2 kubelet[812]: I0929 06:12:41.118263 812 kubelet_node_status.go:...] "Node became not ready" node="worker-2" condition={"type":"Ready","status":"False",...,"message":"container runtime network not ready: ... cni plugin not initialized"}
...
관찰 포인트: 7) 의 실험 흔적이 kubelet 로그에 그대로 남아 있습니다. 노드가 NotReady 일 때 원인 문장은 거의 항상 여기 있습니다.
| 옵션 | 뜻 |
|---|---|
-u kubelet |
kubelet 유닛만 |
-n 30 |
마지막 30줄 |
-f |
계속 따라가기 (Ctrl+C 로 종료) |
--since "10 min ago" |
시간 범위 |
--no-pager |
less 없이 바로 출력 (파이프 · 파일로 보낼 때) |
kubelet 을 멈추고 고치는 실험은 19강 · NotReady 복구 에서 합니다.
9) CRI 패키지 설치 (dpkg · systemctl enable --now · sysctl 영구 설정)¶
시험에는 "이 노드에 cri-dockerd 를 설치하고 서비스로 띄운 뒤, 커널 파라미터를 영구 설정하라" 가 나옵니다. cri-dockerd 는 Docker 엔진을 CRI 로 감싸 주는 어댑터입니다. 손이 가는 곳은 세 군데입니다.
| 단계 | 명령 | 확인 |
|---|---|---|
| 패키지 설치 | sudo dpkg -i <파일>.deb |
dpkg -s <패키지> 의 Status: install ok installed |
| 서비스 | sudo systemctl enable --now <유닛> |
systemctl is-enabled · is-active |
| 커널 파라미터 | /etc/sysctl.d/<이름>.conf 에 쓰고 sudo sysctl --system |
sysctl <키> |
이 실습 노드에는 Docker 엔진이 없습니다
이 과정의 클러스터는 containerd 를 쓰고 Docker 엔진은 깔지 않았습니다. 그래서 cri-docker 서비스는 enable 은 되지만 active 로 유지되지 못합니다(시작 실패 · 재시작 반복). 여기서는 그것이 정상입니다. kubelet 은 계속 containerd 를 쓰므로 노드는 Ready 로 남습니다. 실제 시험 환경에서는 Docker 가 있어 active 까지 확인됩니다.
① 패키지 파일 고르기와 내려받기¶
GitHub 릴리스에는 배포판 · 아키텍처별 .deb 가 따로 있습니다. 노드에 맞는 이름을 먼저 확인합니다.
파일 이름은 cri-dockerd_<버전>.3-0.ubuntu-<코드네임>_<아키텍처>.deb 형태입니다.
# worker-2 에서
mkdir -p ~/guide-cri && cd ~/guide-cri
ARCH=$(dpkg --print-architecture)
curl -fLO "https://github.com/Mirantis/cri-dockerd/releases/download/v0.3.26/cri-dockerd_0.3.26.3-0.ubuntu-noble_${ARCH}.deb"
ls -l
② dpkg 로 설치¶
# worker-2 에서
sudo dpkg -i ~/guide-cri/cri-dockerd_*.deb
dpkg -s cri-dockerd | grep -E '^(Status|Version|Depends)'
Selecting previously unselected package cri-dockerd.
(Reading database ... 8xxxx files and directories currently installed.)
Preparing to unpack .../cri-dockerd_0.3.26.3-0.ubuntu-noble_amd64.deb ...
Unpacking cri-dockerd (0.3.26~3-0~ubuntu-noble) ...
Setting up cri-dockerd (0.3.26~3-0~ubuntu-noble) ...
Status: install ok installed
Version: 0:0.3.26~3-0~ubuntu-noble
Depends: containerd (>= 1.2.2-3) | containerd.io (>= 1.2.2-3) | moby-containerd (>= 1.2.2), iptables, libseccomp2 (>= 2.3.0), libc6 (>= 2.34)
관찰 포인트: dpkg 는 의존성을 검사만 하고 받아 오지 않습니다. 이 노드에는 apt 로 깐 containerd 가 있어서 바로 install ok installed 가 됩니다. 의존성이 모자라면 install ok unpacked 에 멈추고 dependency problems 가 나옵니다. 그때는 sudo apt-get install -f -y 로 채웁니다.
패키지 이름과 서비스 이름이 다르다
패키지는 cri-dockerd, 서비스는 cri-docker.service 와 cri-docker.socket 입니다. 이름이 헷갈리면 목록으로 찾습니다.
③ enable --now — 그리고 Docker 없는 노드에서 보이는 것¶
데비안 패키지는 설치하면서 유닛을 스스로 enable 해 두기도 합니다. 그래도 시험에서는 지문대로 직접 enable 하고 시작합니다.
# worker-2 에서
sudo systemctl enable --now cri-docker.service
systemctl is-enabled cri-docker.service cri-docker.socket
systemctl is-active cri-docker.service
A dependency job for cri-docker.service failed. See 'journalctl -xe' for details.
enabled
enabled
failed ← Docker 엔진이 없어서 (이 실습에서는 정상)
왜 실패하는지 유닛과 로그로 확인합니다.
# worker-2 에서
systemctl cat cri-docker.socket | grep -E 'ListenStream|SocketGroup'
sudo journalctl -u cri-docker.socket -n 5 --no-pager
ListenStream=%t/cri-dockerd.sock
SocketGroup=docker
... cri-docker.socket: Failed to resolve group docker: No such process
... Failed to listen on cri-docker.socket - CRI Docker Socket for the API.
관찰 포인트: 소켓 유닛이 docker 그룹 소유로 소켓을 만들려는데, Docker 엔진을 깔지 않아 그 그룹이 없습니다. 메시지는 버전마다 조금 다르지만 원인은 같습니다 — 이 노드에 Docker 가 없다. kubelet 은 여전히 containerd 소켓을 씁니다.
| 명령 | 하는 일 | 재부팅 뒤 |
|---|---|---|
systemctl start |
지금 시작 | 안 뜬다 |
systemctl enable |
부팅 때 시작하도록 링크 생성 | 뜬다 (지금은 안 뜸) |
systemctl enable --now |
둘 다 | 뜬다 |
④ sysctl 영구 설정¶
sysctl -w 는 지금만 바꿉니다. 재부팅 뒤에도 남기려면 /etc/sysctl.d/ 에 파일로 씁니다. 먼저 지금 값을 적어 둡니다(정리할 때 되돌리려고).
# worker-2 에서
sysctl net.ipv6.conf.all.forwarding net.netfilter.nf_conntrack_max | tee ~/guide-cri/sysctl-before.txt
net.ipv6.conf.all.forwarding = 0
net.netfilter.nf_conntrack_max = 131072 ← kube-proxy 가 이미 맞춰 둔 값일 수 있다
# worker-2 에서
cat <<'EOF' | sudo tee /etc/sysctl.d/90-guide-cri.conf
net.bridge.bridge-nf-call-iptables = 1
net.ipv6.conf.all.forwarding = 1
net.ipv4.ip_forward = 1
net.netfilter.nf_conntrack_max = 131072
EOF
sudo sysctl --system | grep -A4 90-guide-cri
* Applying /etc/sysctl.d/90-guide-cri.conf ...
net.bridge.bridge-nf-call-iptables = 1
net.ipv6.conf.all.forwarding = 1
net.ipv4.ip_forward = 1
net.netfilter.nf_conntrack_max = 131072
| 키 | 뜻 |
|---|---|
net.bridge.bridge-nf-call-iptables |
브리지를 지나는 트래픽도 iptables 를 거친다 (br_netfilter 모듈 필요) |
net.ipv4.ip_forward · net.ipv6.conf.all.forwarding |
노드가 패킷을 다른 인터페이스로 넘겨 준다 (파드 ↔ 바깥) |
net.netfilter.nf_conntrack_max |
연결 추적 테이블 크기 (nf_conntrack 모듈이 올라와 있어야 키가 보인다) |
시험 팁 — 파일 하나만 다시 읽기
sudo sysctl --system 은 모든 설정 파일을 다시 읽습니다. 방금 쓴 파일만 적용하려면 sudo sysctl -p /etc/sysctl.d/<이름>.conf.
sysctl: cannot stat /proc/sys/net/bridge/... 가 나오면 br_netfilter 모듈이 안 올라온 것입니다 → sudo modprobe br_netfilter (영구는 1) 의 표 참고).
대조 실험 — 파일 없이 -w 만 하면¶
# worker-2 에서
sudo sysctl -w net.ipv6.conf.all.forwarding=0
sudo sysctl -p /etc/sysctl.d/90-guide-cri.conf | grep ipv6
관찰 포인트: 재부팅은 "파일을 다시 읽는 것" 과 같습니다. -w 만 한 값은 재부팅에서 사라지고, 파일에 있는 값만 살아남습니다. 채점도 보통 두 가지를 다 봅니다 — 지금 값(sysctl)과 파일.
⑤ 되돌리기¶
# worker-2 에서
sudo dpkg --purge cri-dockerd
sudo systemctl reset-failed cri-docker.service cri-docker.socket 2>/dev/null
sudo rm -f /etc/sysctl.d/90-guide-cri.conf
cat ~/guide-cri/sysctl-before.txt # ④ 에서 적어 둔 값
sudo sysctl -w net.ipv6.conf.all.forwarding=0 # 적어 둔 값으로
sudo sysctl --system > /dev/null # k8s.conf 의 ip_forward · bridge = 1 은 그대로 유지된다
sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables
rm -rf ~/guide-cri
ip_forward 와 bridge-nf-call-iptables 를 0 으로 되돌리지 마세요
이 두 값은 클러스터가 원래 쓰던 값(1)입니다(1) 의 /etc/sysctl.d/k8s.conf). 0 으로 바꾸면 파드 통신과 Service 가 망가집니다.
dpkg --purge 는 cri-dockerd 만 지우고 의존성인 containerd 는 건드리지 않습니다. apt autoremove 는 쓰지 않습니다.
시험에서 빠르게¶
# 조인 명령 새로 뽑기 (control-plane)
sudo kubeadm token create --print-join-command
# 워커에서 (다시 조인이면 먼저 reset)
sudo kubeadm reset -f && sudo rm -rf /etc/cni/net.d
sudo kubeadm join <CP>:6443 --token <토큰> --discovery-token-ca-cert-hash sha256:<해시>
# crictl (노드 안, sudo)
sudo crictl ps -a --name <이름>
sudo crictl logs "$(sudo crictl ps -q --name <이름> | head -1)" > <파일> 2>&1
sudo crictl pods --namespace kube-system
sudo crictl inspect <ID>
# CNI 확인
kubectl -n kube-system get pods,ds | grep -Ei 'cilium|calico|flannel'
ls /etc/cni/net.d/ /opt/cni/bin/
kubectl describe node <노드> | grep -i network
# kubelet
systemctl cat kubelet
sudo grep staticPodPath /var/lib/kubelet/config.yaml
sudo journalctl -u kubelet -n 30 --no-pager
# CNI 매니페스트를 받아서 cidr 맞추기
kubectl -n kube-system get cm kubeadm-config -o yaml | grep podSubnet
curl -LO <custom-resources URL> && grep -n cidr custom-resources.yaml
sed -i 's#cidr: 192.168.0.0/16#cidr: <podSubnet>#' custom-resources.yaml
kubectl create -f custom-resources.yaml # operator 매니페스트가 있으면 그것 먼저
# 노드 준비 (영구)
echo br_netfilter | sudo tee /etc/modules-load.d/<이름>.conf
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/<이름>.conf && sudo sysctl --system
sudo dpkg -i <패키지>.deb && sudo systemctl enable --now <서비스>
dpkg -s <패키지> | grep Status # install ok installed
systemctl list-unit-files '<이름>*' # 패키지 이름 ≠ 서비스 이름 (cri-dockerd → cri-docker)
정리¶
5) 와 7) 에서 바꾼 것이 원래대로 돌아왔는지 먼저 확인합니다. 9) 는 그 절의 ⑤ 되돌리기를 했는지 봅니다.
# control-plane 에서
kubectl get nodes -o wide # 세 노드 모두 Ready · worker-1 INTERNAL-IP 확인
kubectl delete namespace guide-cluster
rm -rf ~/guide-cluster
sudo kubeadm token list # 실습용 토큰이 남아 있으면 지운다
sudo kubeadm token delete <토큰ID> # 예: 7k2m9x
# worker-2 에서
ls /etc/cni/net.d/ # 05-cilium.conflist 가 있어야 한다
sudo ls /root/05-cilium.conflist.guide 2>/dev/null || echo "백업 파일 없음 (정상)"
dpkg -s cri-dockerd 2>/dev/null | grep Status || echo "cri-dockerd 없음 (정상)"
ls /etc/sysctl.d/90-guide-cri.conf 2>/dev/null || echo "실습 sysctl 파일 없음 (정상)"
일부가 끝내 돌아오지 않으면
0) 에서 찍은 VirtualBox 스냅샷으로 세 VM 을 함께 되돌리는 것이 가장 빠릅니다.
확인 질문¶
1. kubeadm init 직후 kubectl get nodes 가 NotReady 입니다. 무엇이 빠졌나요?
CNI 입니다. describe node 의 Ready 조건 메시지에 NetworkPluginNotReady · cni plugin not initialized 가 보입니다.
CNI 를 설치하면 노드가 Ready 가 되고 Pending 이던 CoreDNS 도 뜹니다.
2. join 명령의 --token 과 --discovery-token-ca-cert-hash 는 각각 무엇을 증명하나요?
토큰은 노드가 클러스터에 들어올 자격을, 해시는 노드가 접속한 API 서버가 진짜 그 클러스터라는 것을(CA 공개키 고정) 증명합니다. 토큰은 24시간이면 만료되지만 해시는 CA 가 바뀌지 않는 한 같습니다.
3. init 할 때 받은 join 명령을 잃어버렸습니다. 어떻게 하나요?
control-plane 에서 sudo kubeadm token create --print-join-command 로 완성된 명령을 새로 뽑습니다.
4. 이미 클러스터에 있던 워커를 다시 조인하려는데 preflight 에서 막힙니다. 무엇을 먼저 하나요?
워커에서 sudo kubeadm reset -f 로 옛 kubelet 설정 · 인증서 · 매니페스트를 지우고, 출력 안내대로 /etc/cni/net.d 도 지웁니다.
control-plane 쪽에서는 kubectl delete node <이름> 으로 옛 노드 오브젝트를 지우는 것이 깔끔합니다.
5. API 서버가 죽어 kubectl 이 안 됩니다. 스케줄러 컨테이너의 로그는 어떻게 보나요?
control-plane 에 들어가 sudo crictl ps -a --name kube-scheduler 로 ID 를 찾고 sudo crictl logs <ID> 로 봅니다.
파일로 남길 때는 stderr 까지 담도록 > 파일 2>&1 을 붙입니다.
6. crictl 로 컨테이너를 stop 했더니 곧 다시 떴습니다. 왜 그런가요?
kubelet 이 파드 스펙(원하는 상태)을 보고 컨테이너를 다시 만들기 때문입니다. POD ID 는 그대로이고 ATTEMPT · RESTARTS 만 늘어납니다.
7. 이 클러스터의 CNI 를 어떻게 알아내나요?
kubectl -n kube-system get pods 에서 cilium-* · calico-node-* 같은 이름을 보거나, 노드의 /etc/cni/net.d/ 파일 이름(예: 05-cilium.conflist)을 봅니다.
8. /etc/systemd/system/kubelet.service.d/ 의 드롭인을 고쳤는데 반영이 안 됩니다. 무엇을 빠뜨렸나요?
sudo systemctl daemon-reload 입니다. 유닛 · 드롭인을 고친 뒤에는 daemon-reload 후 restart kubelet 을 해야 새 설정을 읽습니다.
9. 문서의 kubectl create -f <custom-resources URL> 를 그대로 쓰면 안 되는 이유는? cidr 에는 어떤 값을 넣나요?
IP 풀 cidr 가 문서 기본값(192.168.0.0/16)이라 클러스터의 파드 CIDR 와 다르고, Installation 은 operator 가 만드는 CRD 종류라 operator 없이는 no matches for kind 로 실패합니다.
파일로 받아 cidr 를 kubeadm-config 의 podSubnet 으로 고친 뒤 kubectl create -f <고친 파일> 로 적용합니다. 노드의 spec.podCIDR(/24 조각)를 그대로 넣지 않습니다.
10. .deb 로 설치한 서비스가 재부팅 뒤에도 뜨게 하고, 커널 파라미터도 유지하려면?
sudo dpkg -i <파일>.deb → sudo systemctl enable --now <서비스>(enable 이 재부팅 뒤 시작을 맡는다) → /etc/sysctl.d/<이름>.conf 에 값을 쓰고 sudo sysctl --system.
확인은 dpkg -s 의 install ok installed, systemctl is-enabled, sysctl <키>. 이 실습 노드처럼 Docker 엔진이 없으면 cri-docker 는 enabled 여도 active 가 되지 못합니다.
다음 단계 — 채점 실습¶
개념이 잡혔으면 시험 형식으로 풀어 봅니다. 이번에는 답이 없습니다.
| 문제 | 이 페이지에서 연습한 것 |
|---|---|
| Q1 워커 노드를 클러스터에 조인 | 4) 토큰 · 5) 빼고 다시 넣기 |
| Q2 crictl 로 컨테이너 로그 수집 | 6) crictl logs · 2>&1 |
| Q3 CNI 매니페스트 cidr 를 클러스터 값으로 고치기 | 7) 문서의 매니페스트가 바로 안 될 때 — 내려받아 cidr 맞추기 |
| Q4 CRI 패키지 설치와 커널 파라미터 영구 설정 | 9) CRI 패키지 설치 · 1) 영구 설정 표 |