28 min to read
Kubernetes Bootstrapping and Upgrade with kubeadm — 0
Cloudnet@ K8S Deploy — Week3
Kubeadm
Kubeadm은 Kubernetes 클러스터를 Bootstrap하고 업그레이드하기 위한 표준화된 도구로, 최소한의 실행 가능한 단계만 제공하여 클러스터 설치와 관리 과정을 단순화합니다.
Deploy
Kubeadm을 활용한 구축 시 가장 중요한 점은 관리 주체에 따른 컴포넌트 분리입니다. Kubeadm은 모든 인프라를 자동으로 설치하지 않으며, 특정 계층의 상태를 전제로 동작합니다.
즉 Kubeadm은 호스트 레벨에서 다음의 컴포넌트가 이미 설치 및 구성되어 있다고 가정합니다.
- Container Runtime Interface (CRI):
containerd와 같은 런타임 인터페이스가 사전에 설치되어 있어야 합니다. - Kubelet: Kubelet은 시스템 서비스(systemd 등)로 실행 중이어야 합니다.
Kubeadm은 Control Plane의 주요 컴포넌트를 Static Pod 형태로 구성합니다.
Deployed by Kubeadm | Generated with Nano Banana
이는 초기 부트스트랩 시점에 kube-apiserver, scheduler, controller-manager가 아직 Kubernetes API 서버에 등록되기 전이기 때문에 kubelet이 직접 manifest를 읽어서 실행하도록 Static Pod를 사용하게 됩니다.
이후 API Server가 정상적으로 시작되면 kubeadm은 CoreDNS(Deployment)와 kube-proxy(DaemonSet)는 일반적인 Kubernetes 리소스로 배포합니다.
Static Pod은 kubelet이 직접 관리하는 Pod으로 API 서버로 생성, 삭제되지 않고 노드 파일시스템의 특정 경로에 있는 manifest 파일을 읽어서 실행합니다.
Hands-on Labs
실습은 인프라 준비부터 모니터링 시스템 구축까지 총 7단계의 절차에 따라 진행됩니다.
- Prerequisites: 호스트 이름 설정, Swap 비활성화, 커널 파라미터 및 방화벽 설정
- CRI Installation: Containerd 설치 및 Systemd Cgroup 드라이버 설정
- K8s Binary Setup: Kubeadm, Kubelet, Kubectl 패키지 저장소 등록 및 설치
- Control Plane: 클러스터 구성 및 Flannel CNI 배포를 통한 네트워크 활성화
- Worker Nodes: Worker Node를 클러스터에 Join
- Monitoring: Helm을 이용한 Prometheus-Stack 배포 및 인증서 모니터링 연동
- Certificate Renewal: 인증서 갱신 테스트
Lab Environment with Vagrant
실습 환경은 Rocky Linux 10.0을 기반으로 구성되며, 주요 설치하려는 버전 정보는 아래와 같습니다.
| Component | Version |
|---|---|
| Operating System | Rocky Linux 10.0 |
| Container Runtime (containerd) | v2.1.5 |
| Low-level Runtime (runc) | v1.3.3 |
| Kubernetes Components (kubeadm, kubelet, kubectl) | v1.32.11 |
| Package Manager (Helm) | v3.18.6 |
| Network Plugin (Flannel) | v0.27.3 |
Vagrantfile은 아래와 같습니다.
# Base Image
BOX_IMAGE = "bento/rockylinux-10.0"
BOX_VERSION = "202510.26.0"
N = 2 # max number of Worker Nodes
Vagrant.configure("2") do |config|
# ControlPlane Node
config.vm.define "k8s-ctr" do |subconfig|
subconfig.vm.box = BOX_IMAGE
subconfig.vm.box_version = BOX_VERSION
subconfig.vm.provider "vmware_desktop" do |vmware|
vmware.vmx["ethernet0.virtualDev"] = "vmxnet3"
vmware.vmx["ethernet1.virtualDev"] = "vmxnet3"
vmware.vmx["memsize"] = "3072"
vmware.vmx["numvcpus"] = "4"
vmware.gui = false
vmware.whitelist_verified = true
end
subconfig.vm.hostname = "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 "vmware_desktop" do |vmware|
vmware.vmx["ethernet0.virtualDev"] = "vmxnet3"
vmware.vmx["ethernet1.virtualDev"] = "vmxnet3"
vmware.vmx["memsize"] = "2048"
vmware.vmx["numvcpus"] = "2"
vmware.gui = false
vmware.whitelist_verified = true
end
subconfig.vm.hostname = "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
Prerequisites
Kubernetes v1.32.11은 네트워크 기능과 자원 격리를 위해 최신 커널 사양을 요구합니다. kube-proxy의 nftables 모드와 같은 기능을 정상적으로 수행하기 위해서는 최소 5.13 이상의 커널 버전이 필요합니다.
For Kubernetes 1.35, the
nftablesmode of kube-proxy requires version 1.0.1 or later of the nft command-line tool, as well as kernel 5.13 or later.
실습 환경인 Rocky Linux 10.0은 Kernel 6.12를 탑재하고 있어서 위의 요구사항을 충족합니다.
# Host Info, Kernel
hostnamectl # Kernel: Linux 6.12.0-55.39.1.el10_0.aarch64
uname -r
rpm -aq | grep release
Cgroup(Control Groups)은 프로세스 군에 대한 자원(CPU, Memory 등) 사용량을 제한하고 격리하는 커널 기능으로 Kubernetes에서 Pod에 리소스 제한을 설정하면, 내부적으로는 해당 컨테이너 프로세스가 특정 Cgroup에 배치되며, 커널은 이 경로에 정의된 설정에 따라 자원을 제어합니다.
Cgroup에는 두 가지 주요 버전이 있습니다. Cgroup v1은 오랫동안 사용되어 온 버전으로, CPU, 메모리, I/O 등 각 리소스 타입마다 별도의 디렉토리 계층 구조를 가지고 있었습니다. 예를 들어 CPU 제한은 /sys/fs/cgroup/cpu/에서 관리되고, 메모리 제한은 /sys/fs/cgroup/memory/에서 관리되는 식이었습니다. 이러한 구조는 복잡하고 일관성이 부족했으며, 때로는 서로 다른 컨트롤러 간에 예상치 못한 상호작용이 발생하기도 했습니다.
Cgroup v2는 이러한 문제들을 해결하기 위해 완전히 재설계되었습니다. 모든 리소스를 하나의 통합된 계층 구조에서 관리하며, 더 단순하고 일관된 API를 제공합니다. 메모리 추적이 더 정확해졌고, 리소스 격리가 더 강력해졌으며, 성능도 개선되었습니다. Kubernetes는 v1.25부터 Cgroup v2를 공식적으로 지원하기 시작했고, v1.31부터는 Cgroup v1을 유지보수 모드로 전환했습니다.
Cgroup | Generated with Nano Banana
어떤 버전을 사용하는지는 아래의 명령어를 통해서 확인할 수 있습니다.
# cgroup v2의 경우, cgroup2fs가 출력되며 cgroup v1은 tmpfs가 출력
stat -fc %T /sys/fs/cgroup
# systemd cgroup 계층 구조 확인
systemd-cgls --no-pager
Kubernetes의 kubelet과 컨테이너 런타임이 커널의 Cgroup 기능을 사용하는 방법에는 두 가지가 있습니다. 이를 cgroup driver라 합니다.
- cgroupfs driver: kubelet이 Cgroup 파일시스템을 직접 조작하는 방식
- systemd driver: kubelet이 Cgroup을 직접 조작하지 않고, systemd에게 요청하는 방식
root cgroup을 관리하는 systemd는 init system이며, 부팅 시 PID 1을 점유하게 됩니다. systemd는 각 단위(Unit)가 실행될 때마다 전용 Cgroup을 자동으로 할당합니다.
예를 들어 nginx.service가 시작되면 systemd는 해당 서비스를 위한 별도의 Cgroup 경로를 생성하고 그 안에서 프로세스를 실행합니다.
기본적으로 systemd는 시스템에 단 하나의 cgroup 관리자만 있을 것으로 기대합니다. 여기서 관리자란 Cgroup 계층 구조를 생성하고, 리소스 할당 정책을 결정하며, 생명주기를 관리하는 주체를 의미합니다.
하지만 여기서 문제가 발생합니다. systemd를 init 시스템으로 사용하고 kubelet에서 cgroupfs driver를 사용하면, 그 시스템은 두 개의 다른 cgroup 관리자를 갖게 됩니다.
즉, kubelet이 cgroupfs driver를 사용하면 systemd와 별도로 Cgroup 계층을 직접 관리하게 된다는 점입니다. 이는 두 개의 Cgroup 관리자가 존재하게 되고, 동일한 리소스에 대해 systemd와 kubelet이 서로 다른 방식으로 접근할 수 있습니다. 이로 인해 리소스 제한이 적용되지 않거나, Pod와 시스템 서비스 간 충돌이 발생할 수 있습니다.
따라서 Cgroup v2를 사용할 경우에는 cgroupfs를 직접 조작하는 대신 systemd driver를 사용하는 것이 권장됩니다. 이는 systemd가 이미 cgroup 관리자로 동작하고 있기 때문에, systemd를 통해 간접적으로 cgroup을 제어하는 것이 더 안정적이고 효율적이기 때문입니다.
v1.22 이후, kubeadm은 사용자가 별도 설정하지 않으면 자동으로 systemd로 기본 설정하며, v1.32에서는 kubelet이 컨테이너 런타임으로부터 적절한 드라이버를 자동 감지할 수 있습니다.
Kubernetes는 구성 요소 간 TLS 인증서를 사용하여 통신합니다. 노드 간 시간이 어긋나면 인증서가 유효하지 않거나 이미 만료된 것으로 판단되어 클러스터 형성이 불가능해집니다. timedatectl 서비스를 통해 한국 표준시(KST)로 설정하고, 상위 계층 NTP 서버와 동기화합니다.
# RTC(Real Time Clock)를 UTC 기준으로 설정
timedatectl set-local-rtc 0
# 타임존을 한국(KST)으로 변경
timedatectl set-timezone Asia/Seoul
# NTP(Network Time Protocol) 서비스 활성화
timedatectl set-ntp true
Rocky Linux 10은 기존의 ntpd를 대체하여 chrony를 기본 NTP 서비스로 채택하고 있습니다. chrony source -v 로 어떤 NTP서버들을 알고, 그 중 어떤 서버를 기준으로 시간을 맞추는지 확인할 수 있습니다
chrony sources -v
Stratum 2는 원자 시계와 직접 연결된 최상위 서버(Stratum 1)로부터 시간을 전달받는 매우 신뢰도 높은 서버임을 의미합니다. Kubernetes 클러스터는 통상적으로 Stratum 2 또는 3 수준의 서버와 동기화하는 것이 권장됩니다.- 출력 결과에서
377이라는 숫자는 8진수로 표현된 비트맵 값입니다. 이는 최근 8번의 시간 동기화 시도가 모두 성공했음을 의미하는 최대값(11111111)을 의미합니다. ^는 NTP 서버들을 의미하며, 두 번째 문자 상태와 결합되어 현재 사용 중인 서버^*를 확인할 수 있습니다.
Kubernetes 구성 요소들은 호스트의 파일 시스템에 접근하거나 동적인 네트워크 규칙을 생성합니다. 따라서 충돌하지 않도록 운영체제의 보안 정책을 조정해야 합니다.
# SELinux 모드를 확인하고 일시적으로 Permissive로 전환
getenforce # Current mode: enforcing
setenforce 0
# 재부팅 시에도 영구 적용되도록 설정 파일 수정
sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
Kubernetes는 kube-proxy와 CNI(Container Network Interface)를 통해 iptables 또는 nftables 규칙을 실시간으로 생성합니다. 호스트의 firewalld가 켜져 있으면 Kubernetes가 생성한 네트워크 규칙과 충돌할 수 있습니다.
# firewalld(방화벽) 끄기
systemctl disable --now firewalld
systemctl status firewalld
Kubelet은 노드의 물리 메모리를 직접 계산하여 Pod를 배치합니다. 만약 Swap이 켜져 있으면 메모리 부족 시 데이터를 디스크로 넘기게 되는데, 이는 성능 저하를 유발할 뿐만 아니라 Kubelet이 실제 가용 메모리를 정확히 파악하지 못하게 만들어 클러스터 전체의 스케줄링 신뢰도를 떨어뜨립니다.
따라서 Swap을 비활성화합니다.
# 즉시 Swap 비활성화
swapoff -a
# 재부팅 시에도 적용되도록 /etc/fstab에서 swap 관련 설정 삭제
sed -i '/swap/d' /etc/fstab
free -h
리눅스 커널은 기본적으로 호스트 운영체제 관점의 네트워크 트래픽 처리에 최적화되어 있습니다. 하지만 Kubernetes는 노드 내부에 가상 브릿지를 생성하고 Pod 간 통신을 위해 패킷을 포워딩하는 복잡한 네트워크 구조를 가집니다. 따라서 커널이 컨테이너의 오버레이 파일 시스템을 이해하고, 브릿지 트래픽을 가로채어 Kubernetes의 로드 밸런싱 규칙(IPTables)을 적용할 수 있도록 사전에 모듈 로드 및 파라미터 조정이 이루어져야 합니다.
컨테이너 런타임이 호스트 자원을 효율적으로 사용하기 위해 필요한 두 가지 핵심 커널 모듈을 활성화합니다.
# 커널 모듈 로드 및 부팅 시 자동 로드 설정
modprobe overlay
modprobe br_netfilter
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
- 컨테이너는 여러 개의 이미지 레이어를 하나의 파일 시스템으로 결합하여 사용하는 OverlayFS 구조를 가집니다. 이 모듈은 하위 레이어(LowerDir)와 상위 레이어(UpperDir)를 통합하여 컨테이너가 읽고 쓸 수 있는 가상 스토리지 계층을 생성하는 역할을 수행합니다.
- Linux Bridge(L2)를 통과하는 패킷이 네트워크 스택의 필터링 프레임워크인 Netfilter에 의해 처리될 수 있도록 허용합니다. Kubernetes는 이 모듈을 통해 가상 네트워크 브릿지에서 발생하는 트래픽을 감시하고 제어할 수 있는 기반을 마련합니다.
로드된 모듈이 실제 트래픽 흐름에 개입하도록 sysctl 파라미터를 조정합니다.
# 커널 파라미터 설정: 브릿지 트래픽의 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
- Linux Bridge를 통과하는 패킷이 L3 계층의 IPTables 규칙을 따르도록 강제합니다. Kubernetes 서비스(ClusterIP)의 부하 분산은 IPTables 규칙에 의존하므로, 이 설정이 비활성화되면 Pod 간의 서비스 통신이 정상적으로 이루어지지 않습니다.
- 노드가 수신한 패킷의 목적지가 자신이 아닐 경우, 이를 적절한 네트워크 인터페이스로 Routing하는 기능을 활성화합니다. 이는 노드 외부에서 유입된 트래픽이 노드 내부의 Pod로 전달되거나, Pod가 외부 네트워크와 통신하기 위해 필요한 Routing 전제 조건입니다.
Kubernetes 클러스터 구성 시 노드 간 신뢰할 수 있는 통신 경로를 확보하기 위해 로컬 도메인 해석 설정을 수행합니다.
# 기본 127.0.x.x 루프백 주소 중 모호한 설정 삭제
sed -i '/^127\.0\.\(1\|2\)\.1/d' /etc/hosts
# 클러스터 참여 노드들의 고정 IP 및 호스트네임 등록
cat << EOF >> /etc/hosts
192.168.10.100 k8s-ctr
192.168.10.101 k8s-w1
192.168.10.102 k8s-w2
EOF
# 통신 확인
ping -c 1 k8s-ctr
ping -c 1 k8s-w1
ping -c 1 k8s-w2
CRI Installation
Kubernetes는 컨테이너를 직접 실행하는 대신 Container Runtime Interface(CRI)라는 표준 규격을 통해 외부 런타임과 통신합니다.
containerd architecture | https://github.com/containerd/containerd
Kubernetes v1.32 버전과 호환되는 containerd v2.1.5를 설치하고, 운영체제의 systemd와 자원 관리 충돌을 방지하기 위한 기술적 설정을 수행합니다.
Kubernetes와 containerd 간의 호환성은 공식 문서에서 명확하게 정의되어 있습니다. Kubernetes 특정 버전과 호환되는 containerd 버전을 준수하지 않으면 예상치 못한 동작이나 기능 제한이 발생할 수 있습니다.
containerd를 설치하면 두 가지 버전 개념이 있습니다.
- containerd 소프트웨어 자체의 버전
/etc/containerd/config.toml파일 내의 version 필드
version 필드는 containerd가 설정 파일을 읽을 때, 사용할 문법 규칙과 구조를 정의합니다.
예를 들어 version1에서는 [plugins.cri.containerd] 형태로 플러그인을 설정하지만, version2는 [plugins."io.containerd.grpc.v1.cri".containerd] 형태로 더 명시적인 경로를 사용합니다.
그러나 하위 호환성을 위해 최신 containerd 버전에서도 이전 버전의 설정 파일 구조를 사용할 수 있도록 지원합니다. 여기서는 v2.1.5를 설치합니다.
패키지 관리자인 dnf를 사용하여 설치를 진행합니다. Docker Engine 전체를 설치할 필요는 없으며, 런타임 핵심 요소인 containerd.io 패키지만 설치합니다.
# Docker 공식 저장소 추가
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 설치 가능한 버전 확인 후 v2.1.5 명시적 설치
dnf list --showduplicates containerd.io
dnf install -y containerd.io-2.1.5-1.el10
현재 설치된 containerd 버전에 맞는 /etc/containerd/config.toml 를 생성합니다. 생성하게 되면 규격에 맞게 version 필드가 3으로 설정된 것을 확인할 수 있습니다.
containerd config default | tee /etc/containerd/config.toml
위에서 Cgroupv2를 사용하는 경우, systemd driver를 사용하는 것이 권장된다고 했기 때문에 자원 관리 주체를 systemd로 일원화 합니다.
# SystemdCgroup 옵션을 true로 변경
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
# 설정 적용 및 서비스 활성화
systemctl daemon-reload
systemctl enable --now containerd
설치 완료 후에는 구성 요소들이 정상적으로 준비되었는지 검증해야 합니다.
# 유닉스 도메인 소켓 상태 확인
ls -l /run/containerd/containerd.sock
ss -xl | grep containerd
# 활성화된 플러그인 목록 점검
ctr plugins ls
verification
/run/containerd/containerd.sock은 Kubernetes의 Kubelet이 containerd와 통신하는 기술적 접점입니다. 소켓 파일이 존재하고 수신(Listening) 상태임을 확인할 수 있습니다.ctr plugins ls명령을 통해 overlayfs snapshotter가ok상태인지 확인합니다. 이는 컨테이너의 레이어드 파일 시스템을 생성할 수 있는 준비가 되었음을 나타냅니다.
snapshotter는 containerd에서 컨테이너 이미지의 레이어를 관리하는 컴포넌트
K8s Binary Setup
Kubernetes 컴포넌트들은 버전 호환성이 매우 중요합니다. 이는 이후 업그레이드를 할 때에도 동일하기 때문에 exclude 필드에 주요 바이너리를 등록함으로써, 운영자가 명시하지 않은 dnf update 명령 수행 시 Kubernetes 관련 패키지가 자동으로 업데이트되는 것을 차단합니다.
# Kubernetes v1.32 저장소 추가 및 업그레이드 방지 설정
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 옵션을 명시하여, exclude 규칙을 무시할 수 있습니다.
저장소로부터 필요한 버전을 명시적으로 호출하여 설치합니다.
# 보호 설정을 일시적으로 무시하고 v1.32.11 설치
dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
# Kubelet 서비스 활성화 (부팅 시 자동 시작 설정)
systemctl enable --now kubelet
kubeadm version 및 kubectl version --client=true를 통해 설치된 바이너리가 대상 버전인 v1.32.11과 정확히 일치하는지 검증합니다.
verification
다만 현재 단계에서 systemctl status 확인 시 실패하는 것은 정상입니다. 이는 아직 kubeadm init을 통해 필요한 설정 파일(/var/lib/kubelet/config.yaml)이 생성되지 않았기 때문입니다.
systemctl status kubelet — no-pager
cri-tools는 Kubernetes의 CRI(Container Runtime Interface) 사양을 준수하는 런타임을 디버깅하기 위한 도구입니다.
# crictl이 containerd 소켓을 바라보도록 설정
cat << EOF > /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
EOF
# 설정 확인 및 CRI 상태 점검
crictl info | jq
crictl은 기본적으로 여러 경로의 소켓을 탐색하지만,/etc/crictl.yaml에 명시적인 엔드포인트를 정의함으로써 containerd와의 통신 지연을 줄이고 명확한 디버깅 환경을 제공합니다.
설치된 바이너리들이 시스템 내에서 차지하는 구조와 경로를 확인합니다.
tree /opt/cni
kubernetes-cni 패키지는 /opt/cni/bin에 bridge, loopback, portmap 등 Pod의 네트워크 구성을 위한 핵심 바이너리들을 설치합니다. 이 바이너리들은 추후 CNI 플러그인에 의해 호출되어 가상 인터페이스를 생성합니다.
Control Plane
kubeadm은 설정 파일(kubeadm-init.yaml)을 통해 클러스터를 관리할 수 있습니다. Kubernetes v1.32에서 사용하는 설정 파일의 API 버전은 v1beta4입니다. 설정 파일을 통해 Bootstrap Token, NodeRegistration, 네트워크 설정 등을 명시적으로 정의할 수 있습니다.
kubeadm init이 실행될 때, 아래와 같은 내부 단계를 순차적으로 수행합니다.
- Preflight Checks: 시스템 상태 검증 및 CRI Socket 응답 확인
- Certificates:
/etc/kubernetes/pki경로에 인증서 생성 - Kubeconfig Generation: 컴포넌트들이 API Server에 인증 없이 접속할 수 있도록 설정 파일을
/etc/kubernetes에 생성 - Static Pod Deployment: Control Plane 구성 요소(kube-apiserver, etcd 등)를
/etc/kubernetes/manifests에 Static Pod로 배치 - Health Monitoring: Kubelet을 활성화하고, API Server의
/healthz엔드포인트 응답 확인 - Upload Config: 클러스터 정보를 참조할 수 있도록
ClusterConfiguration을kube-system네임스페이스의kubeadm-configConfigMap에 저장 - Mark Control Plane: 초기화된 노드에
node-role.kubernetes.io/control-plane=""라벨을 부여하고, 일반 Pod가 배치되지 않도록NoScheduleTaint를 적용 - Bootstrap Token: 노드가 클러스터에 조인할 때 사용할 Bootstrap Token을 생성하고, Token 기반 노드 조인을 위한 RBAC 규칙과 ConfigMap을 설정
- Addon: DNS 서버(CoreDNS)와 kube-proxy Addon을 API Server를 통해 설치
Kubeadm은 클러스터 초기화를 진행하기 전, 호스트 시스템이 Kubernetes 요구 사항을 충족하는지 다양한 검사를 수행합니다. 이는 클러스터 구성 실패를 사전에 방지하기 위한 것으로, 에러와 경고로 구분됩니다. --ignore-preflight-errors 플래그로 특정 검사를 건너뛸 수 있습니다.
우선 아래와 같이 kubeadm 설정 파일을 작성합니다.
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
업그레이드 작업 시, 작업 시간 단축을 위해서 kubeadm config images pull 를 수행합니다.
만일 이미지 Pulling이 실패하는 경우, ip route 를 통해 default 경로를 제거하고 외부로 구글 DNS를 추가합니다.
# enp18s0 default 라우트 제거
nmcli connection modify enp18s0 ipv4.never-default yes
nmcli connection up enp18s0
# enp2s0에 구글 DNS 추가
nmcli connection modify enp2s0 ipv4.dns "8.8.8.8 1.1.1.1"
nmcli connection up enp2s0
준비된 설정 파일로 초기화를 실행하면 Kubeadm은 내부적으로 정의된 Phases를 순차적으로 통과합니다.
# Control Plane 초기화 실행
kubeadm init --config="kubeadm-init.yaml"
클러스터가 정상적으로 초기화되면 아래와 같이 kubectl 설정과 CNI Plugin을 설치하라고 문구가 나오게 됩니다.
kubeadm init
초기화 완료 후, 가장 먼저 Container Runtime 수준에서 구성 요소들이 정상적으로 구동 중인지 확인해야 합니다.
kube-apiserver, etcd, kube-scheduler, kube-controller-manager가 Running 상태여야 합니다. 이들은 Static Pod이므로 Kubelet에 의해 직접 관리됩니다.
crictl ps
kubeadm init 과정에서 생성된 admin.conf는 클러스터 관리자를 위한 클라이언트 인증서와 개인키, 그리고 CA 인증서를 포함하고 있습니다.
kubectl은 기본적으로 ~/.kube/config 파일에서 클러스터 인증 정보를 찾습니다. 하지만 kubeadm init은 인증 정보를 /etc/kubernetes/admin.conf에 생성하고 root 소유로 만듭니다. 따라서 kubectl이 인증 정보를 찾을 수 있도록 ~/.kube/config로 복사하고, 현재 사용자가 읽을 수 있도록 소유권을 변경해야 합니다.
# 1. Kubeconfig 저장을 위한 디렉토리 생성
mkdir -p $HOME/.kube
# 2. 생성된 관리자 설정 파일을 사용자 경로로 복사
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
# 3. 현재 사용자에게 파일 소유권 부여
chown $(id -u):$(id -g) $HOME/.kube/config
구성이 완료되면 kubectl이 실제 Control Plane의 API Server와 정상적으로 통신하는지 확인합니다.
# 클러스터 정보 확인
kubectl cluster-info
# 노드 리스트 확인
kubectl get node -o wide
노드가 Ready 상태가 되기 위해서는 Container Network Interface (CNI) Plugin이 설치되어 네트워크 인터페이스를 생성할 수 있어야 합니다. 현재는 CNI가 설치되지 않아 노드의 상태가 NotReady 상태로 나오게 됩니다. 이는 Pod에도 IP를 할당할 수 없기 때문에 Pod들도 스케줄링은 되었으나 실행되지 못하고 Pending 됩니다.
Cluster/Node Status
Kubeadm은 TLS Bootstrap 과정을 지원하기 위해 kube-public 네임스페이스에 cluster-info ConfigMap을 생성합니다.
# cluster-info 확인
kubectl -n kube-public get configmap cluster-info -o yaml
아직 Cluster에 Join하지 않은 Worker Node는 API Server와 통신할 수 있는 인증서가 없습니다. 이를 해결하기 위해 Kubeadm은 다음과 같은 보안 모델을 구축합니다.
cluster-info ConfigMap 내부의 kubeconfig 데이터에는 클러스터의 CA 인증서와 API Server 엔드포인트 정보가 포함되어 있습니다.
Kubeadm은 system:unauthenticated 그룹에 대해 오직 이 cluster-info ConfigMap만 get 권한을 부여하는 Role과 RoleBinding을 자동 생성합니다. kube-public 네임스페이스의 다른 리소스나 다른 네임스페이스는 여전히 인증이 필요합니다.
# cluster-info는 인증 없이 접근 가능
curl -k https://192.168.10.100:6443/api/v1/namespaces/kube-public/configmaps/cluster-info
# 다른 리소스는 인증 필요
curl -k https://192.168.10.100:6443/api/v1/namespaces/default/pods
# RBAC 확인
kubectl -n kube-public get role,rolebinding
노드는 kubeadm join 수행 시, 인증 없이 cluster-info ConfigMap을 먼저 읽어들여 자신이 접속하려는 API Server의 CA가 올바른지 검증합니다. CA 검증이 완료된 후에야 Bootstrap Token을 사용하여 정식 인증서 발급(CSR) 절차를 밟게 됩니다.
Install CLI Productivity Tools
Shell의 기능을 확장하여 Alias와 completion 설정을 적용합니다.
# kubectl 및 kubeadm 자동 완성 스크립트 로드
source <(kubectl completion bash)
source <(kubeadm completion bash)
# 시스템 전역 설정에 반영하여 재접속 시에도 유지
echo 'source <(kubectl completion bash)' >> /etc/profile
echo 'source <(kubeadm completion bash)' >> /etc/profile
# kubectl을 '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
kubectl의 기본 출력은 단색 텍스트로 구성되어 있어, 리소스의 상태가 Running인지 Pending인지 한눈에 파악하기 어렵습니다.
# kubecolor 저장소 추가 및 설치
dnf install -y 'dnf-command(config-manager)'
dnf config-manager --add-repo https://kubecolor.github.io/packages/rpm/kubecolor.repo
dnf install -y kubecolor
# 단축어 설정
alias kc=kubecolor
echo 'alias kc=kubecolor' >> /etc/profile
여러 네임스페이스가 존재하는 클러스터에서 매번 -n <namespace> 옵션을 붙이는 것은 번거롭습니다. kubectx와 kubens는 이 과정을 자동화합니다.
# git 설치 및 도구 클론
dnf install -y git
git clone https://github.com/ahmetb/kubectx /opt/kubectx
# 실행 바이너리를 PATH 경로로 심볼릭 링크 생성
ln -s /opt/kubectx/kubens /usr/local/bin/kubens
ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx
현재 내가 어느 클러스터의 어느 네임스페이스에서 작업 중인지 Shell에 표시되도록 설정합니다.
# kube-ps1 설치 및 프롬프트(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
PS1='$(kube_ps1)'$PS1
EOT
마지막으로 패키지 관리 도구인 Helm과 k9s를 추가합니다.
# Helm 3 설치 (v3.18.6)
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | DESIRED_VERSION=v3.18.6 bash
# k9s 설치 (TUI 기반 클러스터 관리 도구)
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
chown root:root k9s
mv k9s /usr/local/bin/
chmod +x /usr/local/bin/k9s
Install Flannel CNI
다시 돌아와서 CNI Plugin이 설치되지 않았기에, 노드는 NotReady 상태에 머물렀습니다. Flannel 설치를 통해 가상 인터페이스를 생성하고 라우팅 규칙을 정의함으로써, 비로소 파드들이 고유 IP를 할당받고 Ready 상태로 전환되도록 구성합니다.
Flannel을 설치하기 전, kubeadm init 시 설정했던 네트워크 대역이 올바르게 클러스터에 반영되었는지 확인해야 합니다.
# 클러스터 전체 Pod CIDR 확인
kc describe pod -n kube-system kube-controller-manager-k8s-ctr | grep cluster-cidr
# 각 노드에 할당된 개별 Pod CIDR 확인
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
Helm을 통해 CNI를 관리합니다. Flannel이 올바른 네트워크 인터페이스를 사용하도록 지정하는 것이 중요합니다.
# Helm 저장소 추가 및 업데이트
helm repo add flannel https://flannel-io.github.io/flannel
helm repo update
# Flannel 설정 파일(values) 작성
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=enp18s0" # 실제 통신이 이루어질 인터페이스 명시
backend: "vxlan" # 가상 터널링 기법 선택
EOF
# Helm Install(v0.27.3)
kubectl create namespace kube-flannel
helm install flannel flannel/flannel --namespace kube-flannel --version 0.27.3 -f flannel.yaml
Helm 배포가 완료되면 Flannel DaemonSet이 각 노드에 바이너리를 배치하고 설정을 작성합니다. Kubelet은 Pod을 생성할 때, /etc/cni/net.d의 설정을 읽고, /opt/cni/bin/flannel 바이너리를 호출하여 네트워크 네임스페이스를 구성합니다.
# CNI 바이너리 및 설정 파일 생성 확인
ls -l /opt/cni/bin/flannel
cat /etc/cni/net.d/10-flannel.conflist
ls -l /opt/cni/bin/
CNI가 설치되면 crictl 상태와 CoreDNS가 정상화됩니다.
crictl info | jq ".status.conditions"
status
이제 다시 Pod가 Running 상태인 것을 확인할 수 있습니다.
kubectl get pod -n kube-system -o wide
kubeadm init 실행 전후의 환경 정보를 기록하고 어떤 것이 변경되었는지 확인할 수 있습니다.
# 기본 환경 정보 출력 저장
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
vi -d sysctl-1.txt sysctl-2.txt
etc_kubernetes[1|2].txt를 비교해보면 디렉토리에 pki 폴더와 manifests 폴더등이 생성된 것을 확인할 수 있습니다.
kubernetes는 컨테이너 기반 워크로드를 안정적으로 실행하기 위해 특정 커널 설정을 요구합니다. kubelet이 시작할 때 아래의 5개 커널 파라미터를 검사합니다. 하나라도 기대값과 다르면 kubelet 시작을 거부합니다.
vm.overcommit_memory: 메모리 할당 정책kernel.panic: 커널 패닉 발생 시 몇 초 후 재부팅할지 결정vm.panic_on_oom: 메모리 부족(OOM) 발생 시 동작 방식kernel.panic_on_oops: 커널 오류 발생 동작 방식kernel.keys.root_maxkeys: root 사용자가 가질 수 있는 최대 보안 키 개수
vi -d sysctl-1.txt sysctl-2.txt 의 비교를 통해 아래의 파라미터가 변경된 것을 확인할 수 있습니다.
kernel.panic: 0 → 10 변경vm.overcommit_memory: 0 → 1 변경, 모든 메모리 할당 요청 허용
--protect-kernel-defaults 플래그를 통해 kubelet이 커널 파라미터를 검증할지 결정할 수 있습니다. 기본값은 False입니다.
vi -d sysctl-1.txt sysctl-2.txt 의 비교를 통해 시스템이 kubeadm init을 수행하며 소스코드에 정의된 커널 파라미터로 시스템을 튜닝합니다.
kubeadm init은 /etc/kubernetes/pki 경로에 수많은 인증서를 자동으로 생성합니다. Root CA는 10년, 개별 컴포넌트 인증서는 1년의 유효 기간을 가집니다.
kc describe cm -n kube-system kubeadm-config
인증서는 아래의 경로에서 확인할 수 있습니다.
tree /etc/kubernetes/pki
각 인증서는 PEM형식으로 인코딩되어 있기 때문에 openssl x509 -text -noout 을 통해 텍스트 형태로 여러 필드를 확인할 수 있습니다.
cat /etc/kubernetes/pki/ca.crt | openssl x509 -text -noout
또한 Kubeadm은 인증서 만료를 쉽게 확인할 수 있는 도구를 제공합니다.
kubeadm certs check-expiration
Kubernetes의 모든 컴포넌트는 API 서버와 통신할 때 신원을 증명해야 합니다. 이를 위해 Kubeadm은 최소 권한 원칙에 따라 각 컴포넌트 전용 Kubeconfig를 생성합니다.
ls -l /etc/kubernetes
Kubelet의 경우, Client와 Server 두 가지 역할을 동시에 수행하며, 각 역할에 맞는 별도의 인증서를 사용합니다.
kubelet-client-current.pem: 만료 기간에 따라 자동 갱신되며 심볼릭 링크로 최신 인증서 파일을 가리키도록 설계되어 있습니다.kubelet.crt,kubelet.key: 노드가 self-signed한 서버 인증서
ls -l /var/lib/kubelet/pki
Kubernetes의 컴포넌트들은 /etc/kubernetes/manifests 경로에 위치한 YAML 파일들을 통해 정의됩니다. Kubelet은 이 경로를 지속적으로 스캔하며, 파일에 변경이 생기면 컨테이너를 즉시 재시작합니다.
tree /etc/kubernetes/manifests
/etc/kubernetes/manifests/kube-apiserver.yaml 만 살펴보면 네트워크 설정 및 인증/인가와 etcd연결등이 명시되어 있습니다.
kubectl get pod -n kube-system -l component=kube-apiserver -o json | jq -r ‘.items[].spec.containers[0].command’
Kubernetes 컨트롤러들은 클러스터의 상태를 계속해서 변경합니다. 만약 두 개의 kube-controller-manager(KCM)가 동시에 동일한 리소스를 수정하려고 하면 데이터 정합성이 깨질 수 있습니다. 이를 방지하기 위해 Lease라는 객체를 사용합니다.
kubernetes에서 Lease 객체는 여러 개의 Control Plane 노드가 존재할 때(High Availability, HA), 특정 시점에 단 하나의 컴포넌트가 활성화되어 작업을 수행하도록 Leader Election을 따릅니다.
kubectl get lease -n kube-system kube-controller-manager -o json 실행 시 출력되는 핵심 필드들의 의미는 다음과 같습니다.
spec.holderIdentity: 현재 어떤 Pod가 클러스터의 컨트롤러 역할을 수행 중인지 나타냅니다.spec.renewTime: 리더는leaseDurationSeconds가 지나기 전에 계속해서 이 시간을 갱신해야 합니다. 만약 일정 시간 동안 갱신이 없으면 다른 Pod가 리더 자리를 가로챕니다.
kubectl get lease -n kube-system kube-controller-manager -o yaml
CoreDNS의 배포 상태와 상위 DNS Forwarding설정을 확인합니다.
# Deployment, Pod 확인
kubectl get deploy -n kube-system coredns -o wide
kubectl get pod -n kube-system -l k8s-app=kube-dns -o wide
# Service VIP와 실제 Pod IP(Endpoints) 매핑 확인
kubectl get svc,ep -n kube-system
# 프로메테우스 메트릭 수집 엔드포인트 응답 확인
curl -s http://10.96.0.10:9153/metrics | head
# DNS 설정(Corefile) 및 호스트 상위 DNS 정보 대조
kc describe cm -n kube-system coredns
cat /etc/resolv.conf
kc describe cm -n kube-system coredns
DNS Query가 발생하면 Corefile에 정의된 Plugin 순서로 처리됩니다.
kubernetes:cluster.local도메인에 대한 쿼리를 처리하며,pods insecure는 Pod DNS 레코드를 활성화합니다.forward: 클러스터 외부 도메인에 대한 쿼리를 호스트의/etc/resolv.conf에 정의된 DNS 서버로 전달cache: 30초 동안 응답을 캐싱합니다.
kube-proxy는 모든 노드에서 실행되며, 서비스 IP로 들어오는 패킷을 실제 Pod에게 전달하기 위한 커널 규칙(iptables)이 생성되고 관리되는지 확인합니다.
# DaemonSet 배포 상태 확인
kubectl get ds -n kube-system -owide
kubectl get pod -n kube-system -l k8s-app=kube-proxy -owide
# 동작 모드(iptables 등) 및 클러스터 대역 설정 확인
kc describe cm -n kube-system kube-proxy
# 프로세스 리스닝 포트 확인 (10249: Healthcheck, 10256: Metric)
ss -tnlp | grep kube-proxy
# 헬스체크 엔드포인트 응답 확인
curl 127.0.0.1:10249/healthz ; echo
Comments