
들어가며
안녕하세요! Devlos입니다.
이번 포스팅은 CloudNet@ 커뮤니티에서 주최하는 K8S Deploy 3주 차 주제인 "Kubeadm & K8S Upgrade"중 kubeadm 기반 kubernetes cluster 설치에 대해서 정리한 내용입니다.
1주차에서는 Kubernetes The Hard Way를 통해 쿠버네티스 클러스터를 수동으로 구축하는 과정을 배웠고,
2주차에서는 Ansible을 사용하여 인프라 자동화의 기초를 배웠다면,
이번 3주차에서는 kubeadm을 사용하여 쿠버네티스 클러스터를 구축하고 업그레이드하는 방법을 배워봅니다.
kubeadm의 기본 개념부터 클러스터 구축, CNI 설치, 모니터링 툴 설치, 그리고 클러스터 업그레이드까지 전반적인 내용을 다뤄봅니다.
Kubeadm이란?

kubeadm은 쿠버네티스 클러스터를 빠르고 쉽게 구축할 수 있도록 도와주는 공식 도구입니다.
최소한의 안전한 클러스터를 신속하게 구축할 수 있게 해주며, 사용자 친화적인 방식으로 필요한 작업(kubeadm init, kubeadm join 등)을 수행합니다.
kubeadm의 주요 역할은 로컬 노드의 파일 시스템 및 Kubernetes API에 한정되어 있으며, 다른 고수준 도구의 빌딩 블록으로 사용하기 적합합니다.
주요 명령어는 다음과 같습니다.
kubeadm init: 초기 컨트롤 플레인 노드를 부트스트랩할 때 사용합니다.kubeadm join: 워커 노드 또는 추가 컨트롤 플레인 노드를 클러스터에 조인할 때 사용합니다.kubeadm upgrade: 클러스터를 더 최신 버전으로 업그레이드할 때 사용합니다.kubeadm reset: kubeadm init/join을 통해 변경된 모든 설정을 원상 복구할 때 사용합니다.
설치 워크플로우는 다음과 같습니다.

업그레이드 워크플로우는 다음과 같습니다.

실습환경 구성
# 가시다님께서 실습을 위해 작성한 vagrant file
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-kubeadm/Vagrantfile
# Base Image https://portal.cloud.hashicorp.com/vagrant/discover/bento/rockylinux-10.0
BOX_IMAGE = "bento/rockylinux-10.0" # "bento/rockylinux-9"
BOX_VERSION = "202510.26.0"
N = 2 # max number of Worker Nodes
Vagrant.configure("2") do |config|
# ControlPlane Nodes
config.vm.define "k8s-ctr" do |subconfig|
subconfig.vm.box = BOX_IMAGE
subconfig.vm.box_version = BOX_VERSION
subconfig.vm.provider "virtualbox" do |vb|
vb.customize ["modifyvm", :id, "--groups", "/K8S-Upgrade-Lab"]
vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
vb.name = "k8s-ctr"
vb.cpus = 4
vb.memory = 3072 # 2048 2560 3072 4096
vb.linked_clone = true
end
subconfig.vm.host_name = "k8s-ctr"
subconfig.vm.network "private_network", ip: "192.168.10.100"
subconfig.vm.network "forwarded_port", guest: 22, host: "60000", auto_correct: true, id: "ssh"
subconfig.vm.synced_folder "./", "/vagrant", disabled: true
end
# Worker Nodes
(1..N).each do |i|
config.vm.define "k8s-w#{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", "/K8S-Upgrade-Lab"]
vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
vb.name = "k8s-w#{i}"
vb.cpus = 2
vb.memory = 2048
vb.linked_clone = true
end
subconfig.vm.host_name = "k8s-w#{i}"
subconfig.vm.network "private_network", ip: "192.168.10.10#{i}"
subconfig.vm.network "forwarded_port", guest: 22, host: "6000#{i}", auto_correct: true, id: "ssh"
subconfig.vm.synced_folder "./", "/vagrant", disabled: true
end
end
end
# Vagrant up
vagrant up
설치 절차 및 정보
k8s 구성 절차는 다음 과 같습니다.
- [공통] 사전 설정
- [공통] CRI 설치 : containerd
- [공통] kubeadm, kubelet 및 kubectl 설치
- [Controlplane node] kubeadm 으로 k8s 클러스터 구성 → Flannel CNI 설치 → 편의성 설치 및 확인
- [Worker nodes] kubeadm 으로 k8s 클러스터 join → 확인
- 모니터링 툴 설치 : 프로메테우스-스택 설치 → 인증서 익스포터 설치 → 그라파나 대시보드 확인
- 샘플 애플리케이션 배포
- kubeadm 인증서 갱신
주요 설치 버전 정보는 다음과 같습니다.
k8s 1.32.11 버전을 설치할 것이고, 해당 버전에 호환되는 구성 요소들을 사용합니다.
| 항목 | 버전 | 역할 | k8s 버전 호환성 |
| Rocky Linux | 10.0-1.6 | OS(모든 노드) | RHEL 10 소스 기반 배포판으로 RHEL 정보 참고 |
| containerd | v2.1.5 | 컨테이너 런타임(모든 노드) | CRI Version(v1), k8s 1.32~1.35 지원 - Link |
| runc | v1.3.3 | 컨테이너 런타임 하위 실행 엔진(모든 노드) | https://github.com/opencontainers/runc |
| kubelet | v1.32.11 | k8s 에이전트(모든 노드) | k8s 버전 정책 문서 참고 - Docs |
| kubeadm | v1.32.11 | 클러스터 초기화/관리(모든 노드) | 상동 |
| kubectl | v1.32.11 | 클러스터 관리 CLI(컨트롤 플레인, 운영PC) | 상동 |
| helm | v3.18.6 | 패키지 매니저(컨트롤 플레인, 운영PC) | k8s 1.30.x ~ 1.33.x 지원 - Docs |
| flannel cni | v0.27.3 | 네트워크 CNI(모든 노드) | k8s 1.28~ 이후 - Release |
1. [공통] 사전 설정
기본 정보 확인 : vagrant ssh k8s-ctr
# User 정보
whoami
# vagrant
id
# uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant) ...
pwd
# /home/vagrant
# cpu, mem
lscpu
# Architecture: aarch64
# CPU(s): 4
# Vendor ID: Apple
# ...
free -h
# total used free shared buff/cache available
# Mem: 2.8Gi 289Mi 2.4Gi 18Mi 147Mi 2.5Gi
# Swap: 3.8Gi 0B 3.8Gi
# ...
# Disk
lsblk
# NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
# sda 8:0 0 64G 0 disk
# ├─sda1 8:1 0 600M 0 part /boot/efi
# ├─sda2 8:2 0 3.8G 0 part [SWAP]
# └─sda3 8:3 0 59.6G 0 part /
# ...
df -hT
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda3 xfs 60G 2.5G 58G 5% /
# /dev/sda1 vfat 599M 13M 587M 3% /boot/efi
# ...
# Network
ip -br -c -4 addr
# lo UNKNOWN 127.0.0.1/8
# enp0s8 UP 10.0.2.15/24
# enp0s9 UP 192.168.10.100/24
# ...
ip -c route
# default via 10.0.2.2 dev enp0s8 proto dhcp src 10.0.2.15 metric 100
# default via 192.168.10.1 dev enp0s9 proto static metric 101
# 10.0.2.0/24 dev enp0s8 proto kernel scope link src 10.0.2.15 metric 100
# 192.168.10.0/24 dev enp0s9 proto kernel scope link src 192.168.10.100 metric 101
# ...
ip addr
# enp0s8 / enp0s9 상세 IPv4/IPv6, MAC 주소 등 상세 정보 출력 ...
# Host Info, Kernel
hostnamectl
# Static hostname: k8s-ctr
# Operating System: Rocky Linux 10.0 (Red Quartz)
# OS Support End: Thu 2035-05-31
# Kernel: Linux 6.12.0-55.39.1.el10_0.aarch64
# Architecture: arm64
# ...
uname -r
# 6.12.0-55.39.1.el10_0.aarch64
rpm -aq | grep release
# rocky-release-10.0-1.6.el10.noarch ...
# cgroup 버전 확인
stat -fc %T /sys/fs/cgroup
# cgroup2fs (cgroup v2 사용)
findmnt
# TARGET SOURCE FSTYPE OPTIONS
# / /dev/sda3
# ├─/boot/efi /dev/sda1 vfat ...
# ...
mount | grep cgroup
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot)
# systemd cgroup 계층 구조 확인
systemd-cgls --no-pager
# CGroup /:
# -.slice
# ├─user.slice
# ├─init.scope
# └─system.slice
# ├─NetworkManager.service
# ├─sshd.service
# └─...
# Process
pstree
# systemd─┬─NetworkManager───3*[{NetworkManager}]
# ├─VBoxService───8*[{VBoxService}]
# ├─sshd───sshd-session───sshd-session───bash───pstree
# └─...
lsns # 현재 세션의 time/cgroup/pid/user/net 등 namespace ID 확인
# NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 2 4519 vagrant -bash
# 4026531835 cgroup 2 4519 vagrant -bash
# 4026531836 pid 2 4519 vagrant -bash
# 4026531837 user 2 4519 vagrant -bash
# 4026531838 uts 2 4519 vagrant -bash
# 4026531839 ipc 2 4519 vagrant -bash
# 4026531840 net 2 4519 vagrant -bash
# 4026531841 mnt 2 4519 vagrant -bash
다음으로 root 권한으로 전환합니다.
sudo su -
Time, NTP 설정
인증서 만료 시간, 로그 타임스탬프 등 모든 노드에 동기화된 시간 필요
# timedatectl 정보 확인
timedatectl status
# Local time: Tue 2026-01-20 15:21:15 UTC
# Universal time: Tue 2026-01-20 15:21:15 UTC
# Time zone: UTC (UTC, +0000)
# System clock synchronized: yes, NTP service: active, RTC in local TZ: yes
# Warning: RTC 를 local time 으로 사용 중이라 문제를 유발할 수 있다는 경고 출력
timedatectl set-local-rtc 0
# RTC in local TZ: no 로 변경, 이후 경고 메시지 사라짐
timedatectl status
# Local time / Universal time / RTC time 값 유지, RTC in local TZ: no 로 표시됨
# 시스템 타임존(Timezone)을 한국(KST, UTC+9) 으로 설정 : 시스템 시간은 UTC 기준 유지, 표시만 KST로 변환
date
# Tue Jan 20 03:21:27 PM UTC 2026
timedatectl set-timezone Asia/Seoul
date
# Wed Jan 21 12:21:34 AM KST 2026 (UTC+9 로 변경된 것 확인)
# systemd가 시간 동기화 서비스(chronyd) 를 관리하도록 설정되어 있음 : ntpd 대신 chrony 사용 (Rocky 9/10 기본)
timedatectl status
# Local time: Wed 2026-01-21 00:21:38 KST
# Universal time: Tue 2026-01-20 15:21:38 UTC
# Time zone: Asia/Seoul (KST, +0900)
# System clock synchronized: yes, NTP service: active, RTC in local TZ: no
timedatectl set-ntp true
# System clock synchronized: yes -> NTP service: active
# chronyc 확인
# chrony가 어떤 NTP 서버들을 알고 있고, 그중 어떤 서버를 기준으로 시간을 맞추는지를 보여줍니다.
## Stratum 2: 매우 신뢰도 높은 서버
## Reach 377: 최근 8회 연속 응답 성공 (최대값)
chronyc sources -v
# MS Name/IP address Stratum Poll Reach LastRx Last sample
# ^- ec2-3-39-176-65.ap-north> 2 9 377 25 -784us[ -784us] +/- 9261us
# ^- 175.210.18.47 2 8 377 343 -558us[ -558us] +/- 21ms
# ^- 203.32.26.46 2 9 377 353 +15ms[ +15ms] +/- 226ms
# ^* 211.108.117.211 2 9 377 479 -382us[ -146us] +/- 6780us
# 현재 시스템 시간이 얼마나 정확한지 종합 성적표
chronyc tracking
# Reference ID : D36C75D3 (211.108.117.211)
# Stratum : 3
# System time : 0.000211809 seconds slow of NTP time
# Last offset : +0.000236014 seconds
# RMS offset : 0.009700730 seconds
# Frequency : 6.022 ppm slow
# Root delay : 0.013224309 seconds, Root dispersion : 0.004029782 seconds
# ...
SELinux 설정, firewalld(방화벽) 끄기
SELinux(Security Enhanced Linux)는 리눅스 커널에 적용되는 보안 기능으로, 시스템의 파일, 프로세스, 네트워크 등에서 발생할 수 있는 악의적이거나 비정상적인 접근을 세밀하게 제어하는 역할을 합니다. 기본적인 리눅스 권한(파일 소유자, 그룹, 퍼미션)만으로는 부족할 수 있는 보안을, 정책 기반의 다양한 규칙들로 한층 더 강화해줍니다.
즉, SELinux는 시스템에 접근하는 모든 행위에 대해 "정책에 의해 허용된 것인지"를 검사하고, 허용되지 않은 동작은 차단하거나 경고합니다.
Enforcing: SELinux가 활성화된 상태로, 보안 정책에 어긋나는 동작을 탐지하면 실제로 실행을 차단합니다. 즉, 허용되지 않은 접근 시도가 발생하면 해당 작업이 막히고, 로그에도 기록됩니다.
Permissive: SELinux는 적용되어 있지만, 정책을 어긴 동작이 발생해도 차단하지 않고 허용하며, 단지 로그로만 남깁니다. 디버깅이나 정책 검증에 주로 사용됩니다.
Kubernetes 공식 가이드에서는 예측하지 못한 차단으로 인한 문제를 줄이기 위해 "Permissive" 모드를 권장합니다. 실제 서비스 운영 환경에서는 보안 정책에 따라 "Enforcing"을 쓸 수도 있지만, 실습 및 문제 원인 파악에는 Permissive가 더 편리합니다.
# SELinux 설정 : Kubernetes는 Permissive 권장
getenforce
# Enforcing
sestatus
# SELinux status: enabled
# Current mode: enforcing
# Loaded policy name: targeted
# ...
setenforce 0
getenforce
# Permissive
sestatus
# SELinux status: enabled
# Current mode: permissive
# Loaded policy name: targeted
# ...
# 재부팅 시에도 Permissive 적용
cat /etc/selinux/config | grep ^SELINUX
# SELINUX=enforcing
# SELINUXTYPE=targeted
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
cat /etc/selinux/config | grep ^SELINUX
# SELINUX=permissive
# SELINUXTYPE=targeted
# firewalld(방화벽) 끄기
systemctl status firewalld
# ● firewalld.service - firewalld - dynamic firewall daemon
# Loaded: loaded (...; enabled; ...)
# Active: active (running) since Tue 2026-01-20 23:18:36 KST; 1h ...
# Main PID: 644 (firewalld)
# Tasks: 2 (limit: 18742)
# Memory: 49.9M (peak: 70.9M)
# ...
systemctl disable --now firewalld
# Removed '/etc/systemd/system/multi-user.target.wants/firewalld.service'.
# Removed '/etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service'.
systemctl status firewalld
# ○ firewalld.service - firewalld - dynamic firewall daemon
# Loaded: loaded (...; disabled; ...)
# Active: inactive (dead)
# ...
Swap 비활성화
Kubernetes는 안정적인 노드 자원 관리를 위해 swap이 꺼진 상태(비활성화)를 요구합니다. swap이 활성화되어 있으면 kubelet이 정상적으로 동작하지 않거나 클러스터 조인이 실패할 수 있으므로 반드시 swap을 비활성화해야 합니다.
Swap은 시스템의 물리적 메모리(RAM)가 부족할 때, 하드디스크의 일부 공간을 마치 메모리처럼 사용하는 기능입니다. 즉, 사용하지 않는 메모리 페이지를 디스크로 옮겨 일시적으로 여유 메모리를 확보하지만, 디스크 I/O 성능 저하로 인해 시스템 반응 속도가 느려질 수 있습니다.
# Swap 비활성화
lsblk
# NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
# sda 8:0 0 64G 0 disk
# ├─sda1 8:1 0 600M 0 part /boot/efi
# ├─sda2 8:2 0 3.8G 0 part [SWAP]
# └─sda3 8:3 0 59.6G 0 part /
free -h | grep Swap
# Swap: 3.8Gi 0B 3.8Gi
swapoff -a
lsblk
# NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
# sda 8:0 0 64G 0 disk
# ├─sda1 8:1 0 600M 0 part /boot/efi
# ├─sda2 8:2 0 3.8G 0 part
# └─sda3 8:3 0 59.6G 0 part /
free -h | grep Swap
# Swap: 0B 0B 0B
# 재부팅 시에도 'Swap 비활성화' 적용되도록 /etc/fstab에서 swap 라인 삭제
cat /etc/fstab | grep swap
# UUID=2270fed4-fef4-43c1-909f-b08a96bb14e9 none swap defaults 0 0
sed -i '/swap/d' /etc/fstab
cat /etc/fstab | grep swap
# (출력 없음: swap 항목 제거됨)
커널 모듈 및 커널 파라미터 설정 (네트워크 설정)
브릿지 트래픽이란, 리눅스 커널에서 네트워크 브릿지(bridge) 기능을 사용할 때, 두 네트워크 인터페이스를 연결하여 패킷이 서로를 통과하도록 하는 네트워크 트래픽을 의미합니다. 예를 들어, 가상머신(VM)이나 컨테이너 환경에서 여러 네트워크 인터페이스를 하나의 브릿지에 연결하면, 한 인터페이스로 들어온 데이터가 브릿지를 통해 다른 인터페이스로 전달될 수 있습니다.
기본적으로 네트워크 브릿지를 통과하는 트래픽은 브릿지 레이어에서만 처리되고, 리눅스의 iptables(방화벽) 규칙에는 적용되지 않습니다. 하지만 Kubernetes 같은 컨테이너 오케스트레이션 환경에서는 pod 간 통신, 서비스 접근 제어 등 다양한 네트워크 보안 정책을 구현할 때, 이 브릿지 트래픽도 iptables 규칙의 검사를 받게 해야 합니다.
즉, net.bridge.bridge-nf-call-iptables=1 과 같이 sysctl 파라미터를 설정하면 브릿지를 넘나드는 패킷도 iptables의 filter/NAT 룰을 적용받아서 보안 정책, 트래픽 제어, 네트워크 격리 등이 가능합니다.
# 커널 모듈 확인
lsmod
# Module Size Used by
# rfkill 36864 1
# nft_fib_inet 12288 0
# ...
lsmod | grep -iE 'overlay|br_netfilter'
# (처음에는 overlay / br_netfilter 모듈이 로드되어 있지 않음)
# 커널 모듈 로드
modprobe overlay
modprobe br_netfilter
lsmod | grep -iE 'overlay|br_netfilter'
# br_netfilter 32768 0
# bridge 327680 1 br_netfilter
# overlay 200704 0
# 부팅 시 자동 로드를 위해 설정 파일 생성
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
overlay
br_netfilter
tree /etc/modules-load.d/
# /etc/modules-load.d/
# └── k8s.conf
#
# 1 directory, 1 file
# 커널 파라미터 설정 : 네트워크 설정 - 브릿지 트래픽이 iptables를 거치도록 함
cat <<EOF | tee /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
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
tree /etc/sysctl.d/
# /etc/sysctl.d/
# ├── 99-sysctl.conf -> ../sysctl.conf
# └── k8s.conf
#
# 1 directory, 2 files
# 설정 적용
sysctl --system
# * Applying /usr/lib/sysctl.d/10-default-yama-scope.conf ...
# * Applying /usr/lib/sysctl.d/10-map-count.conf ...
# ...
# * Applying /etc/sysctl.d/k8s.conf ...
# net.bridge.bridge-nf-call-iptables = 1
# net.bridge.bridge-nf-call-ip6tables = 1
# net.ipv4.ip_forward = 1
# ...
# 적용 확인
sysctl net.bridge.bridge-nf-call-iptables
# net.bridge.bridge-nf-call-iptables = 1
sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 1
hosts 설정
# hosts 설정
cat /etc/hosts
# # Loopback entries; do not change.
# # For historical reasons, localhost precedes localhost.localdomain:
# 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
# ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
# # See hosts(5) for proper format and other examples:
# # 192.168.1.10 foo.example.org foo
# # 192.168.1.13 bar.example.org bar
# 127.0.1.1 k8s-ctr k8s-ctr
sed -i '/^127\.0\.\(1\|2\)\.1/d' /etc/hosts
cat << EOF >> /etc/hosts
192.168.10.100 k8s-ctr
192.168.10.101 k8s-w1
192.168.10.102 k8s-w2
EOF
cat /etc/hosts
# # Loopback entries; do not change.
# # For historical reasons, localhost precedes localhost.localdomain:
# 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
# ::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
# # See hosts(5) for proper format and other examples:
# # 192.168.1.10 foo.example.org foo
# # 192.168.1.13 bar.example.org bar
# 192.168.10.100 k8s-ctr
# 192.168.10.101 k8s-w1
# 192.168.10.102 k8s-w2
# 확인
ping -c 1 k8s-ctr
# PING k8s-ctr (192.168.10.100) 56(84) bytes of data.
# 64 bytes from k8s-ctr (192.168.10.100): icmp_seq=1 ttl=64 time=0.024 ms
# --- k8s-ctr ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
ping -c 1 k8s-w1
# PING k8s-w1 (192.168.10.101) 56(84) bytes of data.
# 64 bytes from k8s-w1 (192.168.10.101): icmp_seq=1 ttl=64 time=0.789 ms
# --- k8s-w1 ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
ping -c 1 k8s-w2
# PING k8s-w2 (192.168.10.102) 56(84) bytes of data.
# 64 bytes from k8s-w2 (192.168.10.102): icmp_seq=1 ttl=64 time=1.05 ms
# --- k8s-w2 ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
2. containerd(runc) v2.1.5 설치
containerd는 CNCF 'Graduated' 단계의 공식 컨테이너 런타임으로, 신뢰성과 범용성을 갖춘 오픈소스입니다.
주요 특징:
- 단순성, 견고함, 이식성에 중점을 둔 표준 컨테이너 런타임
- Linux/Windows에서 데몬으로 동작하며 컨테이너의 생애주기(이미지 관리, 실행, 네트워크/저장소 연결 등) 전체를 담당
- 개발자 또는 최종 사용자가 직접 사용하기보다는, Kubernetes 등 상위 시스템에 내장되어 사용되는 용도에 최적화됨
Containerd 와 K8S(1.32~1.34) 호환 버전 확인(2.1.0+, 2.0.1+, 1.7.24+, 1.6.36+…) - Docs
Kubernetes와 containerd 버전 호환성 표는 각 Kubernetes 버전에서 사용 가능한 containerd 버전을 보여줍니다. 예를 들어, Kubernetes 1.32는 containerd 2.1.0 이상, 2.0.1 이상, 1.7.24 이상, 1.6.36 이상 버전과 호환됩니다. 여러 버전이 나열된 경우, 그 중 하나를 선택하여 사용할 수 있습니다. CRI Version은 Container Runtime Interface 버전으로, Kubernetes와 컨테이너 런타임 간 통신 프로토콜 버전을 의미합니다. 현재 모든 버전이 CRI v1을 사용합니다.
| Kubernetes Version | containerd Version | CRI Version |
|---|---|---|
| 1.32* | 2.1.0+, 2.0.1+, 1.7.24+, 1.6.36+ | v1 |
| 1.33 | 2.1.0+, 2.0.4+, 1.7.24+, 1.6.36+ | v1 |
| 1.34 | 2.1.3+, 2.0.6+, 1.7.28+, 1.6.39+ | v1 |
| 1.35 | 2.2.0+, 2.1.5+, 1.7.28+ | v1 |
containerd 설정 파일 버전 호환성 표는 /etc/containerd/config.toml 파일에서 사용할 수 있는 설정 형식 버전과 해당 버전을 지원하는 최소 containerd 버전을 나타냅니다. Configuration Version 3은 containerd v2.0.0 이상에서만 사용 가능하며, 더 많은 기능과 향상된 설정 옵션을 제공합니다. 실습에서는 containerd v2.1.5를 사용하므로 Configuration Version 3을 사용할 수 있습니다.
Daemon Configuration : /etc/containerd/config.toml
| Configuration Version | Minimum containerd version |
|---|---|
| 1 | v1.0.0 |
| 2 | v1.3.0 |
| 3 | v2.0.0 |
설치
# dnf == yum, 버전 정보 확인
dnf
yum
dnf --version
yum --version
# Docker 저장소 추가 : dockerd 설치 X, containerd 설치 OK
dnf repolist
tree /etc/yum.repos.d/
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf repolist
tree /etc/yum.repos.d/
cat /etc/yum.repos.d/docker-ce.repo
dnf makecache
# 설치 가능한 모든 containerd.io 버전 확인
dnf list --showduplicates containerd.io
*Available Packages
containerd.io.aarch64 1.7.23-3.1.el10 docker-ce-stable
containerd.io.aarch64 1.7.24-3.1.el10 docker-ce-stable
containerd.io.aarch64 1.7.25-3.1.el10 docker-ce-stable
containerd.io.aarch64 1.7.26-3.1.el10 docker-ce-stable
containerd.io.aarch64 1.7.27-3.1.el10 docker-ce-stable
containerd.io.aarch64 1.7.28-1.el10 docker-ce-stable
containerd.io.aarch64 1.7.28-2.el10 docker-ce-stable
containerd.io.aarch64 1.7.29-1.el10 docker-ce-stable
containerd.io.aarch64 2.1.5-1.el10 docker-ce-stable
containerd.io.aarch64 2.2.0-2.el10 docker-ce-stable
containerd.io.aarch64 2.2.1-1.el10 docker-ce-stable*
# containerd 설치
dnf install -y containerd.io-2.1.5-1.el10
# Package Arch Version Repository Size
# Installing:
# containerd.io aarch64 2.1.5-1.el10 docker-ce-stable 31 M
# Transaction Summary
# Install 1 Package
# Total download size: 31 M
# Installed size: 110 M
# Downloading Packages:
# containerd.io-2.1.5-1.el10.aarch64.rpm 11 MB/s | 31 MB 00:02
# Importing GPG key 0x621E9F35:
# Userid : "Docker Release (CE rpm) <docker@docker.com>"
# Fingerprint: 060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35
# Installed:
# containerd.io-2.1.5-1.el10.aarch64
# Complete!
# 설치된 파일 확인
which runc && runc --version
# /usr/bin/runc
# runc version 1.3.3
# commit: v1.3.3-0-gd842d771
# spec: 1.2.1
# go: go1.24.9
# libseccomp: 2.5.3
which containerd && containerd --version
# /usr/bin/containerd
# containerd containerd.io v2.1.5 fcd43222d6b07379a4be9786bda52438f0dd16a1
which containerd-shim-runc-v2 && containerd-shim-runc-v2 -v
# /usr/bin/containerd-shim-runc-v2
# containerd-shim-runc-v2:
# Version: v2.1.5
# Revision: fcd43222d6b07379a4be9786bda52438f0dd16a1
# Go version: go1.24.9
which ctr && ctr --version
# /usr/bin/ctr
# ctr containerd.io v2.1.5
cat /etc/containerd/config.toml
# disabled_plugins = ["cri"]
# (기본 설정 파일은 CRI 플러그인이 비활성화된 상태로 생성됨)
tree /usr/lib/systemd/system | grep containerd
# ├── containerd.service
cat /usr/lib/systemd/system/containerd.service
# [Unit]
# Description=containerd container runtime
# Documentation=https://containerd.io
# After=network.target dbus.service
# [Service]
# ExecStartPre=-/sbin/modprobe overlay
# ExecStart=/usr/bin/containerd
# Type=notify
# Delegate=yes
# KillMode=process
# Restart=always
# RestartSec=5
# LimitNPROC=infinity
# LimitCORE=infinity
# TasksMax=infinity
# OOMScoreAdjust=-999
# [Install]
# WantedBy=multi-user.target
# 기본 설정 생성 및 SystemdCgroup 활성화 (매우 중요)
containerd config default | tee /etc/containerd/config.toml
# version = 3
# root = '/var/lib/containerd'
# state = '/run/containerd'
# temp = ''
# disabled_plugins = []
# required_plugins = []
# oom_score = 0
# imports = []
# [grpc]
# address = '/run/containerd/containerd.sock'
# tcp_address = ''
# uid = 0
# gid = 0
# [plugins]
# [plugins.'io.containerd.cri.v1.images']
# snapshotter = 'overlayfs'
# [plugins.'io.containerd.cri.v1.runtime']
# [plugins.'io.containerd.cri.v1.runtime'.containerd]
# default_runtime_name = 'runc'
# [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc]
# runtime_type = 'io.containerd.runc.v2'
# [plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
# SystemdCgroup = false
# (기본 설정 파일이 생성됨, SystemdCgroup = false 상태)
# https://v1-32.docs.kubernetes.io/ko/docs/setup/production-environment/container-runtimes/#cgroupfs-cgroup-driver
# cgroupfs 드라이버는 kubelet의 기본 cgroup 드라이버이다.
# cgroupfs 드라이버가 사용될 때, kubelet과 컨테이너 런타임은 직접적으로 cgroup 파일시스템과 상호작용하여 cgroup들을 설정한다.
# cgroupfs 드라이버가 권장되지 않는 때가 있는데, systemd가 init 시스템인 경우이다.
# 이것은 systemd가 시스템에 단 하나의 cgroup 관리자만 있을 것으로 기대하기 때문이다.
# 또한, cgroup v2를 사용할 경우에도 cgroupfs 대신 systemd cgroup 드라이버를 사용한다.
# -----------------------------------------------------------
# https://github.com/containerd/containerd/blob/main/docs/cri/config.md
*## In containerd 2.x
version = 3
[plugins.'io.containerd.cri.v1.images']
snapshotter = "overlayfs"
## In containerd 1.x
version = 2
[plugins."io.containerd.grpc.v1.cri".containerd]
snapshotter = "overlayfs"*
# -----------------------------------------------------------
cat /etc/containerd/config.toml | grep -i systemdcgroup
# SystemdCgroup = false
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
cat /etc/containerd/config.toml | grep -i systemdcgroup
# SystemdCgroup = true
# systemd unit 파일 최신 상태 읽기
systemctl daemon-reload
# containerd start 와 enabled
systemctl enable --now containerd
# Created symlink '/etc/systemd/system/multi-user.target.wants/containerd.service' → '/usr/lib/systemd/system/containerd.service'.
#
systemctl status containerd --no-pager
# ● containerd.service - containerd container runtime
# Loaded: loaded (/usr/lib/systemd/system/containerd.service; enabled; preset: disabled)
# Active: active (running) since Wed 2026-01-21 01:49:11 KST; 4s ago
# Docs: https://containerd.io
# Process: 5337 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS)
# Main PID: 5338 (containerd)
# Tasks: 9
# Memory: 15.1M (peak: 23.9M)
# CPU: 43ms
# CGroup: /system.slice/containerd.service
# └─5338 /usr/bin/containerd
journalctl -u containerd.service --no-pager
# Jan 21 01:49:11 k8s-ctr systemd[1]: Starting containerd.service - containerd container runtime...
# Jan 21 01:49:11 k8s-ctr containerd[5338]: time="2026-01-21T01:49:11.389012040+09:00" level=info msg="starting containerd" revision=fcd43222d6b07379a4be9786bda52438f0dd16a1 version=v2.1.5
# Jan 21 01:49:11 k8s-ctr containerd[5338]: time="2026-01-21T01:49:11.398493722+09:00" level=info msg="Connect containerd service"
# Jan 21 01:49:11 k8s-ctr containerd[5338]: time="2026-01-21T01:49:11.404242840+09:00" level=info msg=serving... address=/run/containerd/containerd.sock
# Jan 21 01:49:11 k8s-ctr containerd[5338]: time="2026-01-21T01:49:11.404430841+09:00" level=info msg="containerd successfully booted in 0.016186s"
# Jan 21 01:49:11 k8s-ctr systemd[1]: Started containerd.service - containerd container runtime.
pstree -alnp
# systemd,1
# └─containerd,5338
# ├─{containerd},5340
# ├─{containerd},5341
# └─... (containerd 프로세스 트리 확인)
systemd-cgls --no-pager
# CGroup /:
# └─system.slice
# └─containerd.service
# └─5338 /usr/bin/containerd
# containerd의 유닉스 도메인 소켓 확인 : kubelet에서 사용 , containerd client 3종(ctr, nerdctr, crictl)도 사용
containerd config dump | grep -n containerd.sock
# 11: address = '/run/containerd/containerd.sock'
ls -l /run/containerd/containerd.sock
# srw-rw----. 1 root root 0 Jan 21 01:49 /run/containerd/containerd.sock
ss -xl | grep containerd
# u_str LISTEN 0 4096 /run/containerd/containerd.sock.ttrpc 26779 * 0
# u_str LISTEN 0 4096 /run/containerd/containerd.sock 24836 * 0
ss -xnp | grep containerd
# u_str ESTAB 0 0 * 24831 * 26766 users:(("containerd",pid=5338,fd=2),("containerd",pid=5338,fd=1))
# 플러그인 확인
ctr --address /run/containerd/containerd.sock version
# Client:
# Version: v2.1.5
# Revision: fcd43222d6b07379a4be9786bda52438f0dd16a1
# Go version: go1.24.9
# Server:
# Version: v2.1.5
# Revision: fcd43222d6b07379a4be9786bda52438f0dd16a1
# UUID: 5c9ff538-c469-424c-9027-d5b9904302e9
ctr plugins ls
# TYPE ID PLATFORMS STATUS
# io.containerd.content.v1 content - ok # 이미지 레이어 저장
# io.containerd.snapshotter.v1 native linux/arm64/v8 ok # 기본 스냅샷터, 단순 파일시스템 스냅샷 생성/복원
# io.containerd.snapshotter.v1 overlayfs linux/arm64/v8 ok # Kubernetes 기본 snapshotter, Overlay 파일시스템 사용
# io.containerd.snapshotter.v1 zfs linux/arm64/v8 skip # ZFS 볼륨 스냅샷 지원, 현재 사용되지 않음(skip)
# io.containerd.metadata.v1 bolt - ok # 메타데이터 DB (bolt), 컨테이너/이미지 정보 저장
# io.containerd.cri.v1 images - ok # Kubernetes CRI 이미지 기능 인터페이스
# io.containerd.cri.v1 runtime linux/arm64/v8 ok # 컨테이너 런타임 기능 (Kubernetes CRI용)
# io.containerd.grpc.v1 cri - ok # Kubernetes CRI와 gRPC 통신 지원

위 그림은 command-line containerd clients(ctr, nerdctl, crictl)가 containerd 데몬과 어떻게 통신하는지를 보여줍니다.
ctr와 nerdctl는 /run/containerd/containerd.sock 유닉스 도메인 소켓을 통해 직접 gRPC로 containerd에 요청을 보내고, crictl는 Kubernetes CRI API를 통해 간접적으로 containerd에 접근합니다. containerd는 containerd-shim을 거쳐 OCI 런타임(runc)을 호출하여 실제 컨테이너 프로세스를 생성·관리합니다.
3. [공통] kubeadm, kubelet 및 kubectl 설치 v1.32.11
kubeadm, kubelet, kubectl은 Kubernetes 클러스터 구축 및 관리에 필수적인 세 가지 핵심 도구입니다. 이 세 도구는 반드시 동일한 버전으로 설치되어야 하며, 버전 불일치는 클러스터 오작동을 유발할 수 있습니다.
- kubeadm: 클러스터를 부트스트랩하고 초기화하는 도구로, 컨트롤 플레인 노드와 워커 노드를 클러스터에 조인시키는 역할을 합니다.
- kubelet: 각 노드에서 실행되는 에이전트로, 컨테이너 런타임(containerd)과 통신하여 Pod를 생성·관리합니다.
- kubectl: 클러스터와 상호작용하기 위한 명령줄 도구로, 리소스 생성, 조회, 삭제 등의 작업을 수행합니다.
중요 사항:
- Kubernetes 저장소 설정 시
exclude옵션을 사용하여 실수로dnf update시 자동 업그레이드되는 것을 방지합니다. --disableexcludes=kubernetes옵션을 사용하면 exclude 규칙을 일시적으로 무시하고 설치할 수 있습니다.kubelet은kubeadm init실행 전에는/var/lib/kubelet/config.yaml파일이 없어서 시작에 실패하는 것이 정상입니다.
# repo 추가
## exclude=... : 실수로 dnf update 시 kubelet 자동 업그레이드 방지
dnf repolist
# repo id repo name
# appstream Rocky Linux 10 - AppStream
# baseos Rocky Linux 10 - BaseOS
# docker-ce-stable Docker CE Stable - aarch64
# extras Rocky Linux 10 - Extras
tree /etc/yum.repos.d/
# /etc/yum.repos.d/
# ├── docker-ce.repo
# ├── rocky-addons.repo
# ├── rocky-devel.repo
# ├── rocky-extras.repo
# └── rocky.repo
# 해당 k8s 버전으로 설치
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.32/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.32/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
# [kubernetes]
# name=Kubernetes
# baseurl=https://pkgs.k8s.io/core:/stable:/v1.32/rpm/
# enabled=1
# gpgcheck=1
# gpgkey=https://pkgs.k8s.io/core:/stable:/v1.32/rpm/repodata/repomd.xml.key
# exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
dnf makecache
# Docker CE Stable - aarch64 13 kB/s | 2.0 kB 00:00
# Kubernetes 11 kB/s | 19 kB 00:01
# Rocky Linux 10 - BaseOS 744 B/s | 4.3 kB 00:05
# Rocky Linux 10 - AppStream 6.9 kB/s | 4.3 kB 00:00
# Rocky Linux 10 - Extras 3.5 kB/s | 3.1 kB 00:00
# Metadata cache created.
# 설치
## --disableexcludes=... kubernetes repo에 설정된 exclude 규칙을 이번 설치에서만 무시(1회성 옵션 처럼 사용)
## 설치 가능 버전 확인
dnf list --showduplicates kubelet # '--disableexcludes=kubernetes' 아래 처럼 있는 경우와 없는 경우 비교해보기
# Error: No matching Packages to list (exclude 규칙으로 인해 목록이 비어있음)
dnf list --showduplicates kubelet --disableexcludes=kubernetes
# Available Packages
# kubelet.aarch64 1.32.0-150500.1.1 kubernetes
# kubelet.aarch64 1.32.1-150500.1.1 kubernetes
# ...
# kubelet.aarch64 1.32.11-150500.1.1 kubernetes
dnf list --showduplicates kubeadm --disableexcludes=kubernetes
# Available Packages
# kubeadm.aarch64 1.32.0-150500.1.1 kubernetes
# kubeadm.aarch64 1.32.1-150500.1.1 kubernetes
# ...
# kubeadm.aarch64 1.32.11-150500.1.1 kubernetes
dnf list --showduplicates kubectl --disableexcludes=kubernetes
# Available Packages
# kubectl.aarch64 1.32.0-150500.1.1 kubernetes
# kubectl.aarch64 1.32.1-150500.1.1 kubernetes
# ...
# kubectl.aarch64 1.32.11-150500.1.1 kubernetes
## 버전 정보 미지정 시, 제공 가능 최신 버전 설치됨.
dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
# Dependencies resolved.
# =====================================================================
# Package Arch Version Repository Size
# =====================================================================
# Installing:
# kubeadm aarch64 1.32.11-150500.1.1 kubernetes 10 M
# kubectl aarch64 1.32.11-150500.1.1 kubernetes 9.4 M
# kubelet aarch64 1.32.11-150500.1.1 kubernetes 13 M
# Installing dependencies:
# cri-tools aarch64 1.32.0-150500.1.1 kubernetes 6.2 M
# kubernetes-cni aarch64 1.6.0-150500.1.1 kubernetes 7.2 M
# Transaction Summary
# Install 5 Packages
# Total download size: 46 M
# Installed size: 280 M
# Importing GPG key 0x9A296436:
# Userid : "isv:kubernetes OBS Project <isv:kubernetes@build.opensuse.org>"
# Fingerprint: DE15 B144 86CD 377B 9E87 6E1A 2346 54DA 9A29 6436
# Installed:
# cri-tools-1.32.0-150500.1.1.aarch64
# kubeadm-1.32.11-150500.1.1.aarch64
# kubectl-1.32.11-150500.1.1.aarch64
# kubelet-1.32.11-150500.1.1.aarch64
# kubernetes-cni-1.6.0-150500.1.1.aarch64
# Complete!
# kubelet 활성화 (실제 기동은 kubeadm init 후에 시작됨)
systemctl enable --now kubelet
# Created symlink '/etc/systemd/system/multi-user.target.wants/kubelet.service' → '/usr/lib/systemd/system/kubelet.service'.
ps -ef |grep kubelet
# (kubelet 프로세스는 아직 실행되지 않음 - kubeadm init 전에는 config.yaml이 없어서 실패)
# 설치 파일들 확인
which kubeadm && kubeadm version -o yaml
# /usr/bin/kubeadm
# clientVersion:
# buildDate: "2025-12-16T18:06:36Z"
# compiler: gc
# gitCommit: 2195eae9e91f2e72114365d9bb9c670d0c08de12
# gitTreeState: clean
# gitVersion: v1.32.11
# goVersion: go1.24.11
# major: "1"
# minor: "32"
# platform: linux/arm64
which kubectl && kubectl version --client=true
# /usr/bin/kubectl
# Client Version: v1.32.11
# Kustomize Version: v5.5.0
which kubelet && kubelet --version
# /usr/bin/kubelet
# Kubernetes v1.32.11
# cri-tools
which crictl && crictl version
# /usr/bin/crictl
# WARN[0000] Config "/etc/crictl.yaml" does not exist, trying next: "/usr/bin/crictl.yaml"
# 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.
# Version: 0.1.0
# RuntimeName: containerd
# RuntimeVersion: v2.1.5
# RuntimeApiVersion: v1
# /etc/crictl.yaml 파일 작성
cat << EOF > /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
EOF
# crictl을 통한 containerd 정보 확인 (설명: crictl은 Kubernetes CRI API를 통해 containerd와 통신)
crictl info | jq
{
"cniconfig": {
"Networks": [
{
"Config": {
"CNIVersion": "0.3.1",
"Name": "cni-loopback",
"Plugins": [
{
"Network": {
"ipam": {},
"type": "loopback"
},
"Source": "{\"type\":\"loopback\"}"
}
],
"Source": "{\n\"cniVersion\": \"0.3.1\",\n\"name\": \"cni-loopback\",\n\"plugins\": [{\n \"type\": \"loopback\"\n}]\n}"
},
"IFName": "lo"
}
],
"PluginConfDir": "/etc/cni/net.d",
"PluginDirs": [
"/opt/cni/bin"
],
"PluginMaxConfNum": 1,
"Prefix": "eth"
},
...
"containerdEndpoint": "/run/containerd/containerd.sock",
"containerdRootDir": "/var/lib/containerd",
...
"status": {
...
{
"message": "Network plugin returns error: cni plugin not initialized",
"reason": "NetworkPluginNotReady",
"status": false,
"type": "NetworkReady"
},
# kubernetes-cni : 파드 네트워크 구성을 위한 CNI 바이너리 파일 확인
ls -al /opt/cni/bin
# total 63200
# drwxr-xr-x. 2 root root 4096 Jan 21 02:09 .
# drwxr-xr-x. 3 root root 17 Jan 21 02:09 ..
# -rwxr-xr-x. 1 root root 3239200 Dec 12 2024 bandwidth
# -rwxr-xr-x. 1 root root 3731632 Dec 12 2024 bridge
# -rwxr-xr-x. 1 root root 9123544 Dec 12 2024 dhcp
# -rwxr-xr-x. 1 root root 3379872 Dec 12 2024 dummy
# -rwxr-xr-x. 1 root root 3742888 Dec 12 2024 firewall
# -rwxr-xr-x. 1 root root 3383408 Dec 12 2024 host-device
# -rwxr-xr-x. 1 root root 2812400 Dec 12 2024 host-local
# -rwxr-xr-x. 1 root root 3380928 Dec 12 2024 ipvlan
# -rwxr-xr-x. 1 root root 2953200 Dec 12 2024 loopback
# -rwxr-xr-x. 1 root root 3448024 Dec 12 2024 macvlan
# -rwxr-xr-x. 1 root root 3312488 Dec 12 2024 portmap
# -rwxr-xr-x. 1 root root 3524072 Dec 12 2024 ptp
# -rwxr-xr-x. 1 root root 3091976 Dec 12 2024 sbr
# -rwxr-xr-x. 1 root root 2526944 Dec 12 2024 static
# -rwxr-xr-x. 1 root root 3516272 Dec 12 2024 tap
# -rwxr-xr-x. 1 root root 2956032 Dec 12 2024 tuning
# -rwxr-xr-x. 1 root root 3380544 Dec 12 2024 vlan
# -rwxr-xr-x. 1 root root 3160560 Dec 12 2024 vrf
tree /opt/cni
# /opt/cni
# └── bin
# ├── bandwidth
# ├── bridge
# ├── dhcp
# ├── dummy
# ├── firewall
# ├── host-device
# ├── host-local
# ├── ipvlan
# ├── LICENSE
# ├── loopback
# ├── macvlan
# ├── portmap
# ├── ptp
# ├── README.md
# ├── sbr
# ├── static
# ├── tap
# ├── tuning
# ├── vlan
# └── vrf
# 2 directories, 20 files
tree /etc/cni/
# /etc/cni/
# └── net.d
# 2 directories, 0 files
# kubelet 상태 확인 (kubeadm init 전에는 config.yaml이 없어서 실패하는 것이 정상)
systemctl is-active kubelet
# activating
systemctl status kubelet --no-pager
# ● kubelet.service - kubelet: The Kubernetes Node Agent
# Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; preset: disabled)
# Drop-In: /usr/lib/systemd/system/kubelet.service.d
# └─10-kubeadm.conf
# Active: activating (auto-restart) (Result: exit-code) since Wed 2026-01-21 02:11:14 KST; 71ms ago
# Process: 5752 ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS (code=exited, status=1/FAILURE)
# Main PID: 5752 (code=exited, status=1/FAILURE)
journalctl -u kubelet --no-pager
# E0121 02:09:42.541728 5622 run.go:72] "command failed" err="failed to load kubelet config file, path: /var/lib/kubelet/config.yaml, error: failed to load Kubelet config file /var/lib/kubelet/config.yaml, error failed to read kubelet config file \"/var/lib/kubelet/config.yaml\", error: open /var/lib/kubelet/config.yaml: no such file or directory"
# (kubeadm init 전에는 /var/lib/kubelet/config.yaml이 없어서 kubelet이 시작되지 않음 - 정상적인 동작)
tree /usr/lib/systemd/system | grep kubelet -A1
# ├── kubelet.service
# ├── kubelet.service.d
# │ └── 10-kubeadm.conf
cat /usr/lib/systemd/system/kubelet.service
# [Unit]
# Description=kubelet: The Kubernetes Node Agent
# Documentation=https://kubernetes.io/docs/
# Wants=network-online.target
# After=network-online.target
# [Service]
# ExecStart=/usr/bin/kubelet
# Restart=always
# StartLimitInterval=0
# RestartSec=10
# [Install]
# WantedBy=multi-user.target
cat /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf
# # Note: This dropin only works with kubeadm and kubelet v1.11+
# [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/sysconfig/kubelet
# ExecStart=
# ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS
tree /etc/kubernetes
# /etc/kubernetes
# └── manifests
# 2 directories, 0 files
tree /var/lib/kubelet
# /var/lib/kubelet
# 0 directories, 0 files
cat /etc/sysconfig/kubelet
# KUBELET_EXTRA_ARGS=
# cgroup , namespace 정보 확인
systemd-cgls --no-pager
# CGroup /:
# └─system.slice
# ├─containerd.service
# │ └─5338 /usr/bin/containerd
# └─... (기타 서비스들)
lsns
# NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 134 1 root /usr/lib/systemd/systemd
# 4026531835 cgroup 134 1 root /usr/lib/systemd/systemd
# 4026531836 pid 134 1 root /usr/lib/systemd/systemd
# 4026531837 user 133 1 root /usr/lib/systemd/systemd
# 4026531838 uts 125 1 root /usr/lib/systemd/systemd
# 4026531839 ipc 134 1 root /usr/lib/systemd/systemd
# 4026531840 net 132 1 root /usr/lib/systemd/systemd
# 4026531841 mnt 119 1 root /usr/lib/systemd/systemd
# containerd의 유닉스 도메인 소켓 확인 : kubelet에서 사용 , containerd client 3종(ctr, nerdctr, crictl)도 사용
ls -l /run/containerd/containerd.sock
# srw-rw----. 1 root root 0 Jan 21 01:49 /run/containerd/containerd.sock
ss -xl | grep containerd
# u_str LISTEN 0 4096 /run/containerd/containerd.sock.ttrpc 26779 * 0
# u_str LISTEN 0 4096 /run/containerd/containerd.sock 24836 * 0
ss -xnp | grep containerd
# u_str ESTAB 0 0 * 24831 * 26766 users:(("containerd",pid=5338,fd=2),("containerd",pid=5338,fd=1))
4. [k8s-ctr] kubeadm 으로 k8s 클러스터 구성 & Flannel CNI 설치 v0.27.3 & 편의성 설정 등
kubeadm init 명령은 Kubernetes 클러스터의 컨트롤 플레인을 초기화하는 핵심 단계입니다.
이 과정은 다음과 같은 순서로 진행됩니다.
1. 사전 검사 (Preflight Checks)
kubeadm은 클러스터 초기화 전에 시스템 상태를 검사합니다. CRI(Container Runtime Interface) 엔드포인트 연결 확인, 루트 권한 확인, kubelet 버전이 kubeadm이 지원하는 최소 버전(current minor -1) 이상인지 확인합니다. 이러한 검사에서 오류가 발생하면 초기화가 중단됩니다.
2. 인증서 및 키 생성 (Certificates)
Control Plane 구성 요소들이 안전하게 통신할 수 있도록 /etc/kubernetes/pki 디렉토리에 인증서와 키를 생성합니다. 생성되는 인증서는 CA 인증서(클러스터 전체 기본 신뢰체계), API Server용 서버 인증서, API Server와 kubelet 간 통신용 인증서, Front-Proxy CA 및 Front-Proxy 클라이언트 인증서, 그리고 local etcd를 사용하는 경우 etcd 관련 인증서 및 키입니다.
3. kubeconfig 파일 생성
control plane components를 위한 kubeconfig 파일을 /etc/kubernetes 디렉토리에 생성합니다. 생성되는 파일은 kubelet이 TLS bootstrap 중 사용하는 /etc/kubernetes/bootstrap-kubelet.conf, controller-manager용 /etc/kubernetes/controller-manager.conf, scheduler용 /etc/kubernetes/scheduler.conf, 그리고 클러스터 관리용 /etc/kubernetes/admin.conf와 /etc/kubernetes/super-admin.conf입니다.
4. Control Plane 구성 요소 생성 (Static Pods)
kubeadm은 /etc/kubernetes/manifests 디렉토리에 Static Pod 매니페스트를 생성하여 kubelet이 자동으로 Control Plane 구성 요소를 실행하도록 합니다. 모든 Static Pod는 kube-system 네임스페이스에 배포되며, tier:control-plane과 component:{component-name} 레이블을 가집니다. 또한 system-node-critical 우선순위 클래스를 사용하고, Control Plane 시작을 위해 hostNetwork: true가 설정됩니다. 생성되는 구성 요소는 kube-apiserver, kube-controller-manager, kube-scheduler, 그리고 local etcd를 사용하는 경우 etcd입니다.
5. kubelet 시작 및 대기
kubeadm은 manifest 적용 후 kubelet을 재시작하거나 설정을 적용하고, Control Plane이 성공적으로 시작될 때까지 대기합니다. 특히 API Server가 정상적으로 건강 상태를 반환할 때까지 /healthz 또는 /livez 엔드포인트를 확인하며 대기합니다.
6. ClusterConfiguration을 configMap에 저장
kubeadm은 나중에 참조할 수 있도록 ClusterConfiguration을 kube-system 네임스페이스의 kubeadm-config ConfigMap에 저장합니다. 이 ConfigMap은 kubectl get cm -n kube-system kubeadm-config 명령으로 확인할 수 있습니다.
7. 노드 라벨링 및 태인트
컨트롤 플레인 노드에 node-role.kubernetes.io/control-plane="" 레이블을 추가하고, node-role.kubernetes.io/control-plane:NoSchedule 태인트를 적용합니다. 이 태인트는 일반 워크로드가 Control Plane 노드에 스케줄되지 않도록 방지합니다.
8. Bootstrap 토큰 생성 및 클러스터 부트스트랩 설정
Control Plane에 부트스트랩 토큰을 생성하고, 각 노드가 클러스터에 합류할 수 있도록 ConfigMap, RBAC 규칙, CSR 접근 권한 등을 설정합니다. 특히 kube-public 네임스페이스에 공개 cluster-info ConfigMap을 생성하며, 이는 '신원 확인 전, 최소한의 신뢰 부트스트랩 데이터'로 사용됩니다. 또한 인증되지 않은 사용자(RBAC 그룹 system:unauthenticated)가 이 ConfigMap에 접근할 수 있도록 Role과 RoleBinding을 생성합니다. 부트스트랩 토큰은 기본적으로 24시간 유효하며, kubeadm join 명령 시 사용할 수 있습니다.
9. 필수 애드온 설치
kube-proxy와 CoreDNS를 설치합니다. kube-proxy의 경우 kube-system 네임스페이스에 ServiceAccount를 생성한 후 DaemonSet으로 배포됩니다.
CoreDNS의 경우 레거시 kube-dns 애드온과의 호환성을 위해 서비스 이름이 kube-dns로 지정됩니다. kube-system 네임스페이스에 CoreDNS용 ServiceAccount를 생성하고, coredns ServiceAccount를 system:coredns ClusterRole의 권한에 바인딩합니다. 참고로 Kubernetes 버전 1.21부터는 kubeadm에서 kube-dns 사용 지원이 제거되었습니다.
# 기본 환경 정보 출력 저장
crictl images
# (kubeadm init 전에는 이미지가 없음)
crictl ps
# (kubeadm init 전에는 컨테이너가 없음)
cat /etc/sysconfig/kubelet
# KUBELET_EXTRA_ARGS=
tree /etc/kubernetes | tee -a etc_kubernetes-1.txt
# /etc/kubernetes
# └── manifests
# 2 directories, 0 files
tree /var/lib/kubelet | tee -a var_lib_kubelet-1.txt
# /var/lib/kubelet
# 0 directories, 0 files
tree /run/containerd/ -L 3 | tee -a run_containerd-1.txt
# /run/containerd/
# ├── containerd.sock
# ├── containerd.sock.ttrpc
# ├── io.containerd.grpc.v1.cri
# ├── io.containerd.runtime.v2.task
# └── io.containerd.sandbox.controller.v1.shim
# 4 directories, 2 files
pstree -alnp | tee -a pstree-1.txt
# systemd,1 --switched-root --system --deserialize=46 no_timer_check
# |-systemd-journal,457
# |-systemd-userdbd,495
# | |-systemd-userwor,6500
# | |-systemd-userwor,6501
# | `-systemd-userwor,6759
# |-systemd-udevd,505
# |-rpcbind,591 -w -f
# |-auditd,593
# | |-{auditd},594
# | |-sedispatch,595
# | `-{auditd},596
# |-dbus-broker-lau,633 --scope system --audit
# | `-dbus-broker,641 --log 4 --controller 9 --machine-id df4a7ea6da70431281c0868cf44eae5a --max-bytes 536870912 --max-fds 4096 --max-matches 131072 --audit
# |-irqbalance,645
# | `-{irqbalance},663
# |-lsmd,646 -d
# |-systemd-logind,652
# |-chronyd,665 -F 2
# |-VBoxService,763 --pidfile /var/run/vboxadd-service.sh
# | |-{VBoxService},768
# | |-{VBoxService},769
# | |-{VBoxService},771
# | |-{VBoxService},772
# | |-{VBoxService},774
# | |-{VBoxService},776
# | |-{VBoxService},777
# | `-{VBoxService},779
# |-polkitd,812 --no-debug --log-level=err
# | |-{polkitd},814
# | |-{polkitd},815
# | `-{polkitd},816
# |-gssproxy,839 -i
# | |-{gssproxy},866
# | |-{gssproxy},867
# | |-{gssproxy},868
# | |-{gssproxy},869
# | `-{gssproxy},870
# |-sshd,849
# | `-sshd-session,4496
# | `-sshd-session,4518
# | `-bash,4519
# | `-sudo,4584 su -
# | `-sudo,4586 su -
# | `-su,4587 -
# | `-bash,4588
# | |-pstree,6883 -alnp
# | `-tee,6884 -a pstree-1.txt
# |-tuned,851 -Es /usr/sbin/tuned -l -P
# | |-{tuned},1111
# | |-{tuned},1112
# | `-{tuned},1113
# |-atd,877 -f
# |-crond,882 -n
# |-agetty,885 -o -- \\u --noreset --noclear - linux
# |-rsyslogd,1133 -n
# | |-{rsyslogd},1148
# | `-{rsyslogd},1151
# |-NetworkManager,3044 --no-daemon
# | |-{NetworkManager},3045
# | |-{NetworkManager},3046
# | `-{NetworkManager},3047
# |-systemd,4501 --user
# | `-(sd-pam),4503
# |-udisksd,4709
# | |-{udisksd},4710
# | |-{udisksd},4711
# | |-{udisksd},4713
# | |-{udisksd},4714
# | |-{udisksd},4715
# | `-{udisksd},4716
# `-containerd,5338
# |-{containerd},5340
# |-{containerd},5341
# |-{containerd},5342
# |-{containerd},5343
# |-{containerd},5344
# |-{containerd},5345
# |-{containerd},5346
# |-{containerd},5347
# `-{containerd},6430
systemd-cgls --no-pager | tee -a systemd-cgls-1.txt
# CGroup /:
# -.slice
# ├─user.slice
# │ └─user-1000.slice
# │ ├─user@1000.service …
# │ │ └─init.scope
# │ │ ├─4501 /usr/lib/systemd/systemd --user
# │ │ └─4503 (sd-pam)
# │ └─session-4.scope
# │ ├─4496 sshd-session: vagrant [priv]
# │ ├─4518 sshd-session: vagrant@pts/0
# │ ├─4519 -bash
# │ ├─4584 sudo su -
# │ ├─4586 sudo su -
# │ ├─4587 su -
# │ ├─4588 -bash
# │ ├─6893 systemd-cgls --no-pager
# │ └─6894 tee -a systemd-cgls-1.txt
# ├─init.scope
# │ └─1 /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_tim…
# └─system.slice
# ├─irqbalance.service
# │ └─645 /usr/sbin/irqbalance
# ├─libstoragemgmt.service
# │ └─646 /usr/bin/lsmd -d
# ├─vboxadd-service.service
# │ └─763 /usr/sbin/VBoxService --pidfile /var/run/vboxadd-service.sh
# ├─containerd.service …
# │ └─5338 /usr/bin/containerd
# ├─systemd-udevd.service …
# │ └─udev
# │ └─505 /usr/lib/systemd/systemd-udevd
# ├─dbus-broker.service
# │ ├─633 /usr/bin/dbus-broker-launch --scope system --audit
# │ └─641 dbus-broker --log 4 --controller 9 --machine-id df4a7ea6da70431281c08…
# ├─polkit.service
# │ └─812 /usr/lib/polkit-1/polkitd --no-debug --log-level=err
# ├─chronyd.service
# │ └─665 /usr/sbin/chronyd -F 2
# ├─auditd.service
# │ ├─593 /usr/sbin/auditd
# │ └─595 /usr/sbin/sedispatch
# ├─tuned.service
# │ └─851 /usr/bin/python3 -Es /usr/sbin/tuned -l -P
# ├─kubelet.service
# │ └─6885 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-ku…
# ├─systemd-journald.service
# │ └─457 /usr/lib/systemd/systemd-journald
# ├─atd.service
# │ └─877 /usr/sbin/atd -f
# ├─sshd.service
# │ └─849 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
# ├─crond.service
# │ └─882 /usr/sbin/crond -n
# ├─NetworkManager.service
# │ └─3044 /usr/sbin/NetworkManager --no-daemon
# ├─gssproxy.service
# │ └─839 /usr/sbin/gssproxy -i
# ├─rsyslog.service
# │ └─1133 /usr/sbin/rsyslogd -n
# ├─systemd-userdbd.service
# │ ├─ 495 /usr/lib/systemd/systemd-userdbd
# │ ├─6500 systemd-userwork: waiting...
# │ ├─6501 systemd-userwork: waiting...
# │ └─6759 systemd-userwork: waiting...
# ├─rpcbind.service
# │ └─591 /usr/bin/rpcbind -w -f
# ├─udisks2.service
# │ └─4709 /usr/libexec/udisks2/udisksd
# ├─system-getty.slice
# │ └─getty@tty1.service
# │ └─885 /sbin/agetty -o -- \u --noreset --noclear - linux
# └─systemd-logind.service
# └─652 /usr/lib/systemd/systemd-logind
lsns | tee -a lsns-1.txt
# NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 192 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531835 cgroup 192 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531836 pid 192 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531837 user 191 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531838 uts 183 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531839 ipc 192 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531840 net 190 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531841 mnt 177 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026532080 mnt 1 505 root ├─/usr/lib/systemd/systemd-udevd
# 4026532081 mnt 4 495 root ├─/usr/lib/systemd/systemd-userdbd
# 4026532103 uts 4 495 root ├─/usr/lib/systemd/systemd-userdbd
# 4026532104 uts 1 505 root ├─/usr/lib/systemd/systemd-udevd
# 4026532124 mnt 2 633 dbus ├─/usr/bin/dbus-broker-launch --scope system --audit
# 4026532125 mnt 1 665 chrony ├─/usr/sbin/chronyd -F 2
# 4026532127 mnt 1 3044 root ├─/usr/sbin/NetworkManager --no-daemon
# 4026532128 net 1 645 root ├─/usr/sbin/irqbalance
# 4026532188 mnt 1 645 root ├─/usr/sbin/irqbalance
# 4026532190 uts 1 665 chrony ├─/usr/sbin/chronyd -F 2
# 4026532194 uts 1 645 root ├─/usr/sbin/irqbalance
# 4026532196 user 1 645 root ├─/usr/sbin/irqbalance
# 4026532199 mnt 1 652 root ├─/usr/lib/systemd/systemd-logind
# 4026532200 uts 1 652 root ├─/usr/lib/systemd/systemd-logind
# 4026532202 net 1 812 polkitd ├─/usr/lib/polkit-1/polkitd --no-debug --log-level=err
# 4026532265 mnt 1 812 polkitd ├─/usr/lib/polkit-1/polkitd --no-debug --log-level=err
# 4026532266 uts 1 812 polkitd ├─/usr/lib/polkit-1/polkitd --no-debug --log-level=err
# 4026532340 mnt 1 1133 root ├─/usr/sbin/rsyslogd -n
# 4026532342 mnt 1 839 root └─/usr/sbin/gssproxy -i
# 4026531862 mnt 1 38 root kdevtmpfs
ip addr | tee -a ip_addr-1.txt
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# inet 127.0.0.1/8 scope host lo
# valid_lft forever preferred_lft forever
# inet6 ::1/128 scope host noprefixroute
# valid_lft forever preferred_lft forever
# 2: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# link/ether 08:00:27:90:ea:eb brd ff:ff:ff:ff:ff:ff
# altname enx08002790eaeb
# inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s8
# valid_lft 75042sec preferred_lft 75042sec
# inet6 fd17:625c:f037:2:a00:27ff:fe90:eaeb/64 scope global dynamic mngtmpaddr proto kernel_ra
# valid_lft 85858sec preferred_lft 13858sec
# inet6 fe80::a00:27ff:fe90:eaeb/64 scope link proto kernel_ll
# valid_lft forever preferred_lft forever
# 3: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# link/ether 08:00:27:55:33:36 brd ff:ff:ff:ff:ff:ff
# altname enx080027553336
# inet 192.168.10.100/24 brd 192.168.10.255 scope global noprefixroute enp0s9
# valid_lft forever preferred_lft forever
# inet6 fd2a:b6fc:560f:26a3:92ed:39f:41b:9d55/64 scope global dynamic noprefixroute
# valid_lft 2591887sec preferred_lft 604687sec
# inet6 fe80::5112:5b2:39d3:68c5/64 scope link noprefixroute
# valid_lft forever preferred_lft forever
ss -tnlp | tee -a ss-1.txt
# State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
# LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=849,fd=7))
# LISTEN 0 4096 127.0.0.1:41597 0.0.0.0:* users:(("containerd",pid=5338,fd=11))
# LISTEN 0 4096 0.0.0.0:111 0.0.0.0:* users:(("rpcbind",pid=591,fd=5),("systemd",pid=1,fd=164))
# LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=849,fd=8))
# LISTEN 0 4096 [::]:111 [::]:* users:(("rpcbind",pid=591,fd=7),("systemd",pid=1,fd=166))
# LISTEN 0 4096 *:9090 *:* users:(("systemd",pid=1,fd=182))
df -hT | tee -a df-1.txt
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda3 xfs 60G 3.5G 57G 6% /
# devtmpfs devtmpfs 4.0M 0 4.0M 0% /dev
# tmpfs tmpfs 1.4G 0 1.4G 0% /dev/shm
# efivarfs efivarfs 256K 6.3K 250K 3% /sys/firmware/efi/efivars
# tmpfs tmpfs 566M 15M 551M 3% /run
# tmpfs tmpfs 1.0M 0 1.0M 0% /run/credentials/systemd-journald.service
# /dev/sda1 vfat 599M 13M 587M 3% /boot/efi
# tmpfs tmpfs 1.0M 0 1.0M 0% /run/credentials/getty@tty1.service
# tmpfs tmpfs 283M 8.0K 283M 1% /run/user/1000
findmnt | tee -a findmnt-1.txt
# TARGET SOURCE FSTYPE OPTIONS
# / /dev/sda3 xfs rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota
# ├─/var/lib/nfs/rpc_pipefs sunrpc rpc_pipefs rw,relatime
# ├─/boot/efi /dev/sda1 vfat rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=ascii,shortname=winnt,errors=remount-ro
# ├─/dev devtmpfs devtmpfs rw,nosuid,seclabel,size=4096k,nr_inodes=357738,mode=755,inode64
# │ ├─/dev/hugepages hugetlbfs hugetlbfs rw,nosuid,nodev,relatime,seclabel,pagesize=2M
# │ ├─/dev/mqueue mqueue mqueue rw,nosuid,nodev,noexec,relatime,seclabel
# │ ├─/dev/shm tmpfs tmpfs rw,nosuid,nodev,seclabel,inode64
# │ └─/dev/pts devpts devpts rw,nosuid,noexec,relatime,seclabel,gid=5,mode=620,ptmxmode=000
# ├─/sys sysfs sysfs rw,nosuid,nodev,noexec,relatime,seclabel
# │ ├─/sys/fs/selinux selinuxfs selinuxfs rw,nosuid,noexec,relatime
# │ ├─/sys/kernel/debug debugfs debugfs rw,nosuid,nodev,noexec,relatime,seclabel
# │ ├─/sys/kernel/tracing tracefs tracefs rw,nosuid,nodev,noexec,relatime,seclabel
# │ ├─/sys/fs/fuse/connections fusectl fusectl rw,nosuid,nodev,noexec,relatime
# │ ├─/sys/kernel/security securityfs securityfs rw,nosuid,nodev,noexec,relatime
# │ ├─/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot
# │ ├─/sys/fs/pstore pstore pstore rw,nosuid,nodev,noexec,relatime,seclabel
# │ ├─/sys/firmware/efi/efivars efivarfs efivarfs rw,nosuid,nodev,noexec,relatime
# │ ├─/sys/fs/bpf bpf bpf rw,nosuid,nodev,noexec,relatime,mode=700
# │ └─/sys/kernel/config configfs configfs rw,nosuid,nodev,noexec,relatime
# ├─/proc proc proc rw,nosuid,nodev,noexec,relatime
# │ └─/proc/sys/fs/binfmt_misc systemd-1 autofs rw,relatime,fd=35,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=3613
# │ └─/proc/sys/fs/binfmt_misc binfmt_misc binfmt_misc rw,nosuid,nodev,noexec,relatime
# └─/run tmpfs tmpfs rw,nosuid,nodev,seclabel,size=578796k,nr_inodes=819200,mode=755,inode64
# ├─/run/credentials/systemd-journald.service tmpfs tmpfs ro,nosuid,nodev,noexec,relatime,nosymfollow,seclabel,size=1024k,nr_inodes=1024,mode=700,inode64,noswap
# ├─/run/user/1000 tmpfs tmpfs rw,nosuid,nodev,relatime,seclabel,size=289396k,nr_inodes=72349,mode=700,uid=1000,gid=1000,inode64
# └─/run/credentials/getty@tty1.service tmpfs tmpfs ro,nosuid,nodev,noexec,relatime,nosymfollow,seclabel,size=1024k,nr_inodes=1024,mode=700,inode64,noswap
sysctl -a | tee -a sysctl-1.txt
# kernel.watchdog = 1
# kernel.watchdog_cpumask = 0-3
# net.bridge.bridge-nf-call-iptables = 1
# net.bridge.bridge-nf-call-ip6tables = 1
# net.ipv4.ip_forward = 1
# net.ipv4.conf.all.forwarding = 1
# net.ipv4.conf.default.forwarding = 1
# net.ipv4.conf.enp0s8.forwarding = 1
# net.ipv4.conf.enp0s9.forwarding = 1
# net.ipv4.conf.lo.forwarding = 1
# (기타 수많은 커널 파라미터들...)
# kubeadm Configuration 파일 작성
cat << EOF > kubeadm-init.yaml
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
bootstrapTokens:
- token: "123456.1234567890123456"
ttl: "0s"
usages:
- signing
- authentication
nodeRegistration:
kubeletExtraArgs:
- name: node-ip
value: "192.168.10.100" # 미설정 시 10.0.2.15 맵핑
criSocket: "unix:///run/containerd/containerd.sock"
localAPIEndpoint:
advertiseAddress: "192.168.10.100"
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: "1.32.11"
networking:
podSubnet: "10.244.0.0/16"
serviceSubnet: "10.96.0.0/16"
EOF
cat kubeadm-init.yaml
# apiVersion: kubeadm.k8s.io/v1beta4
# kind: InitConfiguration
# bootstrapTokens:
# - token: "123456.1234567890123456"
# ttl: "0s"
# usages:
# - signing
# - authentication
# nodeRegistration:
# kubeletExtraArgs:
# - name: node-ip
# value: "192.168.10.100"
# criSocket: "unix:///run/containerd/containerd.sock"
# localAPIEndpoint:
# advertiseAddress: "192.168.10.100"
# ---
# apiVersion: kubeadm.k8s.io/v1beta4
# kind: ClusterConfiguration
# kubernetesVersion: "1.32.11"
# networking:
# podSubnet: "10.244.0.0/16"
# serviceSubnet: "10.96.0.0/16"
# (옵션) 컨테이너 이미지 미리 다운로드 : 특히 업그레이드 작업 시, 작업 시간 단축을 위해서 수행할 것
kubeadm config images pull
# I0121 02:23:06.171073 6423 version.go:261] remote version is much newer: v1.35.0; falling back to: stable-1.32
# [config/images] Pulled registry.k8s.io/kube-apiserver:v1.32.11
# [config/images] Pulled registry.k8s.io/kube-controller-manager:v1.32.11
# [config/images] Pulled registry.k8s.io/kube-scheduler:v1.32.11
# [config/images] Pulled registry.k8s.io/kube-proxy:v1.32.11
# [config/images] Pulled registry.k8s.io/coredns/coredns:v1.11.3
# [config/images] Pulled registry.k8s.io/pause:3.10
# [config/images] Pulled registry.k8s.io/etcd:3.5.24-0
# k8s controlplane 초기화 설정 수행
kubeadm init --config="kubeadm-init.yaml"
# [init] Using Kubernetes version: v1.32.11
# ...
# [certs] Using certificateDir folder "/etc/kubernetes/pki"
# [certs] Generating "ca" certificate and key
# [certs] Generating "apiserver" certificate and key
# [certs] apiserver serving cert is signed for DNS names [k8s-ctr kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 192.168.10.100]
# [certs] Generating "apiserver-kubelet-client" certificate and key
# [certs] Generating "front-proxy-ca" certificate and key
# [certs] Generating "front-proxy-client" certificate and key
# [certs] Generating "etcd/ca" certificate and key
# [certs] Generating "etcd/server" certificate and key
# [certs] etcd/server serving cert is signed for DNS names [k8s-ctr localhost] and IPs [192.168.10.100 127.0.0.1 ::1]
# [certs] Generating "etcd/peer" certificate and key
# [certs] etcd/peer serving cert is signed for DNS names [k8s-ctr localhost] and IPs [192.168.10.100 127.0.0.1 ::1]
# [certs] Generating "etcd/healthcheck-client" certificate and key
# [certs] Generating "apiserver-etcd-client" certificate and key
# [certs] Generating "sa" key and public key
# [kubeconfig] Using kubeconfig folder "/etc/kubernetes"
# [kubeconfig] Writing "admin.conf" kubeconfig file
# [kubeconfig] Writing "super-admin.conf" kubeconfig file
# [kubeconfig] Writing "kubelet.conf" kubeconfig file
# [kubeconfig] Writing "controller-manager.conf" kubeconfig file
# [kubeconfig] Writing "scheduler.conf" kubeconfig file
# [etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
# [control-plane] Using manifest folder "/etc/kubernetes/manifests"
# [control-plane] Creating static Pod manifest for "kube-apiserver"
# [control-plane] Creating static Pod manifest for "kube-controller-manager"
# [control-plane] Creating static Pod manifest for "kube-scheduler"
# [kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
# [kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
# [kubelet-start] Starting the kubelet
# [wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests"
# [kubelet-check] Waiting for a healthy kubelet at http://127.0.0.1:10248/healthz. This can take up to 4m0s
# [kubelet-check] The kubelet is healthy after 1.003793571s
# [api-check] Waiting for a healthy API server. This can take up to 4m0s
# [api-check] The API server is healthy after 3.004974627s
# [upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
# [kubelet] Creating a ConfigMap "kubelet-config" in namespace kube-system with the configuration for the kubelets in the cluster
# [upload-certs] Skipping phase. Please see --upload-certs
# [mark-control-plane] Marking the node k8s-ctr as control-plane by adding the labels: [node-role.kubernetes.io/control-plane node.kubernetes.io/exclude-from-external-load-balancers]
# [mark-control-plane] Marking the node k8s-ctr as control-plane by adding the taints [node-role.kubernetes.io/control-plane:NoSchedule]
# [bootstrap-token] Using token: 123456.1234567890123456
# [bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
# [bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to get nodes
# [bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
# [bootstrap-token] Configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
# [bootstrap-token] Configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
# [bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
# [kubelet-finalize] Updating "/etc/kubernetes/kubelet.conf" to point to a rotatable kubelet client certificate and key
# [addons] Applied essential addon: CoreDNS
# [addons] Applied essential addon: kube-proxy
# Your Kubernetes control-plane has initialized successfully!
# To start using your cluster, you need to run the following as a regular user:
# mkdir -p $HOME/.kube
# sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
# sudo chown $(id -u):$(id -g) $HOME/.kube/config
# Alternatively, if you are the root user, you can run:
# export KUBECONFIG=/etc/kubernetes/admin.conf
# You should now deploy a pod network to the cluster.
# Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
# https://kubernetes.io/docs/concepts/cluster-administration/addons/
# Then you can join any number of worker nodes by running the following on each as root:
# kubeadm join 192.168.10.100:6443 --token 123456.1234567890123456 \
# --discovery-token-ca-cert-hash sha256:6c9fa5b6cd99aaa8eea57a1f9716e15ae8c4142c748421e3f459b6be0131cf70
# crictl 확인
crictl images
# IMAGE TAG IMAGE ID SIZE
# registry.k8s.io/coredns/coredns v1.11.3 2f6c962e7b831 16.9MB
# registry.k8s.io/etcd 3.5.24-0 1211402d28f58 21.9MB
# registry.k8s.io/kube-apiserver v1.32.11 58951ea1a0b5d 26.4MB
# registry.k8s.io/kube-controller-manager v1.32.11 82766e5f2d560 24.2MB
# registry.k8s.io/kube-proxy v1.32.11 dcdb790dc2bfe 27.6MB
# registry.k8s.io/kube-scheduler v1.32.11 cfa17ff3d6634 19.2MB
# registry.k8s.io/pause 3.10 afb61768ce381 268kB
# kubeadm init 실행 후 모든 Control Plane 구성 요소와 필수 애드온 이미지가 다운로드되었습니다.
crictl ps
# CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE
# 3362d1643ecff dcdb790dc2bfe 33 seconds ago Running kube-proxy 0 e9df7b0d85975 kube-proxy-qhqpz kube-system
# 392f49d53a9d4 1211402d28f58 42 seconds ago Running etcd 0 b990beb24852c etcd-k8s-ctr kube-system
# 32dca52b75ce8 cfa17ff3d6634 43 seconds ago Running kube-scheduler 0 d469c96375645 kube-scheduler-k8s-ctr kube-system
# c176ce83ecb68 82766e5f2d560 43 seconds ago Running kube-controller-manager 0 d8fa016e22810 kube-controller-manager-k8s-ctr kube-system
# 2820f726fb702 58951ea1a0b5d 43 seconds ago Running kube-apiserver 0 b8406c854bf46 kube-apiserver-k8s-ctr kube-system
# Control Plane 구성 요소들이 Static Pod로 실행 중입니다. kube-proxy는 DaemonSet으로 실행됩니다.
# kubeconfig 작성
mkdir -p /root/.kube
cp -i /etc/kubernetes/admin.conf /root/.kube/config
chown $(id -u):$(id -g) /root/.kube/config
# kubectl이 클러스터와 통신하기 위해 kubeconfig 파일을 설정합니다.
# /etc/kubernetes/admin.conf는 클러스터 관리자 권한을 가진 kubeconfig 파일입니다.
# 확인
kubectl cluster-info
# Kubernetes control plane is running at https://192.168.10.100:6443
# CoreDNS is running at https://192.168.10.100:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
#
# To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
# Control Plane이 정상적으로 실행 중이며, API Server 엔드포인트와 CoreDNS 서비스 정보를 확인할 수 있습니다.
kubectl get node -owide
# NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
# k8s-ctr NotReady control-plane 69s v1.32.11 192.168.10.100 <none> Rocky Linux 10.0 (Red Quartz) 6.12.0-55.39.1.el10_0.aarch64 containerd://2.1.5
# 노드가 control-plane 역할로 표시되었지만 NotReady 상태입니다. 이는 CNI(Container Network Interface)가 아직 설치되지 않아서입니다.
kubectl get nodes -o json | jq ".items[] | {name:.metadata.name} + .status.capacity"
# {
# "name": "k8s-ctr",
# "cpu": "4",
# "ephemeral-storage": "60970Mi",
# "hugepages-1Gi": "0",
# "hugepages-2Mi": "0",
# "hugepages-32Mi": "0",
# "hugepages-64Ki": "0",
# "memory": "2893976Ki",
# "pods": "110"
# }
# 노드의 리소스 용량 정보를 확인할 수 있습니다. 최대 110개의 Pod를 스케줄할 수 있습니다.
kubectl get pod -n kube-system -owide
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# coredns-668d6bf9bc-47zx5 0/1 Pending 0 75s <none> <none> <none> <none>
# coredns-668d6bf9bc-77pjv 0/1 Pending 0 75s <none> <none> <none> <none>
# etcd-k8s-ctr 1/1 Running 0 81s 192.168.10.100 k8s-ctr <none> <none>
# kube-apiserver-k8s-ctr 1/1 Running 0 82s 192.168.10.100 k8s-ctr <none> <none>
# kube-controller-manager-k8s-ctr 1/1 Running 0 82s 192.168.10.100 k8s-ctr <none> <none>
# kube-proxy-qhqpz 1/1 Running 0 76s 192.168.10.100 k8s-ctr <none> <none>
# kube-scheduler-k8s-ctr 1/1 Running 0 82s 192.168.10.100 k8s-ctr <none> <none>
# Control Plane 구성 요소들은 모두 Running 상태입니다. CoreDNS Pod는 CNI가 설치되지 않아 Pending 상태입니다.
# coredns 의 service name 확인 : kube-dns
kubectl get svc -n kube-system
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 85s
# CoreDNS 서비스는 레거시 호환성을 위해 kube-dns라는 이름으로 생성됩니다. ClusterIP는 10.96.0.10입니다.
# cluster-info ConfigMap 공개 : cluster-info는 '신원 확인 전, 최소한의 신뢰 부트스트랩 데이터'
kubectl -n kube-public get configmap cluster-info
# NAME DATA AGE
# cluster-info 2 90s
# cluster-info ConfigMap이 kube-public 네임스페이스에 생성되었습니다. 이는 인증되지 않은 사용자도 접근할 수 있는 공개 정보입니다.
kubectl -n kube-public get configmap cluster-info -o yaml
# apiVersion: v1
# data:
# jws-kubeconfig-123456: eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1NiJ9..cu7Nc8yJNTLPRZOiaIUD7y6aANtYy6YKFwNZUL5LpMs
# kubeconfig: |
# apiVersion: v1
# clusters:
# - cluster:
# certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJUVFnd2ViNU1Fd0l3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1qQXhOek0xTlROYUZ3MHpOakF4TVRneE56UXdOVE5hTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUMzanZiYjFhNHVwU2dxSGdIWWZ1cnR0QVJ3akZyWCtOV0JvYTUwdFpNUGhHRWZoQVBJRHFURm00OTYKZ0FDRXk5ZVhBZldDSlVPS0pjaXlvWWdFcHk5M2E2MnVnN2VlbFRXQ0haM3ZhRUFZNEt6UVRpWTdkekZIVGYxLwpXdEZRSU1qOElTYk9oN1ZaTlQyUVRTQTJjZkhjUUNBcWtvMkRLdWxaNHhVbzhxNHVCQ0hwbStLMEhiK3FEb0JECk13c0tLNWJNakt5anZJRURHUzVQNE5KQ0lmeVhHL0ljM3pEMWJpdFFMbW9FWGpJeklyTGZvNzc4VXRwVWdOeFcKaW5YNWc1ZUJvc2FpeHNpcm9sc3ZkMVRxMDMwRHFPNTFaSGlGVmxsblQvVk9xZnh1WmlKU0d3RWx0cVZlcTRMWQp5dldpOHg5TG16aDRHcUh4TmJUaU5HVSt4ajk5QWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRa2JqWStYbHQ1eExHeUlXQ3E1aFR5UDlGZVh6QVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQVVEdXpURkdKRgpjU2RmYm5RL3BZUndSOG9LZ1diMHg3SGNvVEZBT2tDcERhUm9LVVFrOGhmYTJDRnpsS0dYZDdLTHRKOU9JMVBaCkdiUnFKNXJyRWR3dVFhUnB4elNJN1BtWlpmdHMwekoyNHkwYlJyV0orUEFtSlNBdFVyUUM5Y3JORVNVMkRQaHYKYUVVTjVCZGJBYjRPOWk5TFNmdkdaQWV3bW5QNDVTOFAyS1JCZTdBZit0NjU2UE5oMysxdW1jVkdSZldFdi9ZNQpJUVR2MVRIVlJ6NjRlTE1RMnVEQUFHQ0t1cGp0RlFXZEhwSjA1dzNObjdZMFhxeGovTEdaSTRXMHBnVnl4QUJkCnpwN0lZWXJFS21RZDlNUzJrZXRxZVB6WDFOa1VmcXZQckNJbWZtZ1BPVEYrTnZGS1NSM2xUY3dVVHRQVHJDUEoKUzJ6eU1rVlIraVRxCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
# server: https://192.168.10.100:6443
# name: ""
# contexts: null
# current-context: ""
# kind: Config
# preferences: {}
# users: null
# kind: ConfigMap
# metadata:
# creationTimestamp: "2026-01-20T17:40:59Z"
# name: cluster-info
# namespace: kube-public
# resourceVersion: "316"
# uid: dc9196fb-1749-48cc-9ea5-bf788fb48f21
# cluster-info ConfigMap에는 API Server 엔드포인트와 CA 인증서가 포함된 kubeconfig가 저장되어 있습니다.
# jws-kubeconfig-123456는 JWT(JSON Web Token) 서명된 kubeconfig로, kubeadm join 시 사용됩니다.
kubectl -n kube-public get configmap cluster-info -o jsonpath='{.data.kubeconfig}' | grep certificate-authority-data | cut -d ':' -f2 | tr -d ' ' | base64 -d | openssl x509 -text -noout
# Certificate:
# Data:
# Version: 3 (0x2)
# Serial Number: 4686048711720833794 (0x41083079be4c1302)
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: CN=kubernetes
# Validity
# Not Before: Jan 20 17:35:53 2026 GMT
# Not After : Jan 18 17:40:53 2036 GMT
# Subject: CN=kubernetes
# Subject Public Key Info:
# Public Key Algorithm: rsaEncryption
# Public-Key: (2048 bit)
# Modulus:
# 00:b7:8e:f6:db:d5:ae:2e:a5:28:2a:1e:01:d8:7e:
# ...
# Exponent: 65537 (0x10001)
# X509v3 extensions:
# X509v3 Key Usage: critical
# Digital Signature, Key Encipherment, Certificate Sign
# X509v3 Basic Constraints: critical
# CA:TRUE
# X509v3 Subject Key Identifier:
# 24:6E:36:3E:5E:5B:79:C4:B1:B2:21:60:AA:E6:14:F2:3F:D1:5E:5F
# X509v3 Subject Alternative Name:
# DNS:kubernetes
# Signature Algorithm: sha256WithRSAEncryption
# Signature Value:
# 14:0e:ec:d3:14:62:45:71:27:5f:6e:74:3f:a5:84:70:47:ca:
# ...
# CA 인증서의 상세 정보를 확인할 수 있습니다. 이 인증서는 10년간 유효하며(Not After: Jan 18 17:40:53 2036 GMT),
# 클러스터의 루트 CA 역할을 합니다.
curl -s -k https://192.168.10.100:6443/api/v1/namespaces/kube-public/configmaps/cluster-info | jq
# {
# "kind": "ConfigMap",
# "apiVersion": "v1",
# "metadata": {
# "name": "cluster-info",
# "namespace": "kube-public",
# "uid": "dc9196fb-1749-48cc-9ea5-bf788fb48f21",
# "resourceVersion": "316",
# "creationTimestamp": "2026-01-20T17:40:59Z",
# "managedFields": [
# {
# "manager": "kubeadm",
# "operation": "Update",
# "apiVersion": "v1",
# "time": "2026-01-20T17:40:59Z",
# "fieldsType": "FieldsV1",
# "fieldsV1": {
# "f:data": {
# ".": {},
# "f:kubeconfig": {}
# }
# }
# },
# {
# "manager": "kube-controller-manager",
# "operation": "Update",
# "apiVersion": "v1",
# "time": "2026-01-20T17:41:05Z",
# "fieldsType": "FieldsV1",
# "fieldsV1": {
# "f:data": {
# "f:jws-kubeconfig-123456": {}
# }
# }
# }
# ]
# },
# "data": {
# "jws-kubeconfig-123456": "eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1NiJ9..cu7Nc8yJNTLPRZOiaIUD7y6aANtYy6YKFwNZUL5LpMs",
# "kubeconfig": "apiVersion: v1\nclusters:\n- cluster:\n certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJUVFnd2ViNU1Fd0l3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1qQXhOek0xTlROYUZ3MHpOakF4TVRneE56UXdOVE5hTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUMzanZiYjFhNHVwU2dxSGdIWWZ1cnR0QVJ3akZyWCtOV0JvYTUwdFpNUGhHRWZoQVBJRHFURm00OTYKZ0FDRXk5ZVhBZldDSlVPS0pjaXlvWWdFcHk5M2E2MnVnN2VlbFRXQ0haM3ZhRUFZNEt6UVRpWTdkekZIVGYxLwpXdEZRSU1qOElTYk9oN1ZaTlQyUVRTQTJjZkhjUUNBcWtvMkRLdWxaNHhVbzhxNHVCQ0hwbStLMEhiK3FEb0JECk13c0tLNWJNakt5anZJRURHUzVQNE5KQ0lmeVhHL0ljM3pEMWJpdFFMbW9FWGpJeklyTGZvNzc4VXRwVWdOeFcKaW5YNWc1ZUJvc2FpeHNpcm9sc3ZkMVRxMDMwRHFPNTFaSGlGVmxsblQvVk9xZnh1WmlKU0d3RWx0cVZlcTRMWQp5dldpOHg5TG16aDRHcUh4TmJUaU5HVSt4ajk5QWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRa2JqWStYbHQ1eExHeUlXQ3E1aFR5UDlGZVh6QVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQVVEdXpURkdKRgpjU2RmYm5RL3BZUndSOG9LZ1diMHg3SGNvVEZBT2tDcERhUm9LVVFrOGhmYTJDRnpsS0dYZDdLTHRKOU9JMVBaCkdiUnFKNXJyRWR3dVFhUnB4elNJN1BtWlpmdHMwekoyNHkwYlJyV0orUEFtSlNBdFVyUUM5Y3JORVNVMkRQaHYKYUVVTjVCZGJBYjRPOWk5TFNmdkdaQWV3bW5QNDVTOFAyS1JCZTdBZit0NjU2UE5oMysxdW1jVkdSZldFdi9ZNQpJUVR2MVRIVlJ6NjRlTE1RMnVEQUFHQ0t1cGp0RlFXZEhwSjA1dzNObjdZMFhxeGovTEdaSTRXMHBnVnl4QUJkCnpwN0lZWXJFS21RZDlNUzJrZXRxZVB6WDFOa1VmcXZQckNJbWZtZ1BPVEYrTnZGS1NSM2xUY3dVVHRQVHJDUEoKUzJ6eU1rVlIraVRxCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K\n server: https://192.168.10.100:6443\n name: \"\"\ncontexts: null\ncurrent-context: \"\"\nkind: Config\npreferences: {}\nusers: null\n"
# }
# }
# 인증 없이도 cluster-info ConfigMap에 접근할 수 있습니다. 이는 kubeadm join 시 워커 노드가 클러스터 정보를 얻기 위해 필요합니다.
# kube-controller-manager가 bootstrap-signer 컨트롤러를 통해 jws-kubeconfig-123456를 추가했습니다.
curl -s -k https://192.168.10.100:6443/api/v1/namespaces/default/pods # X
# {
# "kind": "Status",
# "apiVersion": "v1",
# "metadata": {},
# "status": "Failure",
# "message": "pods is forbidden: User \"system:anonymous\" cannot list resource \"pods\" in API group \"\" in the namespace \"default\"",
# "reason": "Forbidden",
# "details": {
# "kind": "pods"
# },
# "code": 403
# }
# 인증 없이는 다른 리소스에 접근할 수 없습니다. cluster-info ConfigMap만 예외적으로 인증 없이 접근 가능합니다.
kubectl -n kube-public get role
# NAME CREATED AT
# kubeadm:bootstrap-signer-clusterinfo 2026-01-20T17:40:59Z
# system:controller:bootstrap-signer 2026-01-20T17:40:59Z
# cluster-info ConfigMap에 대한 접근 권한을 부여하는 Role이 생성되었습니다.
kubectl -n kube-public get rolebinding
# NAME ROLE AGE
# kubeadm:bootstrap-signer-clusterinfo Role/kubeadm:bootstrap-signer-clusterinfo 2m17s
# system:controller:bootstrap-signer Role/system:controller:bootstrap-signer 2m17s
# RoleBinding을 통해 system:unauthenticated 그룹에 cluster-info ConfigMap 접근 권한이 부여되었습니다.
kubeadm init 실행 시 클러스터 부트스트랩을 위해 다음과 같은 객체들이 자동으로 생성됩니다.
1. Namespace: kube-public
kube-public 네임스페이스는 클러스터 전체에 공개된 정보를 저장하기 위한 특별한 네임스페이스입니다. 이 네임스페이스의 리소스는 인증되지 않은 사용자도 접근할 수 있도록 설계되었습니다.
2. ConfigMap: cluster-info
cluster-info ConfigMap은 클러스터의 최소한의 부트스트랩 정보를 담고 있습니다. 이 ConfigMap에는 다음 정보가 포함됩니다:
- API Server 엔드포인트:
https://192.168.10.100:6443 - CA 인증서: 클러스터의 루트 인증서 (certificate-authority-data)
- JWT 서명된 kubeconfig: 부트스트랩 토큰과 함께 사용되는 서명된 설정 파일
이 정보는 아직 클러스터에 조인하지 않은 워커 노드가 클러스터에 합류하기 위해 필요한 최소한의 신뢰 데이터입니다.
3. Role + RoleBinding
kubeadm은 kube-public 네임스페이스에 특별한 Role과 RoleBinding을 생성합니다:
- Role:
kubeadm:bootstrap-signer-clusterinfo-cluster-infoConfigMap에 대한get권한을 정의 - RoleBinding:
system:unauthenticated그룹에 위 Role을 바인딩
일반적으로 Kubernetes는 모든 API 요청에 인증을 요구합니다. 그러나 cluster-info ConfigMap은 예외적으로 인증 없이 접근할 수 있습니다. 이는 닭과 달걀 문제(chicken-and-egg problem)를 해결하기 위한 설계입니다:
- 워커 노드는 클러스터에 조인하기 전에는 클러스터 인증서를 가지고 있지 않습니다.
- 하지만 클러스터에 조인하려면 API Server의 엔드포인트와 CA 인증서가 필요합니다.
- 따라서 인증 없이도 접근 가능한 공개 엔드포인트를 통해 최소한의 정보를 제공해야 합니다.
동작 흐름:
- 워커 노드에서
kubeadm join명령 실행 - kubeadm은 부트스트랩 토큰을 사용하여
kube-public네임스페이스의cluster-infoConfigMap에 접근 - ConfigMap에서 API Server 엔드포인트와 CA 인증서를 획득
- 획득한 정보를 사용하여 API Server와 통신하고 CSR(Certificate Signing Request)을 제출
- CSR이 승인되면 노드가 클러스터에 정식으로 조인
보안 고려사항:
cluster-info ConfigMap은 인증 없이 접근 가능하지만, 다음 정보만 포함합니다:
- API Server 엔드포인트 (이미 공개되어야 하는 정보)
- CA 인증서 (공개 키이므로 노출되어도 안전)
- 부트스트랩 토큰 ID (토큰 자체는 포함되지 않음)
실제 인증은 부트스트랩 토큰을 통해 이루어지며, 이 토큰은 제한된 권한만 가지고 있어 노드 조인에 필요한 최소한의 작업만 수행할 수 있습니다.
[k8s-ctr] k8s 관련 작업 편의성 설정 : 자동 완성, kubecolor, kubectx, kubens, kube-ps, helm, k9s 설치
echo "sudo su -" >> /home/vagrant/.bashrc
# Source the completion
source <(kubectl completion bash)
source <(kubeadm completion bash)
echo 'source <(kubectl completion bash)' >> /etc/profile
echo 'source <(kubeadm completion bash)' >> /etc/profile
kubectl get <tab 2번>
# Alias kubectl to k
alias k=kubectl
complete -o default -F __start_kubectl k
echo 'alias k=kubectl' >> /etc/profile
echo 'complete -o default -F __start_kubectl k' >> /etc/profile
k get node
# kubecolor 설치 : https://kubecolor.github.io/setup/install/
dnf install -y 'dnf-command(config-manager)'
dnf config-manager --add-repo https://kubecolor.github.io/packages/rpm/kubecolor.repo
dnf repolist
dnf install -y kubecolor
kubecolor get node
alias kc=kubecolor
echo 'alias kc=kubecolor' >> /etc/profile
kc get node
kc describe node
# Install Kubectx & Kubens"
dnf install -y git
git clone https://github.com/ahmetb/kubectx /opt/kubectx
ln -s /opt/kubectx/kubens /usr/local/bin/kubens
ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx
# Install Kubeps & Setting PS1
git clone https://github.com/jonmosco/kube-ps1.git /root/kube-ps1
cat << "EOT" >> /root/.bash_profile
source /root/kube-ps1/kube-ps1.sh
KUBE_PS1_SYMBOL_ENABLE=true
function get_cluster_short() {
echo "$1" | cut -d . -f1
}
KUBE_PS1_CLUSTER_FUNCTION=get_cluster_short
KUBE_PS1_SUFFIX=') '
PS1='$(kube_ps1)'$PS1
EOT
# 빠져나오기
exit
exit
# 다시 접속
vagrant ssh k8s-ctr
whoami
pwd
kubectl config rename-context "kubernetes-admin@kubernetes" "HomeLab"
kubens default
# helm 3 설치 : https://helm.sh/docs/intro/install
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | DESIRED_VERSION=v3.18.6 bash
helm version
# k9s 설치 : https://github.com/derailed/k9s
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
wget https://github.com/derailed/k9s/releases/latest/download/k9s_linux_${CLI_ARCH}.tar.gz
tar -xzf k9s_linux_*.tar.gz
ls -al k9s
chown root:root k9s
mv k9s /usr/local/bin/
chmod +x /usr/local/bin/k9s
k9s
[k8s-ctr] Flannel CNI 설치 v0.27.3
Flannel은 Kubernetes에서 가장 널리 사용되는 CNI(Container Network Interface) 플러그인 중 하나입니다. VXLAN 백엔드를 사용하여 Pod 간 통신을 제공하며, 각 노드에 DaemonSet으로 배포됩니다.
# 현재 k8s 클러스터에 파드 전체 CIDR 확인
kc describe pod -n kube-system kube-controller-manager-k8s-ctr
# Name: kube-controller-manager-k8s-ctr
# Namespace: kube-system
# Priority: 2000001000
# Priority Class Name: system-node-critical
# Node: k8s-ctr/192.168.10.100
# ...
# Command:
# kube-controller-manager
# --allocate-node-cidrs=true
# --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf
# --authorization-kubeconfig=/etc/kubernetes/controller-manager.conf
# --bind-address=127.0.0.1
# --client-ca-file=/etc/kubernetes/pki/ca.crt
# --cluster-cidr=10.244.0.0/16
# --cluster-name=kubernetes
# --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt
# --cluster-signing-key-file=/etc/kubernetes/pki/ca.key
# --controllers=*,bootstrapsigner,tokencleaner
# --kubeconfig=/etc/kubernetes/controller-manager.conf
# --leader-elect=true
# --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
# --root-ca-file=/etc/kubernetes/pki/ca.crt
# --service-account-private-key-file=/etc/kubernetes/pki/sa.key
# --service-cluster-ip-range=10.96.0.0/16
# --use-service-account-credentials=true
# kube-controller-manager의 --cluster-cidr=10.244.0.0/16 옵션을 통해 클러스터 전체 Pod CIDR을 확인할 수 있습니다.
# --allocate-node-cidrs=true는 각 노드에 Pod CIDR을 할당하도록 설정합니다.
# --service-cluster-ip-range=10.96.0.0/16는 Service의 ClusterIP 범위입니다.
# 노드별 파드 CIDR 확인
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
# k8s-ctr 10.244.0.0/24
# 각 노드는 클러스터 CIDR(10.244.0.0/16)에서 서브넷을 할당받습니다. k8s-ctr 노드는 10.244.0.0/24를 할당받았습니다.
# 이는 최대 254개의 Pod IP를 제공할 수 있습니다 (10.244.0.1 ~ 10.244.0.254).
# Deploying Flannel with Helm
# https://github.com/flannel-io/flannel/blob/master/Documentation/configuration.md
helm repo add flannel https://flannel-io.github.io/flannel
# "flannel" has been added to your repositories
# Flannel 공식 Helm 저장소를 추가합니다.
helm repo update
# Hang tight while we grab the latest from your chart repositories...
# ...Successfully got an update from the "flannel" chart repository
# Update Complete. ⎈Happy Helming!⎈
# Helm 저장소를 업데이트하여 최신 차트 정보를 가져옵니다.
kubectl create namespace kube-flannel
# namespace/kube-flannel created
# Flannel을 설치할 전용 네임스페이스를 생성합니다.
cat << EOF > flannel.yaml
podCidr: "10.244.0.0/16"
flannel:
cniBinDir: "/opt/cni/bin"
cniConfDir: "/etc/cni/net.d"
args:
- "--ip-masq"
- "--kube-subnet-mgr"
- "--iface=enp0s9"
backend: "vxlan"
EOF
# Flannel 설정 파일을 생성합니다.
# podCidr: 클러스터 전체 Pod CIDR (kube-controller-manager의 --cluster-cidr와 일치해야 함)
# cniBinDir: CNI 바이너리 파일이 설치될 디렉토리
# cniConfDir: CNI 설정 파일이 생성될 디렉토리
# --ip-masq: 외부 네트워크로 나가는 트래픽에 IP 마스커레이딩 적용
# --kube-subnet-mgr: Kubernetes API를 통해 서브넷 정보를 관리
# --iface=enp0s9: Flannel이 사용할 네트워크 인터페이스 지정 (192.168.10.100이 할당된 인터페이스)
# backend: vxlan - VXLAN 터널링을 사용하여 Pod 간 통신 제공
helm install flannel flannel/flannel --namespace kube-flannel --version 0.27.3 -f flannel.yaml
# NAME: flannel
# LAST DEPLOYED: Wed Jan 21 02:54:57 2026
# NAMESPACE: kube-flannel
# STATUS: deployed
# REVISION: 1
# TEST SUITE: None
# Flannel v0.27.3을 Helm을 통해 설치합니다. DaemonSet이 각 노드에 배포됩니다.
# 확인
helm list -A
# NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
# flannel kube-flannel 1 2026-01-21 02:54:57.773349078 +0900 KST deployed flannel-v0.27.3 v0.27.3
# 설치된 Helm 릴리스를 확인합니다.
helm get values -n kube-flannel flannel
# USER-SUPPLIED VALUES:
# flannel:
# args:
# - --ip-masq
# - --kube-subnet-mgr
# - --iface=enp0s9
# backend: vxlan
# cniBinDir: /opt/cni/bin
# cniConfDir: /etc/cni/net.d
# podCidr: 10.244.0.0/16
# 적용된 설정 값을 확인합니다.
kubectl get ds,pod,cm -n kube-flannel -owide
# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE CONTAINERS IMAGES SELECTOR
# daemonset.apps/kube-flannel-ds 1 1 0 1 0 <none> 12s kube-flannel ghcr.io/flannel-io/flannel:v0.27.3 app=flannel
#
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# pod/kube-flannel-ds-w9s2j 0/1 Init:1/2 0 12s 192.168.10.100 k8s-ctr <none> <none>
#
# NAME DATA AGE
# configmap/kube-flannel-cfg 2 12s
# configmap/kube-root-ca.crt 1 24s
# DaemonSet이 배포되었지만 아직 초기화 중입니다. Init 컨테이너가 CNI 바이너리와 설정 파일을 설치하고 있습니다.
kc describe cm -n kube-flannel kube-flannel-cfg
# Name: kube-flannel-cfg
# Namespace: kube-flannel
# Labels: app=flannel
# app.kubernetes.io/managed-by=Helm
# tier=node
# Annotations: meta.helm.sh/release-name: flannel
# meta.helm.sh/release-namespace: kube-flannel
#
# Data
# ====
# cni-conf.json:
# ----
# {
# "name": "cbr0",
# "cniVersion": "0.3.1",
# "plugins": [
# {
# "type": "flannel",
# "delegate": {
# "hairpinMode": true,
# "isDefaultGateway": true
# }
# },
# {
# "type": "portmap",
# "capabilities": {
# "portMappings": true
# }
# }
# ]
# }
#
# net-conf.json:
# ----
# {
# "Network": "10.244.0.0/16",
# "Backend": {
# "Type": "vxlan"
# }
# }
# ConfigMap에는 CNI 설정(cni-conf.json)과 Flannel 네트워크 설정(net-conf.json)이 포함되어 있습니다.
# cni-conf.json은 containerd가 사용할 CNI 플러그인 설정이고, net-conf.json은 Flannel의 네트워크 구성입니다.
kc describe ds -n kube-flannel
# ...
# Command:
# /opt/bin/flanneld
# --ip-masq
# --kube-subnet-mgr
# --iface=enp0s9
# Flannel DaemonSet의 컨테이너가 실행하는 명령어를 확인할 수 있습니다.
# flanneld는 각 노드에서 실행되며, Kubernetes API를 통해 서브넷 정보를 관리하고 VXLAN 인터페이스를 구성합니다.
# flannel cni 바이너리 설치 확인
ls -l /opt/cni/bin/
# total 66032
# -rwxr-xr-x. 1 root root 3239200 Dec 12 2024 bandwidth
# -rwxr-xr-x. 1 root root 3731632 Dec 12 2024 bridge
# -rwxr-xr-x. 1 root root 9123544 Dec 12 2024 dhcp
# -rwxr-xr-x. 1 root root 3379872 Dec 12 2024 dummy
# -rwxr-xr-x. 1 root root 3742888 Dec 12 2024 firewall
# -rwxr-xr-x. 1 root root 2903098 Jan 21 02:55 flannel
# -rwxr-xr-x. 1 root root 3383408 Dec 12 2024 host-device
# -rwxr-xr-x. 1 root root 2812400 Dec 12 2024 host-local
# -rwxr-xr-x. 1 root root 3380928 Dec 12 2024 ipvlan
# -rw-r--r--. 1 root root 11357 Dec 12 2024 LICENSE
# -rwxr-xr-x. 1 root root 2953200 Dec 12 2024 loopback
# -rwxr-xr-x. 1 root root 3448024 Dec 12 2024 macvlan
# -rwxr-xr-x. 1 root root 3312488 Dec 12 2024 portmap
# -rwxr-xr-x. 1 root root 3524072 Dec 12 2024 ptp
# -rw-r--r--. 1 root root 2343 Dec 12 2024 README.md
# -rwxr-xr-x. 1 root root 3091976 Dec 12 2024 sbr
# -rwxr-xr-x. 1 root root 2526944 Dec 12 2024 static
# -rwxr-xr-x. 1 root root 3516272 Dec 12 2024 tap
# -rwxr-xr-x. 1 root root 2956032 Dec 12 2024 tuning
# -rwxr-xr-x. 1 root root 3380544 Dec 12 2024 vlan
# -rwxr-xr-x. 1 root root 3160560 Dec 12 2024 vrf
# Flannel Init 컨테이너가 flannel CNI 바이너리를 /opt/cni/bin/ 디렉토리에 설치했습니다.
# 이 디렉토리는 containerd가 CNI 플러그인을 찾는 기본 경로입니다.
# cni 설정 정보 확인
tree /etc/cni/net.d/
# /etc/cni/net.d/
# └── 10-flannel.conflist
#
# 1 directory, 1 file
# CNI 설정 파일이 /etc/cni/net.d/ 디렉토리에 생성되었습니다.
# 파일 이름의 숫자(10)는 정렬 순서를 의미하며, containerd는 이 디렉토리의 .conflist 파일을 읽어 CNI 설정을 로드합니다.
cat /etc/cni/net.d/10-flannel.conflist | jq
# {
# "name": "cbr0",
# "cniVersion": "0.3.1",
# "plugins": [
# {
# "type": "flannel",
# "delegate": {
# "hairpinMode": true,
# "isDefaultGateway": true
# }
# },
# {
# "type": "portmap",
# "capabilities": {
# "portMappings": true
# }
# }
# ]
# }
# CNI 설정 파일의 내용입니다. flannel 플러그인과 portmap 플러그인(포트 포워딩용)이 구성되어 있습니다.
# cbr0은 컨테이너 브리지 이름이며, 각 Pod는 이 브리지를 통해 통신합니다.
# cni 설치 후 아래 상태(conditions) 정상 확인
crictl info | jq
# {
# "cniconfig": {
# "Networks": [
# {
# "Config": {
# "CNIVersion": "0.3.1",
# "Name": "cni-loopback",
# "Plugins": [
# {
# "Network": {
# "ipam": {},
# "type": "loopback"
# },
# "Source": "{\"type\":\"loopback\"}"
# }
# ],
# "Source": "{\n\"cniVersion\": \"0.3.1\",\n\"name\": \"cni-loopback\",\n\"plugins\": [{\n \"type\": \"loopback\"\n}]\n}"
# },
# "IFName": "lo"
# },
# {
# "Config": {
# "CNIVersion": "0.3.1",
# "Name": "cbr0",
# "Plugins": [
# {
# "Network": {
# "ipam": {},
# "type": "flannel"
# },
# "Source": "{\"delegate\":{\"hairpinMode\":true,\"isDefaultGateway\":true},\"type\":\"flannel\"}"
# },
# {
# "Network": {
# "capabilities": {
# "portMappings": true
# },
# "ipam": {},
# "type": "portmap"
# },
# "Source": "{\"capabilities\":{\"portMappings\":true},\"type\":\"portmap\"}"
# }
# ],
# "Source": "{\n \"name\": \"cbr0\",\n \"cniVersion\": \"0.3.1\",\n \"plugins\": [\n {\n \"type\": \"flannel\",\n \"delegate\": {\n \"hairpinMode\": true,\n \"isDefaultGateway\": true\n }\n },\n {\n \"type\": \"portmap\",\n \"capabilities\": {\n \"portMappings\": true\n }\n }\n ]\n}\n"
# },
# "IFName": "eth0"
# }
# ],
# "PluginConfDir": "/etc/cni/net.d",
# "PluginDirs": [
# "/opt/cni/bin"
# ],
# "PluginMaxConfNum": 1,
# "Prefix": "eth"
# },
# ...
# "status": {
# "conditions": [
# {
# "message": "",
# "reason": "",
# "status": true,
# "type": "RuntimeReady"
# },
# {
# "message": "",
# "reason": "",
# "status": true,
# "type": "NetworkReady"
# },
# {
# "message": "",
# "reason": "",
# "status": true,
# "type": "ContainerdHasNoDeprecationWarnings"
# }
# ]
# }
# }
# containerd가 CNI 설정을 정상적으로 로드했으며, NetworkReady 상태가 true입니다.
# cbr0 네트워크가 eth0 인터페이스로 매핑되어 Pod 네트워크가 구성되었습니다.
# coredns 파드 정상 기동 확인
kubectl get pod -n kube-system -owide
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# coredns-668d6bf9bc-47zx5 1/1 Running 0 14m 10.244.0.3 k8s-ctr <none> <none>
# coredns-668d6bf9bc-77pjv 1/1 Running 0 14m 10.244.0.2 k8s-ctr <none> <none>
# etcd-k8s-ctr 1/1 Running 0 14m 192.168.10.100 k8s-ctr <none> <none>
# kube-apiserver-k8s-ctr 1/1 Running 0 14m 192.168.10.100 k8s-ctr <none> <none>
# kube-controller-manager-k8s-ctr 1/1 Running 0 14m 192.168.10.100 k8s-ctr <none> <none>
# kube-proxy-qhqpz 1/1 Running 0 14m 192.168.10.100 k8s-ctr <none> <none>
# kube-scheduler-k8s-ctr 1/1 Running 0 14m 192.168.10.100 k8s-ctr <none> <none>
# CNI 설치 전에는 Pending 상태였던 CoreDNS Pod들이 이제 Running 상태가 되었고, Pod IP(10.244.0.2, 10.244.0.3)가 할당되었습니다.
# 이는 Flannel이 정상적으로 작동하여 Pod 네트워크를 구성했음을 의미합니다.
# network 정보 확인
ip -c route | grep 10.244.
# 10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1
# 노드의 Pod CIDR(10.244.0.0/24)에 대한 라우팅 정보입니다. cni0 브리지를 통해 해당 서브넷으로 트래픽이 전달됩니다.
ip addr
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# inet 127.0.0.1/8 scope host lo
# valid_lft forever preferred_lft forever
# inet6 ::1/128 scope host noprefixroute
# valid_lft forever preferred_lft forever
# 2: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# link/ether 08:00:27:90:ea:eb brd ff:ff:ff:ff:ff:ff
# altname enx08002790eaeb
# inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s8
# valid_lft 73368sec preferred_lft 73368sec
# 3: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# link/ether 08:00:27:55:33:36 brd ff:ff:ff:ff:ff:ff
# altname enx080027553336
# inet 192.168.10.100/24 brd 192.168.10.255 scope global noprefixroute enp0s9
# valid_lft forever preferred_lft forever
# 4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
# link/ether ca:69:44:50:93:9e brd ff:ff:ff:ff:ff:ff
# inet 10.244.0.0/32 scope global flannel.1
# valid_lft forever preferred_lft forever
# inet6 fe80::c869:44ff:fe50:939e/64 scope link proto kernel_ll
# valid_lft forever preferred_lft forever
# 5: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default qlen 1000
# link/ether 1a:7c:2c:59:31:f9 brd ff:ff:ff:ff:ff:ff
# inet 10.244.0.1/24 brd 10.244.0.255 scope global cni0
# valid_lft forever preferred_lft forever
# inet6 fe80::187c:2cff:fe59:31f9/64 scope link proto kernel_ll
# valid_lft forever preferred_lft forever
# 6: vethf88c96d1@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default qlen 1000
# link/ether e6:5d:04:6c:d2:4b brd ff:ff:ff:ff:ff:ff link-netns cni-e5110b94-cd63-c3c5-cbe9-2cae2adf6077
# inet6 fe80::e45d:4ff:fe6c:d24b/64 scope link proto kernel_ll
# valid_lft forever preferred_lft forever
# 7: vethf09464e2@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP group default qlen 1000
# link/ether b6:37:ad:b1:90:89 brd ff:ff:ff:ff:ff:ff link-netns cni-5db9e363-6377-1f95-6d86-ad565ecce772
# inet6 fe80::b437:adff:feb1:9089/64 scope link proto kernel_ll
# valid_lft forever preferred_lft forever
# 네트워크 인터페이스 정보를 확인할 수 있습니다:
# - flannel.1: VXLAN 터널 인터페이스, 노드 간 Pod 통신을 위한 오버레이 네트워크 (10.244.0.0/32)
# - cni0: 컨테이너 브리지, 같은 노드 내 Pod 간 통신을 위한 L2 브리지 (10.244.0.1/24)
# - veth*: 가상 이더넷 쌍, 각 Pod의 네트워크 네임스페이스를 호스트 네임스페이스와 연결
# MTU가 1450인 것은 VXLAN 헤더(50바이트)를 고려한 값입니다.
bridge link
# 6: vethf88c96d1@enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 master cni0 state forwarding priority 32 cost 2
# 7: vethf09464e2@enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 master cni0 state forwarding priority 32 cost 2
# veth 인터페이스들이 cni0 브리지에 연결되어 있음을 확인할 수 있습니다.
# 각 veth는 하나의 Pod를 나타내며, 브리지를 통해 같은 노드의 다른 Pod와 통신할 수 있습니다.
lsns -t net
# NS TYPE NPROCS PID USER NETNSID NSFS COMMAND
# 4026531840 net 169 1 root unassigned /usr/lib/systemd/systemd
# 4026532128 net 1 645 root unassigned ├─/usr/sbin/irqbalance
# 4026532202 net 1 812 polkitd unassigned └─/usr/lib/polkit-1/polkitd
# 4026532300 net 2 10163 65535 0 /run/netns/cni-e5110b94-cd63-c3c5-cbe9-2cae2adf6077 /pause
# 4026532375 net 2 10155 65535 0 /run/netns/cni-5db9e363-6377-1f95-6d86-ad565ecce772 /pause
# 각 Pod는 독립적인 네트워크 네임스페이스를 가지고 있습니다.
# /run/netns/cni-* 경로는 Pod의 네트워크 네임스페이스를 나타내며, pause 컨테이너가 이 네임스페이스를 유지합니다.
# iptables 규칙 확인
iptables -t nat -S
iptables -t filter -S
iptables-save
# (모든 iptables 규칙을 저장 형식으로 출력합니다. 백업 및 복원에 사용할 수 있습니다.)
# iptables 규칙은 kube-proxy와 Flannel에 의해 자동으로 관리됩니다.
# NAT 테이블은 Service의 ClusterIP를 Pod IP로 변환하고, FILTER 테이블은 네트워크 정책을 적용합니다.
[k8s-ctr] 노드 정보 확인, 기본 환경 정보 출력 비교, sysctl 변경 확인
# # kubelet 활성화 확인 : 실제 기동은 kubeadm init 후에 시작됨
systemctl is-active kubelet
# active
# kubelet 서비스가 활성화되어 실행 중입니다.
systemctl status kubelet --no-pager
# ● kubelet.service - kubelet: The Kubernetes Node Agent
# Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; preset: disabled)
# Drop-In: /usr/lib/systemd/system/kubelet.service.d
# └─10-kubeadm.conf
# Active: active (running) since Wed 2026-01-21 02:41:00 KST; 38min ago
# Invocation: c34c2c29a1c744cb87e847e22c9df203
# Docs: https://kubernetes.io/docs/
# Main PID: 8074 (kubelet)
# Tasks: 13 (limit: 18742)
# Memory: 41.7M (peak: 44M)
# CPU: 1min 15.492s
# CGroup: /system.slice/kubelet.service
# └─8074 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kub…
# kubelet이 systemd 서비스로 실행 중이며, 10-kubeadm.conf 드롭인 파일을 통해 kubeadm 설정을 사용합니다.
# kubeadm init 실행 후 kubelet이 시작되어 Control Plane Pod들을 관리하고 있습니다.
# 노드 정보 확인 : 일반 워크로드가 Control Plane에 스케줄 X
kc describe node
# Name: k8s-ctr
# Roles: control-plane
# Labels: beta.kubernetes.io/arch=arm64
# beta.kubernetes.io/os=linux
# kubernetes.io/arch=arm64
# kubernetes.io/hostname=k8s-ctr
# kubernetes.io/os=linux
# node-role.kubernetes.io/control-plane=
# node.kubernetes.io/exclude-from-external-load-balancers=
# Annotations: flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"ca:69:44:50:93:9e"}
# flannel.alpha.coreos.com/backend-type: vxlan
# flannel.alpha.coreos.com/kube-subnet-manager: true
# flannel.alpha.coreos.com/public-ip: 192.168.10.100
# kubeadm.alpha.kubernetes.io/cri-socket: unix:///run/containerd/containerd.sock
# node.alpha.kubernetes.io/ttl: 0
# volumes.kubernetes.io/controller-managed-attach-detach: true
# CreationTimestamp: Wed, 21 Jan 2026 02:40:58 +0900
# Taints: node-role.kubernetes.io/control-plane:NoSchedule
# Unschedulable: false
# 노드에 control-plane 레이블과 NoSchedule 태인트가 설정되어 있어 일반 워크로드가 스케줄되지 않습니다.
# Flannel 관련 어노테이션을 통해 VXLAN 백엔드 정보를 확인할 수 있습니다.
# Conditions:
# Type Status LastHeartbeatTime LastTransitionTime Reason Message
# ---- ------ ----------------- ------------------ ------ -------
# NetworkUnavailable False Wed, 21 Jan 2026 02:55:12 +0900 Wed, 21 Jan 2026 02:55:12 +0900 FlannelIsUp Flannel is running on this node
# MemoryPressure False Wed, 21 Jan 2026 03:16:00 +0900 Wed, 21 Jan 2026 02:40:57 +0900 KubeletHasSufficientMemory kubelet has sufficient memory available
# DiskPressure False Wed, 21 Jan 2026 03:16:00 +0900 Wed, 21 Jan 2026 02:40:57 +0900 KubeletHasNoDiskPressure kubelet has no disk pressure
# PIDPressure False Wed, 21 Jan 2026 03:16:00 +0900 Wed, 21 Jan 2026 02:40:57 +0900 KubeletHasSufficientPID kubelet has sufficient PID available
# Ready True Wed, 21 Jan 2026 03:16:00 +0900 Wed, 21 Jan 2026 02:55:17 +0900 KubeletReady kubelet is posting ready status
# 노드의 상태 조건을 확인할 수 있습니다. NetworkUnavailable이 False이고 Ready가 True이므로 노드가 정상적으로 작동 중입니다.
# Flannel이 실행되어 네트워크가 구성되었고, 리소스 압박 없이 정상 상태입니다.
# Addresses:
# InternalIP: 192.168.10.100
# Hostname: k8s-ctr
# Capacity:
# cpu: 4
# ephemeral-storage: 60970Mi
# hugepages-1Gi: 0
# hugepages-2Mi: 0
# hugepages-32Mi: 0
# hugepages-64Ki: 0
# memory: 2893976Ki
# pods: 110
# Allocatable:
# cpu: 4
# ephemeral-storage: 57538510753
# hugepages-1Gi: 0
# hugepages-2Mi: 0
# hugepages-32Mi: 0
# hugepages-64Ki: 0
# memory: 2791576Ki
# pods: 110
# 노드의 리소스 용량과 할당 가능한 리소스를 확인할 수 있습니다. 최대 110개의 Pod를 스케줄할 수 있습니다.
# PodCIDR: 10.244.0.0/24
# PodCIDRs: 10.244.0.0/24
# 노드에 할당된 Pod CIDR입니다. kube-controller-manager의 --allocate-node-cidrs=true 옵션에 의해 자동 할당되었습니다.
# Non-terminated Pods: (8 in total)
# Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits Age
# --------- ---- ------------ ---------- --------------- ------------- ---
# kube-flannel kube-flannel-ds-w9s2j 100m (2%) 0 (0%) 50Mi (1%) 0 (0%) 24m
# kube-system coredns-668d6bf9bc-47zx5 100m (2%) 0 (0%) 70Mi (2%) 170Mi (6%) 38m
# kube-system coredns-668d6bf9bc-77pjv 100m (2%) 0 (0%) 70Mi (2%) 170Mi (6%) 38m
# kube-system etcd-k8s-ctr 100m (2%) 0 (0%) 100Mi (3%) 0 (0%) 38m
# kube-system kube-apiserver-k8s-ctr 250m (6%) 0 (0%) 0 (0%) 0 (0%) 38m
# kube-system kube-controller-manager-k8s-ctr 200m (5%) 0 (0%) 0 (0%) 0 (0%) 38m
# kube-system kube-proxy-qhqpz 0 (0%) 0 (0%) 0 (0%) 0 (0%) 38m
# kube-system kube-scheduler-k8s-ctr 100m (2%) 0 (0%) 0 (0%) 0 (0%) 38m
# Allocated resources:
# (Total limits may be over 100 percent, i.e., overcommitted.)
# Resource Requests Limits
# -------- -------- ------
# cpu 950m (23%) 0 (0%)
# memory 290Mi (10%) 340Mi (12%)
# 노드에서 실행 중인 Pod들의 리소스 사용량을 확인할 수 있습니다. 총 8개의 Pod가 실행 중이며, CPU는 23%, 메모리는 10% 사용 중입니다.
# 기본 환경 정보 출력 저장
cat /etc/sysconfig/kubelet
# KUBELET_EXTRA_ARGS=
tree /etc/kubernetes | tee -a etc_kubernetes-2.txt
# /etc/kubernetes
# ├── admin.conf
# ├── controller-manager.conf
# ├── kubelet.conf
# ├── manifests
# │ ├── etcd.yaml
# │ ├── kube-apiserver.yaml
# │ ├── kube-controller-manager.yaml
# │ └── kube-scheduler.yaml
# ├── pki
# │ ├── apiserver.crt
# │ ├── apiserver-etcd-client.crt
# │ ├── apiserver-etcd-client.key
# │ ├── apiserver.key
# │ ├── apiserver-kubelet-client.crt
# │ ├── apiserver-kubelet-client.key
# │ ├── ca.crt
# │ ├── ca.key
# │ ├── etcd
# │ │ ├── ca.crt
# │ │ ├── ca.key
# │ │ ├── healthcheck-client.crt
# │ │ ├── healthcheck-client.key
# │ │ ├── peer.crt
# │ │ ├── peer.key
# │ │ ├── server.crt
# │ │ └── server.key
# │ ├── front-proxy-ca.crt
# │ ├── front-proxy-ca.key
# │ ├── front-proxy-client.crt
# │ ├── front-proxy-client.key
# │ ├── sa.key
# │ └── sa.pub
# ├── scheduler.conf
# └── super-admin.conf
#
# 4 directories, 31 files
# kubeadm init 실행 후 생성된 파일들을 확인할 수 있습니다:
# - kubeconfig 파일들: admin.conf, controller-manager.conf, kubelet.conf, scheduler.conf, super-admin.conf
# - manifests: Static Pod 매니페스트 파일들 (etcd, kube-apiserver, kube-controller-manager, kube-scheduler)
# - pki: 인증서 및 키 파일들 (CA, API Server, etcd, front-proxy 등)
tree /var/lib/kubelet | tee -a var_lib_kubelet-2.txt
# /var/lib/kubelet
# ├── checkpoints
# ├── config.yaml
# ├── cpu_manager_state
# ├── device-plugins
# │ └── kubelet.sock
# ├── kubeadm-flags.env
# ├── memory_manager_state
# ├── pki
# │ ├── kubelet-client-2026-01-21-02-40-56.pem
# │ ├── kubelet-client-current.pem -> /var/lib/kubelet/pki/kubelet-client-2026-01-21-02-40-56.pem
# │ ├── kubelet.crt
# │ └── kubelet.key
# ├── plugins
# ├── plugins_registry
# ├── pod-resources
# │ └── kubelet.sock
# └── pods
# ├── 1fac1c35f1565d90c54f12e6a7f31eac
# │ ├── containers
# │ │ └── etcd
# │ │ └── 18d31be2
# │ ├── etc-hosts
# │ ├── plugins
# │ └── volumes
# ├── 20414908-ba02-4c61-a957-4a1c2f992f35
# │ ├── containers
# │ │ └── coredns
# │ │ └── 3614549f
# ├── 4c9e7ba9c576bda9272d29d2be8688bd
# │ ├── containers
# │ │ └── kube-scheduler
# ├── 50c5cf354a58e388996b0acd73e093c6
# │ ├── containers
# │ │ └── kube-apiserver
# ├── 7314ab3f0ec6401c196ca943fad44a05
# │ ├── containers
# │ │ └── kube-controller-manager
# ├── b39e0b94-5da7-44d5-a766-f8ca9cb29201
# │ ├── containers
# │ │ └── coredns
# ├── b8bf8da8-eff5-4c54-81fb-95841b60b327
# │ ├── containers
# │ │ └── kube-proxy
# └── bda4ec80-fa17-444a-a8da-b89a2a3661da
# ├── containers
# │ ├── install-cni
# │ ├── install-cni-plugin
# │ └── kube-flannel
# ├── etc-hosts
# ├── plugins
# │ └── kubernetes.io~empty-dir
# │ ├── wrapped_flannel-cfg
# │ └── wrapped_kube-api-access-mn5s4
# kubelet의 작업 디렉토리 구조입니다:
# - config.yaml: kubelet 설정 파일
# - kubeadm-flags.env: kubeadm이 설정한 kubelet 플래그
# - pki: kubelet 클라이언트 인증서 및 키
# - pods: 각 Pod의 런타임 데이터 (컨테이너, 볼륨, 플러그인 등)
# - device-plugins, pod-resources: 소켓 파일들
tree /run/containerd/ -L 3 | tee -a run_containerd-2.txt
pstree -alnp | tee -a pstree-2.txt
systemd-cgls --no-pager | tee -a systemd-cgls-2.txt
lsns | tee -a lsns-2.txt
ip addr | tee -a ip_addr-2.txt
ss -tnlp | tee -a ss-2.txt
df -hT | tee -a df-2.txt
findmnt | tee -a findmnt-2.txt
sysctl -a | tee -a sysctl-2.txt
다음 표는 kubeadm init 및 Flannel CNI 설치 전후의 시스템 상태를 비교한 것입니다.
주요 디렉토리 및 파일 구조 비교
| 항목 | kubeadm init 전 (1) | kubeadm init 후 (2) | 설명 |
|---|---|---|---|
| /etc/kubernetes | manifests/ (빈 디렉토리) |
admin.conf, controller-manager.conf, kubelet.conf, scheduler.conf, super-admin.conf, manifests/ (Static Pod YAML), pki/ (인증서 및 키) |
kubeadm init 실행 시 kubeconfig 파일, Static Pod 매니페스트, 인증서가 생성됨 |
| /var/lib/kubelet | 빈 디렉토리 | config.yaml, kubeadm-flags.env, pki/ (kubelet 인증서), pods/ (Pod 런타임 데이터), checkpoints/, device-plugins/, pod-resources/ |
kubelet이 시작되면서 설정 파일과 Pod 데이터가 생성됨 |
| /run/containerd/ | containerd.sock, io.containerd.grpc.v1.cri, io.containerd.runtime.v2.task, io.containerd.sandbox.controller.v1.shim |
동일 + io.containerd.grpc.v1.cri/containers/ (10개 컨테이너), io.containerd.grpc.v1.cri/sandboxes/ (8개 샌드박스) |
Control Plane Pod와 CoreDNS Pod가 실행되면서 컨테이너와 샌드박스가 생성됨 |
프로세스 및 cgroup 비교
| 항목 | kubeadm init 전 (1) | kubeadm init 후 (2) | 설명 |
|---|---|---|---|
| pstree | containerd만 실행 중 | containerd + kubelet +여러 containerd-shim 프로세스 |
kubelet이 시작되고, 각 Pod마다 containerd-shim이 생성됨 |
| systemd-cgls | system.slice만 존재 | system.slice + kubepods.slice (Pod cgroup 계층) |
kubelet이 Pod를 cgroup으로 관리하기 시작. kubepods-burstable.slice, kubepods-besteffort.slice 생성 |
네트워크 네임스페이스 비교
| 항목 | kubeadm init 전 (1) | kubeadm init 후 (2) | 설명 |
|---|---|---|---|
| lsns | 기본 네임스페이스만 존재 (time, cgroup, pid, user, uts, ipc, net, mnt) | 기본 네임스페이스 + Pod별 네임스페이스(각 Pod마다 mnt, ipc, pid, cgroup, net 네임스페이스 생성) |
각 Pod가 독립적인 네임스페이스를 가지게 됨. Control Plane Pod는 hostNetwork를 사용하므로 net 네임스페이스를 공유 |
네트워크 인터페이스 비교
| 항목 | kubeadm init 전 (1) | kubeadm init 후 (2) | 설명 |
|---|---|---|---|
| ip addr | lo, enp0s8, enp0s9 | lo, enp0s8, enp0s9 + flannel.1 (VXLAN 터널), cni0 (컨테이너 브리지), veth* (Pod별 가상 이더넷) |
Flannel CNI 설치 후 VXLAN 터널 인터페이스와 컨테이너 브리지가 생성됨. 각 Pod마다 veth 쌍이 생성됨 |
소켓 상태 비교
| 항목 | kubeadm init 전 (1) | kubeadm init 후 (2) | 설명 |
|---|---|---|---|
| ss -tnlp | sshd(22), containerd(41597), rpcbind(111), systemd(9090) | 동일 + kube-apiserver(6443), kubelet(10250, 10248), kube-controller-manager(10257), kube-scheduler(10259), etcd(2379, 2380) |
Control Plane 구성 요소들이 각각의 포트에서 리스닝 시작 |
파일시스템 비교
| 항목 | kubeadm init 전 (1) | kubeadm init 후 (2) | 설명 |
|---|---|---|---|
| df -hT | 기본 파일시스템만 | 기본 파일시스템 + overlay (각 Pod의 컨테이너 레이어), shm (각 Pod의 공유 메모리) |
각 Pod의 컨테이너마다 overlay 파일시스템과 shm이 마운트됨. /dev/sda3 사용량이 2% → 4%로 증가 |
마운트 포인트 비교
| 항목 | kubeadm init 전 (1) | kubeadm init 후 (2) | 설명 |
|---|---|---|---|
| findmnt | 기본 마운트 포인트만 | 기본 마운트 포인트 + /run/containerd/io.containerd.grpc.v1.cri/ (각 Pod의 런타임 디렉토리), /run/netns/cni-* (Pod 네트워크 네임스페이스), /var/lib/kubelet/pods/* (Pod 볼륨 마운트) |
Pod 실행에 필요한 런타임 디렉토리, 네트워크 네임스페이스, 볼륨이 마운트됨 |
커널 파라미터 (sysctl) 변경사항 (kubelet에 의해 변경된 파라미터)
| 파라미터 | kubeadm init 전 (1) | kubeadm init 후 (2) | 설명 |
|---|---|---|---|
kernel.panic |
0 | 10 | 커널 패닉 시 자동 재부팅 시간(초) 설정 |
kernel.panic_on_oops |
1 | 1 (변경 없음) | Oops 발생 시 패닉 유발 여부 |
vm.overcommit_memory |
0 | 1 | 메모리 오버커밋 허용 (항상 성공) |
vm.panic_on_oom |
0 | 0 (변경 없음) | OOM 발생 시 패닉 유발 여부 |
kernel.keys.root_maxkeys |
1000000 | 1000000 (변경 없음) | root 사용자 최대 키 수 |
kernel.keys.root_maxbytes |
25000000 | 25000000 (변경 없음) | root 사용자 최대 키 바이트 수 |
커널 파라미터 (sysctl) 변경사항 (kube-proxy에 의해 변경된 파라미터)
| 파라미터 | kubeadm init 전 (1) | kubeadm init 후 (2) | 설명 |
|---|---|---|---|
net.nf_conntrack_max |
65536 | 131072 | 최대 연결 추적 항목 수 (2배 증가) |
net.netfilter.nf_conntrack_max |
65536 | 131072 | netfilter 연결 추적 최대값 |
net.netfilter.nf_conntrack_count |
1 | 282 | 현재 추적 중인 연결 수 (Pod 통신으로 증가) |
net.netfilter.nf_conntrack_tcp_timeout_close_wait |
60 | 3600 | TCP CLOSE_WAIT 상태 타임아웃 (1시간) |
net.netfilter.nf_conntrack_tcp_timeout_established |
432000 | 86400 | TCP ESTABLISHED 상태 타임아웃 (5일 → 1일) |
파일 시스템 통계 변경
| 파라미터 | kubeadm init 전 (1) | kubeadm init 후 (2) | 설명 |
|---|---|---|---|
fs.file-nr |
1312 | 1888 | 열린 파일 수 증가 (Pod 실행으로 인한 파일 핸들 증가) |
fs.inode-nr |
23971 | 43665 | 사용 중인 inode 수 증가 (Pod 및 컨테이너 파일 생성) |
fs.dentry-state |
27045 19047 | 48944 38417 | 디렉토리 엔트리 캐시 상태 변화 |
vi -d etc_kubernetes-1.txt etc_kubernetes-2.txt
# /etc/kubernetes | /etc/kubernetes
# └── manifests | ├── admin.conf
# --------------------------------| ├── controller-manager.conf
# --------------------------------| ├── kubelet.conf
# --------------------------------| ├── manifests
vi -d var_lib_kubelet-1.txt var_lib_kubelet-2.txt
# 비교 결과: kubeadm init 전에는 /var/lib/kubelet이 비어있었지만, 이후에는 kubelet 설정 파일, 인증서, Pod 런타임 데이터가 생성됨
vi -d run_containerd-1.txt run_containerd-2.txt
# 비교 결과: kubeadm init 전에는 containerd 소켓만 존재했지만, 이후에는 10개의 컨테이너와 8개의 샌드박스가 생성됨
# - containers: Control Plane Pod (etcd, kube-apiserver, kube-controller-manager, kube-scheduler), CoreDNS Pod, kube-proxy Pod, Flannel Pod
# - sandboxes: 각 Pod의 샌드박스 (pause 컨테이너)
vi -d pstree-1.txt pstree-2.txt
# 비교 결과: kubeadm init 전에는 containerd만 실행 중이었지만, 이후에는 kubelet과 여러 containerd-shim 프로세스가 추가됨
# - kubelet: Kubernetes 노드 에이전트
# - containerd-shim: 각 Pod의 컨테이너를 관리하는 프로세스
vi -d systemd-cgls-1.txt systemd-cgls-2.txt
# 비교 결과: kubeadm init 전에는 system.slice만 존재했지만, 이후에는 kubepods.slice가 추가됨
# - kubepods-burstable.slice: Burstable QoS 클래스 Pod들 (Control Plane, CoreDNS, Flannel)
# - kubepods-besteffort.slice: BestEffort QoS 클래스 Pod들 (kube-proxy)
# 각 Pod는 cri-containerd-* 형식의 cgroup으로 관리됨
vi -d lsns-1.txt lsns-2.txt
# 비교 결과: kubeadm init 전에는 기본 네임스페이스만 존재했지만, 이후에는 각 Pod마다 독립적인 네임스페이스가 생성됨
# - 각 Pod는 mnt, ipc, pid, cgroup 네임스페이스를 가짐
# - CoreDNS Pod는 net 네임스페이스도 별도로 가짐 (hostNetwork=false)
# - Control Plane Pod는 hostNetwork=true이므로 net 네임스페이스를 공유
vi -d ip_addr-1.txt ip_addr-2.txt
# 비교 결과: kubeadm init 전에는 lo, enp0s8, enp0s9만 존재했지만, Flannel CNI 설치 후 네트워크 인터페이스가 추가됨
# - flannel.1: VXLAN 터널 인터페이스 (10.244.0.0/32)
# - cni0: 컨테이너 브리지 (10.244.0.1/24)
# - veth*: 각 Pod의 가상 이더넷 인터페이스
vi -d ss-1.txt ss-2.txt
# 비교 결과: kubeadm init 전에는 기본 서비스 포트만 열려있었지만, 이후에는 Control Plane 구성 요소들의 포트가 추가됨
# - kube-apiserver: 6443 (HTTPS), 127.0.0.1:6443, 192.168.10.100:6443
# - kubelet: 10250 (read-only API), 10248 (health check)
# - kube-controller-manager: 10257 (secure port)
# - kube-scheduler: 10259 (secure port)
# - etcd: 2379 (client), 2380 (peer)
vi -d df-1.txt df-2.txt
# 비교 결과: kubeadm init 전에는 기본 파일시스템만 존재했지만, 이후에는 각 Pod의 컨테이너마다 overlay와 shm이 마운트됨
# - overlay: 각 컨테이너의 레이어드 파일시스템
# - shm: 각 Pod의 공유 메모리 (64MB)
# - /dev/sda3 사용량이 2% → 4%로 증가 (Pod 이미지 및 데이터 저장)
vi -d findmnt-1.txt findmnt-2.txt
# 비교 결과: kubeadm init 전에는 기본 마운트 포인트만 존재했지만, 이후에는 Pod 실행에 필요한 마운트가 추가됨
# - /run/containerd/io.containerd.grpc.v1.cri/*: 각 Pod의 런타임 디렉토리
# - /run/netns/cni-*: Pod 네트워크 네임스페이스
# - /var/lib/kubelet/pods/*: Pod 볼륨 마운트 포인트
# kubelet 에 --protect-kernel-defaults=false 적용되어 관련 코드에 sysctl 커널 파라미터 적용 : 아래 링크 확왼
## 위 설정 시, 커널 튜닝 가능 항목 중 하나라도 kubelet의 기본값과 다르면 오류가 발생합니다
vi -d sysctl-1.txt sysctl-2.txt
# 비교 결과: kubeadm init 및 kube-proxy 실행 후 커널 파라미터가 변경됨
# 주요 변경사항은 위의 "커널 파라미터 (sysctl) 변경사항" 표에 정리되어 있음
kernel.panic = 0 -> 10 변경
kernel.panic_on_oops = 1 기존값 그대로
vm.overcommit_memory = 0 -> 1 변경
vm.panic_on_oom = 0 기존값 그대로
sysctl kernel.keys.root_maxkeys # 1000000 기존값 그대로
sysctl kernel.keys.root_maxbytes # 25000000 # root_maxkeys * 25 기존값 그대로
# kube-proxy 에서도 관련 코드에 sysctl 커널 파라미터 적용 : 아래 링크 확왼
net.nf_conntrack_max = 65536 -> 131072
net.netfilter.nf_conntrack_max = 65536 -> 131072
net.netfilter.nf_conntrack_count = 1 -> 282
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60 -> 3600
net.netfilter.nf_conntrack_tcp_timeout_established = 432000 -> 86400
[k8s-ctr] 인증서 확인
# kubeadm-config 확인
kc describe cm -n kube-system kubeadm-config
# Name: kubeadm-config
# Namespace: kube-system
# Labels: <none>
# Annotations: <none>
#
# Data
# ====
# ClusterConfiguration:
# ----
# apiServer: {}
# apiVersion: kubeadm.k8s.io/v1beta4
# caCertificateValidityPeriod: 87600h0m0s
# certificateValidityPeriod: 8760h0m0s
# certificatesDir: /etc/kubernetes/pki
# clusterName: kubernetes
# controllerManager: {}
# dns: {}
# encryptionAlgorithm: RSA-2048
# etcd:
# local:
# dataDir: /var/lib/etcd
# imageRepository: registry.k8s.io
# kind: ClusterConfiguration
# kubernetesVersion: v1.32.11
# networking:
# dnsDomain: cluster.local
# podSubnet: 10.244.0.0/16
# serviceSubnet: 10.96.0.0/16
# proxy: {}
# scheduler: {}
# kubeadm-config ConfigMap에는 클러스터 설정 정보가 저장되어 있습니다.
# - caCertificateValidityPeriod: 87600h0m0s (10년) - CA 인증서 유효 기간
# - certificateValidityPeriod: 8760h0m0s (1년) - 일반 인증서 유효 기간
# - certificatesDir: /etc/kubernetes/pki - 인증서 저장 디렉토리
# - encryptionAlgorithm: RSA-2048 - 암호화 알고리즘
# Checks expiration for the certificates in the local PKI managed by kubeadm.
kubeadm certs check-expiration
# [check-expiration] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
# [check-expiration] Use 'kubeadm init phase upload-config --config your-config.yaml' to re-upload it.
#
# CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED
# admin.conf Jan 20, 2027 17:40 UTC 364d ca no
# apiserver Jan 20, 2027 17:40 UTC 364d ca no
# apiserver-etcd-client Jan 20, 2027 17:40 UTC 364d etcd-ca no
# apiserver-kubelet-client Jan 20, 2027 17:40 UTC 364d ca no
# controller-manager.conf Jan 20, 2027 17:40 UTC 364d ca no
# etcd-healthcheck-client Jan 20, 2027 17:40 UTC 364d etcd-ca no
# etcd-peer Jan 20, 2027 17:40 UTC 364d etcd-ca no
# etcd-server Jan 20, 2027 17:40 UTC 364d etcd-ca no
# front-proxy-client Jan 20, 2027 17:40 UTC 364d front-proxy-ca no
# scheduler.conf Jan 20, 2027 17:40 UTC 364d ca no
# super-admin.conf Jan 20, 2027 17:40 UTC 364d ca no
#
# CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED
# ca Jan 18, 2036 17:40 UTC 9y no
# etcd-ca Jan 18, 2036 17:40 UTC 9y no
# front-proxy-ca Jan 18, 2036 17:40 UTC 9y no
# kubeadm이 관리하는 인증서들의 만료 정보를 확인합니다.
# - 일반 인증서: 1년 유효 (364일 남음)
# - CA 인증서: 10년 유효 (9년 남음)
# - EXTERNALLY MANAGED: no - kubeadm이 관리하는 인증서 (외부 관리 아님)
# 인증서는 만료 전에 `kubeadm certs renew` 명령으로 갱신할 수 있습니다.
tree /etc/kubernetes/
# /etc/kubernetes/
# ├── admin.conf
# ├── controller-manager.conf
# ├── kubelet.conf
# ├── manifests
# │ ├── etcd.yaml
# │ ├── kube-apiserver.yaml
# │ ├── kube-controller-manager.yaml
# │ └── kube-scheduler.yaml
# ├── pki
# │ ├── apiserver.crt
# │ ├── apiserver-etcd-client.crt
# │ ├── apiserver-etcd-client.key
# │ ├── apiserver.key
# │ ├── apiserver-kubelet-client.crt
# │ ├── apiserver-kubelet-client.key
# │ ├── ca.crt
# │ ├── ca.key
# │ ├── etcd
# │ │ ├── ca.crt
# │ │ ├── ca.key
# │ │ ├── healthcheck-client.crt
# │ │ ├── healthcheck-client.key
# │ │ ├── peer.crt
# │ │ ├── peer.key
# │ │ ├── server.crt
# │ │ └── server.key
# │ ├── front-proxy-ca.crt
# │ ├── front-proxy-ca.key
# │ ├── front-proxy-client.crt
# │ ├── front-proxy-client.key
# │ ├── sa.key
# │ └── sa.pub
# ├── scheduler.conf
# └── super-admin.conf
#
# 4 directories, 31 files
# /etc/kubernetes/ 디렉토리 구조:
# - kubeconfig 파일들: admin.conf, controller-manager.conf, kubelet.conf, scheduler.conf, super-admin.conf
# - manifests/: Static Pod 매니페스트 파일들
# - pki/: 인증서 및 키 파일들
tree /etc/kubernetes/pki
# /etc/kubernetes/pki
# ├── apiserver.crt
# ├── apiserver-etcd-client.crt
# ├── apiserver-etcd-client.key
# ├── apiserver.key
# ├── apiserver-kubelet-client.crt
# ├── apiserver-kubelet-client.key
# ├── ca.crt
# ├── ca.key
# ├── etcd
# │ ├── ca.crt
# │ ├── ca.key
# │ ├── healthcheck-client.crt
# │ ├── healthcheck-client.key
# │ ├── peer.crt
# │ ├── peer.key
# │ ├── server.crt
# │ └── server.key
# ├── front-proxy-ca.crt
# ├── front-proxy-ca.key
# ├── front-proxy-client.crt
# ├── front-proxy-client.key
# ├── sa.key
# └── sa.pub
#
# 2 directories, 22 files
# 인증서 디렉토리 구조:
# - ca.crt/ca.key: Kubernetes CA 인증서 및 키
# - apiserver.crt/apiserver.key: API Server 인증서 및 키
# - apiserver-kubelet-client.crt/key: API Server가 kubelet과 통신할 때 사용하는 클라이언트 인증서
# - apiserver-etcd-client.crt/key: API Server가 etcd와 통신할 때 사용하는 클라이언트 인증서
# - etcd/: etcd 관련 인증서 (CA, peer, server, healthcheck-client)
# - front-proxy-ca.crt/key: 프론트 프록시 CA 인증서 (aggregation layer용)
# - front-proxy-client.crt/key: 프론트 프록시 클라이언트 인증서
# - sa.key/sa.pub: Service Account 서명 키 쌍
# CA 인증서
cat /etc/kubernetes/pki/ca.crt | openssl x509 -text -noout
# Certificate:
# Data:
# Version: 3 (0x2)
# Serial Number: 4686048711720833794 (0x41083079be4c1302)
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: CN=kubernetes
# Validity
# Not Before: Jan 20 17:35:53 2026 GMT
# Not After : Jan 18 17:40:53 2036 GMT
# Subject: CN=kubernetes
# ...
# X509v3 extensions:
# X509v3 Key Usage: critical
# Digital Signature, Key Encipherment, Certificate Sign
# X509v3 Basic Constraints: critical
# CA:TRUE
# X509v3 Subject Key Identifier:
# 24:6E:36:3E:5E:5B:79:C4:B1:B2:21:60:AA:E6:14:F2:3F:D1:5E:5F
# X509v3 Subject Alternative Name:
# DNS:kubernetes
# Kubernetes CA 인증서는 클러스터의 모든 인증서를 서명하는 루트 인증서입니다.
# - Issuer/Subject: CN=kubernetes (자기 서명 인증서)
# - 유효 기간: 10년 (2026-01-20 ~ 2036-01-18)
# - Key Usage: Digital Signature, Key Encipherment, Certificate Sign (인증서 서명 권한)
# - Basic Constraints: CA:TRUE (인증 기관임을 명시)
# - 이 CA 인증서로 모든 클러스터 구성 요소의 인증서를 서명합니다.
# apiserver 인증서 : 'TLS Web Server' 키 용도 확인
cat /etc/kubernetes/pki/apiserver.crt | openssl x509 -text -noout
# Certificate:
# Data:
# Version: 3 (0x2)
# Serial Number: 4408778139016981249 (0x3d2f205bd59d6b01)
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: CN=kubernetes
# Validity
# Not Before: Jan 20 17:35:53 2026 GMT
# Not After : Jan 20 17:40:53 2027 GMT
# Subject: CN=kube-apiserver
# ...
# X509v3 extensions:
# X509v3 Key Usage: critical
# Digital Signature, Key Encipherment
# X509v3 Extended Key Usage:
# TLS Web Server Authentication
# X509v3 Basic Constraints: critical
# CA:FALSE
# X509v3 Authority Key Identifier:
# 24:6E:36:3E:5E:5B:79:C4:B1:B2:21:60:AA:E6:14:F2:3F:D1:5E:5F
# X509v3 Subject Alternative Name:
# DNS:k8s-ctr, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:10.96.0.1, IP Address:192.168.10.100
# API Server 인증서는 HTTPS 서버 인증에 사용됩니다.
# - Issuer: CN=kubernetes (Kubernetes CA가 서명)
# - Subject: CN=kube-apiserver
# - 유효 기간: 1년 (2026-01-20 ~ 2027-01-20)
# - Extended Key Usage: TLS Web Server Authentication (TLS 서버 인증용)
# - Subject Alternative Name (SAN): API Server에 접근할 수 있는 모든 DNS 이름과 IP 주소
# - DNS: k8s-ctr (노드 호스트명), kubernetes, kubernetes.default, kubernetes.default.svc, kubernetes.default.svc.cluster.local
# - IP: 10.96.0.1 (Service CIDR의 첫 번째 IP, kubernetes Service), 192.168.10.100 (노드 IP)
# 이 인증서는 API Server의 HTTPS 엔드포인트(6443)에서 사용됩니다.
# apiserver-kubelet-client 인증서 : 'TLS Web Client' 키 용도 확인
cat /etc/kubernetes/pki/apiserver-kubelet-client.crt | openssl x509 -text -noout
# Certificate:
# Data:
# Version: 3 (0x2)
# Serial Number: 8822992219099166046 (0x7a7190ad5370f55e)
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: CN=kubernetes
# Validity
# Not Before: Jan 20 17:35:53 2026 GMT
# Not After : Jan 20 17:40:53 2027 GMT
# Subject: O=kubeadm:cluster-admins, CN=kube-apiserver-kubelet-client
# ...
# X509v3 extensions:
# X509v3 Key Usage: critical
# Digital Signature, Key Encipherment
# X509v3 Extended Key Usage:
# TLS Web Client Authentication
# X509v3 Basic Constraints: critical
# CA:FALSE
# X509v3 Authority Key Identifier:
# 24:6E:36:3E:5E:5B:79:C4:B1:B2:21:60:AA:E6:14:F2:3F:D1:5E:5F
# API Server가 kubelet과 통신할 때 사용하는 클라이언트 인증서입니다.
# - Issuer: CN=kubernetes (Kubernetes CA가 서명)
# - Subject: O=kubeadm:cluster-admins, CN=kube-apiserver-kubelet-client
# - O=kubeadm:cluster-admins: 클러스터 관리자 그룹에 속함
# - 유효 기간: 1년 (2026-01-20 ~ 2027-01-20)
# - Extended Key Usage: TLS Web Client Authentication (TLS 클라이언트 인증용)
# - API Server가 kubelet의 10250 포트(읽기 전용 API)에 접근할 때 이 인증서를 사용합니다.
# - kubelet은 이 인증서를 신뢰하여 API Server의 요청을 인증합니다.
[k8s-ctr] kubeconfig 확인
# 관리자 용도
cat /etc/kubernetes/admin.conf
# apiVersion: v1
# clusters:
# - cluster:
# server: https://192.168.10.100:6443
# certificate-authority-data: ... (base64, CA 인증서)
# name: kubernetes
# contexts:
# - context:
# cluster: kubernetes
# user: kubernetes-admin
# name: kubernetes-admin@kubernetes
# current-context: kubernetes-admin@kubernetes
# users:
# - name: kubernetes-admin
# user:
# client-certificate-data: ... (base64, admin 클라이언트 인증서)
# client-key-data: ... (base64, admin 클라이언트 키)
# admin.conf 는 kubectl 에서 사용하는 기본 관리자 kubeconfig 입니다.
cat /etc/kubernetes/super-admin.conf
# apiVersion: v1
# clusters:
# - cluster:
# server: https://192.168.10.100:6443
# certificate-authority-data: ... (base64, CA 인증서)
# name: kubernetes
# contexts:
# - context:
# cluster: kubernetes
# user: kubernetes-super-admin
# name: kubernetes-super-admin@kubernetes
# current-context: kubernetes-super-admin@kubernetes
# users:
# - name: kubernetes-super-admin
# user:
# client-certificate-data: ... (base64, super-admin 인증서)
# client-key-data: ... (base64, super-admin 키)
# super-admin.conf 는 별도의 super-admin 권한을 가진 kubeconfig 입니다.
# kcm
cat /etc/kubernetes/controller-manager.conf
# apiVersion: v1
# clusters:
# - cluster:
# server: https://192.168.10.100:6443
# certificate-authority-data: ... (base64, CA 인증서)
# name: kubernetes
# contexts:
# - context:
# cluster: kubernetes
# user: system:kube-controller-manager
# name: system:kube-controller-manager@kubernetes
# current-context: system:kube-controller-manager@kubernetes
# users:
# - name: system:kube-controller-manager
# user:
# client-certificate-data: ... (base64, 컨트롤러 매니저 클라이언트 인증서)
# client-key-data: ... (base64, 클라이언트 키)
# controller-manager.conf 는 kube-controller-manager Static Pod에서 사용하는 kubeconfig 입니다.
# scheduler
cat /etc/kubernetes/scheduler.conf
# apiVersion: v1
# clusters:
# - cluster:
# server: https://192.168.10.100:6443
# certificate-authority-data: ... (base64, CA 인증서)
# name: kubernetes
# contexts:
# - context:
# cluster: kubernetes
# user: system:kube-scheduler
# name: system:kube-scheduler@kubernetes
# current-context: system:kube-scheduler@kubernetes
# users:
# - name: system:kube-scheduler
# user:
# client-certificate-data: ... (base64, 스케줄러 클라이언트 인증서)
# client-key-data: ... (base64, 클라이언트 키)
# scheduler.conf 는 kube-scheduler Static Pod에서 사용하는 kubeconfig 입니다.
# kubelet
cat /etc/kubernetes/kubelet.conf
# apiVersion: v1
# clusters:
# - cluster:
# server: https://192.168.10.100:6443
# certificate-authority-data: ... (base64, CA 인증서)
# name: kubernetes
# contexts:
# - context:
# cluster: kubernetes
# user: system:node:k8s-ctr
# name: system:node:k8s-ctr@kubernetes
# current-context: system:node:k8s-ctr@kubernetes
# users:
# - name: system:node:k8s-ctr
# user:
# client-certificate: /var/lib/kubelet/pki/kubelet-client-current.pem
# client-key: /var/lib/kubelet/pki/kubelet-client-current.pem
# kubelet.conf 는 kubelet 프로세스가 API Server 와 통신할 때 사용하는 kubeconfig 입니다.
# kubelet 클라이언트 인증서 파일 확인
ls -l /var/lib/kubelet/pki
# -rw-------. 1 root root 2822 Jan 17 09:34 kubelet-client-2026-01-17-09-34-41.pem
# lrwxrwxrwx. 1 root root 59 Jan 17 09:34 kubelet-client-current.pem -> /var/lib/kubelet/pki/kubelet-client-2026-01-17-09-34-41.pem
# -rw-r--r--. 1 root root 2262 Jan 17 09:34 kubelet.crt
# -rw-------. 1 root root 1679 Jan 17 09:34 kubelet.key
# kubelet 디렉토리에는 서버용 인증서(kubelet.crt), 키(kubelet.key), 클라이언트 인증서/키(kubelet-client-*.pem)가 저장됩니다.
# kubelet-client-current.pem 심볼릭 링크를 통해 현재 사용 중인 클라이언트 인증서를 가리킵니다.
# kubelet 서버 역할 : Subject, Key Usage, SAN 확인
cat /var/lib/kubelet/pki/kubelet.crt | openssl x509 -text -noout
# Certificate:
# Issuer: CN=k8s-ctr-ca@1768610081
# Validity
# Not Before: Jan 16 23:34:41 2026 GMT
# Not After : Jan 16 23:34:41 2027 GMT
# Subject: CN=k8s-ctr@1768610081
# ...
# X509v3 Extended Key Usage:
# TLS Web Server Authentication
# X509v3 Basic Constraints: critical
# CA:FALSE
# X509v3 Authority Key Identifier:
# D5:FE:B1:E9:89:9F:46:A8:56:D4:4E:4B:01:7B:2B:49:44:FA:61:85
# X509v3 Subject Alternative Name:
# DNS:k8s-ctr
# kubelet.crt 는 노드에서 동작하는 kubelet 서버(10250 포트 등)에 대한 서버 인증서입니다.
# - Issuer: CN=k8s-ctr-ca@... (노드 로컬 CA가 서명한 인증서)
# - Subject: CN=k8s-ctr@... (노드 식별자)
# - Extended Key Usage: TLS Web Server Authentication (서버 인증 용도)
# - SAN: DNS:k8s-ctr (노드 호스트명으로 접근 시 인증서 검증 가능)
# kubelet 클라이언트 역할 : Subject, Key Usage 확인
cat /var/lib/kubelet/pki/kubelet-client-current.pem | openssl x509 -text -noout
# Certificate:
# Issuer: CN=kubernetes
# Validity
# Not Before: Jan 17 00:28:48 2026 GMT
# Not After : Jan 17 00:33:48 2027 GMT
# Subject: O=system:nodes, CN=system:node:k8s-ctr
# ...
# X509v3 Extended Key Usage:
# TLS Web Client Authentication
# kubelet-client-current.pem 은 kubelet 이 API Server 에 접속할 때 사용하는 클라이언트 인증서입니다.
# - Issuer: CN=kubernetes (클러스터 CA가 서명한 클라이언트 인증서)
# - Subject: O=system:nodes, CN=system:node:k8s-ctr (노드 인증용 계정)
# - Extended Key Usage: TLS Web Client Authentication (클라이언트 인증 용도)
# 이 인증서를 통해 kubelet 은 system:node:k8s-ctr 사용자로서 API Server 에 인증됩니다.
[k8s-ctr] static pod 확인 : etcd, kube-apiserver, kube-scheduler,kube-controller-manager
Static Pod는 kubelet이 관리하고 직접 실행하는 Pod로, 일반적으로 Kubernetes API Server를 거치지 않고 노드의 로컬(manifests) 디렉토리에 있는 YAML 파일을 기반으로 생성됩니다.
즉, master(혹은 control-plane) 구성요소 등 시스템 핵심 역할을 하는 Pod는 관리자에 의해 노드의 /etc/kubernetes/manifests/ 디렉토리에 YAML 파일을 배치해두면, kubelet이 이를 감지하여 자동으로 실행‧관리합니다.
- kubelet은 static pod manifest 파일이 수정/삭제되면 이를 즉시 감지하여 Pod를 자동 반영(생성/종료/변경)합니다.
- static pod는 API Server를 통하지 않고 직접 kubelet이 관리하기 때문에, control-plane이 정상동작하지 않는 상황에서도 동작을 보장할 수 있습니다.
etcd, kube-apiserver, kube-scheduler, kube-controller-manager와 같은 control-plane 구성요소는 Kubernetes 클러스터 전체의 기반이자 핵심 역할을 담당합니다. 이 구성요소들은 Kubernetes 리소스(Pod, Deployment 등) 자체를 관리/조정하는 주체이기 때문에, 클러스터 내부 오브젝트(Pod 등)로 스케줄링하거나 API Server 등 일반적인 제어흐름에 의존할 수 없습니다.
만약 이러한 control-plane 컴포넌트가 일반적인 Deployment/Pod처럼 동작한다면, API Server와 같은 핵심 서비스가 중단될 시 클러스터 전체가 스스로 자가복구하지 못하게 됩니다. 이를 방지하고, API Server가 중단된 상황에서도 control-plane이 즉시 복구될 수 있도록 하기 위해 static pod 형태로 운영합니다.
kubelet이 직접 pod manifest를 읽고 실행함으로써, API Server의 장애와 무관하게 control-plane이 항상 유지됩니다.
# kubelet에 의해 기동되는 static pod 대상 매니페스트 디렉터리 확인
tree /etc/kubernetes/manifests/
# /etc/kubernetes/manifests/
# ├── etcd.yaml
# ├── kube-apiserver.yaml
# ├── kube-controller-manager.yaml
# └── kube-scheduler.yaml
# kubelt 설정 확인
cat /var/lib/kubelet/config.yaml
# authentication:
# anonymous:
# enabled: false
# webhook:
# cacheTTL: 0s
# enabled: true
# x509:
# clientCAFile: /etc/kubernetes/pki/ca.crt
# cgroupDriver: systemd
# staticPodPath: /etc/kubernetes/manifests
# ...
cat /var/lib/kubelet/kubeadm-flags.env
# KUBELET_KUBEADM_ARGS="--container-runtime-endpoint=unix:///run/containerd/containerd.sock --node-ip=192.168.10.100 --pod-infra-container-image=registry.k8s.io/pause:3.10"
# static 파드 확인
kubectl get pod -n kube-system -owide
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# coredns-668d6bf9bc-47zx5 1/1 Running 0 8h 10.244.0.3 k8s-ctr <none> <none>
# coredns-668d6bf9bc-77pjv 1/1 Running 0 8h 10.244.0.2 k8s-ctr <none> <none>
# etcd-k8s-ctr 1/1 Running 0 8h 192.168.10.100 k8s-ctr <none> <none>
# kube-apiserver-k8s-ctr 1/1 Running 0 8h 192.168.10.100 k8s-ctr <none> <none>
# kube-controller-manager-k8s-ctr 1/1 Running 0 8h 192.168.10.100 k8s-ctr <none> <none>
# kube-proxy-qhqpz 1/1 Running 0 8h 192.168.10.100 k8s-ctr <none> <none>
# kube-scheduler-k8s-ctr 1/1 Running 0 8h 192.168.10.100 k8s-ctr <none> <none>
# ...
# etcd : etcd client 는 https://192.168.10.100:2379 호출, metrics 은 http://127.0.0.1:2381 확인
tree /var/lib/etcd/
# /var/lib/etcd/
# └── member
# ├── snap
# │ ├── 0000000000000002-0000000000002711.snap
# │ ├── 0000000000000002-0000000000004e22.snap
# │ └── db
# └── wal
# ├── 0000000000000000-0000000000000000.wal
# └── 0.tmp
cat /etc/kubernetes/manifests/etcd.yaml
# - --advertise-client-urls=https://192.168.10.100:2379
# - --listen-client-urls=https://127.0.0.1:2379,https://192.168.10.100:2379
# - --listen-metrics-urls=http://127.0.0.1:2381
# volumeMounts:
# - mountPath: /var/lib/etcd
# name: etcd-data
# - mountPath: /etc/kubernetes/pki/etcd
# name: etcd-certs
# hostNetwork: true
# priority: 2000001000
# priorityClassName: system-node-critical
...
# kube-apiserver
cat /etc/kubernetes/manifests/kube-apiserver.yaml
# - command:
# - kube-apiserver
# # Listen https://<IP>:6443
# - --advertise-address=192.168.10.100
# - --secure-port=6443
# # etcd client -> etcd server(https://127.0.0.1:2379)
# - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
# - --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
# - --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
# - --etcd-servers=https://127.0.0.1:2379
# # kubelet-client -> kubelet
# - --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
# - --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
# - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
# # k8s servicd cid
# - --service-cluster-ip-range=10.96.0.0/16
ss -tnlp | grep apiserver
# LISTEN 0 4096 *:6443 *:* users:(("kube-apiserver",pid=6400,fd=3))
## k8s 내부에서 api 호출 시 : https://10.96.0.1 혹은 https://kubernetes.default.svc.cluster.local
kubectl get svc,ep
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 4h26m
# NAME ENDPOINTS AGE
# endpoints/kubernetes 192.168.10.100:6443 4h26m
# scheduler
cat /etc/kubernetes/manifests/kube-scheduler.yaml
#...
# spec:
# containers:
# - command:
# - kube-scheduler
# - --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
# - --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
# - --bind-address=127.0.0.1
# - --kubeconfig=/etc/kubernetes/scheduler.conf
# - --leader-elect=true
# ...
## tcp 10259 Listen 포트 확인
ss -tnlp | grep scheduler
# LISTEN 0 4096 127.0.0.1:10259 0.0.0.0:* users:(("kube-scheduler",pid=6397,fd=3))
## scheduler 파드가 1개 이상일 경우 리더 역할 파드 확인
## Lease는 k8s의 경량 coordination 리소스 : 리더 선출 (Leader Election), 노드/컴포넌트 상태 heartbeat, 저부하(high-scale) 상태 갱신
kubectl get leases.coordination.k8s.io -n kube-system kube-scheduler -o yaml
# apiVersion: coordination.k8s.io/v1
# kind: Lease
# metadata:
# creationTimestamp: "2026-01-20T17:41:02Z"
# name: kube-scheduler
# namespace: kube-system
# resourceVersion: "25685"
# uid: 86ff7a16-4648-472b-b356-86ae8007c909
# spec:
# acquireTime: "2026-01-20T17:41:02.043509Z"
# holderIdentity: k8s-ctr_52bdcb3e-29b8-4988-a4f2-d2d3221753b0
# leaseDurationSeconds: 15
# leaseTransitions: 0
# renewTime: "2026-01-21T01:46:36.126156Z"
kubectl get leases.coordination.k8s.io -n kube-system kube-scheduler
# NAME HOLDER AGE
# kube-scheduler k8s-ctr_52bdcb3e-29b8-4988-a4f2-d2d3221753b0 8h
## Node Heartbeat (Node 상태) : node heartbeat 전용 네임스페이스
kubectl get lease -n kube-node-lease
# NAME HOLDER AGE
# k8s-ctr k8s-ctr 8h
kubectl get lease -n kube-node-lease -o yaml
# apiVersion: v1
# items:
# - apiVersion: coordination.k8s.io/v1
# kind: Lease
# metadata:
# creationTimestamp: "2026-01-20T17:41:00Z"
# name: k8s-ctr
# namespace: kube-node-lease
# ownerReferences:
# - apiVersion: v1
# kind: Node
# name: k8s-ctr
# uid: f9c71ccf-bb13-4bdf-825f-059f3d061c18
# resourceVersion: "25693"
# uid: 5c196278-17d9-4fe2-bea4-675ea6be5713
# spec:
# holderIdentity: k8s-ctr
# leaseDurationSeconds: 40
# renewTime: "2026-01-21T01:46:43.187273Z"
# kind: List
# metadata:
# resourceVersion: ""
# kube-controller-manager
cat /etc/kubernetes/manifests/kube-controller-manager.yaml
# - command:
# - kube-controller-manager
# # kcm bind address
# - --bind-address=127.0.0.1
# # 노드별 파드 cidr 할당
# - --allocate-node-cidrs=true
# - --cluster-cidr=10.244.0.0/16
# # k8s svc cidr
# - --service-cluster-ip-range=10.96.0.0/16
# # kcm controller
# - --controllers=*,bootstrapsigner,tokencleaner
# # lease 사용 : 리더
# - --leader-elect=true
# # 모든 컨트롤러가 kcm의 단일 권한(identity) 사용하지 않고, 컨트롤러별 개별 ServiceAccount + RBAC 사용
# - --use-service-account-credentials=true
## tcp 10257 Listen 포트 확인
ss -tnlp | grep controller
# LISTEN 0 4096 127.0.0.1:10257 0.0.0.0:* users:(("kube-controller",pid=7858,fd=3))
## 노드별 파드 CIDR 확인
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
# k8s-ctr 10.244.0.0/24
# ## kcm 파드가 1개 이상일 경우 리더 역할 파드 확인
# kubectl get lease -n kube-system kube-controller-manager -o yaml
# apiVersion: coordination.k8s.io/v1
# kind: Lease
# metadata:
# creationTimestamp: "2026-01-20T17:41:00Z"
# name: kube-controller-manager
# namespace: kube-system
# resourceVersion: "25833"
# uid: f69dd611-06c1-47e7-91ca-f5e46e0747fb
# spec:
# acquireTime: "2026-01-20T17:41:00.318171Z"
# holderIdentity: k8s-ctr_b9a18658-8bdc-4ab4-9862-0a6b91701610
# leaseDurationSeconds: 15
# leaseTransitions: 0
# renewTime: "2026-01-21T01:48:30.587000Z"
kubectl get lease -n kube-system kube-controller-manager
# NAME HOLDER AGE
# kube-controller-manager k8s-ctr_b9a18658-8bdc-4ab4-9862-0a6b91701610 8h
## 컨트롤러별 개별 ServiceAccount + RBAC 사용
kubectl get sa -n kube-system | grep controller
# attachdetach-controller 0 8h
# certificate-controller 0 8h
# clusterrole-aggregation-controller 0 8h
# cronjob-controller 0 8h
...(생략)...
필수 애드온 설치 (coredns, kube-proxy) 확인
# coredns 확인
kc describe deploy -n kube-system coredns
# Name: coredns
# Namespace: kube-system
# CreationTimestamp: Wed, 21 Jan 2026 02:41:00 +0900
# Labels: k8s-app=kube-dns
# Selector: k8s-app=kube-dns
# Replicas: 2 desired | 2 updated | 2 total | 2 available | 0 unavailable
# Containers:
# coredns:
# Image: registry.k8s.io/coredns/coredns:v1.11.3
# Ports: 53/UDP, 53/TCP, 9153/TCP
# Args:
# -conf
# /etc/coredns/Corefile
# Limits:
# memory: 170Mi
# Requests:
# cpu: 100m
# memory: 70Mi
# Liveness: http-get http://:8080/health delay=60s timeout=5s period=10s #success=1 #failure=5
# Readiness: http-get http://:8181/ready delay=0s timeout=1s period=10s #success=1 #failure=3
# Volumes:
# config-volume:
# Type: ConfigMap
# Name: coredns
# Events: <none>
# CoreDNS는 클러스터 DNS(서비스 디스커버리) 역할을 담당합니다.
# - 53/UDP,53/TCP: DNS 질의 포트
# - 9153/TCP: Prometheus 메트릭 포트
kubectl get deploy -n kube-system coredns -owide
# NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
# coredns 2/2 2 2 8h coredns registry.k8s.io/coredns/coredns:v1.11.3 k8s-app=kube-dns
# READY=2/2 → CoreDNS 파드 2개가 모두 Ready 상태입니다.
## label 도 아직 예전 kube-dns 사용
kubectl get pod -n kube-system -l k8s-app=kube-dns -owide
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# coredns-668d6bf9bc-47zx5 1/1 Running 0 8h 10.244.0.3 k8s-ctr <none> <none>
# coredns-668d6bf9bc-77pjv 1/1 Running 0 8h 10.244.0.2 k8s-ctr <none> <none>
# CoreDNS 파드는 Pod CIDR(10.244.0.0/16)에서 IP를 할당받아 동작합니다.
##
kubectl get svc,ep -n kube-system
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 8h
#
# NAME ENDPOINTS AGE
# endpoints/kube-dns 10.244.0.2:53,10.244.0.3:53,10.244.0.2:53 + 3 more... 8h
# kube-dns Service(10.96.0.10)는 CoreDNS Pod 엔드포인트로 트래픽을 분산합니다.
## 프로메테우스 메트릭 엔드포인트 확인
curl -s http://10.96.0.10:9153/metrics | head
# # HELP coredns_build_info A metric with a constant '1' value labeled by version, revision, and goversion from which CoreDNS was built.
# # TYPE coredns_build_info gauge
# coredns_build_info{goversion="go1.21.11",revision="a6338e9",version="1.11.3"} 1
# # HELP coredns_cache_entries The number of elements in the cache.
# # TYPE coredns_cache_entries gauge
# coredns_cache_entries{server="dns://:53",type="denial",view="",zones="."} 1
# coredns_cache_entries{server="dns://:53",type="success",view="",zones="."} 0
# # HELP coredns_cache_misses_total The count of cache misses. Deprecated, derive misses from cache hits/requests counters.
# # TYPE coredns_cache_misses_total counter
# coredns_cache_misses_total{server="dns://:53",view="",zones="."} 1
# Service IP로 메트릭(9153) 접근이 되면 kube-proxy(Service 라우팅)와 CoreDNS가 정상 동작 중임을 간접 확인할 수 있습니다.
## configmap 확인
kc describe cm -n kube-system coredns
# Name: coredns
# Namespace: kube-system
# Data
# ====
# Corefile:
# ----
# .:53 {
# errors
# health {
# lameduck 5s
# }
# ready
# kubernetes cluster.local in-addr.arpa ip6.arpa {
# pods insecure
# fallthrough in-addr.arpa ip6.arpa
# ttl 30
# }
# prometheus :9153
# forward . /etc/resolv.conf {
# max_concurrent 1000
# }
# cache 30 {
# disable success cluster.local
# disable denial cluster.local
# }
# loop
# reload
# loadbalance
# }
## forward 정보 확인
cat /etc/resolv.conf
# Generated by NetworkManager
# search Davolink
# nameserver 1.214.68.2
# nameserver 61.41.153.2
# CoreDNS의 forward 플러그인은 기본적으로 여기(/etc/resolv.conf)에 설정된 nameserver로 외부 질의를 포워딩합니다.
# kube-proxy 확인
kubectl get ds -n kube-system -owide
# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE CONTAINERS IMAGES SELECTOR
# kube-proxy 1 1 1 1 1 kubernetes.io/os=linux 8h kube-proxy registry.k8s.io/kube-proxy:v1.32.11 k8s-app=kube-proxy
# kube-proxy는 노드마다 1개씩 동작하는 DaemonSet이며, Service 라우팅/로드밸런싱 규칙을 적용합니다.
kc describe pod -n kube-system -l k8s-app=kube-proxy
kubectl get pod -n kube-system -l k8s-app=kube-proxy -owide
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# kube-proxy-qhqpz 1/1 Running 0 8h 192.168.10.100 k8s-ctr <none> <none>
# kube-proxy는 hostNetwork로 실행되어 IP가 노드 IP(192.168.10.100)로 보입니다.
kc describe cm -n kube-system kube-proxy
# Name: kube-proxy
# Namespace: kube-system
# Labels: app=kube-proxy
# Annotations: kubeadm.kubernetes.io/component-config.hash: sha256:... (생략)
#
# Data
# ====
# config.conf:
# ----
# apiVersion: kubeproxy.config.k8s.io/v1alpha1
# bindAddress: 0.0.0.0
# clusterCIDR: 10.244.0.0/16
# mode: ""
# conntrack:
# maxPerCore: null
# min: null
# ...
#
# kubeconfig.conf:
# ----
# apiVersion: v1
# kind: Config
# clusters:
# - cluster:
# certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# server: https://192.168.10.100:6443
# name: default
# users:
# - name: default
# user:
# tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
# ...
# kube-proxy 설정 포인트:
# - clusterCIDR: Pod CIDR(10.244.0.0/16) 인지 → masquerade/conntrack 등에 사용
# - mode: "" (기본값/자동) → 보통 iptables 모드로 동작(이후 iptables 규칙이 생성됨)
# - kubeconfig.conf는 ServiceAccount 토큰으로 API Server에 접근합니다.
##
ss -tnlp | grep kube-proxy
# LISTEN 0 4096 127.0.0.1:10249 0.0.0.0:* users:(("kube-proxy",pid=8168,fd=12)) # 헬스 체크 전용
# LISTEN 0 4096 *:10256 *:* users:(("kube-proxy",pid=8168,fd=11)) # 메트릭 노출용
# 10249: healthz(로컬), 10256: metrics(전체 인터페이스)
curl 127.0.0.1:10249/healthz ; echo
# ok
## iptables rule 확인
iptables -t nat -S
iptables -t filter -S
iptables-save
## conntrack 전용 툴 설치
# conntrack-tools 패키지는 커널의 netfilter(conntrack) 연결 추적 표(table)를 조회/조작/모니터링할 수 있는 유틸리티입니다.
# 이 툴을 사용하면 실제로 살아있는 커넥션 목록을 보고, 특정 커넥션만 삭제하거나 실시간으로 세션 변화를 추적할 수 있습니다.
# (네트워크/방화벽 문제, Kubernetes kube-proxy 동작 등 트러블슈팅 때 매우 유용)
dnf install -y conntrack-tools
conntrack -V
# conntrack v1.4.8 (conntrack-tools)
## conntrack 툴 사용
conntrack -L # 전체 conntrack 엔트리 조회
conntrack -L -p tcp # TCP 연결만 보기
conntrack -L -p tcp --state ESTABLISHED # 특정 상태 필터링
conntrack -L | grep dport=443 # 특정 포트 관련 연결
conntrack -E # 실시간 이벤트 추적
## conntrack sysctl 주요 파라미터
## nf_conntrack_max : 최대 엔트리 수
## nf_conntrack_count : 현재 사용 중
## nf_conntrack_tcp_timeout_established : TCP 유지 시간
sysctl -a | grep conntrack
5. [k8s-w1/w2] 설정
사전 설정
각각의 워커노드에서 해당 설정을 적용합니다.
vagrant ssh k8s-w1
# vagrant ssh k8s-w2
# root 권한(로그인 환경) 전환
echo "sudo su -" >> /home/vagrant/.bashrc
sudo su -
# Time, NTP 설정
timedatectl set-local-rtc 0
# 시스템 타임존(Timezone)을 한국(KST, UTC+9) 으로 설정 : 시스템 시간은 UTC 기준 유지, 표시만 KST로 변환
timedatectl set-timezone Asia/Seoul
# SELinux 설정 : Kubernetes는 Permissive 권장
setenforce 0
# 재부팅 시에도 Permissive 적용
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
# firewalld(방화벽) 끄기
systemctl disable --now firewalld
# Swap 비활성화
swapoff -a
# 재부팅 시에도 'Swap 비활성화' 적용되도록 /etc/fstab에서 swap 라인 주석 처리
sed -i '/swap/d' /etc/fstab
# 커널 모듈 로드
modprobe overlay
modprobe br_netfilter
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
# 커널 파라미터 설정 : 네트워크 설정 - 브릿지 트래픽이 iptables를 거치도록 함
cat <<EOF | tee /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
# hosts 설정
sed -i '/^127\.0\.\(1\|2\)\.1/d' /etc/hosts
cat << EOF >> /etc/hosts
192.168.10.100 k8s-ctr
192.168.10.101 k8s-w1
192.168.10.102 k8s-w2
EOF
cat /etc/hosts
CRI 설치 : containerd(runc) v2.1.5
# Docker 저장소 추가
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# containerd 설치
dnf install -y containerd.io-*2.1.5-1.el10*
# 기본 설정 생성 및 SystemdCgroup 활성화
containerd config default | tee /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
# systemd unit 파일 새로고침
systemctl daemon-reload
# containerd 시작 및 부팅 시 활성화
systemctl enable --now containerd
kubeadm, kubelet, kubectl 설치 v1.32.11
# Kubernetes 저장소 추가
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.32/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.32/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
# kubeadm, kubelet, kubectl 설치
dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
# kubelet 부팅 시 활성화(초기화 후 실제 기동)
systemctl enable --now kubelet
# /etc/crictl.yaml 파일 작성
cat << EOF > /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
EOF
kubeadm 으로 k8s join 하기
kubeadm join은 워커 노드를 Kubernetes 클러스터에 합류시키는 과정으로, 크게 두 단계로 구성됩니다. 첫 번째는 디스커버리(Discovery) 단계로, 워커 노드가 Kubernetes API 서버를 신뢰하도록 하는 과정입니다. 두 번째는 TLS 부트스트랩(Bootstrap) 단계로, Kubernetes API 서버가 워커 노드를 신뢰하도록 하는 과정입니다.
1. Preflight Checks
워커 노드에서 kubeadm init과 동일한 사전 검증을 수행합니다. 커널 모듈, 네트워크 설정, 컨테이너 런타임, 포트 사용 여부 등을 확인하여 클러스터 합류에 필요한 최소 요구사항을 충족하는지 검증합니다.
2. Discovery: 클러스터 정보 발견 및 신뢰 검증
워커 노드는 사용자가 제공한 토큰과 --discovery-token-ca-cert-hash 값을 사용하여 API 서버의 kube-public/cluster-info ConfigMap을 익명으로 요청합니다. 이 ConfigMap은 클러스터의 공개 정보(API 서버 엔드포인트, CA 인증서 등)를 포함하고 있으며, system:unauthenticated 그룹에게도 읽기 권한이 부여되어 있습니다.
가져온 ConfigMap 내의 CA 인증서 해시값이 사용자가 입력한 --discovery-token-ca-cert-hash sha256:xxxx 값과 일치하는지 확인합니다. 이를 통해 워커 노드는 접속하려는 마스터 노드가 진짜 클러스터의 마스터인지 검증할 수 있습니다. 이 단계에서 워커 노드는 마스터를 신뢰할 수 있게 됩니다.
Discovery 방식은 --discovery-token(Shared token discovery) 또는 --discovery-file(File/https discovery)을 사용할 수 있습니다.
3. TLS Bootstrap: 인증서 발급 요청
이제 마스터를 신뢰할 수 있으므로, 워커 노드는 토큰을 사용해 API 서버에 정식으로 인증을 시도합니다. 워커 노드는 자신만의 개인키를 생성하고, API 서버에 "나를 위한 인증서를 서명해달라"는 CSR(Certificate Signing Request)을 전송합니다. 마스터의 Certificate 발급자(approver)는 이 요청이 유효한 토큰을 통한 것임을 확인하고 자동으로 승인하여 인증서를 발급합니다.
4. Kubelet 설정 및 기동
발급받은 정식 인증서를 사용하여 /etc/kubernetes/kubelet.conf 파일을 생성합니다. kubelet이 이 설정을 가지고 실행되면서 API 서버에 자신을 "Node" 리소스로 등록합니다. 마스터는 이 노드에 kube-proxy 등의 시스템 컴포넌트를 배포하며, 노드 상태가 Ready가 될 준비를 마칩니다.
# 기본 환경 정보 출력 저장
crictl images
# IMAGE TAG IMAGE ID SIZE
# join 전 워커 노드에는 아직 Kubernetes 관련 이미지가 없습니다.
crictl ps
# CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE
# join 전 워커 노드에는 아직 실행 중인 컨테이너가 없습니다.
cat /etc/sysconfig/kubelet
# KUBELET_EXTRA_ARGS=
# kubelet 추가 인자 설정 파일이 비어있습니다. join 후 kubelet이 자동으로 설정됩니다.
tree /etc/kubernetes | tee -a etc_kubernetes-1.txt
# /etc/kubernetes
# └── manifests
#
# 2 directories, 0 files
# join 전에는 manifests 디렉토리만 존재하며, 아직 static pod 매니페스트가 없습니다.
tree /var/lib/kubelet | tee -a var_lib_kubelet-1.txt
# /var/lib/kubelet
#
# 0 directories, 0 files
# join 전에는 kubelet 데이터 디렉토리가 비어있습니다. join 후 설정 파일과 인증서가 생성됩니다.
tree /run/containerd/ -L 3 | tee -a run_containerd-1.txt
# /run/containerd/
# ├── containerd.sock
# ├── containerd.sock.ttrpc
# ├── io.containerd.grpc.v1.cri
# ├── io.containerd.runtime.v2.task
# └── io.containerd.sandbox.controller.v1.shim
#
# 4 directories, 2 files
# containerd는 이미 실행 중이며, CRI 소켓과 런타임 디렉토리가 준비되어 있습니다.
pstree -alnp | tee -a pstree-1.txt
# systemd,1 --switched-root --system --deserialize=46 no_timer_check
# |-systemd-journal,434
# |-systemd-userdbd,469
# |-systemd-udevd,478
# |-rpcbind,550 -w -f
# |-auditd,553
# |-dbus-broker-lau,592 --scope system --audit
# |-irqbalance,604
# |-lsmd,605 -d
# |-systemd-logind,608
# |-chronyd,613 -F 2
# |-VBoxService,710 --pidfile /var/run/vboxadd-service.sh
# |-polkitd,762 --no-debug --log-level=err
# |-gssproxy,793 -i
# |-sshd,794
# |-tuned,795 -Es /usr/sbin/tuned -l -P
# |-rsyslogd,991 -n
# |-atd,1034 -f
# |-crond,1040 -n
# |-agetty,1058 -o -- \\u --noreset --noclear - linux
# |-fwupd,1123
# |-gpg-agent,1263 --homedir /var/lib/fwupd/gnupg --use-standard-socket --daemon
# |-udisksd,1416
# |-NetworkManager,3072 --no-daemon
# |-systemd,6560 --user
# `-containerd,6965
# join 전 워커 노드의 프로세스 트리입니다. containerd는 실행 중이지만 kubelet은 아직 시작되지 않았습니다.
systemd-cgls --no-pager | tee -a systemd-cgls-1.txt
# CGroup /:
# -.slice
# ├─user.slice
# ├─init.scope
# │ └─1 /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_tim…
# └─system.slice
# ├─irqbalance.service
# ├─libstoragemgmt.service
# ├─vboxadd-service.service
# ├─containerd.service …
# │ └─6965 /usr/bin/containerd
# ├─systemd-udevd.service …
# ├─dbus-broker.service
# ├─polkit.service
# ├─chronyd.service
# ├─auditd.service
# ├─tuned.service
# ├─systemd-journald.service
# ├─atd.service
# ├─sshd.service
# ├─fwupd.service
# ├─crond.service
# ├─NetworkManager.service
# ├─gssproxy.service
# ├─rsyslog.service
# ├─systemd-userdbd.service
# ├─rpcbind.service
# ├─udisks2.service
# ├─system-getty.slice
# └─systemd-logind.service
# join 전 워커 노드의 cgroup 구조입니다. containerd 서비스는 실행 중이지만 kubelet 관련 cgroup은 아직 없습니다.
lsns | tee -a lsns-1.txt
# NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 119 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531835 cgroup 119 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531836 pid 119 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531837 user 118 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531838 uts 110 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531839 ipc 119 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531840 net 117 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531841 mnt 101 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# ... (기타 프로세스별 네임스페이스)
# join 전 워커 노드의 네임스페이스 목록입니다. 시스템 기본 네임스페이스와 일부 서비스별 네임스페이스만 존재하며, 컨테이너 관련 네임스페이스는 아직 없습니다.
ip addr | tee -a ip_addr-1.txt
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# inet 127.0.0.1/8 scope host lo
# valid_lft forever preferred_lft forever
# inet6 ::1/128 scope host noprefixroute
# valid_lft forever preferred_lft forever
# 2: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# link/ether 08:00:27:90:ea:eb brd ff:ff:ff:ff:ff:ff
# inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s8
# valid_lft 45065sec preferred_lft 45065sec
# 3: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# link/ether 08:00:27:1f:02:49 brd ff:ff:ff:ff:ff:ff
# inet 192.168.10.101/24 brd 192.168.10.255 scope global noprefixroute enp0s9
# valid_lft forever preferred_lft forever
# join 전 워커 노드의 네트워크 인터페이스입니다. lo(루프백), enp0s8(NAT), enp0s9(호스트 전용, 192.168.10.101)만 존재하며, CNI 관련 인터페이스(cni0, flannel.1 등)는 아직 없습니다.
ss -tnlp | tee -a ss-1.txt
# State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
# LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=794,fd=7))
# LISTEN 0 4096 0.0.0.0:111 0.0.0.0:* users:(("rpcbind",pid=550,fd=5),("systemd",pid=1,fd=194))
# LISTEN 0 4096 127.0.0.1:46103 0.0.0.0:* users:(("containerd",pid=6965,fd=12))
# LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=794,fd=8))
# LISTEN 0 4096 [::]:111 [::]:* users:(("rpcbind",pid=550,fd=7),("systemd",pid=1,fd=196))
# LISTEN 0 4096 *:9090 *:* users:(("systemd",pid=1,fd=191))
# join 전 워커 노드의 리스닝 소켓입니다. sshd(22), rpcbind(111), containerd(46103)만 리스닝 중이며, kubelet(10250, 10248 등)이나 kube-proxy(10249, 10256) 관련 포트는 아직 없습니다.
df -hT | tee -a df-1.txt
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda3 xfs 60G 2.9G 57G 5% /
# devtmpfs devtmpfs 4.0M 0 4.0M 0% /dev
# tmpfs tmpfs 913M 0 913M 0% /dev/shm
# efivarfs efivarfs 256K 6.3K 250K 3% /sys/firmware/efi/efivars
# tmpfs tmpfs 366M 12M 354M 4% /run
# tmpfs tmpfs 1.0M 0 1.0M 0% /run/credentials/systemd-journald.service
# /dev/sda1 vfat 599M 13M 587M 3% /boot/efi
# tmpfs tmpfs 1.0M 0 1.0M 0% /run/credentials/getty@tty1.service
# tmpfs tmpfs 183M 8.0K 183M 1% /run/user/1000
# join 전 워커 노드의 파일시스템 사용량입니다. 루트 파일시스템(/dev/sda3)은 5%만 사용 중이며, Kubernetes 관련 마운트 포인트는 아직 없습니다.
findmnt | tee -a findmnt-1.txt
# TARGET SOURCE FSTYPE OPTIONS
# / /dev/sda3 xfs rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota
# ├─/var/lib/nfs/rpc_pipefs sunrpc rpc_pipefs rw,relatime
# ├─/dev devtmpfs devtmpfs rw,nosuid,seclabel,size=4096k,nr_inodes=229647,mode=755,inode64
# ├─/sys sysfs sysfs rw,nosuid,nodev,noexec,relatime,seclabel
# │ ├─/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot
# │ └─/sys/firmware/efi/efivars efivarfs efivarfs rw,nosuid,nodev,noexec,relatime
# ├─/proc proc proc rw,nosuid,nodev,noexec,relatime
# ├─/run tmpfs tmpfs rw,nosuid,nodev,seclabel,size=373852k,nr_inodes=819200,mode=755,inode64
# └─/boot/efi /dev/sda1 vfat rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=ascii,shortname=winnt,errors=remount-ro
# join 전 워커 노드의 마운트 포인트 목록입니다. 시스템 기본 마운트 포인트만 존재하며, Kubernetes 관련 마운트(예: /var/lib/kubelet/pods, /run/containerd/io.containerd.runtime.v2.task 등)는 아직 없습니다.
sysctl -a | tee -a sysctl-1.txt
# kubeadm Configuration 파일 작성
NODEIP=$(ip -4 addr show enp0s9 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
echo $NODEIP
cat << EOF > kubeadm-join.yaml
apiVersion: kubeadm.k8s.io/v1beta4
kind: JoinConfiguration
discovery:
bootstrapToken:
token: "123456.1234567890123456"
apiServerEndpoint: "192.168.10.100:6443"
unsafeSkipCAVerification: true #토큰 검증 스킵하는 설정
nodeRegistration:
criSocket: "unix:///run/containerd/containerd.sock"
kubeletExtraArgs:
- name: node-ip
value: "$NODEIP"
EOF
cat kubeadm-join.yaml
# join
kubeadm join --config="kubeadm-join.yaml"
# [preflight] Running pre-flight checks
# [preflight] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
# [preflight] Use 'kubeadm init phase upload-config --config your-config.yaml' to re-upload it.
# [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-check] Waiting for a healthy kubelet at http://127.0.0.1:10248/healthz. This can take up to 4m0s
# [kubelet-check] The kubelet is healthy after 501.406438ms
# [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.
# Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
# crictl 확인
crictl images
# IMAGE TAG IMAGE ID SIZE
# ghcr.io/flannel-io/flannel-cni-plugin v1.7.1-flannel1 127562bd9047f 5.14MB
# ghcr.io/flannel-io/flannel v0.27.3 d84558c0144bc 33.1MB
# registry.k8s.io/kube-proxy v1.32.11 dcdb790dc2bfe 27.6MB
# registry.k8s.io/pause 3.10 afb61768ce381 268kB
# join 후 워커 노드에 필요한 이미지들이 자동으로 다운로드되었습니다. Flannel CNI 플러그인, Flannel 데몬, kube-proxy, pause 이미지가 포함되어 있습니다.
crictl ps
# CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE
# 6e3da4a24a0b7 d84558c0144bc 6 seconds ago Running kube-flannel 0 eecbd2b79d45e kube-flannel-ds-dmx6v kube-flannel
# 43a4e45996ac5 dcdb790dc2bfe 17 seconds ago Running kube-proxy 0 b7584ea4268f2 kube-proxy-7vlpt kube-system
# join 후 워커 노드에서 kube-flannel과 kube-proxy 파드가 실행 중입니다. 이들은 DaemonSet으로 배포되어 각 노드마다 하나씩 실행됩니다.
# cluster-info cm 호출 가능 확인
curl -s -k https://192.168.10.100:6443/api/v1/namespaces/kube-public/configmaps/cluster-info | jq
# {
# "kind": "ConfigMap",
# "apiVersion": "v1",
# "metadata": {
# "name": "cluster-info",
# "namespace": "kube-public",
# "uid": "dc9196fb-1749-48cc-9ea5-bf788fb48f21",
# "resourceVersion": "316",
# "creationTimestamp": "2026-01-20T17:40:59Z",
# "managedFields": [
# {
# "manager": "kubeadm",
# "operation": "Update",
# "apiVersion": "v1",
# "time": "2026-01-20T17:40:59Z",
# "fieldsType": "FieldsV1",
# "fieldsV1": {
# "f:data": {
# ".": {},
# "f:kubeconfig": {}
# }
# }
# },
# {
# "manager": "kube-controller-manager",
# "operation": "Update",
# "apiVersion": "v1",
# "time": "2026-01-20T17:41:05Z",
# "fieldsType": "FieldsV1",
# "fieldsV1": {
# "f:data": {
# "f:jws-kubeconfig-123456": {}
# }
# }
# }
# ]
# },
# "data": {
# "jws-kubeconfig-123456": "eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQ1NiJ9..cu7Nc8yJNTLPRZOiaIUD7y6aANtYy6YKFwNZUL5LpMs",
# "kubeconfig": "apiVersion: v1\nclusters:\n- cluster:\n certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJUVFnd2ViNU1Fd0l3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1qQXhOek0xTlROYUZ3MHpOakF4TVRneE56UXdOVE5hTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUMzanZiYjFhNHVwU2dxSGdIWWZ1cnR0QVJ3akZyWCtOV0JvYTUwdFpNUGhHRWZoQVBJRHFURm00OTYKZ0FDRXk5ZVhBZldDSlVPS0pjaXlvWWdFcHk5M2E2MnVnN2VlbFRXQ0haM3ZhRUFZNEt6UVRpWTdkekZIVGYxLwpXdEZRSU1qOElTYk9oN1ZaTlQyUVRTQTJjZkhjUUNBcWtvMkRLdWxaNHhVbzhxNHVCQ0hwbStLMEhiK3FEb0JECk13c0tLNWJNakt5anZJRURHUzVQNE5KQ0lmeVhHL0ljM3pEMWJpdFFMbW9FWGpJeklyTGZvNzc4VXRwVWdOeFcKaW5YNWc1ZUJvc2FpeHNpcm9sc3ZkMVRxMDMwRHFPNTFaSGlGVmxsblQvVk9xZnh1WmlKU0d3RWx0cVZlcTRMWQp5dldpOHg5TG16aDRHcUh4TmJUaU5HVSt4ajk5QWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRa2JqWStYbHQ1eExHeUlXQ3E1aFR5UDlGZVh6QVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQVVEdXpURkdKRgpjU2RmYm5RL3BZUndSOG9LZ1diMHg3SGNvVEZBT2tDcERhUm9LVVFrOGhmYTJDRnpsS0dYZDdLTHRKOU9JMVBaCkdiUnFKNXJyRWR3dVFhUnB4elNJN1BtWlpmdHMwekoyNHkwYlJyV0orUEFtSlNBdFVyUUM5Y3JORVNVMkRQaHYKYUVVTjVCZGJBYjRPOWk5TFNmdkdaQWV3bW5QNDVTOFAyS1JCZTdBZit0NjU2UE5oMysxdW1jVkdSZldFdi9ZNQpJUVR2MVRIVlJ6NjRlTE1RMnVEQUFHQ0t1cGp0RlFXZEhwSjA1dzNObjdZMFhxeGovTEdaSTRXMHBnVnl4QUJkCnpwN0lZWXJFS21RZDlNUzJrZXRxZVB6WDFOa1VmcXZQckNJbWZtZ1BPVEYrTnZGS1NSM2xUY3dVVHRQVHJDUEoKUzJ6eU1rVlIraVRxCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K\n server: https://192.168.10.100:6443\n name: \"\"\ncontexts: null\ncurrent-context: \"\"\nkind: Config\npreferences: {}\nusers: null\n"
# }
# }
join 후에도 워커 노드에서 cluster-info ConfigMap을 익명으로 조회할 수 있습니다. 이는 kube-public 네임스페이스의 cluster-info가 system:unauthenticated 그룹에게 읽기 권한이 부여되어 있기 때문입니다.
data 필드에는 kubeconfig(CA 인증서 포함)와 JWS(JSON Web Signature) 토큰이 포함되어 있습니다.
[k8s-ctr] k8s-w1/w2 관련 정보 확인
# join 된 워커 노드 확인
kubectl get node -owide
# NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
# k8s-ctr Ready control-plane 10h v1.32.11 192.168.10.100 <none> Rocky Linux 10.0 (Red Quartz) 6.12.0-55.39.1.el10_0.aarch64 containerd://2.1.5
# k8s-w1 Ready <none> 8m18s v1.32.11 192.168.10.101 <none> Rocky Linux 10.0 (Red Quartz) 6.12.0-55.39.1.el10_0.aarch64 containerd://2.1.5
# k8s-w2 Ready <none> 6m3s v1.32.11 192.168.10.102 <none> Rocky Linux 10.0 (Red Quartz) 6.12.0-55.39.1.el10_0.aarch64 containerd://2.1.5
# 모든 노드가 Ready 상태이며, 워커 노드(k8s-w1, k8s-w2)는 ROLES가 <none>으로 표시됩니다.
# 노드별 파드 CIDR 확인
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
# k8s-ctr 10.244.0.0/24
# k8s-w1 10.244.1.0/24
# k8s-w2 10.244.2.0/24
# 각 노드마다 /24 서브넷이 할당되어 있습니다. kube-controller-manager의 --allocate-node-cidrs=true 설정에 의해 자동으로 할당됩니다.
# 다른 노드의 파드 CIDR(Per Node Pod CIDR)에 대한 라우팅이 자동으로 커널 라우팅에 추가됨을 확인 : flannel.1 을 통해 VXLAN 통한 라우팅
ip -c route | grep flannel
# 10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink
# 10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink
# Flannel이 각 워커 노드의 Pod CIDR에 대한 라우팅 규칙을 자동으로 추가했습니다. flannel.1 인터페이스를 통해 VXLAN 오버레이 네트워크로 통신합니다.
# k8s-ctr 에서 10.244.1.0 IP로 통신 가능(vxlan overlay 사용) 확인
ping -c 1 10.244.1.0
# PING 10.244.1.0 (10.244.1.0) 56(84) bytes of data.
# 64 bytes from 10.244.1.0: icmp_seq=1 ttl=64 time=1.10 ms
#
# --- 10.244.1.0 ping statistics ---
# 1 packets transmitted, 1 received, 0% packet loss, time 0ms
# rtt min/avg/max/mdev = 1.102/1.102/1.102/0.000 ms
# 컨트롤 플레인 노드에서 워커 노드의 Pod CIDR 게이트웨이 IP로 통신이 가능합니다. 이는 Flannel VXLAN 오버레이 네트워크가 정상적으로 동작하고 있음을 의미합니다.
# 워커 노드에 Taints 정보 확인
kc describe node k8s-w1
# Name: k8s-w1
# Roles: <none>
# Labels: beta.kubernetes.io/arch=arm64
# beta.kubernetes.io/os=linux
# kubernetes.io/arch=arm64
# kubernetes.io/hostname=k8s-w1
# kubernetes.io/os=linux
# Annotations: flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"d6:6c:c0:b2:b0:73"}
# flannel.alpha.coreos.com/backend-type: vxlan
# flannel.alpha.coreos.com/kube-subnet-manager: true
# flannel.alpha.coreos.com/public-ip: 192.168.10.101
# kubeadm.alpha.kubernetes.io/cri-socket: unix:///run/containerd/containerd.sock
# node.alpha.kubernetes.io/ttl: 0
# volumes.kubernetes.io/controller-managed-attach-detach: true
# CreationTimestamp: Wed, 21 Jan 2026 13:31:39 +0900
# Taints: <none>
# Unschedulable: false
# Conditions:
# Type Status LastHeartbeatTime LastTransitionTime Reason Message
# ---- ------ ----------------- ------------------ ------ -------
# NetworkUnavailable False Wed, 21 Jan 2026 13:32:04 +0900 Wed, 21 Jan 2026 13:32:04 +0900 FlannelIsUp Flannel is running on this node
# MemoryPressure False Wed, 21 Jan 2026 13:37:15 +0900 Wed, 21 Jan 2026 13:31:39 +0900 KubeletHasSufficientMemory kubelet has sufficient memory available
# DiskPressure False Wed, 21 Jan 2026 13:37:15 +0900 Wed, 21 Jan 2026 13:31:39 +0900 KubeletHasNoDiskPressure kubelet has no disk pressure
# PIDPressure False Wed, 21 Jan 2026 13:37:15 +0900 Wed, 21 Jan 2026 13:31:39 +0900 KubeletHasSufficientPID kubelet has sufficient PID available
# Ready True Wed, 21 Jan 2026 13:37:15 +0900 Wed, 21 Jan 2026 13:32:02 +0900 KubeletReady kubelet is posting ready status
# Addresses:
# InternalIP: 192.168.10.101
# Hostname: k8s-w1
# Capacity:
# cpu: 2
# ephemeral-storage: 60970Mi
# memory: 1869252Ki
# pods: 110
# Allocatable:
# cpu: 2
# ephemeral-storage: 57538510753
# memory: 1766852Ki
# pods: 110
# System Info:
# Machine ID: a09cfafd9d7f4497a86778f2b560eb3e
# System UUID: a09cfafd9d7f4497a86778f2b560eb3e
# Boot ID: f6c25a37-b665-4943-820d-8c7a0012ee2a
# Kernel Version: 6.12.0-55.39.1.el10_0.aarch64
# OS Image: Rocky Linux 10.0 (Red Quartz)
# Operating System: linux
# Architecture: arm64
# Container Runtime Version: containerd://2.1.5
# Kubelet Version: v1.32.11
# Kube-Proxy Version: v1.32.11
# PodCIDR: 10.244.1.0/24
# PodCIDRs: 10.244.1.0/24
# Non-terminated Pods: (2 in total)
# Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits Age
# --------- ---- ------------ ---------- --------------- ------------- ---
# kube-flannel kube-flannel-ds-dmx6v 100m (5%) 0 (0%) 50Mi (2%) 0 (0%) 8m38s
# kube-system kube-proxy-7vlpt 0 (0%) 0 (0%) 0 (0%) 0 (0%) 8m38s
# Allocated resources:
# (Total limits may be over 100 percent, i.e., overcommitted.)
# Resource Requests Limits
# -------- -------- ------
# cpu 100m (5%) 0 (0%)
# memory 50Mi (2%) 0 (0%)
# ephemeral-storage 0 (0%) 0 (0%)
# 워커 노드의 상세 정보입니다. Taints가 없으므로 모든 파드를 스케줄링할 수 있습니다. Flannel 관련 어노테이션과 Pod CIDR이 설정되어 있으며, kube-flannel과 kube-proxy 파드가 실행 중입니다.
# k8s-w1 노드에 배치된 파드 확인
kubectl get pod -A -owide | grep k8s-w2
# kube-flannel kube-flannel-ds-2dprr 1/1 Running 0 6m28s 192.168.10.102 k8s-w2 <none> <none>
# kube-system kube-proxy-98mm5 1/1 Running 0 6m28s 192.168.10.102 k8s-w2 <none> <none>
# k8s-w2 노드에는 kube-flannel과 kube-proxy 파드가 DaemonSet으로 배포되어 실행 중입니다.
kubectl get pod -A -owide | grep k8s-w1
# kube-flannel kube-flannel-ds-dmx6v 1/1 Running 0 8m46s 192.168.10.101 k8s-w1 <none> <none>
# kube-system kube-proxy-7vlpt 1/1 Running 0 8m46s 192.168.10.101 k8s-w1 <none> <none>
# k8s-w1 노드에도 kube-flannel과 kube-proxy 파드가 DaemonSet으로 배포되어 실행 중입니다. 각 워커 노드마다 이 두 파드가 필수적으로 실행됩니다.
[k8s-w1/w2 노드 정보 확인, 기본 환경 정보 출력 비교, sysctl 변경 확인
# # kubelet 활성화 확인
systemctl status kubelet --no-pager
# ● kubelet.service - kubelet: The Kubernetes Node Agent
# Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; preset: disabled)
# Drop-In: /usr/lib/systemd/system/kubelet.service.d
# └─10-kubeadm.conf
# Active: active (running) since Wed 2026-01-21 13:31:38 KST; 12min ago
# Invocation: ec2bd8770af1401dad7fd81ad99b9595
# Docs: https://kubernetes.io/docs/
# Main PID: 9652 (kubelet)
# Tasks: 10 (limit: 12337)
# Memory: 30.3M (peak: 31.1M)
# CPU: 10.528s
# CGroup: /system.slice/kubelet.service
# └─9652 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kub…
# join 후 워커 노드에서 kubelet이 정상적으로 실행 중입니다. bootstrap-kubeconfig를 통해 TLS 부트스트랩을 완료한 후 정식 kubeconfig를 사용하여 API 서버와 통신합니다.
# 기본 환경 정보 출력 저장
cat /etc/sysconfig/kubelet
# KUBELET_EXTRA_ARGS=
# join 후에도 kubelet 추가 인자 설정 파일은 비어있습니다. kubelet은 kubeadm이 생성한 설정 파일을 사용합니다.
tree /etc/kubernetes | tee -a etc_kubernetes-2.txt
# /etc/kubernetes
# ├── kubelet.conf
# ├── manifests
# └── pki
# └── ca.crt
#
# 3 directories, 2 files
# join 후 워커 노드의 /etc/kubernetes 디렉토리입니다. kubelet.conf(kubelet용 kubeconfig), manifests(비어있음, static pod 없음), pki/ca.crt(클러스터 CA 인증서)만 존재합니다.
cat /etc/kubernetes/kubelet.conf
# apiVersion: v1
# clusters:
# - cluster:
# certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJUVFnd2ViNU1Fd0l3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5T...
# server: https://192.168.10.100:6443
# name: default-cluster
# contexts:
# - context:
# cluster: default-cluster
# namespace: default
# user: default-auth
# name: default-context
# current-context: default-context
# kind: Config
# preferences: {}
# users:
# - name: default-auth
# user:
# client-certificate: /var/lib/kubelet/pki/kubelet-client-current.pem
# client-key: /var/lib/kubelet/pki/kubelet-client-current.pem
# 워커 노드의 kubelet.conf입니다. TLS 부트스트랩을 통해 발급받은 인증서(/var/lib/kubelet/pki/kubelet-client-current.pem)를 사용하여 API 서버에 인증합니다.
tree /var/lib/kubelet | tee -a var_lib_kubelet-2.txt
# /var/lib/kubelet
# ├── checkpoints
# ├── config.yaml
# ├── cpu_manager_state
# ├── device-plugins
# │ └── kubelet.sock
# ├── kubeadm-flags.env
# ├── memory_manager_state
# ├── pki
# │ ├── kubelet-client-2026-01-21-13-31-39.pem
# │ ├── kubelet-client-current.pem -> /var/lib/kubelet/pki/kubelet-client-2026-01-21-13-31-39.pem
# │ ├── kubelet.crt
# │ └── kubelet.key
# ├── plugins
# ├── plugins_registry
# ├── pod-resources
# │ └── kubelet.sock
# └── pods
# ├── 052c4cfb-1ef6-4da1-a58a-add170de6420
# │ ├── containers
# │ │ └── kube-proxy
# │ ├── etc-hosts
# │ ├── plugins
# │ └── volumes
# └── db0d3e41-be5f-4a3f-8068-e2d620fd2a9b
# ├── containers
# │ ├── install-cni
# │ ├── install-cni-plugin
# │ └── kube-flannel
# ├── etc-hosts
# ├── plugins
# └── volumes
#
# 34 directories, 30 files
# join 후 워커 노드의 kubelet 데이터 디렉토리입니다. pki 디렉토리에 TLS 부트스트랩으로 발급받은 인증서가 저장되어 있으며, pods 디렉토리에 kube-proxy와 kube-flannel 파드가 실행 중입니다.
tree /run/containerd/ -L 3 | tee -a run_containerd-2.txt
# /run/containerd/
# ├── containerd.sock
# ├── containerd.sock.ttrpc
# ├── io.containerd.grpc.v1.cri
# │ ├── containers
# │ │ ├── 43a4e45996ac5af7015cd3bde5d7a79f75ec597d7b3f88b948ec8272a50bb72e
# │ │ ├── 542a456b89b982e240eef75e835d53e0865c61a3c0ae6cd705e7c2aa703350bf
# │ │ ├── 6e3da4a24a0b7814b66972ddacfb7443798f040653799c706bae75a5ee1fcb69
# │ │ └── de07fe95d7d875bd16d8dc8c43fe33f91a5b3eeb2ac9fac434e19b5b83fbfeaa
# │ └── sandboxes
# │ ├── b7584ea4268f2c0540b4b0f5ec61e8e83b54b62081f4e9a26e67f9744cb8731d
# │ └── eecbd2b79d45e4a681a08ac2734085ddb5f4ec4ea91f73218e4a68533500af5d
# ├── io.containerd.runtime.v2.task
# │ └── k8s.io
# │ ├── 43a4e45996ac5af7015cd3bde5d7a79f75ec597d7b3f88b948ec8272a50bb72e
# │ ├── 6e3da4a24a0b7814b66972ddacfb7443798f040653799c706bae75a5ee1fcb69
# │ ├── b7584ea4268f2c0540b4b0f5ec61e8e83b54b62081f4e9a26e67f9744cb8731d
# │ └── eecbd2b79d45e4a681a08ac2734085ddb5f4ec4ea91f73218e4a68533500af5d
# ├── io.containerd.sandbox.controller.v1.shim
# ├── runc
# │ └── k8s.io
# │ ├── 43a4e45996ac5af7015cd3bde5d7a79f75ec597d7b3f88b948ec8272a50bb72e
# │ ├── 6e3da4a24a0b7814b66972ddacfb7443798f040653799c706bae75a5ee1fcb69
# │ ├── b7584ea4268f2c0540b4b0f5ec61e8e83b54b62081f4e9a26e67f9744cb8731d
# │ └── eecbd2b79d45e4a681a08ac2734085ddb5f4ec4ea91f73218e4a68533500af5d
# └── s
# ├── b3835352d7dcc04fd672851fb420ef2e1987820eb2ad72a603e72503f6795609
# └── c98546d363a5ff159d3be4e14e78fc9852e1b0ca49c6599ebf5584490cb28bd2
#
# 24 directories, 4 files
# join 후 워커 노드의 containerd 런타임 디렉토리입니다. kube-proxy와 kube-flannel 파드의 컨테이너와 sandbox가 생성되어 있습니다.
pstree -alnp | tee -a pstree-2.txt
# systemd,1 --switched-root --system --deserialize=46 no_timer_check
# |-systemd-journal,434
# |-systemd-userdbd,469
# |-systemd-udevd,478
# |-rpcbind,550 -w -f
# |-auditd,553
# |-dbus-broker-lau,592 --scope system --audit
# |-irqbalance,604
# |-lsmd,605 -d
# |-systemd-logind,608
# |-chronyd,613 -F 2
# |-VBoxService,710 --pidfile /var/run/vboxadd-service.sh
# |-polkitd,762 --no-debug --log-level=err
# |-gssproxy,793 -i
# |-sshd,794
# |-tuned,795 -Es /usr/sbin/tuned -l -P
# |-rsyslogd,991 -n
# |-atd,1034 -f
# |-crond,1040 -n
# |-agetty,1058 -o -- \\u --noreset --noclear - linux
# |-fwupd,1123
# |-gpg-agent,1263 --homedir /var/lib/fwupd/gnupg --use-standard-socket --daemon
# |-udisksd,1416
# |-NetworkManager,3072 --no-daemon
# |-systemd,6560 --user
# |-containerd,6965
# |-kubelet,9652 --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --container-runtime-endpoint=unix:///run/containerd/containerd.sock --node-ip=192.168.10.101 --pod-infra-container-image=registry.k8s.io/pause:3.10
# |-containerd-shim,9720 -namespace k8s.io -id b7584ea4268f2c0540b4b0f5ec61e8e83b54b62081f4e9a26e67f9744cb8731d -address /run/containerd/containerd.sock
# | |-pause,9766
# | |-kube-proxy,9816 --config=/var/lib/kube-proxy/config.conf --hostname-override=k8s-w1
# |-containerd-shim,9725 -namespace k8s.io -id eecbd2b79d45e4a681a08ac2734085ddb5f4ec4ea91f73218e4a68533500af5d -address /run/containerd/containerd.sock
# | |-pause,9768
# | `-flanneld,10124 --ip-masq --kube-subnet-mgr --iface=enp0s9
# join 후 워커 노드의 프로세스 트리입니다. kubelet이 실행 중이며, kube-proxy와 flanneld 프로세스가 containerd-shim을 통해 실행되고 있습니다.
systemd-cgls --no-pager | tee -a systemd-cgls-2.txt
# CGroup /:
# -.slice
# ├─user.slice
# ├─init.scope
# │ └─1 /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_tim…
# └─system.slice
# ├─irqbalance.service
# ├─libstoragemgmt.service
# ├─vboxadd-service.service
# ├─containerd.service …
# │ ├─6965 /usr/bin/containerd
# │ ├─9720 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -id b7584ea4268f2…
# │ └─9725 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -id eecbd2b79d45e…
# ├─systemd-udevd.service …
# ├─dbus-broker.service
# ├─polkit.service
# ├─chronyd.service
# ├─auditd.service
# ├─tuned.service
# ├─kubelet.service
# │ └─9652 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-ku…
# ├─systemd-journald.service
# ├─atd.service
# ├─sshd.service
# ├─fwupd.service
# ├─crond.service
# ├─NetworkManager.service
# ├─gssproxy.service
# ├─rsyslog.service
# ├─systemd-userdbd.service
# ├─rpcbind.service
# ├─udisks2.service
# ├─system-getty.slice
# └─systemd-logind.service
# └─kubepods.slice
# ├─kubepods-burstable.slice
# │ └─kubepods-burstable-poddb0d3e41_be5f_4a3f_8068_e2d620fd2a9b.slice
# │ ├─cri-containerd-6e3da4a24a0b7814b66972ddacfb7443798f040653799c706bae75a5ee1fcb69.scope …
# │ │ └─10124 /opt/bin/flanneld --ip-masq --kube-subnet-mgr --iface=enp0s9
# │ └─cri-containerd-eecbd2b79d45e4a681a08ac2734085ddb5f4ec4ea91f73218e4a68533500af5d.scope …
# │ └─9768 /pause
# └─kubepods-besteffort.slice
# └─kubepods-besteffort-pod052c4cfb_1ef6_4da1_a58a_add170de6420.slice
# ├─cri-containerd-43a4e45996ac5af7015cd3bde5d7a79f75ec597d7b3f88b948ec8272a50bb72e.scope …
# │ └─9816 /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.co…
# └─cri-containerd-b7584ea4268f2c0540b4b0f5ec61e8e83b54b62081f4e9a26e67f9744cb8731d.scope …
# │ └─9766 /pause
# join 후 워커 노드의 cgroup 구조입니다. kubepods.slice 하위에 kube-proxy와 kube-flannel 파드가 cgroup으로 관리되고 있습니다.
lsns | tee -a lsns-2.txt
# NS TYPE NPROCS PID USER COMMAND
# 4026531834 time 125 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531835 cgroup 124 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531836 pid 121 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531837 user 124 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531838 uts 116 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531839 ipc 121 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531840 net 123 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# 4026531841 mnt 103 1 root /usr/lib/systemd/systemd --switched-root --system --deserialize=46 no_timer_check
# ... (기타 프로세스별 네임스페이스)
# 4026532126 mnt 1 9766 65535 /pause
# 4026532189 ipc 2 9766 65535 /pause
# 4026532191 pid 1 9766 65535 /pause
# 4026532267 mnt 1 9768 65535 /pause
# 4026532268 ipc 2 9768 65535 /pause
# 4026532269 pid 1 9768 65535 /pause
# 4026532270 mnt 1 9816 root /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=k8s-w1
# 4026532271 pid 1 9816 root /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/config.conf --hostname-override=k8s-w1
# 4026532272 mnt 1 10124 root /opt/bin/flanneld --ip-masq --kube-subnet-mgr --iface=enp0s9
# 4026532273 pid 1 10124 root /opt/bin/flanneld --ip-masq --kube-subnet-mgr --iface=enp0s9
# 4026532274 cgroup 1 10124 root /opt/bin/flanneld --ip-masq --kube-subnet-mgr --iface=enp0s9
# join 후 워커 노드의 네임스페이스 목록입니다. pause 컨테이너와 kube-proxy, flanneld 프로세스가 각각의 네임스페이스를 사용하고 있습니다.
ip addr | tee -a ip_addr-2.txt
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# inet 127.0.0.1/8 scope host lo
# valid_lft forever preferred_lft forever
# inet6 ::1/128 scope host noprefixroute
# valid_lft forever preferred_lft forever
# 2: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# link/ether 08:00:27:90:ea:eb brd ff:ff:ff:ff:ff:ff
# inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute enp0s8
# valid_lft 43789sec preferred_lft 43789sec
# 3: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
# link/ether 08:00:27:1f:02:49 brd ff:ff:ff:ff:ff:ff
# inet 192.168.10.101/24 brd 192.168.10.255 scope global noprefixroute enp0s9
# valid_lft forever preferred_lft forever
# 4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN group default
# link/ether d6:6c:c0:b2:b0:73 brd ff:ff:ff:ff:ff:ff
# inet 10.244.1.0/32 scope global flannel.1
# valid_lft forever preferred_lft forever
# join 후 워커 노드의 네트워크 인터페이스입니다. Flannel이 생성한 flannel.1 VXLAN 인터페이스가 추가되었으며, 이 노드의 Pod CIDR 게이트웨이 IP(10.244.1.0)가 할당되어 있습니다.
ss -tnlp | tee -a ss-2.txt
# State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
# LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=794,fd=7))
# LISTEN 0 4096 0.0.0.0:111 0.0.0.0:* users:(("rpcbind",pid=550,fd=5),("systemd",pid=1,fd=115))
# LISTEN 0 4096 127.0.0.1:10249 0.0.0.0:* users:(("kube-proxy",pid=9816,fd=12))
# LISTEN 0 4096 127.0.0.1:10248 0.0.0.0:* users:(("kubelet",pid=9652,fd=18))
# LISTEN 0 4096 127.0.0.1:46103 0.0.0.0:* users:(("containerd",pid=6965,fd=12))
# LISTEN 0 4096 *:10250 *:* users:(("kubelet",pid=9652,fd=21))
# LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=794,fd=8))
# LISTEN 0 4096 *:10256 *:* users:(("kube-proxy",pid=9816,fd=10))
# LISTEN 0 4096 [::]:111 [::]:* users:(("rpcbind",pid=550,fd=7),("systemd",pid=1,fd=117))
# LISTEN 0 4096 *:9090 *:* users:(("systemd",pid=1,fd=112))
# join 후 워커 노드의 리스닝 소켓입니다. kubelet(10248: healthz, 10250: kubelet API)과 kube-proxy(10249: healthz, 10256: metrics) 포트가 추가되었습니다.
df -hT | tee -a df-2.txt
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda3 xfs 60G 3.2G 57G 6% /
# devtmpfs devtmpfs 4.0M 0 4.0M 0% /dev
# tmpfs tmpfs 913M 0 913M 0% /dev/shm
# efivarfs efivarfs 256K 6.3K 250K 3% /sys/firmware/efi/efivars
# tmpfs tmpfs 366M 12M 354M 4% /run
# tmpfs tmpfs 1.0M 0 1.0M 0% /run/credentials/systemd-journald.service
# /dev/sda1 vfat 599M 13M 587M 3% /boot/efi
# tmpfs tmpfs 1.0M 0 1.0M 0% /run/credentials/getty@tty1.service
# tmpfs tmpfs 183M 8.0K 183M 1% /run/user/1000
# tmpfs tmpfs 1.7G 12K 1.7G 1% /var/lib/kubelet/pods/db0d3e41-be5f-4a3f-8068-e2d620fd2a9b/volumes/kubernetes.io~projected/kube-api-access-966m8
# tmpfs tmpfs 1.7G 12K 1.7G 1% /var/lib/kubelet/pods/052c4cfb-1ef6-4da1-a58a-add170de6420/volumes/kubernetes.io~projected/kube-api-access-2klgb
# shm tmpfs 64M 0 64M 0% /run/containerd/io.containerd.grpc.v1.cri/sandboxes/eecbd2b79d45e4a681a08ac2734085ddb5f4ec4ea91f73218e4a68533500af5d/shm
# shm tmpfs 64M 0 64M 0% /run/containerd/io.containerd.grpc.v1.cri/sandboxes/b7584ea4268f2c0540b4b0f5ec61e8e83b54b62081f4e9a26e67f9744cb8731d/shm
# overlay overlay 60G 3.2G 57G 6% /run/containerd/io.containerd.runtime.v2.task/k8s.io/b7584ea4268f2c0540b4b0f5ec61e8e83b54b62081f4e9a26e67f9744cb8731d/rootfs
# overlay overlay 60G 3.2G 57G 6% /run/containerd/io.containerd.runtime.v2.task/k8s.io/eecbd2b79d45e4a681a08ac2734085ddb5f4ec4ea91f73218e4a68533500af5d/rootfs
# overlay overlay 60G 3.2G 57G 6% /run/containerd/io.containerd.runtime.v2.task/k8s.io/43a4e45996ac5af7015cd3bde5d7a79f75ec597d7b3f88b948ec8272a50bb72e/rootfs
# overlay overlay 60G 3.2G 57G 6% /run/containerd/io.containerd.runtime.v2.task/k8s.io/6e3da4a24a0b7814b66972ddacfb7443798f040653799c706bae75a5ee1fcb69/rootfs
# join 후 워커 노드의 파일시스템 사용량입니다. 파드의 projected volume(kube-api-access)과 컨테이너의 overlay 파일시스템이 마운트되어 있습니다. 루트 파일시스템 사용량이 5%에서 6%로 증가했습니다.
findmnt | tee -a findmnt-2.txt
# TARGET SOURCE FSTYPE OPTIONS
# / /dev/sda3 xfs rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota
# ├─/var/lib/nfs/rpc_pipefs sunrpc rpc_pipefs rw,relatime
# ├─/dev devtmpfs devtmpfs rw,nosuid,seclabel,size=4096k,nr_inodes=229647,mode=755,inode64
# ├─/sys sysfs sysfs rw,nosuid,nodev,noexec,relatime,seclabel
# │ ├─/sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,seclabel
# │ └─/sys/firmware/efi/efivars efivarfs efivarfs rw,nosuid,nodev,noexec,relatime
# ├─/proc proc proc rw,nosuid,nodev,noexec,relatime
# ├─/run tmpfs tmpfs rw,nosuid,nodev,seclabel,size=373852k,nr_inodes=819200,mode=755,inode64
# │ ├─/run/containerd/io.containerd.grpc.v1.cri/sandboxes/eecbd2b79d45e4a681a08ac2734085ddb5f4ec4ea91f4ec4ea91f73218e4a68533500af5d/shm shm tmpfs rw,nosuid,nodev,noexec,relatime,seclabel,size=65536k,inode64
# │ ├─/run/containerd/io.containerd.grpc.v1.cri/sandboxes/b7584ea4268f2c0540b4b0f5ec61e8e83b54b62081f4e9a26e67f9744cb8731d/shm shm tmpfs rw,nosuid,nodev,noexec,relatime,seclabel,size=65536k,inode64
# │ ├─/run/containerd/io.containerd.runtime.v2.task/k8s.io/b7584ea4268f2c0540b4b0f5ec61e8e83b54b62081f4e9a26e67f9744cb8731d/rootfs overlay overlay rw,relatime,seclabel,lowerdir=...,upperdir=...,workdir=...,uuid=on
# │ ├─/run/containerd/io.containerd.runtime.v2.task/k8s.io/43a4e45996ac5af7015cd3bde5d7a79f75ec597d7b3f88b948ec8272a50bb72e/rootfs overlay overlay rw,relatime,seclabel,lowerdir=...,upperdir=...,workdir=...,uuid=on
# │ ├─/run/containerd/io.containerd.runtime.v2.task/k8s.io/eecbd2b79d45e4a681a08ac2734085ddb5f4ec4ea91f73218e4a68533500af5d/rootfs overlay overlay rw,relatime,seclabel,lowerdir=...,upperdir=...,workdir=...,uuid=on
# │ └─/run/containerd/io.containerd.runtime.v2.task/k8s.io/6e3da4a24a0b7814b66972ddacfb7443798f040653799c706bae75a5ee1fcb69/rootfs overlay overlay rw,relatime,seclabel,lowerdir=...,upperdir=...,workdir=...,uuid=on
# ├─/boot/efi /dev/sda1 vfat rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=ascii,shortname=winnt,errors=remount-ro
# ├─/var/lib/kubelet/pods/db0d3e41-be5f-4a3f-8068-e2d620fd2a9b/volumes/kubernetes.io~projected/kube-api-access-966m8 tmpfs tmpfs rw,relatime,seclabel,size=1766852k,inode64,noswap
# └─/var/lib/kubelet/pods/052c4cfb-1ef6-4da1-a58a-add170de6420/volumes/kubernetes.io~projected/kube-api-access-2klgb tmpfs tmpfs rw,relatime,seclabel,size=1766852k,inode64,noswap
# join 후 워커 노드의 마운트 포인트 목록입니다. 파드의 projected volume(kube-api-access)과 컨테이너의 overlay 파일시스템, shm(공유 메모리)이 마운트되어 있습니다.
sysctl -a | tee -a sysctl-2.txt
# kubelet 에 --protect-kernel-defaults=false 적용되어 관련 코드에 sysctl 커널 파라미터 적용
vi -d sysctl-1.txt sysctl-2.txt
kernel.panic = 0 -> 10 변경
vm.overcommit_memory = 0 -> 1 변경
# 파일 출력 비교 : 빠져나오기 ':q' -> ':q' => 변경된 부분이 어떤 동작과 역할인지 조사해보기!
vi -d etc_kubernetes-1.txt etc_kubernetes-2.txt
vi -d var_lib_kubelet-1.txt var_lib_kubelet-2.txt
vi -d run_containerd-1.txt run_containerd-2.txt
vi -d pstree-1.txt pstree-2.txt
vi -d systemd-cgls-1.txt systemd-cgls-2.txt
vi -d lsns-1.txt lsns-2.txt
vi -d ip_addr-1.txt ip_addr-2.txt
vi -d ss-1.txt ss-2.txt
vi -d findmnt-1.txt findmnt-2.txt
결과 비교
| 구분 | join 전 (sysctl-1.txt / *_1.txt) | join 후 (sysctl-2.txt / *_2.txt) | 의미 |
|---|---|---|---|
/etc/kubernetes |
manifests/만 존재 (2 directories, 0 files) |
kubelet.conf, pki/ca.crt 생성 (3 directories, 2 files) |
워커는 static pod를 직접 갖지 않음(manifests는 비어있을 수 있음). 대신 kubelet이 API 서버 통신을 위한 kubeconfig/CA를 받음 |
/var/lib/kubelet |
비어있음 | config.yaml, kubeadm-flags.env, pki/*, pods/* 생성 |
TLS bootstrap 이후 kubelet이 자기 설정/인증서를 만들고, 실행 중인 파드(kube-proxy, flannel) 상태/볼륨/마운트를 관리 |
/run/containerd/ |
소켓/기본 런타임 디렉토리만 | io.containerd.grpc.v1.cri/containers, sandboxes, runtime.v2.task/k8s.io/* 증가 |
join 후 CRI 컨테이너/샌드박스/태스크가 생성되어 실제 파드가 실행됨 |
프로세스(pstree) |
containerd만 주로 보임 |
kubelet 추가 + containerd-shim 아래 kube-proxy, flanneld 실행 |
워커에서 kubelet이 기동되고, 필수 DaemonSet이 노드에 배포되어 동작 |
cgroup(systemd-cgls) |
kubepods.slice 없음 |
kubepods.slice 생성, 파드별 slice/scope 생성 |
kubelet이 cgroup(v2) 하위에서 파드 리소스/격리를 관리하기 시작 |
네임스페이스(lsns) |
시스템 기본 ns만 | pause, kube-proxy, flanneld 등 컨테이너/프로세스별 ns 추가 |
컨테이너 런타임이 pid/mnt/ipc/net 네임스페이스를 생성해 파드를 격리 |
네트워크(ip addr) |
lo, enp0s8, enp0s9만 |
flannel.1 추가, 10.244.1.0/32 할당 |
Flannel VXLAN 오버레이 구성. 노드별 Pod CIDR 라우팅에 사용 |
리스닝 포트(ss -tnlp) |
22, 111, containerd 정도 |
kubelet(10248/10250), kube-proxy(10249/10256) 추가 |
join 후 kubelet/kube-proxy가 서비스/헬스/메트릭 포트를 노출 |
파일시스템(df -hT) |
루트 사용량 낮음, k8s 마운트 없음 | tmpfs(projected volume), shm, overlay(rootfs) 마운트 추가 |
파드가 뜨면서 ServiceAccount 토큰/CA(projected), 컨테이너 rootfs(overlay), sandbox shm이 마운트됨 |
마운트(findmnt) |
시스템 기본만 | /var/lib/kubelet/pods/..., /run/containerd/.../rootfs, /run/containerd/.../shm 추가 |
파드/컨테이너 실행을 위한 kubelet/containerd 마운트 트리가 실제로 생성됨 |
| sysctl 항목 | join 전 | join 후 | 의미/추정 원인 |
|---|---|---|---|
kernel.panic |
0 |
10 |
커널 패닉 시 10초 후 자동 재부팅. 노드 장애 시 자동 복구 성격(설정 적용 경로는 kubelet/시스템 정책 확인 필요) |
vm.overcommit_memory |
0 |
1 |
메모리 오버커밋 허용(일부 워크로드/컴포넌트 안정성/성능 목적). 변경 주체는 kubelet/런타임/OS 튜닝 설정 가능 |
6. 모니터링 툴 설치
# metrics-server
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm upgrade --install metrics-server metrics-server/metrics-server --set 'args[0]=--kubelet-insecure-tls' -n kube-system
# 확인
kubectl top node
# NAME CPU(cores) CPU(%) MEMORY(bytes) MEMORY(%)
# k8s-ctr 193m 4% 823Mi 30%
# k8s-w1 38m 1% 378Mi 21%
# k8s-w2 30m 1% 329Mi 19%
kubectl top pod -A --sort-by='cpu'
# NAMESPACE NAME CPU(cores) MEMORY(bytes)
# kube-system kube-apiserver-k8s-ctr 82m 202Mi
# kube-system etcd-k8s-ctr 46m 48Mi
# kube-system kube-controller-manager-k8s-ctr 38m 50Mi
# kube-system kube-scheduler-k8s-ctr 12m 23Mi
# kube-flannel kube-flannel-ds-w9s2j 12m 15Mi
# kube-flannel kube-flannel-ds-2dprr 9m 13Mi
# kube-flannel kube-flannel-ds-dmx6v 8m 13Mi
# kube-system metrics-server-5dd7b49d79-4m8tk 5m 21Mi
# kube-system coredns-668d6bf9bc-77pjv 4m 16Mi
# kube-system coredns-668d6bf9bc-47zx5 3m 16Mi
# kube-system kube-proxy-qhqpz 1m 16Mi
# kube-system kube-proxy-98mm5 1m 17Mi
# kube-system kube-proxy-7vlpt 1m 17Mi
kubectl top pod -A --sort-by='memory'
# NAMESPACE NAME CPU(cores) MEMORY(bytes)
# kube-system kube-apiserver-k8s-ctr 82m 202Mi
# kube-system kube-controller-manager-k8s-ctr 38m 50Mi
# kube-system etcd-k8s-ctr 46m 48Mi
# kube-system kube-scheduler-k8s-ctr 12m 23Mi
# kube-system metrics-server-5dd7b49d79-4m8tk 5m 21Mi
# kube-system kube-proxy-7vlpt 1m 17Mi
# kube-system kube-proxy-98mm5 1m 17Mi
# kube-system kube-proxy-qhqpz 1m 16Mi
# kube-system coredns-668d6bf9bc-77pjv 4m 16Mi
# kube-system coredns-668d6bf9bc-47zx5 3m 16Mi
# kube-flannel kube-flannel-ds-w9s2j 12m 15Mi
# kube-flannel kube-flannel-ds-dmx6v 8m 13Mi
# kube-flannel kube-flannel-ds-2dprr 9m 13Mi
프로메테우스-스택 설치
# kube-prometheus-stack 설치
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
# 파라미터 파일 생성
cat <<EOT > monitor-values.yaml
prometheus:
prometheusSpec:
scrapeInterval: "20s"
evaluationInterval: "20s"
externalLabels:
cluster: "myk8s-cluster"
service:
type: NodePort
nodePort: 30001
grafana:
defaultDashboardsTimezone: Asia/Seoul
adminPassword: prom-operator
service:
type: NodePort
nodePort: 30002
alertmanager:
enabled: true
defaultRules:
create: true
kubeProxy:
enabled: false
prometheus-windows-exporter:
prometheus:
monitor:
enabled: false
EOT
cat monitor-values.yaml
# 배포
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack --version 80.13.3 \
-f monitor-values.yaml --create-namespace --namespace monitoring
# 확인
helm list -n monitoring
# NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
# kube-prometheus-stack monitoring 1 2026-01-21 14:49:56 +0900 KST deployed kube-prometheus-stack-80.13.3 v0.87.1
kubectl get pod,svc,ingress,pvc -n monitoring
# NAME READY STATUS RESTARTS AGE
# pod/alertmanager-kube-prometheus-stack-alertmanager-0 2/2 Running 0 15m
# pod/kube-prometheus-stack-grafana-5cb7c586f9-87jg2 3/3 Running 0 15m
# pod/kube-prometheus-stack-kube-state-metrics-7846957b5b-rgvkj 1/1 Running 0 15m
# pod/kube-prometheus-stack-operator-584f446c98-zxd9l 1/1 Running 0 15m
# pod/kube-prometheus-stack-prometheus-node-exporter-fxwmf 1/1 Running 0 15m
# pod/kube-prometheus-stack-prometheus-node-exporter-rxgr4 1/1 Running 0 15m
# pod/kube-prometheus-stack-prometheus-node-exporter-wmxrz 1/1 Running 0 15m
# pod/prometheus-kube-prometheus-stack-prometheus-0 2/2 Running 0 15m
#
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/alertmanager-operated ClusterIP None <none> 9093/TCP,9094/TCP,9094/UDP 15m
# service/kube-prometheus-stack-alertmanager ClusterIP 10.96.124.194 <none> 9093/TCP,8080/TCP 15m
# service/kube-prometheus-stack-grafana NodePort 10.96.165.91 <none> 80:30002/TCP 15m
# service/kube-prometheus-stack-kube-state-metrics ClusterIP 10.96.17.224 <none> 8080/TCP 15m
# service/kube-prometheus-stack-operator ClusterIP 10.96.118.10 <none> 443/TCP 15m
# service/kube-prometheus-stack-prometheus NodePort 10.96.178.162 <none> 9090:30001/TCP,8080:30152/TCP 15m
# service/kube-prometheus-stack-prometheus-node-exporter ClusterIP 10.96.148.157 <none> 9100/TCP 15m
# service/prometheus-operated ClusterIP None <none> 9090/TCP 15m
# (ingress/pvc는 구성에 따라 없을 수 있으며, 이 환경에서는 출력에 나타나지 않습니다.)
kubectl get prometheus,servicemonitors,alertmanagers -n monitoring
# NAME VERSION DESIRED READY RECONCILED AVAILABLE AGE
# prometheus.monitoring.coreos.com/kube-prometheus-stack-prometheus v3.9.1 1 1 True True 15m
#
# NAME AGE
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-alertmanager 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-apiserver 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-coredns 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-grafana 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-kube-controller-manager 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-kube-etcd 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-kube-scheduler 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-kube-state-metrics 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-kubelet 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-operator 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-prometheus 15m
# servicemonitor.monitoring.coreos.com/kube-prometheus-stack-prometheus-node-exporter 15m
#
# NAME VERSION REPLICAS READY RECONCILED AVAILABLE AGE
# alertmanager.monitoring.coreos.com/kube-prometheus-stack-alertmanager v0.30.0 1 1 True True 15m
kubectl get crd | grep monitoring
# alertmanagerconfigs.monitoring.coreos.com 2026-01-21T05:49:55Z
# alertmanagers.monitoring.coreos.com 2026-01-21T05:49:55Z
# podmonitors.monitoring.coreos.com 2026-01-21T05:49:55Z
# probes.monitoring.coreos.com 2026-01-21T05:49:55Z
# prometheusagents.monitoring.coreos.com 2026-01-21T05:49:55Z
# prometheuses.monitoring.coreos.com 2026-01-21T05:49:55Z
# prometheusrules.monitoring.coreos.com 2026-01-21T05:49:55Z
# scrapeconfigs.monitoring.coreos.com 2026-01-21T05:49:55Z
# servicemonitors.monitoring.coreos.com 2026-01-21T05:49:55Z
# thanosrulers.monitoring.coreos.com 2026-01-21T05:49:55Z
# kube-prometheus-stack 설치 시 Prometheus Operator 관련 CRD들이 함께 설치됩니다.
# 각각 웹 접속 실행 : NodePort 접속
open http://192.168.10.100:30001 # prometheus
open http://192.168.10.100:30002 # grafana : 접속 계정 admin / prom-operator
# 프로메테우스 버전 확인
kubectl exec -it sts/prometheus-kube-prometheus-stack-prometheus -n monitoring -c prometheus -- prometheus --version
prometheus, version 3.9.1
# 그라파나 버전 확인
kubectl exec -it -n monitoring deploy/kube-prometheus-stack-grafana -- grafana --version
grafana version 12.3.1
# [K8S Dashboard 추가] Dashboard → New → Import → 15661, 15757 입력 후 Load ⇒ 데이터소스(Prometheus 선택) 후 Import 클릭
Promentheus 대시보드를 확인해보면 다음과 같이 Targets가 DOWN 상태로 표시되며, connect: connection refused가 발생합니다.
이는 kube-controller-manager(10257), kube-scheduler(10259), etcd(2381)의 메트릭 엔드포인트가 기본적으로 127.0.0.1에만 바인딩되어 있는데, Prometheus(ServiceMonitor)가 노드 IP(예: 192.168.10.100) 로 접속을 시도하기 때문입니다. 따라서 아래와 같이 각 컴포넌트의 bind-address / listen-metrics-urls를 외부(노드 IP)에서도 접근 가능하도록 조정해야 정상적으로 수집됩니다.

kube-controller-manager, etcd, kube-scheduler 메트릭 수집 설정을 통해 모든 매트릭이 정상적으로 나올 수 있도록 합니다.
# kube-controller-manager bind-address 127.0.0.1 => 0.0.0.0 변경
sed -i 's|--bind-address=127.0.0.1|--bind-address=0.0.0.0|g' /etc/kubernetes/manifests/kube-controller-manager.yaml
cat /etc/kubernetes/manifests/kube-controller-manager.yaml | grep bind-address
- --bind-address=0.0.0.0
# kube-scheduler bind-address 127.0.0.1 => 0.0.0.0 변경
sed -i 's|--bind-address=127.0.0.1|--bind-address=0.0.0.0|g' /etc/kubernetes/manifests/kube-scheduler.yaml
cat /etc/kubernetes/manifests/kube-scheduler.yaml | grep bind-address
- --bind-address=0.0.0.0
# etcd metrics-url(http) 127.0.0.1 에 192.168.10.100 추가
sed -i 's|--listen-metrics-urls=http://127.0.0.1:2381|--listen-metrics-urls=http://127.0.0.1:2381,http://192.168.10.100:2381|g' /etc/kubernetes/manifests/etcd.yaml
cat /etc/kubernetes/manifests/etcd.yaml | grep listen-metrics-urls
- --listen-metrics-urls=http://127.0.0.1:2381,http://192.168.10.100:2381
해당 파일들을 수정하면 static pod가 재기동 되고, 일정 시간후에 정상적으로 매트릭이 수집되는 것을 확인할 수 있습니다.

인증서 익스포터 설치
먼저 인증서들을 확인합니다.
# Check certificates expiration for a Kubernetes cluster
kubeadm certs check-expiration
CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED
admin.conf Jan 17, 2027 00:33 UTC 364d ca no
apiserver Jan 17, 2027 00:33 UTC 364d ca no
apiserver-etcd-client Jan 17, 2027 00:33 UTC 364d etcd-ca no
apiserver-kubelet-client Jan 17, 2027 00:33 UTC 364d ca no
controller-manager.conf Jan 17, 2027 00:33 UTC 364d ca no
etcd-healthcheck-client Jan 17, 2027 00:33 UTC 364d etcd-ca no
etcd-peer Jan 17, 2027 00:33 UTC 364d etcd-ca no
etcd-server Jan 17, 2027 00:33 UTC 364d etcd-ca no
front-proxy-client Jan 17, 2027 00:33 UTC 364d front-proxy-ca no
scheduler.conf Jan 17, 2027 00:33 UTC 364d ca no
super-admin.conf Jan 17, 2027 00:33 UTC 364d ca no
CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED
ca Jan 15, 2036 00:33 UTC 9y no
etcd-ca Jan 15, 2036 00:33 UTC 9y no
front-proxy-ca Jan 15, 2036 00:33 UTC 9y no
#
tree /etc/kubernetes
/etc/kubernetes
|-- admin.conf # kubeconfig file
|-- controller-manager.conf # kubeconfig file
|-- kubelet.conf # kubeconfig file -> 인증서/키 파일은 /var/lib/kubelet/pki/ 에 위치
|-- manifests
| |-- etcd.yaml
| |-- kube-apiserver.yaml
| |-- kube-controller-manager.yaml
| `-- kube-scheduler.yaml
|-- pki # 해당 디렉터리 아래에는 인증서와 키파일 위치 : /etc/kubernetes/pki
| |-- apiserver-etcd-client.crt
| |-- apiserver-etcd-client.key
| |-- apiserver-kubelet-client.crt
| |-- apiserver-kubelet-client.key
| |-- apiserver.crt
| |-- apiserver.key
| |-- ca.crt
| |-- ca.key
| |-- etcd # 해당 디렉터리 아래에는 ETCD 관련 인증서와 키파일 위치 : /etc/kubernetes/pki/etcd
| | |-- ca.crt
| | |-- ca.key
| | |-- healthcheck-client.crt
| | |-- healthcheck-client.key
| | |-- peer.crt
| | |-- peer.key
| | |-- server.crt
| | `-- server.key
| |-- front-proxy-ca.crt
| |-- front-proxy-ca.key
| |-- front-proxy-client.crt
| |-- front-proxy-client.key
| |-- sa.key
| `-- sa.pub
|-- scheduler.conf # kubeconfig file
`-- super-admin.conf # kubeconfig file
# 위 kubelet.conf 에 대한 인증서/키 파일 위치 : worker 노드 동일
tree /var/lib/kubelet/pki/
/var/lib/kubelet/pki/
├── kubelet-client-2026-01-21-02-40-56.pem
├── kubelet-client-current.pem -> /var/lib/kubelet/pki/kubelet-client-2026-01-21-02-40-56.pem
├── kubelet.crt
└── kubelet.key
x509 인증서 만료 모니터링 대시보드는 인증서 만료에 대한 가시성을 확보할 수 있어서 매우 유용할 것 같습니다.
- 대시보드
- 주요 기능
- x509 인증서 만료일 및 유효성 메트릭을 Prometheus로 수집
- 알람 : 만료 임박(28일/14일 전)에 대해 경고/크리티컬 알림
- 지원 소스
- Kubernetes 시크릿(TLS)
- 시스템/디렉터리 내 PEM 인코딩 파일
- kubeconfig(인증서 직접/파일 참조 포함)
- 노드별로 컨트롤플레인/워커 데몬셋 2종 분리 배포
- 주요 메트릭:
x509_cert_not_before,x509_cert_not_after(시작·만료일시)x509_cert_expired(만료여부)x509_cert_expires_in_seconds,x509_cert_valid_since_seconds(남은/지남 초, 옵션)x509_cert_error,x509_read_errors(에러 감지)x509_exporter_build_info(버전 등)
- values.yaml에서 파일 경로 및 kubeconfig 경로 지정 가능
- Grafana 대시보드 연동 및 Prometheus Alertmanager 알람 지원
values 파일 주요 내용
- 데몬셋 2종류 배포 : 컨트롤 플레인 노드들 수집(cp), 워커 노드들 수집(nodes)
- 수집 종류 : 인증서(crt) 파일, kubeconfig 파일 → 직접 파일 위치 설정
- 프로메테우스 알람 설정 활성화 : warning Days Left(28 일), critical Days Left(14일)
- 그라파나 대시보드 추가 활성화
kubectl label node k8s-w1 worker="true" --overwrite
kubectl label node k8s-w2 worker="true" --overwrite
kubectl get nodes -l worker=true
# NAME STATUS ROLES AGE VERSION
# k8s-w1 Ready <none> 104m v1.32.11
# k8s-w2 Ready <none> 102m v1.32.11
# values 파일 작성
cat << EOF > cert-export-values.yaml
# -- hostPaths Exporter
hostPathsExporter:
hostPathVolumeType: Directory
daemonSets:
cp:
nodeSelector:
node-role.kubernetes.io/control-plane: ""
tolerations:
- effect: NoSchedule
key: node-role.kubernetes.io/control-plane
operator: Exists
watchFiles:
- /var/lib/kubelet/pki/kubelet-client-current.pem
- /var/lib/kubelet/pki/kubelet.crt
- /etc/kubernetes/pki/apiserver.crt
- /etc/kubernetes/pki/apiserver-etcd-client.crt
- /etc/kubernetes/pki/apiserver-kubelet-client.crt
- /etc/kubernetes/pki/ca.crt
- /etc/kubernetes/pki/front-proxy-ca.crt
- /etc/kubernetes/pki/front-proxy-client.crt
- /etc/kubernetes/pki/etcd/ca.crt
- /etc/kubernetes/pki/etcd/healthcheck-client.crt
- /etc/kubernetes/pki/etcd/peer.crt
- /etc/kubernetes/pki/etcd/server.crt
watchKubeconfFiles:
- /etc/kubernetes/admin.conf
- /etc/kubernetes/controller-manager.conf
- /etc/kubernetes/scheduler.conf
nodes:
nodeSelector:
worker: "true"
watchFiles:
- /var/lib/kubelet/pki/kubelet-client-current.pem
- /etc/kubernetes/pki/ca.crt
prometheusServiceMonitor:
create: true
scrapeInterval: 15s
scrapeTimeout: 10s
extraLabels:
release: kube-prometheus-stack
prometheusRules:
create: true
warningDaysLeft: 28
criticalDaysLeft: 14
extraLabels:
release: kube-prometheus-stack
grafana:
createDashboard: true
secretsExporter:
enabled: false
EOF
# helm chart 설치
helm repo add enix https://charts.enix.io
helm install x509-certificate-exporter enix/x509-certificate-exporter -n monitoring --values cert-export-values.yaml
# 설치 확인
helm list -n monitoring
## x509 대시보드 추가 : grafana sidecar 컨테이너가 configmap 확인 후 추가
kubectl get cm -n monitoring x509-certificate-exporter-dashboard
kubectl get cm -n monitoring x509-certificate-exporter-dashboard -o yaml
# 데몬셋 확인 : cp, nodes 각각
kubectl get ds -n monitoring -l app.kubernetes.io/instance=x509-certificate-exporter
# NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
# x509-certificate-exporter-cp 1 1 1 1 1 node-role.kubernetes.io/control-plane= 21s
# x509-certificate-exporter-nodes 2 2 2 2 2 worker=true 21s
# 파드 정보 확인 : IP 확인
kubectl get pod -n monitoring -l app.kubernetes.io/instance=x509-certificate-exporter -owide
# NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
# x509-certificate-exporter-cp-22v9p 1/1 Running 0 39s 10.244.0.4 k8s-ctr <none> <none>
# x509-certificate-exporter-nodes-5rhqf 1/1 Running 0 39s 10.244.1.5 k8s-w1 <none> <none>
# x509-certificate-exporter-nodes-v5jg2 1/1 Running 0 39s 10.244.2.7 k8s-w2 <none> <none>
# 프로메테우스 서비스모니터 수집을 위한 Service(ClusterIP) 정보 확인
kubectl get svc,ep -n monitoring x509-certificate-exporter
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/x509-certificate-exporter ClusterIP 10.96.87.136 <none> 9793/TCP 91s
# NAME ENDPOINTS AGE
# endpoints/x509-certificate-exporter 10.244.0.4:9793,10.244.1.5:9793,10.244.2.7:9793 91s
# 컨트롤플레인 노드에 배포된 'x509 익스포터' 파드에 메트릭 호출 확인
curl -s 10.244.1.5:9793/metrics | grep '^x509' | head -n 3
# x509_cert_expired{filename="ca.crt",filepath="/etc/kubernetes/pki/ca.crt",issuer_CN="kubernetes",serial_number="4686048711720833794",subject_CN="kubernetes"} 0
# x509_cert_expired{filename="kubelet-client-current.pem",filepath="/var/lib/kubelet/pki/kubelet-client-current.pem",issuer_CN="kubernetes",serial_number="191383947306253808361404236898124720804",subject_CN="system:node:k8s-w1",subject_O="system:nodes"} 0
# x509_cert_not_after{filename="ca.crt",filepath="/etc/kubernetes/pki/ca.crt",issuer_CN="kubernetes",serial_number="4686048711720833794",subject_CN="kubernetes"} 2.084290853e+09
# 워커 노드에 배포된 'x509 익스포터' 파드에 메트릭 호출 확인
curl -s 10.244.2.7:9793/metrics | grep '^x509' | head -n 3
# x509_cert_expired{filename="ca.crt",filepath="/etc/kubernetes/pki/ca.crt",issuer_CN="kubernetes",serial_number="4686048711720833794",subject_CN="kubernetes"} 0
# x509_cert_expired{filename="kubelet-client-current.pem",filepath="/var/lib/kubelet/pki/kubelet-client-current.pem",issuer_CN="kubernetes",serial_number="327906239552731223600543922899282080707",subject_CN="system:node:k8s-w2",subject_O="system:nodes"} 0
# x509_cert_not_after{filename="ca.crt",filepath="/etc/kubernetes/pki/ca.crt",issuer_CN="kubernetes",serial_number="4686048711720833794",subject_CN="kubernetes"} 2.084290853e+09
# 프로메테우스 CR 정보 확인 : 서비스모니터와 룰 셀렉터 정보 확인
kubectl get prometheuses.monitoring.coreos.com -n monitoring -o yaml
# ...
# serviceMonitorSelector:
# matchLabels:
# release: kube-prometheus-stack
# ruleSelector:
# matchLabels:
# release: kube-prometheus-stack
# ...
# helm 배포 시, label 추가 해둠
kubectl edit servicemonitors -n monitoring x509-certificate-exporter
# ...
# labels:
# app.kubernetes.io/instance: x509-certificate-exporter
# app.kubernetes.io/managed-by: Helm
# app.kubernetes.io/name: x509-certificate-exporter
# app.kubernetes.io/version: 3.19.1
# helm.sh/chart: x509-certificate-exporter-3.19.1
# release: kube-prometheus-stack
# ...
# helm 배포 시, label 추가 해둠
kubectl get prometheusrules.monitoring.coreos.com -n monitoring x509-certificate-exporter -o yaml | head -n 20
# apiVersion: monitoring.coreos.com/v1
# kind: PrometheusRule
# metadata:
# ...
# labels:
# app.kubernetes.io/instance: x509-certificate-exporter
# app.kubernetes.io/managed-by: Helm
# app.kubernetes.io/name: x509-certificate-exporter
# app.kubernetes.io/version: 3.19.1
# helm.sh/chart: x509-certificate-exporter-3.19.1
# release: kube-prometheus-stack
# name: x509-certificate-exporter
# ...
프로메테우스 그라파나 대시보드 확인
아래 화면은 Prometheus에서 x509-certificate-exporter가 노출한 메트릭이 실제로 수집되는지 확인하기 위한 PromQL 예시입니다. x509_cert_not_after, x509_cert_expired, x509_cert_expires_in_seconds 같은 지표를 조회해서 인증서 만료 시각/만료 여부/남은 만료 시간을 확인합니다.

아래 Grafana 대시보드는 수집된 x509 메트릭을 기반으로 노드/경로별 인증서의 만료 임박(Warning/Critical) 현황을 시각화합니다. 인증서별 만료일(not_after)과 남은 시간, 만료 여부를 한 화면에서 확인해 운영 중 만료 리스크를 빠르게 파악할 수 있습니다.

7. 샘플 애플리케이션 배포
# 샘플 애플리케이션 배포
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
type: ClusterIP
EOF
# 배포 확인
kubectl get deploy,svc,ep webpod -owide
# NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
# deployment.apps/webpod 2/2 2 2 8s webpod traefik/whoami app=webpod
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
# service/webpod ClusterIP 10.96.136.104 <none> 80/TCP 8s app=webpod
# NAME ENDPOINTS AGE
# endpoints/webpod 10.244.1.6:80,10.244.2.8:80 8s
# webpod service clusterip 변수 지정
SVCIP=$(kubectl get svc webpod -o jsonpath='{.spec.clusterIP}')
echo $SVCIP
# 통신 확인
curl -s $SVCIP
curl -s $SVCIP | grep Hostname
# Hostname: webpod-697b545f57-p28xd
# 반복 호출(신규 터미널)
while true; do curl -s $SVCIP | grep Hostname; sleep 1; done
8. kubeadm 인증서 갱신
현재 인증서 만료 정보 확인
kubeadm-config ConfigMap에는 기본 유효 기간이 정의되어 있습니다.
kc describe cm -n kube-system kubeadm-config | grep -i cert
# caCertificateValidityPeriod: 87600h0m0s # CA 기본 10년
# certificateValidityPeriod: 8760h0m0s # 클라이언트/서버 인증서 기본 1년
# certificatesDir: /etc/kubernetes/pki
현재 인증서 만료 현황을 확인합니다.
kubeadm certs check-expiration -v 6
# [check-expiration] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
# ...
# CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED
# admin.conf Jan 20, 2027 17:40 UTC 364d ca no
# apiserver Jan 20, 2027 17:40 UTC 364d ca no
# apiserver-etcd-client Jan 20, 2027 17:40 UTC 364d etcd-ca no
# apiserver-kubelet-client Jan 20, 2027 17:40 UTC 364d ca no
# controller-manager.conf Jan 20, 2027 17:40 UTC 364d ca no
# etcd-healthcheck-client Jan 20, 2027 17:40 UTC 364d etcd-ca no
# etcd-peer Jan 20, 2027 17:40 UTC 364d etcd-ca no
# etcd-server Jan 20, 2027 17:40 UTC 364d etcd-ca no
# front-proxy-client Jan 20, 2027 17:40 UTC 364d front-proxy-ca no
# scheduler.conf Jan 20, 2027 17:40 UTC 364d ca no
# super-admin.conf Jan 20, 2027 17:40 UTC 364d ca no
# CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED
# ca Jan 18, 2036 17:40 UTC 9y no
# etcd-ca Jan 18, 2036 17:40 UTC 9y no
# front-proxy-ca Jan 18, 2036 17:40 UTC 9y no
인증서 파일의 생성 시각/경로도 함께 확인합니다.
tree /etc/kubernetes/pki/
ls -l /etc/kubernetes/pki/etcd/
# -rw-r--r--. 1 root root 1094 Jan 21 02:40 ca.crt
# -rw-------. 1 root root 1675 Jan 21 02:40 ca.key
# -rw-r--r--. 1 root root 1123 Jan 21 02:40 healthcheck-client.crt
# -rw-------. 1 root root 1675 Jan 21 02:40 healthcheck-client.key
# -rw-r--r--. 1 root root 1196 Jan 21 02:40 peer.crt
# -rw-------. 1 root root 1679 Jan 21 02:40 peer.key
# -rw-r--r--. 1 root root 1196 Jan 21 02:40 server.crt
# -rw-------. 1 root root 1679 Jan 21 02:40 server.key
ls -l /etc/kubernetes/pki | head
# -rw-r--r--. 1 root root 1281 Jan 21 02:40 apiserver.crt
# -rw-r--r--. 1 root root 1123 Jan 21 02:40 apiserver-etcd-client.crt
# -rw-------. 1 root root 1675 Jan 21 02:40 apiserver-etcd-client.key
# -rw-------. 1 root root 1675 Jan 21 02:40 apiserver.key
# -rw-r--r--. 1 root root 1176 Jan 21 02:40 apiserver-kubelet-client.crt
# -rw-------. 1 root root 1675 Jan 21 02:40 apiserver-kubelet-client.key
# -rw-r--r--. 1 root root 1107 Jan 21 02:40 ca.crt
# -rw-------. 1 root root 1675 Jan 21 02:40 ca.key
# drwxr-xr-x. 2 root root 162 Jan 21 02:40 etcd
# ...
# apiserver 인증서(예시) : Not Before/After 확인
cat /etc/kubernetes/pki/apiserver.crt | openssl x509 -text -noout | egrep 'Not Before|Not After|Subject:|Issuer:' -n
# 6: Issuer: CN=kubernetes
# 8: Not Before: Jan 20 17:35:53 2026 GMT
# 9: Not After : Jan 20 17:40:53 2027 GMT
# 10: Subject: CN=kube-apiserver
수동 인증서 갱신 개요 (kubeadm)
kubeadm certs renew: CA는 유지한 채(control-plane 관련) 인증서를 재서명/재발급하고, 관련 kubeconfig(admin.conf,controller-manager.conf,scheduler.conf,super-admin.conf)도 갱신됩니다.- CA 갱신 여부: 기본적으로 CA(
ca.crt/ca.key,etcd/ca.crt/ca.key,front-proxy-ca.*)는 renew 대상 아님(기본 10년). - 영향도
- k8s API 요청 / kubectl: control-plane static pod 재기동 구간에 수초~수십 초 일시 중단 가능
- 워크로드: 일반적으로 영향 없음(노드/파드는 계속 실행)
- etcd
- single-node etcd: 재기동 구간에 API server 접근이 잠시 불가
- multi-node etcd: 순차 갱신 필요(동시 수행 시 쿼럼 붕괴 위험)
- worker node
- kubelet client cert는 자동 갱신이 기본이며,
kubelet.conf는 보통kubeadm certs renew all갱신 목록에 포함되지 않음(순환 가능한 인증서로 자동 갱신 구성) - kubeadm rejoin 불필요
- kubelet client cert는 자동 갱신이 기본이며,
참고 문서: Certificate Management with kubeadm
수동 인증서 갱신 실행 (control-plane)
# (테스트) 샘플 애플리케이션 반복 호출(신규 터미널)
SVCIP=$(kubectl get svc webpod -o jsonpath='{.spec.clusterIP}')
while true; do curl -s $SVCIP | grep Hostname; sleep 1; done
# 사전 백업 (HA control-plane이면 모든 control-plane에서 수행 권장)
cp -r /etc/kubernetes/pki /etc/kubernetes/pki.backup.$(date +%F)
mkdir -p /etc/kubernetes/backup-conf.$(date +%F)
cp /etc/kubernetes/*.conf /etc/kubernetes/backup-conf.$(date +%F)
# 갱신 전 만료 상태 확인
kubeadm certs check-expiration
# [check-expiration] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
# [check-expiration] Use 'kubeadm init phase upload-config --config your-config.yaml' to re-upload it.
# CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED
# admin.conf Jan 20, 2027 17:40 UTC 364d ca no
# apiserver Jan 20, 2027 17:40 UTC 364d ca no
# apiserver-etcd-client Jan 20, 2027 17:40 UTC 364d etcd-ca no
# apiserver-kubelet-client Jan 20, 2027 17:40 UTC 364d ca no
# controller-manager.conf Jan 20, 2027 17:40 UTC 364d ca no
# etcd-healthcheck-client Jan 20, 2027 17:40 UTC 364d etcd-ca no
# etcd-peer Jan 20, 2027 17:40 UTC 364d etcd-ca no
# etcd-server Jan 20, 2027 17:40 UTC 364d etcd-ca no
# front-proxy-client Jan 20, 2027 17:40 UTC 364d front-proxy-ca no
# scheduler.conf Jan 20, 2027 17:40 UTC 364d ca no
# super-admin.conf Jan 20, 2027 17:40 UTC 364d ca no
# CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED
# ca Jan 18, 2036 17:40 UTC 9y no
# etcd-ca Jan 18, 2036 17:40 UTC 9y no
# front-proxy-ca Jan 18, 2036
# 인증서 전체 갱신 (CA는 유지, 나머지는 재발급)
kubeadm certs renew all
# [renew] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
# [renew] Use 'kubeadm init phase upload-config --config your-config.yaml' to re-upload it.
# certificate embedded in the kubeconfig file for the admin to use and for kubeadm itself renewed
# certificate for serving the Kubernetes API renewed
# certificate the apiserver uses to access etcd renewed
# certificate for the API server to connect to kubelet renewed
# certificate embedded in the kubeconfig file for the controller manager to use renewed
# certificate for liveness probes to healthcheck etcd renewed
# certificate for etcd nodes to communicate with each other renewed
# certificate for serving etcd renewed
# certificate for the front proxy client renewed
# certificate embedded in the kubeconfig file for the scheduler manager to use renewed
# certificate embedded in the kubeconfig file for the super-admin renewed
# Done renewing certificates. You must restart the kube-apiserver, kube-controller-manager, kube-scheduler and etcd, so that they can use the new certificates.
# 갱신 후 만료 상태 확인
kubeadm certs check-expiration
# CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED
# admin.conf Jan 21, 2027 06:38 UTC 364d ca no
# apiserver Jan 21, 2027 06:38 UTC 364d ca no
# apiserver-etcd-client Jan 21, 2027 06:38 UTC 364d etcd-ca no
# apiserver-kubelet-client Jan 21, 2027 06:38 UTC 364d ca no
# controller-manager.conf Jan 21, 2027 06:38 UTC 364d ca no
# etcd-healthcheck-client Jan 21, 2027 06:38 UTC 364d etcd-ca no
# etcd-peer Jan 21, 2027 06:38 UTC 364d etcd-ca no
# etcd-server Jan 21, 2027 06:38 UTC 364d etcd-ca no
# front-proxy-client Jan 21, 2027 06:38 UTC 364d front-proxy-ca no
# scheduler.conf Jan 21, 2027 06:38 UTC 364d ca no
# super-admin.conf Jan 21, 2027 06:38 UTC 364d ca no
# CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED
# ca Jan 18, 2036 17:40 UTC 9y no
# etcd-ca Jan 18, 2036 17:40 UTC 9y no
# front-proxy-ca Jan 18, 2036 17:40 UTC 9y no
갱신 결과(파일 타임스탬프/kubeconfig 갱신)를 확인합니다.
ls -lt /etc/kubernetes/pki/ | head
# total 56
# -rw-r--r--. 1 root root 1119 Jan 21 15:38 front-proxy-client.crt
# -rw-------. 1 root root 1675 Jan 21 15:38 front-proxy-client.key
# -rw-r--r--. 1 root root 1176 Jan 21 15:38 apiserver-kubelet-client.crt
# -rw-------. 1 root root 1675 Jan 21 15:38 apiserver-kubelet-client.key
# -rw-r--r--. 1 root root 1123 Jan 21 15:38 apiserver-etcd-client.crt
# -rw-------. 1 root root 1675 Jan 21 15:38 apiserver-etcd-client.key
# -rw-r--r--. 1 root root 1281 Jan 21 15:38 apiserver.crt
# -rw-------. 1 root root 1675 Jan 21 15:38 apiserver.key
# -rw-------. 1 root root 1679 Jan 21 02:40 sa.key
ls -lt /etc/kubernetes/pki/etcd/ | head
# total 32
# -rw-r--r--. 1 root root 1196 Jan 21 15:38 server.crt
# -rw-------. 1 root root 1679 Jan 21 15:38 server.key
# -rw-r--r--. 1 root root 1196 Jan 21 15:38 peer.crt
# -rw-------. 1 root root 1675 Jan 21 15:38 peer.key
# -rw-r--r--. 1 root root 1123 Jan 21 15:38 healthcheck-client.crt
# -rw-------. 1 root root 1675 Jan 21 15:38 healthcheck-client.key
# -rw-r--r--. 1 root root 1094 Jan 21 02:40 ca.crt
# -rw-------. 1 root root 1675 Jan 21 02:40 ca.key
ls -lt /etc/kubernetes/*.conf
# -rw-------. 1 root root 5682 Jan 21 15:38 /etc/kubernetes/super-admin.conf
# -rw-------. 1 root root 5630 Jan 21 15:38 /etc/kubernetes/scheduler.conf
# -rw-------. 1 root root 5678 Jan 21 15:38 /etc/kubernetes/controller-manager.conf
# -rw-------. 1 root root 5654 Jan 21 15:38 /etc/kubernetes/admin.conf
# -rw-------. 1 root root 1974 Jan 21 02:40 /etc/kubernetes/kubelet.conf
# admin.conf 변경 확인(백업본 vs 현재)
ls -l /etc/kubernetes/admin.conf
# -rw-------. 1 root root 5654 Jan 21 15:38 /etc/kubernetes/admin.conf
ls -l /etc/kubernetes/backup-conf.$(date +%F)/admin.conf
# -rw-------. 1 root root 5658 Jan 21 15:38 /etc/kubernetes/backup-conf.2026-01-21/admin.conf
vi -d /etc/kubernetes/backup-conf.$(date +%F)/admin.conf /etc/kubernetes/admin.conf
# 2 files to edit
control-plane static pod 재기동 & kubeconfig 재적용
kube-apiserver, kube-controller-manager, kube-scheduler, etcd는 static pod로 실행되며, 인증서 동적 reload가 제한적이므로 재기동이 필요합니다.
# 사전 백업 : static pod 매니페스트
cp -r /etc/kubernetes/manifests /etc/kubernetes/manifests.backup.$(date +%F)
# static pod 모니터링(신규 터미널)
watch -d crictl ps
# 매니페스트 제거 → kubelet이 static pod 종료
rm -f /etc/kubernetes/manifests/*.yaml
# 종료 확인
crictl ps
# 매니페스트 복구 → kubelet이 static pod 재생성
cp /etc/kubernetes/manifests.backup.$(date +%F)/*.yaml /etc/kubernetes/manifests
tree /etc/kubernetes/manifests
# 기동 확인
crictl ps
# CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE
# 253ec71bd0839 58951ea1a0b5d 23 seconds ago Running kube-apiserver 1 7d62578f02aa4 kube-apiserver-k8s-ctr kube-system
# 74a54d4fbc656 82766e5f2d560 25 seconds ago Running kube-controller-manager 0 f68180f56508f kube-controller-manager-k8s-ctr kube-system
# 3eab35c38ab3e cfa17ff3d6634 25 seconds ago Running kube-scheduler 0 2a0de6f0a42d1 kube-scheduler-k8s-ctr kube-system
# 2c600dd715e11 1211402d28f58 25 seconds ago Running etcd 0 fbba0d45cc6d0 etcd-k8s-ctr kube-system
# c466ad24073b4 11873b3fefc46 25 minutes ago Running x509-certificate-exporter 0 85397dc9546a8 x509-certificate-exporter-cp-22v9p monitoring
# c8af35d077d15 6b5bc413b280c 51 minutes ago Running node-exporter 0 276c6248f1110 kube-prometheus-stack-prometheus-node-exporter-wmxrz monitoring
# e0c55fadff42d 2f6c962e7b831 13 hours ago Running coredns 0 9d6d22b21f9c2 coredns-668d6bf9bc-47zx5 kube-system
# 211e6cf45ecdb 2f6c962e7b831 13 hours ago Running coredns 0 7e8120ca9c5e3 coredns-668d6bf9bc-77pjv kube-system
# 7910a12ecad5e d84558c0144bc 13 hours ago Running kube-flannel 0 a6ab4291b889d kube-flannel-ds-w9s2j kube-flannel
# 3362d1643ecff dcdb790dc2bfe 13 hours ago Running kube-proxy 0 e9df7b0d85975 kube-proxy-qhqpz kube-system
kubectl get pod -n kube-system -owide -v=6
# admin.conf kubeconfig 재적용(로컬 kubectl)
yes | cp /etc/kubernetes/admin.conf ~/.kube/config ; echo
chown $(id -u):$(id -g) ~/.kube/config
kubectl config rename-context "kubernetes-admin@kubernetes" "HomeLab"
kubens default
인증서를 수동으로 갱신하는 과정에서 worker node 애플리케이션은 거의 멈추지 않았고, static pod 재기동시 다음과 같이 잠시 매트릭이 수집안되는 현상만 발생했습니다.

9. 모든 실습 완료 후 가상 머신 삭제
vagrant destroy -f && rm -rf .vagrant
마치며
이번 글에서는 kubeadm을 활용한 Kubernetes 클러스터의 설치부터 업그레이드, 인증서 갱신, 그리고 노드 리셋과 재조인까지 실제 운영 환경에서 경험할 수 있는 주요 절차들을 하나하나 실습해보았습니다.
이어지는 글에서 kubeadm을 이용한 업데이트 시나리오에 대한 내용을 정리하도록 하겠습니다.
긴 글 읽어주셔서 감사합니다!
'클라우드 컴퓨팅 & NoSQL > [K8S Deploy] K8S 디플로이 스터디' 카테고리의 다른 글
| [3주차 - K8S Deploy] Kubeadm & K8S Upgrade 2/2 (26.01.23) (0) | 2026.01.23 |
|---|---|
| [2주차 - K8S Deploy] Ansible 기초 (26.01.11) (1) | 2026.01.15 |
| [1주차 - K8S Deploy] Bootstrap Kubernetes the hard way (26.01.04) (1) | 2026.01.09 |