[Kubernetes] Cluster: Kubeadm을 이용해 클러스터 구성하기 - 1.6. 노드 정보, 인증서 및 kubeconfig 확인
서종호(가시다)님의 On-Premise K8s Hands-on Study 3주차 학습 내용을 기반으로 합니다.
TL;DR
이번 글의 목표는 노드 정보, 인증서, kubeconfig 확인이다.
- 노드 정보: kubelet 상태 및 설정, 노드 상세 정보, 커널 파라미터 확인
- 인증서: PKI 구조, 인증서 내용, 만료 시간 확인
- kubeconfig: 각 컴포넌트별 kubeconfig 구조 확인
들어가며
이전 글에서 Flannel CNI를 설치했다. 이번 글에서는 CNI 설치가 완료되어 Ready 상태가 된 노드의 정보, 인증서, kubeconfig를 상세히 확인한다.
노드 정보 확인
CNI 설치가 완료되어 노드가 Ready 상태가 되었다. 이 시점에서 노드의 상세 정보를 확인하면 kubeadm, kubelet, Flannel이 각각 어떤 설정을 추가했는지 파악할 수 있다.
kubelet 상태 및 설정 확인
1-3에서 kubelet을 설치했을 때는 설정 파일이 아직 생성되지 않아 crashloop 상태였다. kubeadm init과 CNI 설치를 거친 지금, kubelet이 정상 실행 중이고 당시 비어 있던 설정 파일들이 채워진 것을 확인할 수 있다.
systemctl is-active kubelet
# active
systemctl status kubelet --no-pager
# ● kubelet.service - kubelet: The Kubernetes Node Agent
# Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; preset: disabled)
# Drop-In: /usr/lib/systemd/system/kubelet.service.d
# └─10-kubeadm.conf
# Active: active (running) since Fri 2026-01-23 19:41:11 KST; 46min ago
# Main PID: 16072 (kubelet)
# Tasks: 12 (limit: 18742)
# Memory: 39.6M (peak: 40.9M)
# CPU: 52.578s
# CGroup: /system.slice/kubelet.service
# └─16072 /usr/bin/kubelet --bootstrap-kubeconfig=... --kubeconfig=...
1-3에서 비어 있던 /var/lib/kubelet/에 kubeadm init이 설정 파일들을 생성했다. 각 설정의 역할은 1-3에서 설명했고, 여기서는 실제 값을 확인한다.
cat /var/lib/kubelet/config.yaml | grep -E 'staticPodPath|cgroupDriver|clusterDNS|clusterDomain|rotateCertificates'
# cgroupDriver: systemd
# clusterDNS:
# - 10.96.0.10
# clusterDomain: cluster.local
# rotateCertificates: true
# authentication:
# anonymous:
# enabled: false
# webhook:
# cacheTTL: 0s
# enabled: true
# x509:
# clientCAFile: /etc/kubernetes/pki/ca.crt
# authorization:
# mode: Webhook
# ...
# cgroupDriver: systemd
# staticPodPath: /etc/kubernetes/manifests
kubelet 설정 파일(/var/lib/kubelet/config.yaml)
cat /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
anonymous:
enabled: false
webhook:
cacheTTL: 0s
enabled: true
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: Webhook
webhook:
cacheAuthorizedTTL: 0s
cacheUnauthorizedTTL: 0s
cgroupDriver: systemd
clusterDNS:
- 10.96.0.10
clusterDomain: cluster.local
containerRuntimeEndpoint: ""
cpuManagerReconcilePeriod: 0s
crashLoopBackOff: {}
evictionPressureTransitionPeriod: 0s
fileCheckFrequency: 0s
healthzBindAddress: 127.0.0.1
healthzPort: 10248
httpCheckFrequency: 0s
imageMaximumGCAge: 0s
imageMinimumGCAge: 0s
kind: KubeletConfiguration
logging:
flushFrequency: 0
options:
json:
infoBufferSize: "0"
text:
infoBufferSize: "0"
verbosity: 0
memorySwap: {}
nodeStatusReportFrequency: 0s
nodeStatusUpdateFrequency: 0s
rotateCertificates: true
runtimeRequestTimeout: 0s
shutdownGracePeriod: 0s
shutdownGracePeriodCriticalPods: 0s
staticPodPath: /etc/kubernetes/manifests
streamingConnectionIdleTimeout: 0s
syncFrequency: 0s
volumeStatsAggPeriod: 0s
| 설정 | 값 | 출처 | 설명 |
|---|---|---|---|
staticPodPath |
/etc/kubernetes/manifests |
config.yaml |
Static Pod 매니페스트 감시 경로 |
cgroupDriver |
systemd |
config.yaml |
containerd의 SystemdCgroup 설정과 일치해야 함 |
clusterDNS |
10.96.0.10 |
config.yaml |
CoreDNS Service ClusterIP |
rotateCertificates |
true |
config.yaml |
kubelet 클라이언트 인증서 자동 갱신 |
cat /var/lib/kubelet/kubeadm-flags.env
# KUBELET_KUBEADM_ARGS="--container-runtime-endpoint=unix:///run/containerd/containerd.sock --node-ip=192.168.10.100 --pod-infra-container-image=registry.k8s.io/pause:3.10"
| 설정 | 값 | 출처 | 설명 |
|---|---|---|---|
--container-runtime-endpoint |
unix:///run/containerd/containerd.sock |
kubeadm-flags.env |
containerd CRI 소켓 경로 |
--node-ip |
192.168.10.100 |
kubeadm-flags.env |
노드 IP (kubeadm 설정에서 지정한 값) |
--pod-infra-container-image |
registry.k8s.io/pause:3.10 |
kubeadm-flags.env |
Pod의 인프라 컨테이너(pause) 이미지 |
프로세스 및 네임스페이스 전체 확인
프로세스 트리와 네임스페이스를 확인해 보자. containerd 설치 직후의 baseline과 비교하면 변화가 뚜렷하다.
pstree
systemd─┬─NetworkManager───3*[{NetworkManager}]
├─VBoxService───8*[{VBoxService}]
├─agetty
├─atd
├─auditd─┬─sedispatch
│ └─2*[{auditd}]
├─chronyd
├─containerd───12*[{containerd}]
├─containerd-shim─┬─kube-scheduler───9*[{kube-scheduler}]
│ ├─pause
│ └─12*[{containerd-shim}]
├─containerd-shim─┬─kube-controller───6*[{kube-controller}]
│ ├─pause
│ └─12*[{containerd-shim}]
├─containerd-shim─┬─etcd───10*[{etcd}]
│ ├─pause
│ └─12*[{containerd-shim}]
├─containerd-shim─┬─kube-apiserver───11*[{kube-apiserver}]
│ ├─pause
│ └─12*[{containerd-shim}]
├─containerd-shim─┬─kube-proxy───7*[{kube-proxy}]
│ ├─pause
│ └─12*[{containerd-shim}]
├─containerd-shim─┬─flanneld───9*[{flanneld}]
│ ├─pause
│ └─11*[{containerd-shim}]
├─containerd-shim─┬─coredns───8*[{coredns}]
│ ├─pause
│ └─12*[{containerd-shim}]
├─containerd-shim─┬─coredns───8*[{coredns}]
│ ├─pause
│ └─11*[{containerd-shim}]
├─crond
├─dbus-broker-lau───dbus-broker
├─gssproxy───5*[{gssproxy}]
├─irqbalance───{irqbalance}
├─kubelet───11*[{kubelet}]
├─lsmd
├─polkitd───3*[{polkitd}]
├─rpcbind
├─rsyslogd───2*[{rsyslogd}]
├─sshd─┬─...
├─systemd───(sd-pam)
├─systemd-journal
├─systemd-logind
├─systemd-udevd
├─systemd-userdbd───3*[systemd-userwor]
├─tuned───3*[{tuned}]
└─udisksd───6*[{udisksd}]
기존 baseline과 비교했을 때 주요 변화는 아래와 같다:
- kubelet: 정상 실행 중 (crashloop 해소)
- containerd-shim: Pod마다 하나씩, 총 8개. 각각 pause + 실제 워크로드를 자식으로 가지는데, 이것이 Pod sandbox 모델이다.
- 총 8개 Pod: static pod 4개(apiserver, etcd, scheduler, controller-manager) + DaemonSet 2개(kube-proxy, flannel) + Deployment 2개(coredns)
lsns
NS TYPE NPROCS PID USER COMMAND
4026531834 time 155 1 root /usr/lib/systemd/systemd ...
4026531835 cgroup 148 1 root /usr/lib/systemd/systemd ...
4026531836 pid 139 1 root /usr/lib/systemd/systemd ...
4026531837 user 154 1 root /usr/lib/systemd/systemd ...
4026531838 uts 142 1 root /usr/lib/systemd/systemd ...
4026531839 ipc 139 1 root /usr/lib/systemd/systemd ...
4026531840 net 149 1 root /usr/lib/systemd/systemd ...
4026531841 mnt 124 1 root /usr/lib/systemd/systemd ...
... (시스템 서비스 네임스페이스 생략)
4026532126 mnt 1 7600 65535 /pause
4026532190 ipc 2 7600 65535 /pause
4026532195 pid 1 7600 65535 /pause
4026532267 mnt 1 7601 65535 /pause
4026532268 ipc 2 7601 65535 /pause
4026532269 pid 1 7601 65535 /pause
4026532270 mnt 1 7650 root kube-scheduler ...
4026532271 pid 1 7650 root kube-scheduler ...
4026532272 cgroup 1 7650 root kube-scheduler ...
4026532273 mnt 1 7656 root kube-controller-manager ...
4026532274 pid 1 7656 root kube-controller-manager ...
4026532275 cgroup 1 7656 root kube-controller-manager ...
4026532276 mnt 1 7750 65535 /pause
4026532277 ipc 2 7750 65535 /pause
4026532278 pid 1 7750 65535 /pause
4026532279 mnt 1 7758 65535 /pause
4026532280 ipc 2 7758 65535 /pause
4026532281 pid 1 7758 65535 /pause
4026532282 mnt 1 7806 root kube-apiserver ...
4026532283 pid 1 7806 root kube-apiserver ...
4026532284 cgroup 1 7806 root kube-apiserver ...
4026532285 mnt 1 7807 root etcd ...
4026532286 pid 1 7807 root etcd ...
4026532287 cgroup 1 7807 root etcd ...
4026532288 mnt 1 7971 65535 /pause
4026532289 ipc 2 7971 65535 /pause
4026532290 pid 1 7971 65535 /pause
4026532291 mnt 1 7997 root /usr/local/bin/kube-proxy ...
4026532292 pid 1 7997 root /usr/local/bin/kube-proxy ...
4026532293 mnt 1 10250 65535 /pause
4026532294 ipc 2 10250 65535 /pause
4026532295 pid 1 10250 65535 /pause
4026532296 mnt 1 10400 root /opt/bin/flanneld --ip-masq --kube-subnet-mgr --iface=enp0s9
4026532297 pid 1 10400 root /opt/bin/flanneld --ip-masq --kube-subnet-mgr --iface=enp0s9
4026532298 cgroup 1 10400 root /opt/bin/flanneld --ip-masq --kube-subnet-mgr --iface=enp0s9
4026532300 net 2 10702 65535 /pause
4026532375 net 2 10704 65535 /pause
4026532443 mnt 1 10702 65535 /pause
4026532444 uts 2 10702 65535 /pause
4026532445 ipc 2 10702 65535 /pause
4026532446 pid 1 10702 65535 /pause
4026532447 mnt 1 10704 65535 /pause
4026532448 uts 2 10704 65535 /pause
4026532449 ipc 2 10704 65535 /pause
4026532450 pid 1 10704 65535 /pause
4026532451 mnt 1 10755 65532 /coredns -conf /etc/coredns/Corefile
4026532452 pid 1 10755 65532 /coredns -conf /etc/coredns/Corefile
4026532453 cgroup 1 10755 65532 /coredns -conf /etc/coredns/Corefile
4026532454 mnt 1 10762 65532 /coredns -conf /etc/coredns/Corefile
4026532455 pid 1 10762 65532 /coredns -conf /etc/coredns/Corefile
4026532456 cgroup 1 10762 65532 /coredns -conf /etc/coredns/Corefile
baseline에서는 시스템 네임스페이스만 존재했는데, 이제 컨테이너별 네임스페이스가 대량으로 추가되었다.
pause(user 65535)가 공유 네임스페이스의 소유자: ipc 네임스페이스의NPROCS=2는 pause와 워크로드 컨테이너가 해당 네임스페이스를 공유한다는 뜻이다- 호스트 네트워크 Pod (control plane, kube-proxy, flannel): 별도 net 네임스페이스가 없다. 호스트의
4026531840 net(NPROCS=149)에 포함되어 호스트 네트워크를 직접 사용한다.hostNetwork: true인 Pod의 pause는 별도 net 네임스페이스를 생성하지 않는다 - Pod 네트워크 Pod (coredns): pause가 소유하는 별도 net 네임스페이스(
4026532300,4026532375)가 존재하고,NPROCS=2로 pause와 coredns가 공유한다
노드 상세 정보 확인
노드 주요 정보를 살펴 보자.
kc describe node k8s-ctr
노드 정보 확인 전문
Name: k8s-ctr
Roles: control-plane
Labels: beta.kubernetes.io/arch=arm64
beta.kubernetes.io/os=linux
kubernetes.io/arch=arm64
kubernetes.io/hostname=k8s-ctr
kubernetes.io/os=linux
node-role.kubernetes.io/control-plane=
node.kubernetes.io/exclude-from-external-load-balancers=
Annotations: flannel.alpha.coreos.com/backend-data: {"VNI":1,"VtepMAC":"7e:e7:01:d7:1f:7c"}
flannel.alpha.coreos.com/backend-type: vxlan
flannel.alpha.coreos.com/kube-subnet-manager: true
flannel.alpha.coreos.com/public-ip: 192.168.10.100
kubeadm.alpha.kubernetes.io/cri-socket: unix:///run/containerd/containerd.sock
node.alpha.kubernetes.io/ttl: 0
volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp: Sun, 01 Mar 2026 23:15:13 +0900
Taints: node-role.kubernetes.io/control-plane:NoSchedule
Unschedulable: false
Lease:
HolderIdentity: k8s-ctr
AcquireTime: <unset>
RenewTime: Mon, 02 Mar 2026 13:10:49 +0900
Conditions:
Type Status LastHeartbeatTime LastTransitionTime Reason Message
---- ------ ----------------- ------------------ ------ -------
NetworkUnavailable False Sun, 01 Mar 2026 23:44:52 +0900 Sun, 01 Mar 2026 23:44:52 +0900 FlannelIsUp Flannel is running on this node
MemoryPressure False Mon, 02 Mar 2026 13:08:06 +0900 Sun, 01 Mar 2026 23:15:13 +0900 KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure False Mon, 02 Mar 2026 13:08:06 +0900 Sun, 01 Mar 2026 23:15:13 +0900 KubeletHasNoDiskPressure kubelet has no disk pressure
PIDPressure False Mon, 02 Mar 2026 13:08:06 +0900 Sun, 01 Mar 2026 23:15:13 +0900 KubeletHasSufficientPID kubelet has sufficient PID available
Ready True Mon, 02 Mar 2026 13:08:06 +0900 Sun, 01 Mar 2026 23:45:01 +0900 KubeletReady kubelet is posting ready status
Addresses:
InternalIP: 192.168.10.100
Hostname: k8s-ctr
Capacity:
cpu: 4
ephemeral-storage: 60970Mi
hugepages-1Gi: 0
hugepages-2Mi: 0
hugepages-32Mi: 0
hugepages-64Ki: 0
memory: 2893976Ki
pods: 110
Allocatable:
cpu: 4
ephemeral-storage: 57538510753
hugepages-1Gi: 0
hugepages-2Mi: 0
hugepages-32Mi: 0
hugepages-64Ki: 0
memory: 2791576Ki
pods: 110
System Info:
Machine ID: 3cf0cc5101474d6490f7225f4890667b
System UUID: 3cf0cc5101474d6490f7225f4890667b
Boot ID: 1ef6fdee-ec07-4467-bb56-132c41e90f60
Kernel Version: 6.12.0-55.39.1.el10_0.aarch64
OS Image: Rocky Linux 10.0 (Red Quartz)
Operating System: linux
Architecture: arm64
Container Runtime Version: containerd://2.1.5
Kubelet Version: v1.32.13
Kube-Proxy Version: v1.32.13
PodCIDR: 10.244.0.0/24
PodCIDRs: 10.244.0.0/24
Non-terminated Pods: (8 in total)
Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits Age
--------- ---- ------------ ---------- --------------- ------------- ---
kube-flannel kube-flannel-ds-bdlq4 100m (2%) 0 (0%) 50Mi (1%) 0 (0%) 13h
kube-system coredns-668d6bf9bc-bzdwl 100m (2%) 0 (0%) 70Mi (2%) 170Mi (6%) 13h
kube-system coredns-668d6bf9bc-qzk56 100m (2%) 0 (0%) 70Mi (2%) 170Mi (6%) 13h
kube-system etcd-k8s-ctr 100m (2%) 0 (0%) 100Mi (3%) 0 (0%) 13h
kube-system kube-apiserver-k8s-ctr 250m (6%) 0 (0%) 0 (0%) 0 (0%) 13h
kube-system kube-controller-manager-k8s-ctr 200m (5%) 0 (0%) 0 (0%) 0 (0%) 13h
kube-system kube-proxy-pclch 0 (0%) 0 (0%) 0 (0%) 0 (0%) 13h
kube-system kube-scheduler-k8s-ctr 100m (2%) 0 (0%) 0 (0%) 0 (0%) 13h
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 950m (23%) 0 (0%)
memory 290Mi (10%) 340Mi (12%)
ephemeral-storage 0 (0%) 0 (0%)
hugepages-1Gi 0 (0%) 0 (0%)
hugepages-2Mi 0 (0%) 0 (0%)
hugepages-32Mi 0 (0%) 0 (0%)
hugepages-64Ki 0 (0%) 0 (0%)
Events: <none>
Labels & Annotations
Labels:
node-role.kubernetes.io/control-plane= # kubeadm이 추가
node.kubernetes.io/exclude-from-external-load-balancers=
Annotations:
flannel.alpha.coreos.com/backend-type: vxlan # Flannel이 추가
flannel.alpha.coreos.com/public-ip: 192.168.10.100
kubeadm.alpha.kubernetes.io/cri-socket: unix:///run/containerd/containerd.sock
Taints:
node-role.kubernetes.io/control-plane:NoSchedule # 일반 워크로드 스케줄 방지
Conditions
CNI 설치 전에는 NetworkUnavailable=True였지만, Flannel 설치 후 False로 변경되었다.
| Condition | Status | Reason |
|---|---|---|
NetworkUnavailable |
False | FlannelIsUp |
MemoryPressure |
False | KubeletHasSufficientMemory |
DiskPressure |
False | KubeletHasNoDiskPressure |
PIDPressure |
False | KubeletHasSufficientPID |
Ready |
True | KubeletReady |
리소스 사용량
컨트롤 플레인 컴포넌트들이 CPU의 약 23%, 메모리의 약 10%를 사용 중이다.
PodCIDR: 10.244.0.0/24
Capacity: cpu: 4, memory: 2893976Ki, pods: 110
Allocatable: cpu: 4, memory: 2791576Ki, pods: 110
Non-terminated Pods: (8 in total)
Namespace Name CPU Requests Memory Requests
--------- ---- ------------ ---------------
kube-flannel kube-flannel-ds-hv2xd 100m (2%) 50Mi (1%)
kube-system coredns-668d6bf9bc-n8jxf 100m (2%) 70Mi (2%)
kube-system coredns-668d6bf9bc-z6h69 100m (2%) 70Mi (2%)
kube-system etcd-k8s-ctr 100m (2%) 100Mi (3%)
kube-system kube-apiserver-k8s-ctr 250m (6%) 0 (0%)
kube-system kube-controller-manager-k8s-ctr 200m (5%) 0 (0%)
kube-system kube-proxy-5p6jx 0 (0%) 0 (0%)
kube-system kube-scheduler-k8s-ctr 100m (2%) 0 (0%)
Allocated resources:
cpu: 950m (23%)
memory: 290Mi (10%)
Events
CNI 설치 후 NodeReady 이벤트가 발생했다.
Events:
Normal Starting 46m kubelet Starting kubelet.
Normal RegisteredNode 46m node-controller Node k8s-ctr event: Registered Node k8s-ctr in Controller
Normal NodeReady 4m30s kubelet Node k8s-ctr status is now: NodeReady
커널 파라미터 변경 사항
kubelet과 kube-proxy가 클러스터 안정성을 위해 일부 커널 파라미터를 자동으로 변경한다. sysctl을 이용해 변경된 값을 확인해 보자.
참고: sysctl
sysctl은 Linux 커널 파라미터를 런타임에 조회/변경하는 인터페이스다. 내부적으로/proc/sys/디렉토리의 파일을 읽고 쓴다. 예를 들어sysctl net.ipv4.ip_forward는/proc/sys/net/ipv4/ip_forward파일의 값을 조회한다. Kubernetes에서는 네트워크 포워딩, 메모리 관리, 연결 추적 등의 커널 설정이 중요하므로 kubelet과 kube-proxy가 시작 시 관련 파라미터를 자동으로 설정한다.sysctl의 동작 원리와/etc/sysctl.d/파일 체계에 대한 자세한 설명은 리눅스 커널 파라미터와 sysctl 글을 참고한다.
kubelet이 변경하는 파라미터
kubelet은 --protect-kernel-defaults=false(기본값)일 때 아래 파라미터들을 자동으로 설정한다. true로 설정하면 kubelet이 직접 변경하지 않고, 기존 값이 kubelet의 기대값과 다르면 오류가 발생한다.
| 파라미터 | 변경 전 | 변경 후 | 이유 |
|---|---|---|---|
kernel.panic |
0 | 10 | 커널 패닉 시 10초 후 자동 재부팅 (노드 복구) |
vm.overcommit_memory |
0 | 1 | 메모리 오버커밋 허용 (컨테이너 메모리 할당 유연성) |
참고
vm.overcommit_memory
vm.overcommit_memory는 Linux가 메모리 할당 요청(malloc() 등)을 어떻게 처리할지 결정하는 파라미터다. 자세한 설명은 메모리, 페이지, 스왑 글의 메모리 오버커밋 섹션을 참고한다.
kubelet이 vm.overcommit_memory=1로, 기본값 0(휴리스틱)이 아닌 1(항상 허용)로 설정하는 이유는 컨테이너 환경에서 장점이 있기 때문이다:
- 컨테이너가 메모리를 요청할 때 즉시 실패하지 않음 (가상 주소 공간만 예약)
- 많은 컨테이너가 메모리를 “예약”만 하고 실제로는 일부만 사용하는 패턴에 유리
- 오버프로비저닝이 가능해져 노드당 더 많은 컨테이너 실행 가능
- Pod의
requests는 낮게,limits는 높게 설정하는 패턴을 지원
더 근본적인 이유는, Kubernetes 노드에서 프로세스 시작 자체가 실패하는 것을 방지하기 위해서다. 기본값 0(휴리스틱)에서는 커널이 “너무 크다”고 판단한 메모리 할당 요청을 거부(ENOMEM)할 수 있는데, 이 경우 kubelet, 컨테이너 런타임(containerd, CRI-O), 파드 내 애플리케이션 등이 메모리를 예약(reserve)하는 시점에 프로세스가 시작조차 못 하는 상황이 발생할 수 있다.
특히 kubelet은 Go로 작성되어 있는데, Go 런타임은 시작 시 GC와 goroutine 스택 관리를 위해 큰 가상 메모리 영역을 mmap()으로 미리 예약한다. 이때 실제 물리 메모리 사용량(RSS)은 예약한 가상 메모리(VSZ)에 비해 훨씬 적다. 예를 들어, kubelet의 VSZ가 수 GB에 달해도 RSS는 수백 MB 수준인 것이 일반적이다. 휴리스틱 모드에서는 이처럼 큰 가상 메모리 예약을 커널이 거부할 수 있으므로, 1로 설정하여 가상 메모리 예약은 항상 허용하고 실제 물리 메모리가 부족할 때만 OOM Killer로 처리하는 것이 Kubernetes 환경에서 더 안전하다.
다만 단점 및 주의할 점도 있다:
- 물리 메모리가 실제로 부족해지면 OOM Killer가 예고 없이 프로세스를 종료
- 모든 컨테이너가 예약한 메모리를 동시에 사용하면 Thrashing 발생 가능
- 메모리 사용량 예측이 어려워 용량 계획이 복잡해짐
Kubernetes는 이러한 단점을 Resource Requests/Limits와 QoS Class로 보완한다.
requests로 최소 보장 메모리를 설정하고,limits로 최대 사용량을 제한하여 OOM Killer 발동을 제어한다.
아래 파라미터들은 kubelet이 확인하지만, 기본값이 이미 kubelet의 기대값과 일치하므로 변경되지 않는다:
| 파라미터 | 기존값 | 설명 |
|---|---|---|
kernel.panic_on_oops |
1 | oops 발생 시 패닉 |
vm.panic_on_oom |
0 | OOM 시 패닉하지 않음 (OOM Killer가 처리) |
kernel.keys.root_maxkeys |
1000000 | root 사용자의 최대 키 개수 |
kernel.keys.root_maxbytes |
25000000 | root 사용자의 최대 키 바이트 (root_maxkeys × 25) |
참고: kubespray에서의 처리
--protect-kernel-defaults=true로 운영할 경우, kubelet이 커널 파라미터를 직접 변경하지 않으므로 사전에 설정해야 한다. kubespray는 아래와 같이 Ansible로 처리한다:- name: Ensure kube-bench parameters are set ansible.posix.sysctl: name: "" value: "" state: present reload: yes with_items: - { name: kernel.keys.root_maxbytes, value: 25000000 } - { name: kernel.keys.root_maxkeys, value: 1000000 } - { name: kernel.panic, value: 10 } - { name: kernel.panic_on_oops, value: 1 } - { name: vm.overcommit_memory, value: 1 } - { name: vm.panic_on_oom, value: 0 } when: kubelet_protect_kernel_defaults | bool
kube-proxy가 변경하는 파라미터
kube-proxy는 iptables/IPVS를 통해 Service 트래픽을 라우팅하므로, conntrack(연결 추적) 테이블 관련 파라미터를 자동으로 설정한다.
| 파라미터 | 변경 전 | 변경 후 | 이유 |
|---|---|---|---|
net.nf_conntrack_max |
65536 | 131072 | conntrack 테이블 크기 증가 (더 많은 연결 추적) |
net.netfilter.nf_conntrack_tcp_timeout_close_wait |
60 | 3600 | CLOSE_WAIT 타임아웃 증가 (연결 정리 지연) |
net.netfilter.nf_conntrack_tcp_timeout_established |
432000 | 86400 | ESTABLISHED 타임아웃 감소 (5일 → 1일, 오래된 연결 정리) |
트러블슈팅 팁
커널 파라미터 튜닝 중 설정 값이 자꾸 원복되거나 변경된다면 kubelet 또는 kube-proxy를 의심해볼 수 있다. 이들 컴포넌트는 시작 시 위 파라미터들을 자동으로 설정하므로, 수동 튜닝 값과 충돌할 수 있다.
인증서 확인
인증서 만료 시간 확인
Kubernetes 클러스터의 인증서는 기본적으로 1년 후 만료된다. 운영 환경에서는 인증서 만료 전에 갱신해야 클러스터 장애를 방지할 수 있다. kubeadm certs renew 명령으로 갱신하거나, kubeadm upgrade를 수행하면 자동으로 갱신된다.
kubeadm-config에서 유효 기간 확인
kubeadm-config ConfigMap은 클러스터 업그레이드 시 참조되므로, 설정 변경이 필요하면 이 ConfigMap을 업데이트해야 한다.
kc describe cm -n kube-system kubeadm-config
# Data
# ====
# ClusterConfiguration:
# ----
# apiVersion: kubeadm.k8s.io/v1beta4
# caCertificateValidityPeriod: 87600h0m0s # CA 인증서: 10년 (87600시간)
# certificateValidityPeriod: 8760h0m0s # 일반 인증서: 1년 (8760시간)
# certificatesDir: /etc/kubernetes/pki
# kubernetesVersion: v1.32.11
# networking:
# dnsDomain: cluster.local
# podSubnet: 10.244.0.0/16
# serviceSubnet: 10.96.0.0/16
인증서 만료 시간 확인
kubeadm certs check-expiration
# CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED
# admin.conf Jan 23, 2027 10:41 UTC 364d ca no
# apiserver Jan 23, 2027 10:41 UTC 364d ca no
# apiserver-etcd-client Jan 23, 2027 10:41 UTC 364d etcd-ca no
# apiserver-kubelet-client Jan 23, 2027 10:41 UTC 364d ca no
# controller-manager.conf Jan 23, 2027 10:41 UTC 364d ca no
# etcd-healthcheck-client Jan 23, 2027 10:41 UTC 364d etcd-ca no
# etcd-peer Jan 23, 2027 10:41 UTC 364d etcd-ca no
# etcd-server Jan 23, 2027 10:41 UTC 364d etcd-ca no
# front-proxy-client Jan 23, 2027 10:41 UTC 364d front-proxy-ca no
# scheduler.conf Jan 23, 2027 10:41 UTC 364d ca no
# super-admin.conf Jan 23, 2027 10:41 UTC 364d ca no
#
# CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED
# ca Jan 21, 2036 10:41 UTC 9y no
# etcd-ca Jan 21, 2036 10:41 UTC 9y no
# front-proxy-ca Jan 21, 2036 10:41 UTC 9y no
| 인증서 유형 | 유효 기간 | 만료일 |
|---|---|---|
| 일반 인증서 (apiserver, admin.conf 등) | 1년 | 2027-01-23 |
| CA 인증서 (ca, etcd-ca, front-proxy-ca) | 10년 | 2036-01-21 |
운영 팁: 인증서 만료 30일 전에 알림을 받도록 모니터링을 설정하고, 정기적인 클러스터 업그레이드를 통해 인증서를 갱신하는 것이 좋다.
인증서 파일 구조
이전 글에서 설명한 PKI 구조가 실제로 생성되었는지 확인한다.
tree /etc/kubernetes/pki
# /etc/kubernetes/pki
# ├── apiserver.crt # API Server 서버 인증서
# ├── apiserver.key
# ├── apiserver-etcd-client.crt # API Server → etcd 클라이언트 인증서
# ├── apiserver-etcd-client.key
# ├── apiserver-kubelet-client.crt # API Server → kubelet 클라이언트 인증서
# ├── apiserver-kubelet-client.key
# ├── ca.crt # 클러스터 CA (루트)
# ├── ca.key
# ├── front-proxy-ca.crt # API Aggregation용 CA
# ├── front-proxy-ca.key
# ├── front-proxy-client.crt # API Aggregation 클라이언트 인증서
# ├── front-proxy-client.key
# ├── sa.key # ServiceAccount 토큰 서명용 키
# ├── sa.pub
# └── etcd/ # etcd 전용 PKI
# ├── ca.crt # etcd CA
# ├── ca.key
# ├── healthcheck-client.crt # etcd 헬스체크용
# ├── healthcheck-client.key
# ├── peer.crt # etcd 노드 간 통신용
# ├── peer.key
# ├── server.crt # etcd 서버 인증서
# └── server.key
#
# 2 directories, 22 files
총 22개 파일 (3개 CA + 8개 인증서/키 쌍 + SA 키 쌍)이 생성되었다. Kubernetes The Hard Way에서 OpenSSL로 수동 생성했던 인증서들이 kubeadm에 의해 자동으로 생성되었다.
주요 인증서 내용 확인
인증서 내용을 직접 확인하면 각 인증서가 누구에게 발급되었는지(Subject), 어떤 용도인지(Key Usage), 어디서 유효한지(SAN) 를 이해할 수 있다. 특히 API Server 인증서의 SAN은 클라이언트가 접속할 수 있는 모든 주소를 포함해야 한다.
CA 인증서
cat /etc/kubernetes/pki/ca.crt | openssl x509 -text -noout
# Certificate:
# Data:
# Version: 3 (0x2)
# Serial Number: 9110514726841664365 (0x7e6f0cdfde5cc36d)
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: CN=kubernetes
# Validity
# Not Before: Jan 23 10:36:04 2026 GMT
# Not After : Jan 21 10:41:04 2036 GMT ← 10년 유효
# Subject: CN=kubernetes
# Subject Public Key Info:
# Public Key Algorithm: rsaEncryption
# Public-Key: (2048 bit)
# X509v3 extensions:
# X509v3 Key Usage: critical
# Digital Signature, Key Encipherment, Certificate Sign
# X509v3 Basic Constraints: critical
# CA:TRUE ← 루트 CA 인증서
# X509v3 Subject Alternative Name:
# DNS:kubernetes
- Issuer = Subject =
CN=kubernetes: 자체 서명된 루트 CA - CA:TRUE: 다른 인증서를 서명할 수 있는 CA 인증서
- 유효기간 10년: CA 인증서는 장기간 유효
API Server 인증서
cat /etc/kubernetes/pki/apiserver.crt | openssl x509 -text -noout
# Certificate:
# Data:
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: CN=kubernetes ← CA가 서명
# Validity
# Not Before: Jan 23 10:36:04 2026 GMT
# Not After : Jan 23 10:41:04 2027 GMT ← 1년 유효
# Subject: CN=kube-apiserver
# X509v3 extensions:
# X509v3 Key Usage: critical
# Digital Signature, Key Encipherment
# X509v3 Extended Key Usage:
# TLS Web Server Authentication ← 서버 인증서
# X509v3 Basic Constraints: critical
# CA:FALSE
# X509v3 Subject Alternative Name:
# DNS:k8s-ctr, DNS:kubernetes, DNS:kubernetes.default,
# DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local,
# IP Address:10.96.0.1, IP Address:192.168.10.100
- Subject:
CN=kube-apiserver: API Server의 서버 인증서 - Extended Key Usage:
TLS Web Server Authentication: 서버 인증 전용 - SAN (Subject Alternative Name): 클라이언트가 이 주소들로 접속 시 인증서 검증 통과
kubernetes.default.svc.cluster.local: Pod 내부에서 접근 시10.96.0.1: Service ClusterIP (kubernetes 서비스)192.168.10.100: 외부에서 접근 시
API Server → Kubelet 클라이언트 인증서
cat /etc/kubernetes/pki/apiserver-kubelet-client.crt | openssl x509 -text -noout
# Certificate:
# Data:
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: CN=kubernetes
# Validity
# Not Before: Jan 23 10:36:04 2026 GMT
# Not After : Jan 23 10:41:04 2027 GMT
# Subject: O=kubeadm:cluster-admins, CN=kube-apiserver-kubelet-client
# X509v3 extensions:
# X509v3 Key Usage: critical
# Digital Signature, Key Encipherment
# X509v3 Extended Key Usage:
# TLS Web Client Authentication ← 클라이언트 인증서
# X509v3 Basic Constraints: critical
# CA:FALSE
- Subject:
O=kubeadm:cluster-admins, CN=kube-apiserver-kubelet-client: 조직(O)과 이름(CN) 포함 - Extended Key Usage:
TLS Web Client Authentication: 클라이언트 인증 전용 - API Server가 kubelet에 접속할 때 이 인증서로 자신을 증명
kubeconfig 확인
kubeconfig 파일은 각 컴포넌트가 API Server에 접속할 때 사용하는 인증 정보를 담고 있다. 각 파일의 user 섹션에서 누구로 인증하는지를 확인할 수 있으며, 이 정보가 RBAC 권한과 연결된다.
대부분의 kubeconfig는 인증서가 base64로 내장(client-certificate-data)되어 있지만, kubelet.conf만 외부 파일 경로(client-certificate)를 참조한다. 이는 kubeadm이 kubelet 인증서 자동 갱신(Certificate Rotation)을 지원하기 때문이다.
참고: Hard Way와의 차이 (클라이언트 인증서)
Hard Way에서는 kubelet kubeconfig도 클라이언트 인증서가 base64로 내장되어 있었다:
- 수동으로 인증서를 생성하고 배포했기 때문에 인증서 자동 갱신 기능이 없었다
- kubeconfig 파일 안에 인증서가 포함되어 있어 갱신 시 파일 전체를 교체해야 한다
kubeadm은 kubelet 클라이언트 인증서를 클러스터 CA로 서명하여 생성하고,
rotateCertificates: true설정으로 만료 전 자동 갱신을 지원한다. 이를 위해 kubeconfig가 외부 파일을 참조하는 구조를 사용한다. 자동 갱신 메커니즘 상세는 kubelet 클라이언트 인증서에서, 서버 인증서는 kubelet 서버 인증서에서 설명한다.
| 파일 | 사용자 | 용도 |
|---|---|---|
admin.conf |
kubernetes-admin (O=kubeadm:cluster-admins) |
kubectl 관리 작업용 |
super-admin.conf |
kubernetes-super-admin (O=system:masters) |
비상 복구용 (RBAC 우회) |
controller-manager.conf |
system:kube-controller-manager |
Controller Manager 전용 |
scheduler.conf |
system:kube-scheduler |
Scheduler 전용 |
kubelet.conf |
system:node:<노드명> |
kubelet 전용 (외부 인증서 참조) |
admin.conf
O=kubeadm:cluster-admins 그룹에 속한 kubernetes-admin 사용자로 인증한다. 일반적인 클러스터 관리 작업에 사용한다.
# 관리자용 kubeconfig
cat /etc/kubernetes/admin.conf
# apiVersion: v1
# clusters:
# - cluster:
# certificate-authority-data: LS0t... ← CA 인증서 (base64)
# server: https://192.168.10.100:6443
# name: kubernetes
# contexts:
# - context:
# cluster: kubernetes
# user: kubernetes-admin
# name: kubernetes-admin@kubernetes
# current-context: kubernetes-admin@kubernetes
# users:
# - name: kubernetes-admin
# user:
# client-certificate-data: LS0t... ← 클라이언트 인증서 (base64)
# client-key-data: LS0t... ← 클라이언트 키 (base64)
super-admin.conf
O=system:masters 그룹에 속한 kubernetes-super-admin 사용자로 인증한다. system:masters는 kube-apiserver에 하드코딩된 특수 그룹으로, RBAC 평가를 거치지 않고 모든 권한이 허용된다. 클러스터 복구 시에만 사용해야 한다.
참고
# 슈퍼 관리자용 kubeconfig (비상 복구용)
cat /etc/kubernetes/super-admin.conf
# apiVersion: v1
# clusters:
# - cluster:
# certificate-authority-data: LS0t...
# server: https://192.168.10.100:6443
# name: kubernetes
# contexts:
# - context:
# cluster: kubernetes
# user: kubernetes-super-admin
# name: kubernetes-super-admin@kubernetes
# current-context: kubernetes-super-admin@kubernetes
# users:
# - name: kubernetes-super-admin
# user:
# client-certificate-data: LS0t...
# client-key-data: LS0t...
controller-manager.conf, scheduler.conf
각 컴포넌트는 자신만의 kubeconfig로 API Server에 인증한다. system:kube-controller-manager와 system:kube-scheduler는 Kubernetes가 부트스트랩 시 생성하는 기본 ClusterRoleBinding으로 필요한 권한을 부여받는다.
# 컴포넌트용 kubeconfig
cat /etc/kubernetes/controller-manager.conf
# users:
# - name: system:kube-controller-manager
# user:
# client-certificate-data: LS0t...
# client-key-data: LS0t...
cat /etc/kubernetes/scheduler.conf
# users:
# - name: system:kube-scheduler
# user:
# client-certificate-data: LS0t...
# client-key-data: LS0t...
kubelet.conf
앞서 언급한 대로, kubelet.conf만 인증서를 외부 파일 경로로 참조한다. kubelet-client-current.pem은 현재 유효한 인증서에 대한 심볼릭 링크로, 인증서 갱신 시 이 링크가 새 인증서를 가리키도록 업데이트된다.
참고: PEM 파일과 kubeconfig의 인증서 형식
PEM 파일은 DER의 Base64 인코딩에 헤더/푸터(
-----BEGIN CERTIFICATE-----)를 추가한 형식이다. 다른 kubeconfig의client-certificate-data는 이 헤더/푸터를 제거한 순수 base64가 들어간 것이다.kubelet은 인증서 갱신 시 kubeconfig 파일 수정 없이 새 PEM 파일 생성 후 심볼릭 링크만 업데이트하면 된다. 반면 다른 kubeconfig(admin.conf, controller-manager.conf 등)는 인증서 갱신 시 파일 내의 base64 값을 직접 교체해야 한다.
# kubelet kubeconfig
cat /etc/kubernetes/kubelet.conf
# contexts:
# - context:
# cluster: kubernetes
# user: system:node:k8s-ctr ← 노드명 포함
# name: system:node:k8s-ctr@kubernetes
# users:
# - name: system:node:k8s-ctr
# user:
# client-certificate: /var/lib/kubelet/pki/kubelet-client-current.pem
# client-key: /var/lib/kubelet/pki/kubelet-client-current.pem
kubelet 인증서 확인
kubelet.conf에서 참조하는 인증서 파일들을 확인한다.
ls -l /var/lib/kubelet/pki
# total 12
# -rw-------. 1 root root 2826 Jan 23 19:41 kubelet-client-2026-01-23-19-41-07.pem
# lrwxrwxrwx. 1 root root 59 Jan 23 19:41 kubelet-client-current.pem -> /var/lib/kubelet/pki/kubelet-client-2026-01-23-19-41-07.pem
# -rw-r--r--. 1 root root 2262 Jan 23 19:41 kubelet.crt
# -rw-------. 1 root root 1679 Jan 23 19:41 kubelet.key
| 파일 | 용도 |
|---|---|
kubelet-client-current.pem |
kubelet → API Server 클라이언트 인증서 (심볼릭 링크) |
kubelet-client-2026-01-23-...pem |
실제 클라이언트 인증서 (자동 갱신 시 새 파일 생성) |
kubelet.crt / kubelet.key |
kubelet 서버 인증서 (API Server → kubelet 접속 시) |
kubelet은 클라이언트 인증서와 서버 인증서 두 가지를 모두 사용한다:
- 클라이언트 인증서 (
kubelet-client-*.pem): kubelet → API Server 접속 시 - 서버 인증서 (
kubelet.crt/key): API Server → kubelet 접속 시
두 인증서의 발급자(Issuer)가 다른데, 아래에서 각각 확인해 보자.
kubelet 서버 인증서
API Server가 kubelet의 /logs, /exec, /attach 등 API에 접속할 때 이 인증서로 kubelet을 검증한다.
cat /var/lib/kubelet/pki/kubelet.crt | openssl x509 -text -noout
# Certificate:
# Data:
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: CN=k8s-ctr-ca@1769164867 ← kubelet 자체 생성 CA
# Validity
# Not Before: Jan 23 09:41:06 2026 GMT
# Not After : Jan 23 09:41:06 2027 GMT
# Subject: CN=k8s-ctr@1769164867
# X509v3 extensions:
# X509v3 Extended Key Usage:
# TLS Web Server Authentication ← 서버 인증서
# X509v3 Subject Alternative Name:
# DNS:k8s-ctr ← 노드 호스트명
- Issuer:
CN=k8s-ctr-ca@...: 클러스터 CA가 아닌 kubelet이 자체 생성한 CA로 서명됨 - SAN:
DNS:k8s-ctr: 노드 호스트명만 포함
참고: 왜 자체 CA로 서명되었나?
kubelet 서버 인증서의 서명자를 결정하는 핵심 설정은
serverTLSBootstrap이다(KubeletConfiguration)
serverTLSBootstrap: true→ 클러스터 CA가 서명 (CSR 승인 필요)serverTLSBootstrap: false(기본값) → kubelet이 자체 CA 생성하여 서명이전 글에서
kubelet-configConfigMap이 클러스터 내 모든 kubelet이 공유할 설정을 담고 있다고 설명했다. 실제 ConfigMap 내용을 확인해보면:kubectl get configmap kubelet-config -n kube-system -o yaml # apiVersion: v1 # data: # kubelet: | # apiVersion: kubelet.config.k8s.io/v1beta1 # ... # rotateCertificates: true # ... # (serverTLSBootstrap 설정 없음)
serverTLSBootstrap설정이 명시되지 않았으므로 기본값false가 적용된다. 이 때문에:
- kubelet이 자체 CA를 생성하여 서버 인증서에 서명
- 클러스터 CA와 독립적으로 서버 인증서 관리
만약
serverTLSBootstrap: true로 설정하면 kubelet이 CSR(Certificate Signing Request)을 API Server에 요청한다. 그러나 kube-controller-manager의 기본 서명자는 자동 승인하지 않으므로, 관리자가kubectl certificate approve로 수동 승인하거나 kubelet-csr-approver 같은 써드파티 컨트롤러가 필요하다.현재 실습에서는 이러한 복잡성을 피하기 위해 kubeadm의 기본 설정(비활성화)을 그대로 사용했다.
다만, 프로덕션 환경에서는 통일된 인증서 관리를 위해
serverTLSBootstrap: true를 고려할 수 있다. 이 경우, CSR 승인 자동화 메커니즘을 함께 구성해야 하므로 운영 복잡도가 증가할 수 있다. 아래 글을 참고해 보자.
kubelet 클라이언트 인증서
kubelet이 API Server에 접속하여 Pod 정보 조회, 상태 보고 등을 수행할 때 이 인증서로 자신을 인증한다.
cat /var/lib/kubelet/pki/kubelet-client-current.pem | openssl x509 -text -noout
# Certificate:
# Data:
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: CN=kubernetes ← 클러스터 CA가 서명
# Validity
# Not Before: Jan 23 10:36:04 2026 GMT
# Not After : Jan 23 10:41:04 2027 GMT
# Subject: O=system:nodes, CN=system:node:k8s-ctr
# X509v3 extensions:
# X509v3 Extended Key Usage:
# TLS Web Client Authentication ← 클라이언트 인증서
- Issuer:
CN=kubernetes: 클러스터 CA가 서명 → API Server가 신뢰함 - Subject:
O=system:nodes, CN=system:node:k8s-ctr: RBAC에서system:nodes그룹과system:node:k8s-ctr사용자로 인식됨
참고: 클라이언트 인증서 자동 갱신 (
rotateCertificates)이전 글의
kubelet-configConfigMap에서rotateCertificates: true가 설정되어 있다. 이 설정으로 인해:
- 인증서 만료가 임박하면 kubelet이 CSR(Certificate Signing Request)을 API Server에 제출
- kube-controller-manager가 자동 승인 후
--cluster-signing-cert-file로 지정된 클러스터 CA로 서명- 새 인증서가
kubelet-client-current.pem에 갱신됨위에서 설명한 대로 kubeadm은 kubelet.conf가 외부 파일을 참조하도록 구성하여 이 자동 갱신을 지원한다.
두 인증서의 차이 요약
| 구분 | kubelet.crt (서버) |
kubelet-client-current.pem (클라이언트) |
|---|---|---|
| 용도 | API Server → kubelet 접속 시 | kubelet → API Server 접속 시 |
| 발급자 | kubelet 자체 CA | 클러스터 CA (kubernetes) |
| Extended Key Usage | Server Authentication | Client Authentication |
| 자동 갱신 | kubelet이 자체 관리 | kubelet이 CSR 요청, 컨트롤러가 승인 |
노드 정보, 인증서, kubeconfig를 확인했다. 다음 글에서는 Static Pod와 필수 애드온(CoreDNS, kube-proxy)을 상세히 확인한다.
이 글을 참조하는 글
- [CS] 리눅스 커널 파라미터와 sysctl
- [EKS] EKS: Public-Public EKS 클러스터 - 5. 워커 노드 내부 확인
- [Kubernetes] Cluster: Kubeadm을 이용해 클러스터 구성하기 - 1.0. 실습 구성도
- [Kubernetes] Cluster: Kubeadm을 이용해 클러스터 구성하기 - 1.3. CRI(containerd) 및 kubeadm 구성 요소 설치
- [Kubernetes] Cluster: Kubeadm을 이용해 클러스터 구성하기 - 1.5. Flannel CNI 설치
- [Kubernetes] Cluster: Kubeadm을 이용해 클러스터 구성하기 - 1.7. Static Pod 및 애드온 확인
- [Kubernetes] Cluster: Kubeadm을 이용해 클러스터 구성하기 - 2.2. kubeadm join 실행
댓글남기기