끄적끄적 코딩
728x90

들어가며

이번 포스팅에서는 kubeadm을 사용해 쿠버네티스(Kubernetes, K8S) 클러스터를 구성하는 과정을 정리해보겠습니다. 공통으로 필요한 사전 설정부터 시작해서 containerd 기반 런타임 구성, kubeadm/kubelet/kubectl 설치까지 먼저 준비합니다.

이후 컨트롤 플레인(Control Plane) 노드에서 클러스터를 초기화하고 Flannel CNI(Container Network Interface) 를 적용한 뒤, 워커 노드를 join하여 클러스터 구성이 정상적으로 완료되었는지 확인할 예정입니다. 또한 운영 관점에서 기본적으로 필요한 모니터링을 위해 프로메테우스 스택(Prometheus Stack) 과 인증서 익스포터를 설치하고, 그라파나(Grafana) 대시보드로 상태를 확인합니다.

마지막으로 샘플 애플리케이션 배포로 end-to-end 동작을 점검하고, kubeadm 인증서 갱신까지 진행하며 구성 절차를 마무리하겠습니다.


전체 흐름 살펴보기

kubeadm으로 클러스터를 구성하고, “동작 확인 → 운영 관점 점검”까지 이어지는 흐름으로 진행했습니다. 큰 흐름은 아래와 같습니다.

공통 준비

  • 모든 노드에서 쿠버네티스가 요구하는 기본 설정을 맞춥니다.
  • 이후 단계에서 문제가 생기지 않도록 시간 동기화, 보안/방화벽, 스왑, 커널 모듈·네트워크 파라미터, hosts 설정을 먼저 정리합니다.

컨테이너 런타임 준비

  • containerd를 설치하고 SystemdCgroup = true로 설정합니다.
  • kubelet이 런타임 소켓(/run/containerd/containerd.sock)을 통해 정상적으로 연결될 수 있는 상태를 만듭니다.

쿠버네티스 도구 설치

  • kubeadm, kubelet, kubectl을 설치하고 kubelet을 활성화합니다.
  • crictl 설정(/etc/crictl.yaml)을 추가해 런타임/네트워크 준비 상태를 확인할 수 있게 합니다.

컨트롤 플레인 구성

  • kubeadm init으로 클러스터를 초기화합니다.
  • /etc/kubernetes/pki, /etc/kubernetes/*.conf, /etc/kubernetes/manifests 생성 여부와 컨트롤 플레인 정적 파드 기동 상태를 확인합니다.

파드 네트워크 구성

  • Flannel CNI를 설치해 파드 네트워크를 구성합니다.
  • CNI 적용 후 CoreDNS, 노드 상태(Ready)가 정상으로 바뀌는지 확인합니다.

워커 노드 참여

  • 워커 노드에서도 동일한 준비 과정을 거친 뒤 kubeadm join으로 클러스터에 참여시킵니다.
  • 노드 상태, 노드별 파드 CIDR 할당, 라우팅(flannel 기반)까지 확인합니다.

운영 관점 구성 및 검증

  • 모니터링 스택과 인증서 만료 확인 환경을 구성하고, 대시보드에서 확인합니다.
  • 샘플 애플리케이션을 배포한 뒤 서비스 호출을 반복해 클러스터 동작을 점검합니다.
  • kubeadm certs renew로 인증서를 갱신하고, 정적 파드 재기동 및 kubeconfig 재적용 흐름까지 확인합니다.

Kubeadm deep dive

kubeadm란?

kubeadm은 쿠버네티스 클러스터를 구성할 때, 노드를 클러스터에 올리는 초기화 과정을 표준화해주는 도구입니다. 컨트롤 플레인 노드에서 클러스터를 시작할 수 있도록 기본 환경을 구성해 주며, 워커 노드가 클러스터에 join할 수 있도록 필요한 절차와 설정을 제공합니다.

실습에서 자주 사용하게 되는 명령은 아래 4가지입니다.

  • kubeadm init : 컨트롤 플레인 노드를 초기화하고 클러스터 구성을 시작합니다.
  • kubeadm join : 워커 노드를 클러스터에 join 합니다.
  • kubeadm upgrade : 클러스터 버전을 업그레이드할 때 사용합니다.
  • kubeadm reset : kubeadm으로 적용된 설정을 되돌릴 때 사용합니다.

여기서 주의할 점은 kubeadm이 “모든 설치를 대신해주는 도구”는 아니라는 점입니다. 컨트롤 플레인의 주요 구성요소는 스태틱 파드 형태로 구성되며, containerd 같은 런타임과 kubelet은 사전에 설치되어 있어야 하고 kubeadm은 그 위에서 클러스터 구성을 위한 설정을 잡아주는 흐름으로 진행됩니다.

[공통]  실습 환경

Vagrant로 컨트롤 플레인 1대(k8s-ctr)와 워커 노드 2대(k8s-w1, k8s-w2)를 구성하는 형태로 진행합니다. 각 노드는 private_network로 고정 IP를 부여해, 이후 kubeadm init / kubeadm join 과정에서 노드 구분과 접근이 헷갈리지 않도록 구성했습니다.

아래는 사용한 Vagrantfile입니다.

# 기본 이미지  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|
# 컨트롤 플레인 노드
    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

# 워커 노드
  (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

Vagrantfile은 아래처럼 내려받아 사용할 수 있습니다.

curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/**k8s-kubeadm**/Vagrantfile

이후에는 vagrant up으로 가상머신을 구동하고, vagrant status로 상태를 확인하는 흐름으로 진행합니다.

주요 설치 버전 정보

이번 실습 기준 버전은 v1.32.11이며, 구성요소 버전은 아래 표를 기준으로 진행합니다.

항목 버전 k8s  버전 호환성
Rocky Linux 10.0-1.6 RHEL 10 소스 기반 배포판으로 RHEL 정보 참고
containerd v2.1.5 CRI Version(v1), k8s 1.32~1.35 지원 - Link
runc v1.3.3 정보 조사 필요 Link
kubelet v1.32.11 k8s 버전 정책 문서 참고 - Link
kubeadm v1.32.11 상동
kubectl v1.32.11 상동
helm v3.18.6 k8s 1.30.x ~ 1.33.x 지원 - Link
flannel cni v0.27.3 k8s 1.28~ 이후 - Link

사전 설정

https://v1-32.docs.kubernetes.io/ko/docs/setup/production-environment/tools/kubeadm/install-kubeadm/

 

kubeadm 설치하기

이 페이지에서는 kubeadm 툴박스 설치 방법을 보여준다. 이 설치 프로세스를 수행한 후 kubeadm으로 클러스터를 만드는 방법에 대한 자세한 내용은 kubeadm으로 클러스터 생성하기 페이지를 참고한다.

kubernetes.io

https://v1-32.docs.kubernetes.io/ko/docs/setup/production-environment/container-runtimes/

 

컨테이너 런타임

참고: Dockershim은 쿠버네티스 릴리스 1.24부터 쿠버네티스 프로젝트에서 제거되었다. 더 자세한 내용은 Dockershim 제거 FAQ를 참고한다. 파드가 노드에서 실행될 수 있도록 클러스터의 각 노드에 컨

v1-32.docs.kubernetes.io

kubeadm으로 클러스터를 구성하기 전에, 모든 노드에서 공통으로 맞춰야 하는 기본 설정을 적용하겠습니다. 시간 동기화, 보안 설정, 스왑 비활성화, 커널 모듈/파라미터, hosts 설정처럼 이후 설치/구성 과정에서 문제가 자주 발생하는 지점을 먼저 정리해두는 단계입니다.

아래 예시는 k8s-ctr에서 진행하지만, 동일한 설정을 모든 노드(k8s-ctr, k8s-w1, k8s-w2)에 적용하는 것을 전제로 합니다.

기본 정보 확인

먼저 노드에 접속한 뒤, CPU/메모리/디스크/네트워크/커널 정보와 cgroup 구성을 확인해 환경이 의도대로 구성되어 있는지 점검합니다.

  • 기본 정보 확인
vagrant ssh k8s-ctr
# User 정보
whoami
id                   # uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant)
pwd                  # /home/vagrant

# cpu, mem
lscpu
free -h

# Disk
lsblk                # sda      8:0    0   64G  0 disk
df -hT

# Network
ip -br -c -4 addr    # enp0s9           UP             192.168.10.100/24
ip -c route
ip addr

# Host Info, Kernel
hostnamectl          # Kernel: Linux 6.12.0-55.39.1.el10_0.aarch64
uname -r
**rpm -aq | grep release**
rocky-release-**10.0-1**.6.el10.noarch

# cgroup 버전 확인
**stat -fc %T /sys/fs/cgroup**
*cgroup2fs*

**findmnt**
**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

# Process
pstree
lsns

여기서는 cgroup2fs로 확인되는지, 그리고 네트워크 인터페이스에 IP가 정상 할당되어 있는지를 먼저 체크해두면 이후 단계에서 원인 파악이 훨씬 수월합니다.

root 권한(로그인 환경) 전환

설정 적용 과정에서 시스템 파일을 수정하거나 서비스를 제어해야 하므로 root로 전환합니다.

  • root 권한(로그인 환경) 전환
sudo su -

Time, NTP 설정

클러스터 구성에서는 인증서 만료 시간, 로그 타임스탬프 등 시간에 민감한 요소가 많기 때문에 모든 노드의 시간이 동기화되어 있어야 합니다. 아래에서는 로컬 RTC 설정을 정리하고, 타임존을 KST로 맞춘 뒤 시간 동기화 상태를 확인합니다.

# timedatectl 정보 확인
timedatectl status # RTC in local TZ: yes -> Warning:...
**timedatectl set-local-rtc 0**
timedatectl status

# 시스템 타임존(Timezone)을 한국(KST, UTC+9) 으로 설정 : 시스템 시간은 UTC 기준 유지, 표시만 KST로 변환
date
**timedatectl set-timezone Asia/Seoul**
date

# systemd가 시간 동기화 서비스(chronyd) 를 관리하도록 설정되어 있음 : ntpd 대신 chrony 사용 (Rocky 9/10 기본)
**timedatectl status**
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               
===============================================================================
**^*** 211.108.117.211               2   6   **377**     9    -56us[  -31us] +/- 3253us*
...

# 현재 시스템 시간이 얼마나 정확한지 종합 성적표
**chronyc tracking**
*Reference ID    : D36C75D3 (211.108.117.211)
...*

이 단계에서는 timedatectl status에서 동기화 상태가 잡혔는지와 chronyc 출력에서 기준 서버가 정상적으로 선택되는지를 확인해두면 됩니다.

SELinux 설정, firewalld(방화벽) 끄기

쿠버네티스 구성 과정에서 불필요한 제약을 줄이기 위해 SELinux는 permissive로 전환하고, firewalld는 비활성화합니다.

# SELinux 설정 : Kubernetes는 Permissive 권장
getenforce
sestatus      # Current mode:                   enforcing
**setenforce 0**
getenforce
sestatus      # Current mode:                   permissive

# 재부팅 시에도 Permissive 적용
cat /etc/selinux/config | grep ^SELINUX
**sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config**
cat /etc/selinux/config | grep ^SELINUX

# firewalld(방화벽) 끄기
systemctl status firewalld
**systemctl disable --now firewalld**
systemctl status firewalld

Swap 비활성화

쿠버네티스에서는 스왑이 활성화되어 있으면 구성/동작에 문제가 될 수 있으므로 비활성화합니다. 또한 재부팅 이후에도 유지되도록 fstab에서 스왑 라인을 제거합니다.

# Swap 비활성화
lsblk
free -h
free -h | grep Swap
**swapoff -a**
lsblk
free -h | grep Swap

# 재부팅 시에도 'Swap 비활성화' 적용되도록 /etc/fstab에서 swap 라인 삭제
cat /etc/fstab | grep swap
**sed -i '/swap/d' /etc/fstab**
cat /etc/fstab | grep swap

커널 모듈 및 커널 파라미터 설정(네트워크 설정)

쿠버네티스 네트워크 동작을 위해 필요한 커널 모듈을 로드하고, 브리지 트래픽이 iptables를 거치도록 커널 파라미터를 설정합니다. 설정은 파일로 저장해 재부팅 이후에도 유지되도록 합니다.

# 커널 모듈 확인
lsmod
lsmod | grep -iE 'overlay|br_netfilter'

# 커널 모듈 로드
**modprobe overlay
modprobe br_netfilter**
lsmod | grep -iE 'overlay|br_netfilter'

# 
**cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF**
tree /etc/modules-load.d/

# 커널 파라미터 설정 : 네트워크 설정 - 브릿지 트래픽이 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**
tree /etc/sysctl.d/

# 설정 적용
**sysctl --system**

# 적용 확인
sysctl net.bridge.bridge-nf-call-iptables
sysctl net.ipv4.ip_forward

여기서는 모듈이 정상 로드되는지(lsmod), 그리고 sysctl 값이 기대한 값으로 적용되는지까지 확인하면 됩니다.

hosts 설정

노드 간 통신과 구성이 명확해지도록 hosts에 각 노드의 이름과 IP를 등록합니다. 이후 ping으로 이름 해석이 정상인지 확인합니다.

# hosts 설정
cat /etc/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

# 확인
ping -c 1 k8s-ctr
ping -c 1 k8s-w1
ping -c 1 k8s-w2

[공통] CRI 설치 : containerd(runc) v2.1.5

https://github.com/containerd/containerd/tree/main

 

GitHub - containerd/containerd: An open and reliable container runtime

An open and reliable container runtime. Contribute to containerd/containerd development by creating an account on GitHub.

github.com

https://v1-32.docs.kubernetes.io/ko/docs/setup/production-environment/container-runtimes/

 

컨테이너 런타임

참고: Dockershim은 쿠버네티스 릴리스 1.24부터 쿠버네티스 프로젝트에서 제거되었다. 더 자세한 내용은 Dockershim 제거 FAQ를 참고한다. 파드가 노드에서 실행될 수 있도록 클러스터의 각 노드에 컨

v1-32.docs.kubernetes.io

쿠버네티스 노드는 파드를 실행하기 위해 컨테이너 런타임 인터페이스(CRI) 가 필요합니다. 이번 구성에서는 CRI로 containerd를 사용하고, 실제 컨테이너 실행은 runc가 담당하는 형태로 진행합니다.

kubeadm은 런타임을 직접 설치해주지 않기 때문에, 클러스터 구성 전에 모든 노드에 containerd를 먼저 설치하고, 쿠버네티스에서 권장하는 방식으로 설정을 맞춰두는 것이 핵심입니다. 특히 이번 환경은 systemd 기반이며 cgroup v2를 사용하므로, SystemdCgroup 설정을 반드시 활성화합니다.

버전 호환성 확인

이번 실습에서는 containerd v2.1.5를 사용합니다. 쿠버네티스 버전별 containerd 호환 정보는 아래 표 기준으로 확인했습니다.

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 2.x 계열에서는 설정 파일(/etc/containerd/config.toml)의 구성 버전이 version = 3으로 동작합니다.

Configuration Version Minimum containerd version
1 v1.0.0
2 v1.3.0
3 v2.0.0

containerd(runc) 설치 v2.1.5

아래 과정은 모든 노드에서 공통으로 진행합니다. dnf 환경 확인 → 저장소 추가 → 설치 가능한 버전 확인 → containerd.io 설치 순서로 진행합니다.

# 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***
*Downloading Packages:
containerd.io-2.1.5-1.el10.aarch64.rpm*

# 설치된 파일 확인
which runc && **runc --version**
which containerd && **containerd --version**
which containerd-shim-runc-v2 && containerd-shim-runc-v2 -v
which ctr && ctr --version
cat /etc/containerd/config.toml
tree /usr/lib/systemd/system | grep containerd
cat /usr/lib/systemd/system/containerd.service

여기까지는 설치가 정상적으로 되었는지(바이너리/유닛 파일) 확인하는 단계입니다. 다음 단계에서 기본 설정 파일을 생성하고, 쿠버네티스에서 요구하는 설정을 반영합니다.

기본 설정 생성 및 SystemdCgroup 활성화

containerd 기본 설정을 생성한 뒤, SystemdCgroup을 활성화합니다. 이 설정은 이후 kubelet과 런타임의 cgroup 드라이버 구성을 맞추는 데 중요합니다.

# 기본 설정 생성 및 SystemdCgroup 활성화
**containerd config default | tee /etc/containerd/config.toml**
*version = 3                    # containerd version 2.0 이상 시
root = '/var/lib/containerd'
state = '/run/containerd'
...*

# 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
**sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml**
cat /etc/containerd/config.toml | grep -i systemdcgroup

# systemd unit 파일 최신 상태 읽기
**systemctl daemon-reload**

containerd 기동 및 상태 확인

설정 반영 후 containerd를 기동하고, 서비스/로그/프로세스를 확인합니다. 이후 단계에서 kubelet이 런타임 소켓을 사용하게 되므로, 기동 상태가 안정적인지 먼저 체크해두는 것이 좋습니다.

# containerd start 와 enabled
**systemctl enable --now containerd**

# 
systemctl status containerd --no-pager
journalctl -u containerd.service --no-pager
pstree -alnp
systemd-cgls --no-pager

런타임 소켓 및 플러그인 확인

마지막으로 containerd 소켓이 정상 생성되었는지 확인하고, ctr로 플러그인 상태까지 점검합니다. 이 소켓은 이후 kubelet이 런타임과 통신할 때 사용합니다.

# containerd의 유닉스 도메인 소켓 확인 : kubelet에서 사용 , containerd client 3종(ctr, nerdctr, crictl)도 사용
containerd config dump | grep -n containerd.sock
**ls -l /run/containerd/containerd.sock**
**ss -xl | grep containerd**
ss -xnp | grep containerd

# 플러그인 확인
ctr --address /run/containerd/containerd.sock version
**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**
io.containerd.snapshotter.v1              zfs                      linux/arm64/v8    skip      
...   
**io.containerd.metadata.v1                 bolt                     -                 ok     # 메타데이터 DB (bolt)***

[공통] kubeadm, kubelet 및 kubectl 설치 v1.32.11

https://v1-32.docs.kubernetes.io/ko/docs/setup/production-environment/tools/kubeadm/install-kubeadm/

 

kubeadm 설치하기

이 페이지에서는 kubeadm 툴박스 설치 방법을 보여준다. 이 설치 프로세스를 수행한 후 kubeadm으로 클러스터를 만드는 방법에 대한 자세한 내용은 kubeadm으로 클러스터 생성하기 페이지를 참고한다.

kubernetes.io

앞 장에서 containerd 구성을 마쳤다면, 이제 쿠버네티스 노드 구성에 필요한 기본 도구들을 설치할 차례입니다. 이번 장에서는 모든 노드에 kubeadm, kubelet, kubectl 을 설치하고, 런타임(containerd)과 연결되는지까지 기본 확인을 진행합니다.

특히 kubelet은 서비스로는 먼저 활성화되지만, 실제로 노드가 쿠버네티스 클러스터에 올라가기 전까지는 초기화가 완전히 끝난 상태가 아니기 때문에 로그가 다소 어수선할 수 있습니다. 이 부분은 다음 장에서 kubeadm init을 진행하면서 자연스럽게 정리됩니다.

저장소(repo) 추가

쿠버네티스 패키지를 설치하기 위해 저장소 설정을 추가합니다. 여기서 exclude=... 설정은 추후 실수로 dnf update를 실행했을 때 kubelet 등이 자동으로 업데이트되는 상황을 방지하기 위한 장치입니다.

# repo 추가
## exclude=... : 실수로 dnf update 시 kubelet 자동 업그레이드 방지
dnf repolist
tree /etc/yum.repos.d/
**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**
dnf makecache

설치 및 버전 확인

설치 가능한 버전을 먼저 확인한 뒤, --disableexcludes=kubernetes 옵션으로 이번 설치에 한해서만 exclude 규칙을 무시하고 설치합니다. 버전을 명시하지 않으면 저장소에서 제공하는 최신 버전이 설치됩니다.

# 설치
## --disableexcludes=... kubernetes repo에 설정된 exclude 규칙을 이번 설치에서만 무시(1회성 옵션 처럼 사용)
## 설치 가능 버전 확인
dnf list --showduplicates **kubelet** # '--disableexcludes=kubernetes' 아래 처럼 있는 경우와 없는 경우 비교해보기
dnf list --showduplicates **kubelet --disableexcludes=kubernetes**
dnf list --showduplicates **kubeadm --disableexcludes=kubernetes**
dnf list --showduplicates **kubectl --disableexcludes=kubernetes**
*...
kubectl.x86_64                                        1.32.10-150500.1.1                                        kubernetes
kubectl.aarch64                                       1.32.11-150500.1.1                                        kubernetes
...*

## 버전 정보 미지정 시, 제공 가능 최신 버전 설치됨.
**dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes**
*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*

kubelet 활성화 및 설치 결과 확인

kubelet은 노드에서 파드를 관리하는 핵심 컴포넌트이므로 서비스로 먼저 올려두는 흐름입니다. 다만 이 시점에는 아직 클러스터 초기화 전이기 때문에, 실행은 되더라도 정상 상태로 보이지 않을 수 있습니다.

# kubelet 활성화 (실제 기동은 kubeadm init 후에 시작됨)
**systemctl enable --now kubelet**
ps -ef |grep kubelet

# 설치 파일들 확인
which kubeadm && **kubeadm version -o yaml**

which kubectl && **kubectl version --client=true**
*Client Version: **v1.32.11**
Kustomize Version: v5.5.0*

which kubelet && **kubelet --version**
*Kubernetes **v1.32.11***

cri-tools(crictl) 설정

cri-tools는 런타임 상태를 확인할 때 유용합니다. 다만 기본 설정 파일(/etc/crictl.yaml)이 없으면 소켓을 어디로 붙어야 할지 몰라 경고가 나오기 때문에, 이번 환경에서는 containerd 소켓을 명시해줍니다.

# cri-tools
which crictl && **crictl version**
*WARN[0000] Config "/etc/crictl.yaml" does not exist, trying next: "/usr/bin/crictl.yaml"* 

# /etc/crictl.yaml 파일 작성
**cat << EOF > /etc/crictl.yaml**
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
**EOF**

**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"
      },*

여기서 cni plugin not initialized 메시지는 아직 CNI를 설치하지 않은 상태이기 때문에 자연스러운 결과입니다. 다음 장에서 컨트롤 플레인 구성 후 CNI를 적용하면 정리됩니다.

CNI 바이너리 확인

kubernetes-cni 패키지가 함께 설치되면서, 파드 네트워크 구성을 위한 CNI 바이너리들이 /opt/cni/bin 경로에 준비됩니다. 또한 CNI 설정 파일이 놓이는 /etc/cni/net.d 디렉터리도 함께 확인합니다.

# kubernetes-cni : 파드 네트워크 구성을 위한 CNI 바이너리 파일 확인
**ls -al /opt/cni/bin**
**tree /opt/cni**
*/opt/cni
└── bin
    ├── bandwidth
    ├── bridge
    ├── portmap
    ...*

**tree /etc/cni/**
*/etc/cni/
└── net.d*

kubelet 서비스 상태 및 구성 파일 확인

마지막으로 kubelet 서비스 상태와 로그, 그리고 kubeadm이 사용하는 단위 파일/설정 파일 위치를 확인해두면 이후 트러블슈팅이 훨씬 편해집니다.

#
systemctl is-active kubelet
systemctl status kubelet --no-pager
**journalctl -u kubelet --no-pager**
**tree /usr/lib/systemd/system | grep kubelet -A1**
*├── kubelet.service
├── kubelet.service.d
│   └── 10-kubeadm.conf*

cat /usr/lib/systemd/system/kubelet.service
**cat /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf**
...

tree /etc/kubernetes
tree /var/lib/kubelet
**cat /etc/sysconfig/kubelet**
*KUBELET_EXTRA_ARGS=*

# cgroup , namespace 정보 확인
systemd-cgls --no-pager
lsns

# containerd의 유닉스 도메인 소켓 확인 : kubelet에서 사용 , containerd client 3종(ctr, nerdctr, crictl)도 사용
**ls -l /run/containerd/containerd.sock**
**ss -xl | grep containerd**
ss -xnp | grep containerd

이제 모든 노드에서 containerd와 쿠버네티스 기본 도구 설치가 완료되었습니다. 다음 장에서는 컨트롤 플레인 노드에서 kubeadm init으로 클러스터를 구성하는 단계로 넘어가겠습니다.


[k8s-ctr] kubeadm 으로 k8s 클러스터 구성 & Flannel CNI 설치 v0.27.3 & 편의성 설정 등

이번 장에서는 k8s-ctr에서 kubeadm init을 수행해 컨트롤 플레인을 먼저 구성합니다. 이 단계가 끝나면 API 서버와 컨트롤 플레인 구성요소들이 스태틱 파드 형태로 올라가고, 기본 애드온(CoreDNS, kube-proxy)도 함께 배포됩니다.

다만 이 시점에는 아직 파드 네트워크(CNI) 가 없기 때문에 coredns가 Pending으로 남아있고, 노드도 NotReady로 보이는 것이 정상입니다. 이어서 Flannel CNI를 설치해 네트워크를 완성하고, 이후 확인/편의 설정까지 진행하겠습니다.

kubeadm init을 위한 설정 파일 준비

kubeadm init은 옵션을 직접 길게 주는 방식도 가능하지만, 이번에는 설정 파일을 만들어 두고 --config로 적용합니다. 특히 여기서는 node-ip를 명시해, 미설정 시 10.0.2.15로 잡히는 상황을 피하도록 구성했습니다.

아래는 kubeadm init 전/후 비교를 위해 기본 정보도 함께 저장하고, kubeadm-init.yaml을 작성하는 과정입니다.

# 기본 환경 정보 출력 저장
crictl images
crictl ps
cat /etc/sysconfig/kubelet
tree /etc/kubernetes  | tee -a etc_kubernetes-1.txt
tree /var/lib/kubelet | tee -a var_lib_kubelet-1.txt
tree /run/containerd/ -L 3 | tee -a run_containerd-1.txt
pstree -alnp | tee -a pstree-1.txt
systemd-cgls --no-pager | tee -a systemd-cgls-1.txt
lsns | tee -a lsns-1.txt
ip addr | tee -a ip_addr-1.txt 
ss -tnlp | tee -a ss-1.txt
df -hT | tee -a df-1.txt
findmnt | tee -a findmnt-1.txt
sysctl -a | tee -a sysctl-1.txt

# 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

[k8s-ctr] kubeadm init 수행 및 초기 상태 확인

먼저 (옵션으로) 필요한 이미지를 미리 받아두고, --dry-run으로 한 번 점검한 뒤 실제 kubeadm init을 수행합니다. 완료되면 /etc/kubernetes/pki에 인증서, /etc/kubernetes에 각 컴포넌트용 kubeconfig 파일이 생성되고, 컨트롤 플레인 구성요소가 스태틱 파드로 올라갑니다.

# (옵션) 컨테이너 이미지 미리 다운로드 : 특히 업그레이드 작업 시, 작업 시간 단축을 위해서 수행할 것
kubeadm config images pull

# k8s controlplane 초기화 설정 수행
*kubeadm init --config="kubeadm-init.yaml" **--dry-run**
tree /etc/kubernetes*
**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*

# 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*

**crictl ps**
*CONTAINER           IMAGE               CREATED             STATE               NAME                      ATTEMPT             POD ID              POD                               NAMESPACE
a04be00090580       dcdb790dc2bfe       26 seconds ago      Running             kube-proxy                0                   1fd91b0a982bb       kube-proxy-7w44b                  kube-system
b005f34739da5       82766e5f2d560       37 seconds ago      Running             kube-controller-manager   0                   555d146c3ec07       kube-controller-manager-k8s-ctr   kube-system
eb42b9c47fdce       cfa17ff3d6634       37 seconds ago      Running             kube-scheduler            0                   e649514d0a1b7       kube-scheduler-k8s-ctr            kube-system
bbe8495d2a205       58951ea1a0b5d       37 seconds ago      Running             kube-apiserver            0                   be25c00dd555c       kube-apiserver-k8s-ctr            kube-system
c00a944599500       1211402d28f58       37 seconds ago      Running             etcd                      0                   ce6b89dea28da       etcd-k8s-ctr                      kube-system*

# kubeconfig 작성
**mkdir -p /root/.kube
cp -i /etc/kubernetes/admin.conf /root/.kube/config
chown $(id -u):$(id -g) /root/.kube/config**

# 확인
**kubectl cluster-info**

**kubectl get node -owide**
*NAME      STATUS     ROLES           AGE     VERSION    INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                        KERNEL-VERSION                  CONTAINER-RUNTIME
k8s-ctr   NotReady   control-plane   6m45s   **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***

**kubectl get nodes -o json | jq ".items[] | {name:.metadata.name} + .status.capacity"**
...

**kubectl get pod -n kube-system -owide**
*NAME                              READY   STATUS    RESTARTS   AGE     IP               NODE      NOMINATED NODE   READINESS GATES
**coredns**-668d6bf9bc-bmdjw          0/1     Pending   0          6m55s   <none>           <none>    <none>           <none>
coredns-668d6bf9bc-cbtn9          0/1     Pending   0          6m55s   <none>           <none>    <none>           <none>
**etcd**-k8s-ctr                      1/1     Running   0          7m2s    192.168.10.100   k8s-ctr   <none>           <none>
kube-**apiserver**-k8s-ctr            1/1     Running   0          7m4s    192.168.10.100   k8s-ctr   <none>           <none>
kube-**controller-manager**-k8s-ctr   1/1     Running   0          7m2s    192.168.10.100   k8s-ctr   <none>           <none>
kube-**proxy**-zfr9d                  1/1     Running   0          6m55s   192.168.10.100   k8s-ctr   <none>           <none>
kube-**scheduler**-k8s-ctr            1/1     Running   0          7m3s    192.168.10.100   k8s-ctr   <none>           <none>*

# 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   3h26m*

**# cluster-info ConfigMap 공개 : cluster-info는 '신원 확인 전, 최소한의 신뢰 부트스트랩 데이터'**
kubectl **-n kube-public** get **configmap cluster-info**
kubectl **-n kube-public** get **configmap cluster-info -o yaml**
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
****
curl -s -k https://192.168.10.100:6443/**api/v1/namespaces/kube-public/configmaps/cluster-info** | jq
curl -s -k https://192.168.10.100:6443/api/v1/namespaces/default/pods # X
kubectl -n kube-public get role
kubectl -n kube-public get rolebinding

# kubeadm init 시 생성되는 객체
- Namespace: kube-public
- ConfigMap: cluster-info
- Role + RoleBinding 
>> 대상: system:unauthenticated (인증 안 된 사용자)
>> 권한: get on configmaps/cluster-info
👉 아직 클러스터 인증서가 없는 노드(worker) 가 (kubeadm join 전) API Server에 처음 접속해서 최소 정보(엔드포인트 + CA)를 얻기 위해 필요

여기까지 확인했을 때 coredns가 Pending이고 노드가 NotReady로 보이는 이유는, 아직 CNI를 설치하지 않아 네트워크 준비가 끝나지 않았기 때문입니다. 다음 절에서 Flannel을 설치해 이 상태를 정상으로 바꾸겠습니다.

[k8s-ctr] Flannel CNI 설치 v0.27.3

이번 구성에서는 kubeadm-init.yaml에서 podSubnet을 10.244.0.0/16으로 설정했기 때문에, Flannel도 동일한 CIDR을 사용하도록 맞춰 설치합니다. 또한 실습 환경에서 트래픽이 흐르는 인터페이스가 enp0s9이므로 --iface=enp0s9를 지정합니다.

# 현재 k8s 클러스터에 파드 전체 CIDR 확인
**kc describe pod -n kube-system kube-controller-manager-k8s-ctr**
*...
    Command:
      kube-controller-manager
      --allocate-node-cidrs=true
      **--cluster-cidr=10.244.0.0/16**
      --service-cluster-ip-range=10.96.0.0/16
      ...*

# 노드별 파드 CIDR 확인 
**kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'**
*k8s-ctr 10.244.0.0/24*

# 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**
helm repo update

**kubectl create namespace kube-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

**helm install flannel flannel/flannel --namespace kube-flannel --version 0.27.3 -f flannel.yaml**

# 확인
helm list -A
helm get values -n kube-flannel flannel
**kubectl get ds,pod,cm -n kube-flannel -owide**
kc describe cm -n kube-flannel kube-flannel-cfg
**kc describe ds -n kube-flannel**
*...
    Command:
      /opt/bin/flanneld
      --ip-masq
      --kube-subnet-mgr
      **--iface=enp0s9***

# flannel cni 바이너리 설치 확인
**ls -l /opt/cni/bin/**
*-rwxr-xr-x. 1 root root 2974540 Jan 17 01:35 flannel*
...

# cni 설정 정보 확인
tree /etc/cni/net.d/
cat /etc/cni/net.d/10-flannel.conflist | jq

# cni 설치 후 아래 상태(conditions) 정상 확인
**crictl info | jq**
  *"status": {
    "conditions": [
      {
        "message": "",
        "reason": "",
        "status": true,
        "type": "RuntimeReady"
      },
      {
        "message": "",
        "reason": "",
        "status": true,
        "type": "NetworkReady"
      },
      {
        "message": "",
        "reason": "",
        "status": true,
        "type": "ContainerdHasNoDeprecationWarnings"
      }
    ]
  }
}*

# coredns 파드 정상 기동 확인
**kubectl get pod -n kube-system -owide**
*NAME                              READY   STATUS    RESTARTS   AGE    IP               NODE      NOMINATED NODE   READINESS GATES
coredns-668d6bf9bc-bmdjw          1/1     Running   0          137m   **10.244.0.3**       k8s-ctr   <none>           <none>
coredns-668d6bf9bc-cbtn9          1/1     Running   0          137m   **10.244.0.2**       k8s-ctr   <none>           <none>
...*

# network 정보 확인
ip -c route | grep 10.244.
ip addr # cni0, flannel.1, vethY.. 확인
bridge link
lsns -t net

# iptables 규칙 확인
iptables -t nat -S
iptables -t filter -S
iptables-save

이제 NetworkReady가 true로 바뀌고, coredns도 Running으로 올라오면서 클러스터가 기본 동작 가능한 형태가 됩니다.


[k8s-ctr] k8s 관련 작업 편의성 설정

이후부터는 kubectl 명령을 자주 다루게 되므로, 자동 완성과 별칭, 그리고 몇 가지 도구를 설치해 작업 편의성을 올립니다. 이 부분은 “필수”라기보다는 실습 흐름을 부드럽게 하기 위한 정리입니다.

#
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] 노드/환경 변화 확인

마지막으로, kubeadm init 이후 노드에 어떤 라벨태인트가 붙는지 확인하고, init 전/후로 저장해둔 출력 파일을 비교합니다. 이 과정은 “어떤 설정이 언제/어디서 바뀌었는지”를 직접 눈으로 확인하는 용도입니다.

# # kubelet 활성화 확인 : 실제 기동은 kubeadm init 후에 시작됨
**systemctl is-active kubelet**
systemctl status kubelet --no-pager

# 노드 정보 확인 : 일반 워크로드가 Control Plane에 스케줄 X
**kc describe node**
***Labels**:             ...
                        node-role.kubernetes.io/control-plane=
**Taints**:             node-role.kubernetes.io/control-plane:NoSchedule*

# 기본 환경 정보 출력 저장
cat /etc/sysconfig/kubelet
tree /etc/kubernetes  | tee -a etc_kubernetes-2.txt
tree /var/lib/kubelet | tee -a var_lib_kubelet-2.txt
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

# 파일 출력 비교 : 빠져나오기 ':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 df-1.txt df-2.txt
vi -d findmnt-1.txt findmnt-2.txt

# kubelet 에 --protect-kernel-defaults=false 적용되어 관련 코드에 sysctl 커널 파라미터 적용 : 아래 링크 확왼
## 위 설정 시, 커널 튜닝 가능 항목 중 하나라도 kubelet의 기본값과 다르면 오류가 발생합니다
**vi -d sysctl-1.txt sysctl-2.txt**
*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-w1/w2] 설정

앞 장에서 k8s-ctr에 컨트롤 플레인을 구성하고 Flannel CNI까지 설치했으니, 이제 워커 노드(k8s-w1, k8s-w2)를 클러스터에 추가하겠습니다. 워커 노드에서는 컨트롤 플레인과 동일하게 기본 사전 설정을 적용한 뒤, containerd와 kubeadm/kubelet/kubectl을 설치합니다. 마지막으로 kubeadm join을 수행하면 워커 노드가 클러스터에 등록되고, 노드 상태가 Ready로 전환되는 흐름입니다.

[k8s-w1/w2] 사전 설정

워커 노드에서도 1장에서 진행했던 공통 사전 설정을 동일하게 적용합니다. 이후 과정에서 시간 불일치, SELinux/방화벽/스왑, 커널 네트워크 설정 때문에 문제가 생기는 경우가 많기 때문에 이 단계는 먼저 정리해두는 것이 안전합니다.

# 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

[k8s-w1/w2] CRI 설치 : containerd(runc) v2.1.5

워커 노드도 파드를 실행해야 하므로 containerd 설치와 설정이 필요합니다. 앞 장과 동일하게 기본 설정을 생성하고 SystemdCgroup을 활성화한 뒤 서비스를 기동합니다.

# 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 start 와 enabled
**systemctl enable --now containerd**

[k8s-w1/w2] kubeadm, kubelet 및 kubectl 설치 v1.32.11

워커 노드에도 kubeadm, kubelet, kubectl을 설치합니다. 설치 후에는 kubelet을 활성화하고, crictl이 containerd 소켓을 바라보도록 /etc/crictl.yaml을 작성합니다.

# repo 추가
**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**

# 설치
**dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes**

# kubelet 활성화 (실제 기동은 kubeadm init 후에 시작됨)
**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

[k8s-w1/w2] kubeadm 으로 k8s join

워커 노드가 클러스터에 참여하는 과정은 크게 두 흐름으로 이해하면 됩니다. 먼저 컨트롤 플레인의 정보를 받아와 “접속 대상이 맞는지”를 확인하는 단계가 있고, 그 다음으로는 워커 노드가 스스로 인증서를 발급받아 정식으로 등록되는 단계가 이어집니다.

  • Kubernetes API 서버를 신뢰하도록 하는 디스커버리 단계 → Kubernetes API 서버가 노드를 신뢰하도록 하는 TLS 부트스트랩 단계

이 흐름을 확인하면서 진행할 수 있도록, kubeadm join 전/후로 기본 출력도 저장해두고 JoinConfiguration 파일을 작성해 적용합니다. 또한 워커 노드는 노드마다 IP가 다르므로, enp0s9에서 IP를 읽어 node-ip로 반영합니다.

# 기본 환경 정보 출력 저장
crictl images
crictl ps
cat /etc/sysconfig/kubelet
tree /etc/kubernetes  | tee -a etc_kubernetes-1.txt
tree /var/lib/kubelet | tee -a var_lib_kubelet-1.txt
tree /run/containerd/ -L 3 | tee -a run_containerd-1.txt
pstree -alnp | tee -a pstree-1.txt
systemd-cgls --no-pager | tee -a systemd-cgls-1.txt
lsns | tee -a lsns-1.txt
ip addr | tee -a ip_addr-1.txt 
ss -tnlp | tee -a ss-1.txt
df -hT | tee -a df-1.txt
findmnt | tee -a findmnt-1.txt
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.164948ms
[**kubelet-start**] Waiting for the kubelet to perform the TLS Bootstrap*

# crictl 확인
crictl images
crictl ps

# cluster-info cm 호출 가능 확인
curl -s -k https://192.168.10.100:6443/**api/v1/namespaces/kube-public/configmaps/cluster-info** | jq

[k8s-ctr] 워커 노드 등록 및 네트워크 확인

워커 노드에서 kubeadm join이 끝나면, 컨트롤 플레인에서 노드가 정상 등록되었는지 확인합니다. 또한 각 노드에 podCIDR이 할당되고, Flannel(VXLAN)을 통해 다른 노드의 podCIDR로 가는 라우팅이 자동으로 추가되는 것도 함께 확인할 수 있습니다.

# 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   6h58m   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>          2m29s   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>          2m29s   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*

# 노드별 파드 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*

# 다른 노드의 파드 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*

# 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.19 ms*

# 워커 노드에 Taints 정보 확인
**kc describe node k8s-w1**
*Taints:             <none>*

**# k8s-w1 노드에 배치된 파드 확인
****kubectl get pod -A -owide | grep k8s-w2**
****kubectl get pod -A -owide | grep k8s-w1**
*kube-flannel   kube-flannel-ds-nhfhh             1/1     Running   0          4m3s    192.168.10.101   k8s-w1    <none>           <none>
kube-system    kube-proxy-7zczb                  1/1     Running   0          4m3s    192.168.10.101   k8s-w1    <none>           <none>*

워커 노드가 등록되면, 각 노드에 kube-proxy와 kube-flannel 데몬셋 파드가 자동으로 배치되는 것도 같이 확인할 수 있습니다.

[k8s-w1/w2] 노드 정보 확인 및 출력 비교

워커 노드에서도 join 전/후로 /etc/kubernetes, /var/lib/kubelet 등이 어떻게 변하는지 확인해두면 좋습니다. 특히 kubelet 설정 파일과 인증서 파일이 생성되는 흐름이 보이기 때문에, 이후 인증서/부트스트랩 동작을 이해하는데 도움이 됩니다.

# # kubelet 활성화 확인
systemctl status kubelet --no-pager

# 기본 환경 정보 출력 저장
cat /etc/sysconfig/kubelet
**tree /etc/kubernetes  | tee -a etc_kubernetes-2.txt**
*/etc/kubernetes
├── kubelet.conf
├── manifests
└── pki
    └── ca.crt*
**cat /etc/kubernetes/kubelet.conf**

tree /var/lib/kubelet | tee -a var_lib_kubelet-2.txt
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

# 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 df-1.txt df-2.txt
vi -d findmnt-1.txt findmnt-2.txt

이제 워커 노드까지 모두 Ready로 올라왔고, 네트워크까지 연결된 클러스터 기본 구성이 완료되었습니다. 다음 장에서는 모니터링 도구 설치(프로메테우스-스택, 인증서 익스포터, 그라파나 대시보드 확인)로 넘어가겠습니다.


모니터링 툴 설치

클러스터 구성이 끝났다면, 이제 “현재 상태를 계속 관찰할 수 있는 기반”을 마련할 차례입니다. 이번 장에서는 노드/파드 리소스 사용량을 확인하기 위한 metrics-server를 먼저 설치하고, 이후 kube-prometheus-stack으로 프로메테우스와 그라파나를 함께 올립니다. 마지막으로 클러스터 운영에서 자주 놓치기 쉬운 인증서 만료를 메트릭으로 노출하기 위해 x509 certificate exporter를 배포하고, 그라파나 대시보드에서 확인하는 흐름으로 진행합니다.

metrics-server 설치

kubectl top은 기본적으로 동작하지 않고, metrics-server가 수집한 메트릭이 있어야 노드/파드의 CPU·메모리 사용량을 확인할 수 있습니다. 설치 후에는 kubectl top 명령으로 바로 확인해봅니다.

# 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
kubectl top pod -A --sort-by='cpu'
kubectl top pod -A --sort-by='memory'

kube-prometheus-stack 설치

모니터링 스택을 설치합니다. 이번 구성에서는 프로메테우스와 그라파나를 NodePort로 열어두고, 수집 주기를 20s로 조정했습니다. 또한 그라파나는 타임존을 Asia/Seoul로 맞추고, 초기 접속을 위한 비밀번호도 지정합니다.

# repo 추가
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
kubectl get pod,svc,ingress,pvc -n monitoring
**kubectl get prometheus,servicemonitors**,**alertmanagers -n monitoring**
~~~~**kubectl get crd | grep monitoring**

# 각각 웹 접속 실행 : 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 클릭

kube-controller-manager, etcd, kube-scheduler 메트릭 수집 설정

기본 상태에서는 컨트롤 플레인 구성요소의 메트릭이 로컬로만 바인딩되어 수집이 제한될 수 있습니다. 여기서는 kube-controller-manager, kube-scheduler의 --bind-address를 0.0.0.0으로 변경하고, etcd는 --listen-metrics-urls에 노드 IP를 추가해 접근 가능한 형태로 조정합니다.

# 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***

k8s 인증서 위치 확인

인증서 익스포터를 설치하기 전에, kubeadm이 생성한 인증서가 어디에 있고 어떤 파일들이 관리 대상인지 먼저 정리해둡니다. kubeadm certs check-expiration으로 만료 시점을 확인하고, /etc/kubernetes 및 /var/lib/kubelet/pki 경로를 함께 확인합니다.

# 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-11-05-06-09.pem
|-- kubelet-client-current.pem -> /var/lib/kubelet/pki/kubelet-client-2026-01-11-05-06-09.pem
|-- kubelet.crt
`-- kubelet.key

x509 certificate exporter 설치

인증서 만료는 “문제가 터지기 전까지는 잘 보이지 않는” 항목이라, 메트릭으로 만들어두면 운영 시점에서 훨씬 안전해집니다. 이번 구성에서는 컨트롤 플레인과 워커 노드에서 각각 다른 범위의 인증서 파일을 수집하도록 데몬셋을 나눠 배포합니다.

먼저 워커 노드에 라벨을 추가해 nodes 데몬셋이 해당 노드에만 배치되도록 합니다.

# w1/w2 에 node label 설정
kubectl label node k8s-w1 worker="true" --overwrite
kubectl label node k8s-w2 worker="true" --overwrite
kubectl get nodes -l worker=true

이후 values 파일을 작성하고 x509-certificate-exporter를 설치합니다.

# 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=   19m
x509-certificate-exporter-nodes   1         1         1       1            1           kubernetes.io/hostname=myk8s-worker      19m

# 파드 정보 확인 : 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-fzwrl      1/1     Running   0          20m   10.244.0.5    myk8s-control-plane   <none>           <none>
x509-certificate-exporter-nodes-fckwl   1/1     Running   0          20m   10.244.1.12   myk8s-worker          <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.6.160   <none>        9793/TCP   54s

NAME                                  ENDPOINTS                          AGE
endpoints/x509-certificate-exporter   **10.244.0.5**:9793,**10.244.1.11**:9793   54s*

# 컨트롤플레인 노드에 배포된 'x509 익스포터' 파드에 메트릭 호출 확인
**curl -s 10.244.0.5:9793/metrics | grep '^x509' | head -n 3**
x509_cert_expired{filename="apiserver-etcd-client.crt",filepath="/etc/kubernetes/pki/apiserver-etcd-client.crt",issuer_CN="etcd-ca",serial_number="3639503804793909323",subject_CN="kube-apiserver-etcd-client"} 0
x509_cert_expired{filename="apiserver.crt",filepath="/etc/kubernetes/pki/apiserver.crt",issuer_CN="kubernetes",serial_number="2465930159994320660",subject_CN="kube-apiserver"} 0
x509_cert_expired{filename="ca.crt",filepath="/etc/kubernetes/pki/ca.crt",issuer_CN="kubernetes",serial_number="7083266840940024496",subject_CN="kubernetes"} 0

# 워커 노드에 배포된 'x509 익스포터' 파드에 메트릭 호출 확인
**curl -s 10.244.1.11:9793/metrics | grep '^x509' | head -n 3**
x509_cert_expired{filename="ca.crt",filepath="/etc/kubernetes/pki/ca.crt",issuer_CN="kubernetes",serial_number="7083266840940024496",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="137383486421320059000355181040497730044",subject_CN="system:node:myk8s-worker",subject_O="system:nodes"} 0
x509_cert_not_after{filename="ca.crt",filepath="/etc/kubernetes/pki/ca.crt",issuer_CN="kubernetes",serial_number="7083266840940024496",subject_CN="kubernetes"} 2.083467966e+09

# 프로메테우스 CR 정보 확인 : 서비스모니터와 룰 셀렉터 정보 확인
**kubectl get prometheuses.monitoring.coreos.com -n monitoring -o yaml**
*...
    **serviceMonitor**Selector:
      **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
...*

프로메테우스 / 그라파나에서 확인

설치가 끝나면, 프로메테우스에서는 ServiceMonitor에 의해 수집된 타겟이 등록되고, x509 관련 메트릭도 조회할 수 있습니다. 또한 룰이 적용되어 인증서 만료가 가까워지면 설정된 기준(14일, 28일)에 따라 알람이 생성됩니다.

그라파나에서는 x509 대시보드가 자동으로 추가되며, Overview/Expiration 화면에서 남은 기간을 한눈에 확인할 수 있습니다.


샘플 애플리케이션 배포

이제 클러스터가 실제 워크로드를 정상적으로 배치하고 서비스로 노출할 수 있는지 확인해볼 차례입니다. 이번 장에서는 간단한 웹 애플리케이션을 Deployment로 배포하고, Service(ClusterIP)로 묶은 뒤 반복 호출을 통해 트래픽이 파드로 분산되는지까지 확인합니다.

샘플 애플리케이션 배포

아래 매니페스트는 webpod라는 이름으로 파드를 2개(replicas: 2) 생성하고, 동일한 라벨(app: webpod)을 가진 파드들을 Service가 선택하도록 구성합니다. 또한 affinity 설정을 포함하고 있어, 스케줄링 시 파드 배치 정책을 함께 확인할 수 있습니다.

# 샘플 애플리케이션 배포
**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**:
          **required**DuringSchedulingIgnoredDuringExecution:
          - 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**

애플리케이션 확인 및 반복 호출

배포가 완료되면 Deployment, Service, Endpoint를 함께 확인합니다. 이후 Service의 clusterIP를 변수로 받아 curl로 호출해보고, 신규 터미널에서 반복 호출을 수행해 응답의 Hostname 값이 바뀌는지 확인합니다.

# 배포 확인
kubectl get deploy,svc,ep webpod -owide

# webpod service clusterip 변수 지정
SVCIP=$(kubectl get svc webpod -o jsonpath='{.spec.clusterIP}')
echo $SVCIP

# 통신 확인
curl -s $SVCIP
curl -s $SVCIP | grep Hostname

# 반복 호출(신규 터미널)
while true; do curl -s $SVCIP | grep Hostname; sleep 1; done

kubeadm 인증서 갱신

kubeadm으로 구성한 클러스터는 컨트롤 플레인 통신에 필요한 인증서를 /etc/kubernetes/pki 아래에 생성합니다. 이 인증서들은 유효 기간이 정해져 있기 때문에, 운영 중에는 “언제 생성됐고 언제 만료되는지”를 먼저 확인한 뒤, 필요 시 갱신 절차를 수행할 수 있어야 합니다. 이번 장에서는 현재 인증서 만료 시점을 확인하고, kubeadm certs renew로 인증서를 갱신한 뒤, 컨트롤 플레인 정적 파드를 재기동하여 새 인증서가 실제로 적용되는 흐름까지 진행합니다.

현재 인증서 만료 시점 확인

먼저 kubeadm-config에 기록된 인증서 유효 기간과 관리 디렉터리를 확인합니다. 이후 kubeadm certs check-expiration으로 실제 만료 시점을 조회하고, 파일 타임스탬프 및 인증서 본문을 통해 생성/만료 시간을 함께 확인합니다.

# Check certificates expiration for a Kubernetes cluster
**kc describe cm -n kube-system kubeadm-config | grep -i cert**
*caCertificateValidityPeriod: 87600h0m0s
certificateValidityPeriod: 8760h0m0s
certificatesDir: **/etc/kubernetes/pki***

# 현재 인증서가 UTC 기준 1월 17일 00:34 분 생성되어서, 유효기간 365일(1년) 이후 만료일은 '27년 1월 17일 00:33분.
**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 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/pki/
ls -l /etc/kubernetes/pki/etcd/
**ls -l /etc/kubernetes/pki**
*-rw-r--r--. 1 root root 1281 **Jan 17 09:34** apiserver.crt
-rw-r--r--. 1 root root 1123 Jan 17 09:34 apiserver-etcd-client.crt
-rw-------. 1 root root 1679 Jan 17 09:34 apiserver-etcd-client.key
-rw-------. 1 root root 1675 Jan 17 09:34 apiserver.key
...*

# apiserver 인증서(예시) : 
**cat /etc/kubernetes/pki/apiserver.crt | openssl x509 -text -noout**
*Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 9019049356910942135 (0x7d2a199aea6457b7)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=kubernetes
        **Validity**
            Not Before: Jan 17 00:28:48 2026 GMT
            **Not After : Jan 17 00:33:48 2027 GMT**
        Subject: CN=kube-apiserver*
...

수동 인증서 갱신 개요

kubeadm certs renew는 CA는 유지한 채, 컨트롤 플레인 인증서를 다시 서명하여 새 인증서로 교체합니다. 이 과정에서 일부 kubeconfig 파일도 함께 재생성되며, 적용을 위해 정적 파드 재기동이 필요합니다. 특히 kubelet의 클라이언트 인증서는 kubeadm이 순환 가능한 인증서를 사용하도록 구성하기 때문에, 목록에 kubelet.conf가 포함되지 않는 흐름도 함께 확인할 수 있습니다.

# 갱신 시 : 기존 cert 삭제 -> CA로 재서명된 새 cert 생성
kubeadm certs renew apiserver
kubeadm certs renew etcd-server
kubeadm certs renew all

수동 인증서 갱신 실행

갱신 전에 영향 여부를 확인하기 위해, 앞 장에서 배포한 샘플 애플리케이션을 반복 호출로 켜 둔 상태에서 진행합니다. 이후 /etc/kubernetes/pki와 *.conf를 먼저 백업하고, kubeadm certs renew all로 인증서를 일괄 갱신합니다. 갱신 이후에는 만료 시점이 변경되었는지, 그리고 실제 파일이 새로 생성되었는지 확인합니다.

# 샘플 애플리케이션 반복 호출(신규 터미널)
SVCIP=$(kubectl get svc webpod -o jsonpath='{.spec.clusterIP}')
while true; do curl -s $SVCIP | grep Hostname; sleep 1; done

# 사전 백업 : HA Controlplane 모두
cp -r /etc/kubernetes/**pki** /etc/kubernetes/**pki.backup**.$(date +%F)
ls -l /etc/kubernetes/pki.backup.$(date +%F)

mkdir /etc/kubernetes/backup-conf.$(date +%F)
cp /etc/kubernetes/***.conf** /etc/kubernetes/**backup-conf**.$(date +%F)
ls -l /etc/kubernetes/backup-conf.$(date +%F)

# 인증서 만료 상태 확인
**kubeadm certs check-expiration**

# 인증서 전체 갱신 : 기존 cert 삭제 -> CA로 재서명된 새 cert 생성
**kubeadm certs renew all**
*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 17, 2027 12:25 UTC**   364d            ca                      no      
**apiserver**                  Jan 17, 2027 12:25 UTC   364d            ca                      no      
apiserver-etcd-client      Jan 17, 2027 12:25 UTC   364d            etcd-ca                 no      
apiserver-kubelet-client   Jan 17, 2027 12:25 UTC   364d            ca                      no      
controller-manager.conf    Jan 17, 2027 12:25 UTC   364d            ca                      no      
etcd-healthcheck-client    Jan 17, 2027 12:25 UTC   364d            etcd-ca                 no      
etcd-peer                  Jan 17, 2027 12:25 UTC   364d            etcd-ca                 no      
etcd-server                Jan 17, 2027 12:25 UTC   364d            etcd-ca                 no      
front-proxy-client         Jan 17, 2027 12:25 UTC   364d            front-proxy-ca          no      
scheduler.conf             Jan 17, 2027 12:25 UTC   364d            ca                      no      
super-admin.conf           Jan 17, 2027 12:25 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* 

# ca 인증서는 그대로, 나머지 인증서는 신규 생성
**ls -lt /etc/kubernetes/pki/**
*-rw-r--r--. 1 root root 1119 Jan 17 21:25 front-proxy-client.crt
-rw-------. 1 root root 1675 Jan 17 21:25 front-proxy-client.key
-rw-r--r--. 1 root root 1176 Jan 17 21:25 apiserver-kubelet-client.crt
-rw-------. 1 root root 1675 Jan 17 21:25 apiserver-kubelet-client.key
-rw-r--r--. 1 root root 1123 Jan 17 21:25 apiserver-etcd-client.crt
-rw-------. 1 root root 1675 Jan 17 21:25 apiserver-etcd-client.key
-rw-r--r--. 1 root root 1281 Jan 17 21:25 apiserver.crt
-rw-------. 1 root root 1675 Jan 17 21:25 apiserver.key
-rw-------. 1 root root 1675 Jan 17 09:34 sa.key
-rw-------. 1 root root  451 Jan 17 09:34 sa.pub
drwxr-xr-x. 2 root root  162 Jan 17 09:34 etcd
-rw-r--r--. 1 root root 1123 Jan 17 09:34 front-proxy-ca.crt
-rw-------. 1 root root 1675 Jan 17 09:34 front-proxy-ca.key
-rw-r--r--. 1 root root 1107 Jan 17 09:34 ca.crt
-rw-------. 1 root root 1679 Jan 17 09:34 ca.key*

**ls -lt /etc/kubernetes/pki/etcd**
*-rw-r--r--. 1 root root 1196 Jan 17 21:25 server.crt
-rw-------. 1 root root 1675 Jan 17 21:25 server.key
-rw-r--r--. 1 root root 1196 Jan 17 21:25 peer.crt
-rw-------. 1 root root 1679 Jan 17 21:25 peer.key
-rw-r--r--. 1 root root 1123 Jan 17 21:25 healthcheck-client.crt
-rw-------. 1 root root 1679 Jan 17 21:25 healthcheck-client.key
-rw-r--r--. 1 root root 1094 Jan 17 09:34 ca.crt
-rw-------. 1 root root 1675 Jan 17 09:34 ca.key*

# apiserver 인증서
**cat /etc/kubernetes/pki/apiserver.crt | openssl x509 -text -noout**
        *Issuer: CN=kubernetes
        Validity
            Not Before: Jan 17 12:20:10 2026 GMT
            Not After : Jan 17 12:25:10 2027 GMT
        Subject: CN=kube-apiserver*

# control component 의 kubeconfig 신규 생성 확인
**ls -lt /etc/kubernetes/*.conf**
*-rw-------. 1 root root 5682 Jan 17 21:25 /etc/kubernetes/super-admin.conf
-rw-------. 1 root root 5626 Jan 17 21:25 /etc/kubernetes/scheduler.conf
-rw-------. 1 root root 5682 Jan 17 21:25 /etc/kubernetes/controller-manager.conf
-rw-------. 1 root root 5654 Jan 17 21:25 /etc/kubernetes/**admin.conf**
-rw-------. 1 root root 1974 Jan 17 09:34 /etc/kubernetes/kubelet.conf*

# 특히 admin.conf 변경 확인
ls -l /etc/kubernetes/admin.conf
ls -l /etc/kubernetes/backup-conf.$(date +%F)/admin.conf 
**vi -d /etc/kubernetes/backup-conf.$(date +%F)/admin.conf /etc/kubernetes/admin.conf**

컨트롤 플레인 정적 파드 재기동 및 kubeconfig 재적용

인증서는 갱신되었더라도, 컨트롤 플레인 구성요소가 새 인증서를 읽어야 실제로 적용됩니다. kubeadm 환경에서는 컨트롤 플레인이 정적 파드로 동작하므로, 매니페스트 파일을 잠시 제거했다가 복구하는 방식으로 재기동을 유도합니다. 이후 admin.conf 변경 내용을 반영해 ~/.kube/config도 다시 적용합니다.

# 사전 백업 : static pod 매니페스트
cp -r /etc/kubernetes/manifests /etc/kubernetes/**manifests.backup.$(date +%F)**
ls -l /etc/kubernetes/**manifests.backup**.$(date +%F)

# static pod 모니터링(신규 터미널)
**watch -d crictl ps**

# static pod manifest 삭제
**rm -rf /etc/kubernetes/manifests/*.yaml**

# static pod manifest 삭제 확인 
**crictl ps**

# static pod manifest 복사 -> 파드 재기동  
cp /etc/kubernetes/**manifests.backup.$(date +%F)/*.yaml** /etc/kubernetes/manifests
tree /etc/kubernetes/manifests

# 파드 기동 확인 : CA가 바뀌지 않았기 때문에 예전 인증서도 신뢰됨 >> 다만, 예전(?) 인증서의 만료 기간을 놓칠 수 있으니, 같이 갱신 할 것!
**crictl ps**
**kubectl get pod -n kube-system -owide -v=6**

# 특히 admin.conf 변경 확인
ls -l /root/.kube/config
ls -l /etc/kubernetes/admin.conf
ls -l /etc/kubernetes/backup-conf.$(date +%F)/admin.conf

**vi -d /root/.kube/config /etc/kubernetes/admin.conf
vi -d /etc/kubernetes/backup-conf.$(date +%F)/admin.conf /etc/kubernetes/admin.conf**

# admin.conf kubeconfig 재적용
**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

갱신 과정 중에는 컨트롤 플레인 구성요소가 재기동되며, 그 시점에는 일부 메트릭 수집이 일시적으로 끊기는 현상도 관찰할 수 있습니다. 또한 kubeadm은 컨트롤 플레인 업그레이드 과정에서도 인증서를 함께 갱신하는 흐름을 포함하고 있으므로, 인증서 갱신 절차는 업그레이드 시나리오와 자연스럽게 연결됩니다.


가상 머신 삭제

이번 실습은 Vagrantfile 기반으로 동일한 환경을 빠르게 구성하고 반복 검증하는 흐름에 초점을 두었습니다. 따라서 모든 작업을 마친 뒤에는 가상 머신을 정리해두는 편이 다음 실습을 진행할 때도 깔끔합니다.

아래 명령은 가상 머신을 강제로 삭제하고, Vagrant가 생성한 로컬 상태 디렉터리까지 함께 제거합니다.

# 모든 실습 완료 후 가상 머신 삭제
**vagrant destroy -f && rm -rf .vagrant**

마무리하며

kubeadm을 이용해 쿠버네티스 클러스터를 직접 구성하는 전체 흐름을 한 번에 따라가 보았습니다. 공통 사전 설정부터 시작해 containerd 기반 런타임을 준비하고, kubeadm, kubelet, kubectl을 설치한 뒤 컨트롤 플레인 초기화와 CNI 적용까지 진행했습니다. 이후 워커 노드를 클러스터에 추가하고, 모니터링 스택과 인증서 만료 관찰 환경을 함께 구성하면서 “클러스터가 운영 가능한 상태”까지 끌어올리는 과정을 확인했습니다.

kubeadm init과 kubeadm join 과정에서 어떤 파일과 리소스가 생성되는지, 그리고 정적 파드 기반 컨트롤 플레인이 어떻게 동작하는지를 출력 결과를 통해 추적해본 점이 핵심입니다. 또한 인증서 갱신에서는 kubeadm certs renew 이후 정적 파드 재기동이 필요한 이유와, admin.conf 갱신에 따라 kubeconfig 재적용이 필요하다는 흐름을 실습으로 연결해 보았습니다. 샘플 애플리케이션 배포와 반복 호출까지 포함했기 때문에, 단순 설치가 아니라 “클러스터가 실제로 트래픽을 처리하는지”까지 확인할 수 있었습니다.

검색 태그