devlos
Devlos Archive
devlos
전체 방문자
오늘
어제
01-13 10:05

최근 글

  • 분류 전체보기 (108)
    • 프로젝트 (1)
    • MSA 설계 & 도메인주도 설계 (9)
    • 클라우드 컴퓨팅 & NoSQL (88)
      • [K8S Deploy] K8S 디플로이 스터디 (1)
      • [Cilium Study] 실리움 스터디 (8)
      • [KANS] 쿠버네티스 네트워크 심화 스터디 (12)
      • [T101] 테라폼 4기 스터디 (8)
      • [CICD] CICD 맛보기 스터디 (3)
      • [T101] 테라폼 기초 입문 스터디 (6)
      • [AEWS] Amazon EKS 워크숍 스터디 (7)
      • [PKOS] 쿠버네티스 실무 실습 스터디 (7)
      • Kubernetes (13)
      • Docker (7)
      • Redis (1)
      • Jenkins (3)
      • Terraform (1)
      • Ansible (4)
      • Kafka (1)
    • 프로그래밍 (7)
      • Spring Boot (5)
      • Broker (1)
    • 성능과 튜닝 (1)
    • ALM (0)
    • 기타 (2)

인기 글

태그

  • 도커
  • terraform
  • DevOps
  • kOps
  • 테라폼
  • cilium
  • MSA
  • t101 4기
  • 쿠버네티스
  • CloudNet@
  • PKOS
  • 쿠버네티스 스터디
  • 데브옵스
  • Kubernetes
  • docker

티스토리

최근 댓글

hELLO · Designed By 정상우.
devlos

Devlos Archive

[1주차 - K8S Deploy] Bootstrap Kubernetes the hard way (26.01.04)
클라우드 컴퓨팅 & NoSQL/[K8S Deploy] K8S 디플로이 스터디

[1주차 - K8S Deploy] Bootstrap Kubernetes the hard way (26.01.04)

2026. 1. 9. 21:16
반응형

들어가며

안녕하세요! Devlos입니다.

 

2026년 새해가 밝았습니다. 지난 한 해 동안 고생 많으셨고, 새해에는 여러분 모두 건강과 행운이 가득하시길 기원합니다. :)

 

이번 포스팅은 CloudNet@ 커뮤니티에서 주최하는 K8S Deploy 1주 차 주제인

 

"Bootstrap Kubernetes the hardway"에 대해서 정리한 내용입니다.

 

쿠버네티스 학습을 위해 kelseyhightower 님께서 만들어 놓은 실습 과정을 따라하며 쿠버네티스 설치 과정을 이해해 봅니다.

 

출처 - https://github.com/kelseyhightower/kubernetes-the-hard-way

 

GitHub - kelseyhightower/kubernetes-the-hard-way: Bootstrap Kubernetes the hard way. No scripts.

Bootstrap Kubernetes the hard way. No scripts. Contribute to kelseyhightower/kubernetes-the-hard-way development by creating an account on GitHub.

github.com

 


kind 기본 설치 방법

kind로 쿠버네티스 클러스터를 생성하고 상태를 확인하며, 각종 정보 조회 및 클러스터/리소스 삭제 방법을 안내합니다.

kubectl config view


# Create a cluster with kind
kind create cluster

# 클러스터 배포 확인
kind get clusters
# kind

kind get nodes
# kind-control-plane

kubectl cluster-info
# Kubernetes control plane is running at https://127.0.0.1:55995
# CoreDNS is running at https://127.0.0.1:55995/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

# 노드 정보 확인
kubectl get node -o wide
# NAME                 STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION     CONTAINER-RUNTIME
# kind-control-plane   Ready    control-plane   58s   v1.31.0   172.18.0.2    <none>        Debian GNU/Linux 12 (bookworm)   6.11.11-linuxkit   containerd://1.7.18

# 파드 정보 확인
kubectl get pod -A
# NAMESPACE            NAME                                         READY   STATUS    RESTARTS   AGE
# kube-system          coredns-6f6b679f8f-qwgbm                     1/1     Running   0          55s
# kube-system          coredns-6f6b679f8f-w2xg7                     1/1     Running   0          55s
# kube-system          etcd-kind-control-plane                      1/1     Running   0          64s
# kube-system          kindnet-cg7l9                                1/1     Running   0          55s
# kube-system          kube-apiserver-kind-control-plane            1/1     Running   0          63s
# kube-system          kube-controller-manager-kind-control-plane   1/1     Running   0          64s
# kube-system          kube-proxy-98972                             1/1     Running   0          55s
# kube-system          kube-scheduler-kind-control-plane            1/1     Running   0          63s
# local-path-storage   local-path-provisioner-57c5987fd4-7qsbc      1/1     Running   0          55s

kubectl get componentstatuses
# Warning: v1 ComponentStatus is deprecated in v1.19+
# NAME                 STATUS    MESSAGE   ERROR
# controller-manager   Healthy   ok        
# scheduler            Healthy   ok        
# etcd-0               Healthy   ok

# 컨트롤플레인 (컨테이너) 노드 1대가 실행
docker ps
# CONTAINER ID   IMAGE                  COMMAND                  CREATED              STATUS              PORTS                       NAMES
# b1a80d132adb   kindest/node:v1.31.0   "/usr/local/bin/entr…"   About a minute ago   Up About a minute   127.0.0.1:55995->6443/tcp   kind-control-plane

docker images
# REPOSITORY     TAG                IMAGE ID       CREATED         SIZE
# kindest/node   <none>             53df588e0408   16 months ago   1.47GB
# node           20.16.0-bullseye   a81bc5c5f842   17 months ago   1.39GB

# kube config 파일 확인
cat ~/.kube/config
# apiVersion: v1
# clusters:
# - cluster:
#     server: https://192.168.2.145:6443
#   name: kubernetes
# contexts: null
# current-context: ""
# kind: Config
# preferences: {}
# users: null
혹은
cat $KUBECONFIG # KUBECONFIG 변수 지정 사용 시

# nginx 파드 배포 및 확인 : 컨트롤플레인 노드인데 파드가 배포 될까요?
kubectl run nginx --image=nginx:alpine
# pod/nginx created

kubectl get pod -owide
# NAME    READY   STATUS              RESTARTS   AGE   IP       NODE                 NOMINATED NODE   READINESS GATES
# nginx   1/1     Running   0          3s    <none>   kind-control-plane   <none>           <none>

# 노드에 Taints 정보 확인
kubectl describe node | grep Taints
# Taints:             <none>

# 클러스터 삭제
kind delete cluster
# Deleting cluster "kind" ...

# kube config 삭제 확인
cat ~/.kube/config
혹은
cat $KUBECONFIG # KUBECONFIG 변수 지정 사용 시

1주차 실습을 위한 설정 (Hardway와 비교)

컨트롤플레인과 워커 노드가 각각 1개씩 있는 클러스터를 생성하고, 포트 매핑 설정과 네트워크 구성을 확인합니다.

# 기존 클러스터 삭제
kind delete cluster
# Deleting cluster "kind" ...
# Deleted nodes: ["kind-control-plane"]

# 컨트롤플레인 1개, 워커 노드 1개로 클러스터 생성 (포트 매핑 포함)
kind create cluster --name myk8s --image kindest/node:v1.32.8 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
- role: worker
EOF
# Creating cluster "myk8s" ...
#  ✓ Ensuring node image (kindest/node:v1.32.8) 🖼 
#  ✓ Preparing nodes 📦 📦  
#  ✓ Writing configuration 📜 
#  ✓ Starting control-plane 🕹️ 
#  ✓ Installing CNI 🔌 
#  ✓ Installing StorageClass 💾 
#  ✓ Joining worker nodes 🚜 
# Set kubectl context to "kind-myk8s"

# 확인
docker images
# REPOSITORY     TAG                IMAGE ID       CREATED         SIZE
# postgres       16-ko              1391a08854f7   7 weeks ago     161MB
# postgres       16                 21f6013073bc   2 months ago    635MB
# kindest/node   v1.32.8            abd489f042d2   4 months ago    1.51GB
# postgres       17.5               aadf2c0696f5   4 months ago    641MB
# nginx          1.27               6784fb0834aa   8 months ago    279MB
# kindest/node   <none>             53df588e0408   16 months ago   1.47GB
# node           20.16.0-bullseye   a81bc5c5f842   17 months ago   1.39GB

kind get nodes --name myk8s
# myk8s-worker
# myk8s-control-plane

kubens default
# Context "kind-myk8s" modified.
# Active namespace is "default".

# kind 는 별도 도커 네트워크 생성 후 사용 : 예) docker 기본값 172.18.0.0/16 , orbstack 192.168.97.0/24
docker network ls
# NETWORK ID     NAME      DRIVER    SCOPE
# 4be48a389143   bridge    bridge    local
# c281adfc6839   host      host      local
# 3b64544eaecb   kind      bridge    local
# 649d9156c623   none      null      local

docker inspect kind | jq
# [
#   {
#     "Name": "kind",
#     "Id": "3b64544eaecb9a4191f6257355d237d2645447da20341337006f398812bb0171",
#     "Created": "2026-01-05T14:27:26.728827669Z",
#     "Scope": "local",
#     "Driver": "bridge",
#     "EnableIPv4": true,
#     "EnableIPv6": true,
#     "IPAM": {
#       "Driver": "default",
#       "Options": {},
#       "Config": [
#         {
#           "Subnet": "172.18.0.0/16",
#           "Gateway": "172.18.0.1"
#         },
#         {
#           "Subnet": "fc00:f853:ccd:e793::/64",
#           "Gateway": "fc00:f853:ccd:e793::1"
#         }
#       ]
#     },
#     "Containers": {
#       "d7079cf7efd7...": {
#         "Name": "myk8s-worker",
#         "IPv4Address": "172.18.0.2/16",
#         "IPv6Address": "fc00:f853:ccd:e793::2/64"
#       },
#       "f5e90783054f...": {
#         "Name": "myk8s-control-plane",
#         "IPv4Address": "172.18.0.3/16",
#         "IPv6Address": "fc00:f853:ccd:e793::3/64"
#       }
#     }
#   }
# ]

# k8s api 주소 확인 : 어떻게 로컬에서 접속이 되는 걸까?
# kind는 컨트롤 플레인 노드(k8s master)를 Docker 컨테이너로 띄우고,
# k8s API 서버(6443포트)를 컨테이너에서 host의 localhost(127.0.0.1)의 임의의 포트(예: 59425)로 포트포워딩 합니다.
# 그래서 로컬 PC에서 kubectl 등으로 localhost:59425 를 통해 k8s API 서버에 접속 가능합니다.
# (docker ps로 포트맵핑 정보 확인 가능)
# 즉, docker의 port forwarding 기능으로 로컬에서 k8s api에 접속이 가능하다!
kubectl cluster-info
# Kubernetes control plane is running at https://127.0.0.1:59425
# CoreDNS is running at https://127.0.0.1:59425/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

docker ps
# CONTAINER ID   IMAGE                  COMMAND                  CREATED          STATUS          PORTS                                                             NAMES
# d7079cf7efd7   kindest/node:v1.32.8   "/usr/local/bin/entr…"   50 seconds ago   Up 49 seconds                                                                     myk8s-worker
# f5e90783054f   kindest/node:v1.32.8   "/usr/local/bin/entr…"   50 seconds ago   Up 49 seconds   0.0.0.0:30000-30001->30000-30001/tcp, 127.0.0.1:59425->6443/tcp   myk8s-control-plane

# 노드 정보 확인 : CRI 는 containerd 사용
kubectl get node -o wide
# NAME                  STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION     CONTAINER-RUNTIME
# myk8s-control-plane   Ready    control-plane   51s   v1.32.8   172.18.0.3    <none>        Debian GNU/Linux 12 (bookworm)   6.11.11-linuxkit   containerd://2.1.3
# myk8s-worker          Ready    <none>          36s   v1.32.8   172.18.0.2    <none>        Debian GNU/Linux 12 (bookworm)   6.11.11-linuxkit   containerd://2.1.3

# 파드 정보 확인 : CNI 는 kindnet 사용
kubectl get pod -A -o wide
# NAMESPACE            NAME                                          READY   STATUS    RESTARTS   AGE   IP           NODE                  NOMINATED NODE   READINESS GATES
# kube-system          coredns-668d6bf9bc-9q8pg                      1/1     Running   0          45s   10.244.0.4   myk8s-control-plane   <none>           <none>
# kube-system          coredns-668d6bf9bc-fwwjm                      1/1     Running   0          45s   10.244.0.2   myk8s-control-plane   <none>           <none>
# kube-system          etcd-myk8s-control-plane                      1/1     Running   0          54s   172.18.0.3   myk8s-control-plane   <none>           <none>
# kube-system          kindnet-4jh6k                                 1/1     Running   0          45s   172.18.0.3   myk8s-control-plane   <none>           <none>
# kube-system          kindnet-dzxxc                                 1/1     Running   0          40s   172.18.0.2   myk8s-worker          <none>           <none>
# kube-system          kube-apiserver-myk8s-control-plane            1/1     Running   0          53s   172.18.0.3   myk8s-control-plane   <none>           <none>
# kube-system          kube-controller-manager-myk8s-control-plane   1/1     Running   0          53s   172.18.0.3   myk8s-control-plane   <none>           <none>
# kube-system          kube-proxy-mcz5m                              1/1     Running   0          40s   172.18.0.2   myk8s-worker          <none>           <none>
# kube-system          kube-proxy-wzx2h                              1/1     Running   0          45s   172.18.0.3   myk8s-control-plane   <none>           <none>
# kube-system          kube-scheduler-myk8s-control-plane            1/1     Running   0          54s   172.18.0.3   myk8s-control-plane   <none>           <none>
# local-path-storage   local-path-provisioner-7dc846544d-hfk85       1/1     Running   0          45s   10.244.0.3   myk8s-control-plane   <none>           <none>

# 네임스페이스 확인
kubectl get namespaces
# NAME                 STATUS   AGE
# default              Active   58s
# kube-node-lease      Active   58s
# kube-public          Active   58s
# kube-system          Active   58s
# local-path-storage   Active   54s

# 컨트롤플레인/워커 노드(컨테이너) 확인 : 도커 컨테이너 이름은 myk8s-control-plane , myk8s-worker 임을 확인
docker ps
# CONTAINER ID   IMAGE                  COMMAND                  CREATED              STATUS              PORTS                                                             NAMES
# d7079cf7efd7   kindest/node:v1.32.8   "/usr/local/bin/entr…"   About a minute ago   Up About a minute                                                                     myk8s-worker
# f5e90783054f   kindest/node:v1.32.8   "/usr/local/bin/entr…"   About a minute ago   Up About a minute   0.0.0.0:30000-30001->30000-30001/tcp, 127.0.0.1:59425->6443/tcp   myk8s-control-plane

docker images
# REPOSITORY     TAG                IMAGE ID       CREATED         SIZE
# kindest/node   v1.32.8            abd489f042d2   4 months ago    1.51GB
# kindest/node   <none>             53df588e0408   16 months ago   1.47GB
# node           20.16.0-bullseye   a81bc5c5f842   17 months ago   1.39GB

docker exec -it myk8s-control-plane ss -tnlp
# State       Recv-Q      Send-Q           Local Address:Port            Peer Address:Port     Process                                       
# LISTEN      0           4096                 127.0.0.1:2381                 0.0.0.0:*         users:(("etcd",pid=636,fd=15))               
# LISTEN      0           4096                 127.0.0.1:2379                 0.0.0.0:*         users:(("etcd",pid=636,fd=8))                
# LISTEN      0           4096                 127.0.0.1:10249                0.0.0.0:*         users:(("kube-proxy",pid=920,fd=17))         
# LISTEN      0           4096                 127.0.0.1:10248                0.0.0.0:*         users:(("kubelet",pid=707,fd=18))            
# LISTEN      0           4096                 127.0.0.1:10259                0.0.0.0:*         users:(("kube-scheduler",pid=520,fd=3))      
# LISTEN      0           4096                 127.0.0.1:10257                0.0.0.0:*         users:(("kube-controller",pid=528,fd=3))     
# LISTEN      0           4096                 127.0.0.1:38181                0.0.0.0:*         users:(("containerd",pid=105,fd=11))         
# LISTEN      0           4096                172.18.0.3:2379                 0.0.0.0:*         users:(("etcd",pid=636,fd=9))                
# LISTEN      0           4096                172.18.0.3:2380                 0.0.0.0:*         users:(("etcd",pid=636,fd=7))                
# LISTEN      0           4096                         *:10250                      *:*         users:(("kubelet",pid=707,fd=14))            
# LISTEN      0           4096                         *:10256                      *:*         users:(("kube-proxy",pid=920,fd=9))          
# LISTEN      0           4096                         *:6443                       *:*         users:(("kube-apiserver",pid=552,fd=3))

# 디버그용 내용 출력에 ~/.kube/config 권한 인증 로드
kubectl get pod -v6
# I0105 23:46:31.388062   43756 loader.go:402] Config loaded from file:  /Users/devlos/.kube/config-local
# I0105 23:46:31.388399   43756 envvar.go:172] "Feature gate default state" feature="ClientsPreferCBOR" enabled=false
# I0105 23:46:31.388407   43756 envvar.go:172] "Feature gate default state" feature="InformerResourceVersion" enabled=false
# I0105 23:46:31.388409   43756 envvar.go:172] "Feature gate default state" feature="InOrderInformers" enabled=true
# I0105 23:46:31.388411   43756 envvar.go:172] "Feature gate default state" feature="WatchListClient" enabled=false
# I0105 23:46:31.388413   43756 envvar.go:172] "Feature gate default state" feature="ClientsAllowCBOR" enabled=false
# I0105 23:46:31.405117   43756 round_trippers.go:632] "Response" verb="GET" url="https://127.0.0.1:59425/api/v1/namespaces/default/pods?limit=500" status="200 OK" milliseconds=13
# No resources found in default namespace.

# kube config 파일 확인
cat ~/.kube/config
혹은
cat $KUBECONFIG

Kubernetes Hardway 실습

출처 - https://github.com/kelseyhightower/kubernetes-the-hard-way

 

GitHub - kelseyhightower/kubernetes-the-hard-way: Bootstrap Kubernetes the hard way. No scripts.

Bootstrap Kubernetes the hard way. No scripts. Contribute to kelseyhightower/kubernetes-the-hard-way development by creating an account on GitHub.

github.com

 

이 튜토리얼은 쿠버네티스를 설정하는 어려운 과정을 단계별로 안내합니다. 학습에 최적화되어 있으며, 쿠버네티스 클러스터 부트스트랩에 필요한 각 작업을 확실히 이해할 수 있도록 하는 예제입니다.

 

실습을 통해 구성하는 k8s 클러스터 인프라 구조는 다음과 같습니다.

 


Kubernetes Hardway 실습에서 구성하는 클러스터의 전체 아키텍처입니다.

전체 구조

  • 클러스터 네트워크: 192.168.10.0/24 서브넷을 사용하는 Kubernetes 클러스터
  • 노드 구성: 컨트롤플레인 서버 1개, 워커 노드 2개로 구성

컨트롤플레인 서버 (Server - 192.168.10.100)

컨트롤플레인 서버에는 다음 systemd 서비스들이 실행됩니다:

  1. kube-apiserver.service (Port: 6443)
    • Kubernetes API 서버로, 모든 클러스터 통신의 중앙 허브 역할
    • 워커 노드의 kubelet과 통신하여 파드 상태 및 로그 수집
    • etcd, kube-scheduler, kube-controller-manager와 통신
  2. etcd.service (http)
    • 클러스터의 모든 상태 정보를 저장하는 분산 키-값 저장소
    • kube-apiserver가 etcd에 클러스터 상태를 저장/조회
  3. kube-scheduler.service
    • 새로 생성된 파드를 어떤 워커 노드에 배치할지 결정
    • kube-apiserver를 통해 파드 정보를 감시하고 스케줄링 결정
  4. kube-controller-manager.service
    • 클러스터의 다양한 컨트롤러를 실행 (ReplicaSet, Deployment, Node 등)
    • kube-apiserver를 통해 클러스터 상태를 감시하고 조정

워커 노드 (Node-0: 192.168.10.101, Node-1: 192.168.10.102)

각 워커 노드에는 다음 systemd 서비스들이 실행됩니다:

  1. containerd.service
    • 컨테이너 런타임으로, 실제 컨테이너를 생성하고 관리
    • kubelet의 지시에 따라 Pod 내 컨테이너를 실행
  2. kubelet.service (Port: 10250)
    • 노드의 에이전트 역할로, kube-apiserver와 통신
    • kube-apiserver로부터 파드 스펙을 받아 containerd를 통해 컨테이너 실행
    • 파드 상태와 로그를 kube-apiserver에 보고
  3. kube-proxy.service
    • 네트워크 프록시로, 서비스와 파드 간 네트워크 라우팅 담당
    • kube-apiserver를 Watch하여 서비스/엔드포인트 변경사항 감지
  4. Pod (nginx)
    • Node-0: 10.200.0.2 IP를 가진 nginx 파드
    • Node-1: 10.200.1.2 IP를 가진 nginx 파드
    • 각 파드는 containerd에 의해 생성되고 관리됨

통신 흐름

  • kube-apiserver.service ↔ kubelet: 두꺼운 실선으로 표시, Status/Logs 교환
  • kube-proxy.service → kube-apiserver.service: 점선으로 표시, Watch를 통한 변경사항 감지
  • containerd.service → Pod: 점선으로 표시, 컨테이너 생성/관리
  • kube-apiserver.service ↔ etcd/scheduler/controller-manage.service: 내부 통신

kind와 달리 실제 VM이나 물리 서버에서 각 컴포넌트를 systemd 서비스로 직접 실행하는 방식을 보여줍니다.

실습용 가상머신 준비

NAME Description CPU RAM NIC1 NIC2 HOSTNAME
jumpbox Administration host 2 1536 MB 10.0.2.15 192.168.10.10 jumpbox
server Kubernetes server 2 2GB 10.0.2.15 192.168.10.100 server.kubernetes.local server
node-0 Kubernetes worker 2 2GB 10.0.2.15 192.168.10.101 node-0.kubernetes.local node-0
node-1 Kubernetes worker 2 2GB 10.0.2.15 192.168.10.102 node-1.kubernetes.local node-1

01 - Pre requisites

VirtualBox 와 Vagarant를 이용한 실습 환경을 설치합니다.

# VirtualBox 설치
brew install --cask virtualbox

VBoxManage --version
# 7.2.4r170995

# Vagrant 설치
brew install --cask vagrant

vagrant version
# Installed Version: 2.4.9

# 작업용 디렉터리 생성
mkdir k8s-hardway
cd k8s-hardway

# Vagrantfile , init_cfg.sh 파일 다운로드
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-hardway/Vagrantfile
curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/k8s-hardway/init_cfg.sh

# 실습용 가상 머신 배포
vagrant up

# 실습용 OS 이미지 자동 다운로드 확인
vagrant box list
# bento/debian-12    (virtualbox, 202510.26.0, (arm64))

# 배포된 가상머신 확인
vagrant status
# Current machine states:
# jumpbox                   running (virtualbox)
# server                    running (virtualbox)
# node-0                    running (virtualbox)
# node-1                    running (virtualbox)
# This environment represents multiple VMs. The VMs are all listed
# above with their current state. For more information about a specific
# VM, run `vagrant status NAME`.

 

init_cfg.sh

#!/usr/bin/env bash
echo ">>>> Initial Config Start <<<<"

echo "[TASK 1] Setting Profile & Bashrc"
echo "sudo su -" >> /home/vagrant/.bashrc # 로그인 시 자동으로 root 전환
echo 'alias vi=vim' >> /etc/profile # vi 명령어를 vim으로 대체(alias 설정)
ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime # 시스템 타임존을 서울로 변경

echo "[TASK 2] Disable AppArmor"
systemctl stop apparmor && systemctl disable apparmor >/dev/null 2>&1 # AppArmor 보안 모듈 비활성화

echo "[TASK 3] Disable and turn off SWAP"
swapoff -a && sed -i '/swap/s/^/#/' /etc/fstab # SWAP 비활성화 및 fstab에서 주석 처리

echo "[TASK 4] Install Packages"
apt update -qq >/dev/null 2>&1 # 패키지 목록 업데이트(조용하게 실행)
apt install tree git jq yq unzip vim sshpass -y -qq >/dev/null 2>&1 # 주요 유틸리티 패키지 설치

echo "[TASK 5] Setting Root Password"
echo "root:qwe123" | chpasswd # root 계정 비밀번호 설정

echo "[TASK 6] Setting Sshd Config"
cat << EOF >> /etc/ssh/sshd_config # SSH 접속을 위한 설정 추가(비밀번호 로그인 허용 및 root 로그인 허용)
PasswordAuthentication yes
PermitRootLogin yes
EOF
systemctl restart sshd >/dev/null 2>&1 # SSH 서비스 재시작

echo "[TASK 7] Setting Local DNS Using Hosts file"
sed -i '/^127.0.(1|2).1/d' /etc/hosts # 기존 localhost 관련 hosts 라인 제거
cat << EOF >> /etc/hosts # 실습용 호스트 네임 및 IP 등록
192.168.10.10 jumpbox
192.168.10.100 server.kubernetes.local server
192.168.10.101 node-0.kubernetes.local node-0
192.168.10.102 node-1.kubernetes.local node-1
EOF

echo ">>>> Initial Config End <<<<"

 

Vagrantfile


# 주요 명령어 및 옵션 설명

BOX_IMAGE = "bento/debian-12"        # 사용할 Vagrant 박스 이미지 지정
BOX_VERSION = "202510.26.0"          # 사용할 박스의 버전 지정

Vagrant.configure("2") do |config|
  # jumpbox VM 생성 정의
  config.vm.define "jumpbox" do |subconfig|
    subconfig.vm.box = BOX_IMAGE                         # 사용할 박스 이미지
    subconfig.vm.box_version = BOX_VERSION               # 박스 버전 지정
    subconfig.vm.provider "virtualbox" do |vb|
      vb.customize ["modifyvm", :id, "--groups", "/Hardway-Lab"]        # VirtualBox VM을 지정된 그룹에 추가
      vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]      # NIC 2의 프라미스큐어스 모드 허용
      vb.name = "jumpbox"                                 # VirtualBox에서 보여질 VM 이름
      vb.cpus = 2                                        # 할당할 CPU 개수
      vb.memory = 1536                                   # 할당할 메모리(MB)
      vb.linked_clone = true                             # 링크드 클론 옵션으로 빠르게 VM 생성
    end
    subconfig.vm.host_name = "jumpbox"                   # VM 내에서 사용될 호스트네임
    subconfig.vm.network "private_network", ip: "192.168.10.10"         # 프라이빗 네트워크에 지정 IP 할당
    subconfig.vm.network "forwarded_port", guest: 22, host: 60010, auto_correct: true, id: "ssh"  # SSH 포트 포워딩(host→guest)
    subconfig.vm.synced_folder "./", "/vagrant", disabled: true         # vagrant 폴더 동기화 해제
    subconfig.vm.provision "shell", path: "init_cfg.sh"                 # VM 생성 후 실행할 Shell 프로비저닝 스크립트 지정
  end
  # server(컨트롤플레인 노드) VM 생성 정의
  config.vm.define "server" do |subconfig|
    subconfig.vm.box = BOX_IMAGE
    subconfig.vm.box_version = BOX_VERSION
    subconfig.vm.provider "virtualbox" do |vb|
      vb.customize ["modifyvm", :id, "--groups", "/Hardway-Lab"]
      vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
      vb.name = "server"
      vb.cpus = 2
      vb.memory = 2048
      vb.linked_clone = true
    end
    subconfig.vm.host_name = "server"
    subconfig.vm.network "private_network", ip: "192.168.10.100"
    subconfig.vm.network "forwarded_port", guest: 22, host: 60100, auto_correct: true, id: "ssh"
    subconfig.vm.synced_folder "./", "/vagrant", disabled: true
    subconfig.vm.provision "shell", path: "init_cfg.sh"
  end
  # node-0(워커 노드 1) VM 생성 정의
  config.vm.define "node-0" do |subconfig|
    subconfig.vm.box = BOX_IMAGE
    subconfig.vm.box_version = BOX_VERSION
    subconfig.vm.provider "virtualbox" do |vb|
      vb.customize ["modifyvm", :id, "--groups", "/Hardway-Lab"]
      vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
      vb.name = "node-0"
      vb.cpus = 2
      vb.memory = 2048
      vb.linked_clone = true
    end
    subconfig.vm.host_name = "node-0"
    subconfig.vm.network "private_network", ip: "192.168.10.101"
    subconfig.vm.network "forwarded_port", guest: 22, host: 60101, auto_correct: true, id: "ssh"
    subconfig.vm.synced_folder "./", "/vagrant", disabled: true
    subconfig.vm.provision "shell", path: "init_cfg.sh"
  end
  # node-1(워커 노드 2) VM 생성 정의
  config.vm.define "node-1" do |subconfig|
    subconfig.vm.box = BOX_IMAGE
    subconfig.vm.box_version = BOX_VERSION
    subconfig.vm.provider "virtualbox" do |vb|
      vb.customize ["modifyvm", :id, "--groups", "/Hardway-Lab"]
      vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
      vb.name = "node-1"
      vb.cpus = 2
      vb.memory = 2048
      vb.linked_clone = true
    end
    subconfig.vm.host_name = "node-1"
    subconfig.vm.network "private_network", ip: "192.168.10.102"
    subconfig.vm.network "forwarded_port", guest: 22, host: 60102, auto_correct: true, id: "ssh"
    subconfig.vm.synced_folder "./", "/vagrant", disabled: true
    subconfig.vm.provision "shell", path: "init_cfg.sh"
  end
end

 

이제 jumpbox 가상 머신에 접속해봅니다.

 

jumpbox란 일반적으로 "bastion(배스천) 호스트" 또는 "bastion 서버"라고도 부르며, 클러스터의 각 노드(server, node-0, node-1)와 외부를 연결해주는 "관리(운영)용 중간 게이트웨이" 역할을 하는 가상 머신입니다.

 

외부에서 직접 각 노드로 접속하지 않고, jumpbox(=bastion)를 경유하여 보다 안전하게 노드에 접근할 수 있습니다.

# jumpbox 접속
vagrant ssh jumpbox
# Linux jumpbox 6.1.0-40-arm64 #1 SMP Debian 6.1.153-1 (2025-09-20) aarch64
# ...
# 
# root@jumpbox:~# 

whoami
# root

pwd
# /root

# OS version 확인
cat /etc/os-release
# PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
# NAME="Debian GNU/Linux"
# VERSION_ID="12"
# VERSION="12 (bookworm)"
# VERSION_CODENAME=bookworm
# ID=debian

# AppArmor 상태 확인
aa-status
# apparmor module is loaded.
# 10 profiles are loaded.
# 10 profiles are in enforce mode.
#    /usr/bin/man
#    /usr/lib/NetworkManager/nm-dhcp-client.action
#    /usr/lib/NetworkManager/nm-dhcp-helper
#    /usr/lib/connman/scripts/dhclient-script
#    /{,usr/}sbin/dhclient
#    lsb_release
#    man_filter
#    man_groff
#    nvidia_modprobe
#    nvidia_modprobe//kmod
# 0 profiles are in complain mode.
# 0 profiles are in kill mode.
# 0 profiles are in unconfined mode.
# 1 processes have profiles defined.
# 1 processes are in enforce mode.
#    /usr/sbin/dhclient (1164) /{,usr/}sbin/dhclient
# 0 processes are in complain mode.
# 0 processes are unconfined but have a profile defined.
# 0 processes are in mixed mode.
# 0 processes are in kill mode.

systemctl is-active apparmor
# inactive

# /etc/hosts 파일 내용 확인
# https://www.debian.org/doc/manuals/debian-reference/ch05.en.html#_the_hostname_resolution
cat /etc/hosts
# 127.0.0.1       localhost
# 
# # The following lines are desirable for IPv6 capable hosts
# ::1     localhost ip6-localhost ip6-loopback
# ff02::1 ip6-allnodes
# ff02::2 ip6-allrouters
# 192.168.10.10  jumpbox
# 192.168.10.100 server.kubernetes.local server 
# 192.168.10.101 node-0.kubernetes.local node-0
# 192.168.10.102 node-1.kubernetes.local node-1

02 - Set Up The Jumpbox

root 사용자 계정으로 컴퓨터 에 로그인이 자동으로 되어있고, 이 튜토리얼 전반에 걸쳐 다양한 작업을 수행하는 데 사용될 명령줄 유틸리티를 설치합니다.

설치 항목 버전
k8s 관련 (kube-apiserver, kubelet 등) 1.32.2
etcd 3.6.0
containerd 2.1.0
runc 1.3.0
vagrant ssh jumpbox
# root 계정 확인
whoami
# root

## vagrant 계정 로그인 시 'sudo su -' 실행으로 root 계정 전환됨
cat /home/vagrant/.bashrc | tail -n 1
# sudo su -

# 툴 설치 : 이미 적용되어 있음 (초기 설정 스크립트에서 이미 설치됨)
apt-get update && apt install tree git jq yq unzip vim sshpass -y
# Hit:1 http://security.debian.org/debian-security bookworm-security InRelease
# Hit:2 http://httpredir.debian.org/debian bookworm InRelease
# Hit:3 http://httpredir.debian.org/debian bookworm-updates InRelease
# Reading package lists... Done
# tree is already the newest version (2.1.0-1).
# git is already the newest version (1:2.39.5-0+deb12u2).
# jq is already the newest version (1.6-2.1+deb12u1).
# yq is already the newest version (3.1.0-3).
# unzip is already the newest version (6.0-28).
# vim is already the newest version (2:9.0.1378-2+deb12u2).
# sshpass is already the newest version (1.09-1).
# 0 upgraded, 0 newly installed, 0 to remove and 2 not upgraded.

# Sync GitHub Repository
pwd
# /root

git clone --depth 1 https://github.com/kelseyhightower/kubernetes-the-hard-way.git
cd kubernetes-the-hard-way
tree
# .
# ├── ca.conf
# ├── configs
# │   ├── 10-bridge.conf
# │   ├── 99-loopback.conf
# │   ├── containerd-config.toml
# │   ├── encryption-config.yaml
# │   ├── kube-apiserver-to-kubelet.yaml
# │   ├── kubelet-config.yaml
# │   ├── kube-proxy-config.yaml
# │   └── kube-scheduler.yaml
# ├── CONTRIBUTING.md
# ├── COPYRIGHT.md
# ├── docs
# │   ├── 01-prerequisites.md
# │   ├── 02-jumpbox.md
# │   ├── 03-compute-resources.md
# │   ├── 04-certificate-authority.md
# │   ├── 05-kubernetes-configuration-files.md
# │   ├── 06-data-encryption-keys.md
# │   ├── 07-bootstrapping-etcd.md
# │   ├── 08-bootstrapping-kubernetes-controllers.md
# │   ├── 09-bootstrapping-kubernetes-workers.md
# │   ├── 10-configuring-kubectl.md
# │   ├── 11-pod-network-routes.md
# │   ├── 12-smoke-test.md
# │   └── 13-cleanup.md
# ├── downloads-amd64.txt
# ├── downloads-arm64.txt
# ├── LICENSE
# ├── README.md
# └── units
#     ├── containerd.service
#     ├── etcd.service
#     ├── kube-apiserver.service
#     ├── kube-controller-manager.service
#     ├── kubelet.service
#     ├── kube-proxy.service
#     └── kube-scheduler.service
#
# 4 directories, 35 files

# Download Binaries : k8s 구성을 위한 컴포넌트 다운로드

# CPU 아키텍처 확인 (macOS는 arm64, Windows/Linux는 amd64)
dpkg --print-architecture
# arm64   # macOS 사용자
# amd64   # Windows 사용자

# CPU 아키텍처 별 다운로드 목록 파일 확인
ls -l downloads-*
# -rw-r--r-- 1 root root 839 Jan  6 01:21 downloads-amd64.txt
# -rw-r--r-- 1 root root 839 Jan  6 01:21 downloads-arm64.txt

# https://kubernetes.io/releases/download/
cat downloads-$(dpkg --print-architecture).txt
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kubectl
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kube-apiserver
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kube-controller-manager
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kube-scheduler
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kube-proxy
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kubelet
# https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.32.0/crictl-v1.32.0-linux-arm64.tar.gz
# https://github.com/opencontainers/runc/releases/download/v1.3.0-rc.1/runc.arm64
# https://github.com/containernetworking/plugins/releases/download/v1.6.2/cni-plugins-linux-arm64-v1.6.2.tgz
# https://github.com/containerd/containerd/releases/download/v2.1.0-beta.0/containerd-2.1.0-beta.0-linux-arm64.tar.gz
# https://github.com/etcd-io/etcd/releases/download/v3.6.0-rc.3/etcd-v3.6.0-rc.3-linux-arm64.tar.gz

# wget 으로 다운로드 실행 : 약 500MB 크기 (Kubernetes 바이너리, containerd, etcd, CNI 플러그인 등)
wget -q --show-progress \
  --https-only \
  --timestamping \
  -P downloads \
  -i downloads-$(dpkg --print-architecture).txt
# kubectl                                                                                         100%[================================>]  53.25M  34.1MB/s    in 1.6s    
# kube-apiserver                                                                                  100%[================================>]  86.06M  37.2MB/s    in 2.3s    
# kube-controller-manager                                                                         100%[================================>]  79.56M  36.0MB/s    in 2.2s    
# kube-scheduler                                                                                  100%[================================>]  61.25M  33.1MB/s    in 1.8s    
# kube-proxy                                                                                      100%[================================>]  62.25M  38.0MB/s    in 1.6s    
# kubelet                                                                                         100%[================================>]  71.75M  44.9MB/s    in 1.6s    
# crictl-v1.32.0-linux-arm64.tar.gz                                                               100%[================================>]  16.98M  18.0MB/s    in 0.9s    
# runc.arm64                                                                                      100%[================================>]  10.78M  25.0MB/s    in 0.4s    
# cni-plugins-linux-arm64-v1.6.2.tgz                                                              100%[================================>]  47.17M  15.7MB/s    in 3.0s    
# containerd-2.1.0-beta.0-linux-arm64.tar.gz                                                      100%[================================>]  33.60M  17.6MB/s    in 1.9s    
# etcd-v3.6.0-rc.3-linux-arm64.tar.gz                                                             100%[================================>]  20.87M  14.0MB/s    in 1.5s

# 다운로드된 파일 확인
ls downloads
# cni-plugins-linux-arm64-v1.6.2.tgz  containerd-2.1.0-beta.0-linux-arm64.tar.gz  crictl-v1.32.0-linux-arm64.tar.gz  etcd-v3.6.0-rc.3-linux-arm64.tar.gz  kube-apiserver  kube-controller-manager  kubectl  kubelet  kube-proxy  kube-scheduler  runc.arm64

# 아키텍처 변수 설정
ARCH=$(dpkg --print-architecture)
echo $ARCH
# arm64

# 바이너리 파일을 용도별로 분류할 디렉토리 생성
# - client: kubectl, etcdctl (클라이언트 도구)
# - controller: etcd, kube-apiserver, kube-controller-manager, kube-scheduler (컨트롤플레인 컴포넌트)
# - worker: containerd, crictl, ctr, kubelet, kube-proxy, runc (워커 노드 컴포넌트)
# - cni-plugins: CNI 네트워크 플러그인
mkdir -p downloads/{client,cni-plugins,controller,worker}
tree -d downloads
# downloads
# ├── client
# ├── cni-plugins
# ├── controller
# └── worker
#
# 5 directories

# 압축 풀기: 워커 노드용 바이너리 (crictl, containerd)
tar -xvf downloads/crictl-v1.32.0-linux-${ARCH}.tar.gz \
  -C downloads/worker/
# crictl

tar -xvf downloads/containerd-2.1.0-beta.0-linux-${ARCH}.tar.gz \
  --strip-components 1 \
  -C downloads/worker/
# bin/containerd-shim-runc-v2
# bin/containerd
# bin/containerd-stress
# bin/ctr

# CNI 플러그인 압축 해제
tar -xvf downloads/cni-plugins-linux-${ARCH}-v1.6.2.tgz \
  -C downloads/cni-plugins/
# ./bandwidth
# ./bridge
# ./dhcp
# ./loopback
# ./portmap
# ... (총 20개 파일)

# etcd 압축 해제: --strip-components 1 옵션으로 디렉토리 구조 제거
# etcd-v3.6.0-rc.3-linux-arm64/etcd → etcd 로 직접 추출
tar -xvf downloads/etcd-v3.6.0-rc.3-linux-${ARCH}.tar.gz \
  -C downloads/ \
  --strip-components 1 \
  etcd-v3.6.0-rc.3-linux-${ARCH}/etcdctl \
  etcd-v3.6.0-rc.3-linux-${ARCH}/etcd

# 압축 해제 확인
tree downloads/worker/
# downloads/worker/
# ├── containerd
# ├── containerd-shim-runc-v2
# ├── containerd-stress
# ├── crictl
# └── ctr
#
# 1 directory, 5 files

tree downloads/cni-plugins
# downloads/cni-plugins
# ├── bandwidth
# ├── bridge
# ├── dhcp
# ├── dummy
# ├── firewall
# ├── host-device
# ├── host-local
# ├── ipvlan
# ├── LICENSE
# ├── loopback
# ├── macvlan
# ├── portmap
# ├── ptp
# ├── README.md
# ├── sbr
# ├── static
# ├── tap
# ├── tuning
# ├── vlan
# └── vrf
#
# 1 directory, 20 files

ls -l downloads/{etcd,etcdctl}
# -rwxr-xr-x 1 vagrant vagrant 24314008 Mar 28  2025 downloads/etcd
# -rwxr-xr-x 1 vagrant vagrant 15925400 Mar 28  2025 downloads/etcdctl

# 파일 이동: 다운로드된 바이너리를 용도별 디렉토리로 분류
mv downloads/{etcdctl,kubectl} downloads/client/
mv downloads/{etcd,kube-apiserver,kube-controller-manager,kube-scheduler} downloads/controller/
mv downloads/{kubelet,kube-proxy} downloads/worker/
mv downloads/runc.${ARCH} downloads/worker/runc

# 파일 이동 확인
tree downloads/client/
# downloads/client/
# ├── etcdctl
# └── kubectl
#
# 1 directory, 2 files

tree downloads/controller/
# downloads/controller/
# ├── etcd
# ├── kube-apiserver
# ├── kube-controller-manager
# └── kube-scheduler
#
# 1 directory, 4 files

tree downloads/worker/
# downloads/worker/
# ├── containerd
# ├── containerd-shim-runc-v2
# ├── containerd-stress
# ├── crictl
# ├── ctr
# ├── kubelet
# ├── kube-proxy
# └── runc
#
# 1 directory, 8 files

# 불필요한 압축 파일 제거 (압축 해제 후 원본 tar.gz 파일 삭제)
ls -l downloads/*gz
# -rw-r--r-- 1 root root 49466083 Jan  7  2025 downloads/cni-plugins-linux-arm64-v1.6.2.tgz
# -rw-r--r-- 1 root root 35229532 Mar 18  2025 downloads/containerd-2.1.0-beta.0-linux-arm64.tar.gz
# -rw-r--r-- 1 root root 17805231 Dec  9  2024 downloads/crictl-v1.32.0-linux-arm64.tar.gz
# -rw-r--r-- 1 root root 21884730 Mar 28  2025 downloads/etcd-v3.6.0-rc.3-linux-arm64.tar.gz

rm -rf downloads/*gz

# 모든 바이너리에 실행 권한 부여
chmod +x downloads/{client,cni-plugins,controller,worker}/*

# 실행 권한 부여 확인 (일부 출력)
ls -l downloads/{client,cni-plugins,controller,worker}/* | head -10
# -rwxr-xr-x 1 vagrant vagrant 15925400 Mar 28  2025 downloads/client/etcdctl
# -rwxr-xr-x 1 root    root    55836824 Mar 12  2025 downloads/client/kubectl
# -rwxr-xr-x 1 root    root     4600029 Jan  7  2025 downloads/cni-plugins/bandwidth
# -rwxr-xr-x 1 root    root     5186762 Jan  7  2025 downloads/cni-plugins/bridge
# ...

# 일부 파일 소유자 변경 (etcd, etcdctl, crictl은 vagrant 소유였으나 root로 변경)
# 압축 해제 시 일부 파일이 vagrant 소유로 생성될 수 있음
tree -ug downloads | grep -E "(vagrant|1001)"
# [vagrant  vagrant ]  etcdctl  (client/)
# [vagrant  vagrant ]  etcd  (controller/)
# [1001     127     ]  crictl  (worker/)

chown root:root downloads/client/etcdctl
chown root:root downloads/controller/etcd
chown root:root downloads/worker/crictl

# 소유자 변경 확인
tree -ug downloads | grep -E "(vagrant|1001)"
# (결과 없음 - 모든 파일이 root 소유로 변경됨)

# Kubernetes 클라이언트 도구인 kubectl 설치 (PATH에 추가)
ls -l downloads/client/kubectl
# -rwxr-xr-x 1 root root 55836824 Mar 12  2025 downloads/client/kubectl

cp downloads/client/kubectl /usr/local/bin/

# kubectl 버전 확인으로 설치 검증
kubectl version --client
# Client Version: v1.32.3
# Kustomize Version: v5.5.0

 


03 - Provisioning Compute Resources: SSH 접속 환경 설정

# Machine Database (서버 속성 저장 파일)
# 형식 : IPV4_ADDRESS FQDN HOSTNAME POD_SUBNET
## 참고) server(controlplane)는 kubelet 동작하지 않아 파드 네트워크 대역(마지막 필드) 불필요
cat <<EOF > machines.txt
192.168.10.100 server.kubernetes.local server
192.168.10.101 node-0.kubernetes.local node-0 10.200.0.0/24
192.168.10.102 node-1.kubernetes.local node-1 10.200.1.0/24
EOF

cat machines.txt               # 머신 정보 파일 내용 확인
# 192.168.10.100 server.kubernetes.local server
# 192.168.10.101 node-0.kubernetes.local node-0 10.200.0.0/24
# 192.168.10.102 node-1.kubernetes.local node-1 10.200.1.0/24

# 머신 DB 내용을 순회(출력 예시)
while read IP FQDN HOST SUBNET; do
  echo "${IP} ${FQDN} ${HOST} ${SUBNET}"
done < machines.txt
# 192.168.10.100 server.kubernetes.local server 
# 192.168.10.101 node-0.kubernetes.local node-0 10.200.0.0/24
# 192.168.10.102 node-1.kubernetes.local node-1 10.200.1.0/24

# === SSH 비밀번호/키 인증 환경 준비 ===

# (1) sshd 설정 파일 확인 => 비밀번호 로그인 및 root 로그인 허용 여부 확인
grep "^[^#]" /etc/ssh/sshd_config
# Include /etc/ssh/sshd_config.d/*.conf
# KbdInteractiveAuthentication no
# UsePAM yes
# X11Forwarding yes
# PrintMotd no
# AcceptEnv LANG LC_*
# Subsystem       sftp    /usr/lib/openssh/sftp-server
# UseDNS no
# GSSAPIAuthentication no
# PasswordAuthentication yes
# PermitRootLogin yes

# (2) 루트 계정에서 새로운 SSH 키 쌍 생성(패스프레이즈 없음)
ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa
# Generating public/private rsa key pair.
# Your identification has been saved in /root/.ssh/id_rsa
# Your public key has been saved in /root/.ssh/id_rsa.pub
# The key fingerprint is:
# SHA256:EPrkd5qBbewP4YlfmAIOAFrMWhWLYt5pIkC1Bq+7+dU root@jumpbox

ls -l /root/.ssh            # 키 파일 생성 확인
# total 8
# -rw------- 1 root root 2602 Jan  6 01:39 id_rsa
# -rw-r--r-- 1 root root  566 Jan  6 01:39 id_rsa.pub

# (3) 각 노드에 SSH 공개키 배포 (sshpass 활용, 키 등록)
while read IP FQDN HOST SUBNET; do
  sshpass -p 'qwe123' ssh-copy-id -o StrictHostKeyChecking=no root@${IP}
done < machines.txt
# /usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/root/.ssh/id_rsa.pub"
# /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
# /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
# Number of key(s) added: 1
# ... (각 노드별로 동일한 메시지 출력)

# (4) authorized_keys 작성 결과 확인 (노드별로 루트 계정에 키가 들어갔는지 체크)
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} cat /root/.ssh/authorized_keys
done < machines.txt
# ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC3fY6Y/5RbqjmLGOULUsiydMJHaZlKMEosoRduZivvR7Iuhf4ogQvrWzH69U/Yrb62WMYq2XYVoxAaBmeifvNbNMgGEYcrvI6XzQ1wV3q0AKgQwbgzz8whuPbmhlVqbqmxi1Rn/n5SQU/4EvdiR+duIUKMT6Nv8LCk00UALBzp3q00LiMlMBbVCNnzAwfay1dGYMsp+1W7GOTjXCOa5VP+I8qlyxNYd7CiV5MAvCmQ3X51u6P41jltp+8LcLNy/f7w1Ycd8MC7nUDvVCzV7ZMTksQmwiAgAnIzyvg4AaKO2LvZJPJbg25xm5/GxW6RXrxPntF70Wpu4s4iyzpFb4jgBeDOoNmowckL3oseVsDO1uwGQJULkU6zT0SK2chPgB/O4ggjhU02JbYZKNGArgFqTjle2q2Qqus3BlqXTN7R3JrJ5nTE2TB0YA4aU+tZoLfhCZSDIrk20lbWJzQZChYxcp8lRFsdTXXQ7Bb8VXTPq3grw+jgtqft4YXyhfw6eUE= root@jumpbox
# ... (각 노드별로 동일한 공개키 출력)

# (5) SSH 공개키 인증으로 root 접속이 가능한지 IP 기준으로 간단 테스트 (각 노드의 hostname 반환)
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} hostname
done < machines.txt
# server
# node-0
# node-1

# === Hostname/호스트네임 관련 추가 확인 ===

# (6) /etc/hosts 파일이 각 노드에 올바로 배포/설정되어 있는지(init_cfg.sh에서 처리됨) 각 노드에서 hosts 파일 확인
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} cat /etc/hosts
done < machines.txt
# 127.0.0.1       localhost
# ::1     localhost ip6-localhost ip6-loopback
# ff02::1 ip6-allnodes
# ff02::2 ip6-allrouters
# 192.168.10.10  jumpbox
# 192.168.10.100 server.kubernetes.local server 
# 192.168.10.101 node-0.kubernetes.local node-0
# 192.168.10.102 node-1.kubernetes.local node-1
# ... (각 노드별로 동일한 내용 출력)

# (7) FQDN 기준으로 각 노드의 hostname --fqdn 출력 확인
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} hostname --fqdn
done < machines.txt
# server.kubernetes.local
# node-0.kubernetes.local
# node-1.kubernetes.local

# (8) (로컬서버 기준) /etc/hosts 확인
cat /etc/hosts
# 127.0.0.1       localhost
# ::1     localhost ip6-localhost ip6-loopback
# ff02::1 ip6-allnodes
# ff02::2 ip6-allrouters
# 192.168.10.10  jumpbox
# 192.168.10.100 server.kubernetes.local server 
# 192.168.10.101 node-0.kubernetes.local node-0
# 192.168.10.102 node-1.kubernetes.local node-1

# (9) Hostname(단축) 기반 ssh 연결 테스트 - hostname 반환되는지 확인(StrictHostKeyChecking=no 사용)
while read IP FQDN HOST SUBNET; do
  sshpass -p 'qwe123' ssh -n -o StrictHostKeyChecking=no root@${HOST} hostname
done < machines.txt
# Warning: Permanently added 'server' (ED25519) to the list of known hosts.
# server
# Warning: Permanently added 'node-0' (ED25519) to the list of known hosts.
# node-0
# Warning: Permanently added 'node-1' (ED25519) to the list of known hosts.
# node-1

# (10) Hostname 기반 ssh + 여러 시스템 정보 출력 테스트(uname -o -m -n)
while read IP FQDN HOST SUBNET; do
  sshpass -p 'qwe123' ssh -n root@${HOST} uname -o -m -n
done < machines.txt
# server aarch64 GNU/Linux
# node-0 aarch64 GNU/Linux
# node-1 aarch64 GNU/Linux

04 - Provisioning a CA and Generating TLS Certificates

이 섹션에서는 Kubernetes 클러스터에서 사용할 자체 서명 인증 기관(CA)과 필요한 TLS 인증서들을 생성합니다. 이를 위해 openssl용 설정 파일(ca.conf)을 활용해 각 구성요소(admin, 각 노드, kube-proxy 등)에 대한 인증서 생성 정보를 미리 정의합니다.

주요 절차는 다음과 같습니다.

  1. ca.conf 파일을 통해 인증서에 필요한 정보(CN, O, SAN 등)를 미리 설정
  2. openssl을 이용해 Root CA의 개인키와 인증서 생성
  3. admin, 각 노드, kube-proxy 등 구성요소별 개인키와 CSR, 인증서 생성

 

이름 개인키 파일 CSR 파일 인증서 파일 주요 CN/O/SAN 정보
Root CA ca.key X ca.crt Root CA 용도
admin admin.key admin.csr admin.crt CN=admin, O=system:masters
node-0 node-0.key node-0.csr node-0.crt CN=system:node:node-0, O=system:nodes
node-1 node-1.key node-1.csr node-1.crt CN=system:node:node-1, O=system:nodes
kube-proxy kube-proxy.key kube-proxy.csr kube-proxy.crt CN=system:kube-proxy, O=system:node-proxier
kube-scheduler kube-scheduler.key kube-scheduler.csr kube-scheduler.crt CN=system:kube-scheduler, O=system:kube-scheduler
kube-controller-manager kube-controller-manager.key kube-controller-manager.csr kube-controller-manager.crt CN=system:kube-controller-manager, O=system:kube-controller-manager
kube-api-server kube-api-server.key kube-api-server.csr kube-api-server.crt CN=kubernetes, SAN: IP(127.0.0.1, 10.32.0.1), DNS(kubernetes, ...)
service-accounts service-accounts.key service-accounts.csr service-accounts.crt CN=service-accounts
항목 네트워크 대역 or IP
clusterCIDR 10.200.0.0/16
└─ node-0 PodCIDR 10.200.0.0/24
└─ node-1 PodCIDR 10.200.1.0/24
ServiceCIDR 10.32.0.0/24
└─ api clusterIP 10.32.0.1

cat ca.conf 파일 내용 확인

구분 역할
[req] OpenSSL 요청 기본 동작
[ca_*] CA 인증서
[admin] 관리자 (kubectl)
[service-accounts] ServiceAccount 토큰 서명
[node-*] 워커 노드(kubelet)
[kube-proxy] kube-proxy
[kube-controller-manager] 컨트롤러
[kube-scheduler] 스케줄러
[kube-api-server] API Server
[default_req_extensions] 공통 CSR 옵션

 

ca.conf 파일은 이 튜토리얼에서 직접 만들어서 사용합니다.

 

각 주요 컴포넌트(Root CA, 관리자(admin), 각 워커 노드(node-0, node-1), 컨트롤러, 스케줄러, kube-proxy, 서비스 계정 등)에 대해

ca.conf 파일 내에 별도의 섹션([admin], [node-0], ...)을 만들어 인증서 설정을 구분해서 관리하도록 구성합니다.

 

각 섹션에는 보통 다음과 같은 정보가 포함됩니다:

  • distinguished_name (DN), CN(Common Name), O(Organization), SAN(Subject Alternative Name), 인증서 용도(클라이언트/서버), keyUsage(디지털 서명, 키 암호화), 필요에 따라 FQDN/내부 IP 등
  • Pod/Service CIDR, 각 노드의 이름, 클러스터의 내부 DNS가 SAN에 포함됨

전체적으로 주요 인증서 생성 규칙은 이렇습니다:

  • Root CA: 클러스터 전체를 위한 신뢰의 근원(CA) 역할, CA 서명 및 CRL 발급 가능
  • admin: 클러스터 관리자가 사용하는 클라이언트 인증서 (system:masters 그룹)
  • 각 워커 노드: NodeAuthorizer를 위해 system:nodes 그룹과 system:node:<노드이름> 형식의 CN 사용
  • kube-proxy, controller-manager, scheduler, service-accounts: 각 역할별로 구분된 인증서와 관련 그룹명 명시
  • kube-api-server: 클러스터 내부 DNS, 컨트롤플레인 호스트명(IP/DNS 등)이 SAN에 포함되며, client/server 인증 모두 지원
# ca.conf 파일 내용 확인

cat ca.conf
# === Root CA 설정 ===
# [req]
# distinguished_name = req_distinguished_name  # DN(Distinguished Name) 섹션 지정
# prompt             = no                       # 대화형 프롬프트 비활성화
# x509_extensions    = ca_x509_extensions       # CA 인증서 확장 옵션 지정
# 
# [ca_x509_extensions]
# basicConstraints = CA:TRUE                   # 이 인증서가 CA(Certificate Authority)임을 명시
# keyUsage         = cRLSign, keyCertSign      # CRL 서명 및 인증서 서명 용도로 사용
# 
# [req_distinguished_name]                      # CA 인증서의 DN 정보
# C   = US                                      # Country (국가)
# ST  = Washington                              # State (주/도)
# L   = Seattle                                 # Locality (도시)
# CN  = CA                                      # Common Name (인증 기관 이름)
# 
# === Admin (kubectl 클라이언트) 설정 ===
# [admin]
# distinguished_name = admin_distinguished_name # Admin DN 섹션 지정
# prompt             = no                       # 대화형 프롬프트 비활성화
# req_extensions     = default_req_extensions   # 기본 확장 옵션 사용
# 
# [admin_distinguished_name]
# CN = admin                                    # Common Name: admin
# O  = system:masters                           # Organization: Kubernetes의 masters 그룹 (클러스터 관리 권한)
# 
# === Service Accounts 설정 ===
# # Service Accounts
# #
# # The Kubernetes Controller Manager leverages a key pair to generate
# # and sign service account tokens as described in the
# # [managing service accounts](https://kubernetes.io/docs/admin/service-accounts-admin/)
# # documentation.
# # Kubernetes Controller Manager가 ServiceAccount 토큰을 생성하고 서명하기 위해 사용하는 키 쌍
# 
# [service-accounts]
# distinguished_name = service-accounts_distinguished_name
# prompt             = no
# req_extensions     = default_req_extensions   # 기본 확장 옵션 사용 (clientAuth)
# 
# [service-accounts_distinguished_name]
# CN = service-accounts                        # Common Name: service-accounts
# 
# === Worker Nodes 설정 ===
# # Worker Nodes
# #
# # Kubernetes uses a [special-purpose authorization mode](https://kubernetes.io/docs/admin/authorization/node/)
# # called Node Authorizer, that specifically authorizes API requests made
# # by [Kubelets](https://kubernetes.io/docs/concepts/overview/components/#kubelet).
# # In order to be authorized by the Node Authorizer, Kubelets must use a credential
# # that identifies them as being in the `system:nodes` group, with a username
# # of `system:node:<nodeName>`.
# # Node Authorizer는 kubelet의 API 요청을 인증하기 위해 사용되며,
# # kubelet은 `system:nodes` 그룹의 `system:node:<nodeName>` 형식의 인증서를 사용해야 함
# 
# [node-0]
# distinguished_name = node-0_distinguished_name
# prompt             = no
# req_extensions     = node-0_req_extensions
# 
# [node-0_req_extensions]
# basicConstraints     = CA:FALSE              # CA가 아님을 명시
# extendedKeyUsage     = clientAuth, serverAuth # 클라이언트/서버 인증 용도
# keyUsage             = critical, digitalSignature, keyEncipherment  # 디지털 서명 및 키 암호화
# nsCertType           = client                # 클라이언트 인증서
# nsComment            = "Node-0 Certificate"
# subjectAltName       = DNS:node-0, IP:127.0.0.1  # SAN: DNS 이름 및 로컬 IP
# subjectKeyIdentifier = hash                   # 주체 키 식별자 해시
# 
# [node-0_distinguished_name]
# CN = system:node:node-0                      # Common Name: Node Authorizer 인증 형식
# O  = system:nodes                            # Organization: nodes 그룹
# C  = US
# ST = Washington
# L  = Seattle
# 
# [node-1]
# distinguished_name = node-1_distinguished_name
# prompt             = no
# req_extensions     = node-1_req_extensions
# 
# [node-1_req_extensions]
# basicConstraints     = CA:FALSE
# extendedKeyUsage     = clientAuth, serverAuth
# keyUsage             = critical, digitalSignature, keyEncipherment
# nsCertType           = client
# nsComment            = "Node-1 Certificate"
# subjectAltName       = DNS:node-1, IP:127.0.0.1
# subjectKeyIdentifier = hash
# 
# [node-1_distinguished_name]
# CN = system:node:node-1                      # Common Name: Node Authorizer 인증 형식
# O  = system:nodes                            # Organization: nodes 그룹
# C  = US
# ST = Washington
# L  = Seattle
# 
# 
# === Kube Proxy 설정 ===
# # Kube Proxy Section
# [kube-proxy]
# distinguished_name = kube-proxy_distinguished_name
# prompt             = no
# req_extensions     = kube-proxy_req_extensions
# 
# [kube-proxy_req_extensions]
# basicConstraints     = CA:FALSE
# extendedKeyUsage     = clientAuth, serverAuth
# keyUsage             = critical, digitalSignature, keyEncipherment
# nsCertType           = client
# nsComment            = "Kube Proxy Certificate"
# subjectAltName       = DNS:kube-proxy, IP:127.0.0.1
# subjectKeyIdentifier = hash
# 
# [kube-proxy_distinguished_name]
# CN = system:kube-proxy                      # Common Name: kube-proxy 시스템 사용자
# O  = system:node-proxier                    # Organization: node-proxier 그룹
# C  = US
# ST = Washington
# L  = Seattle
# 
# 
# === Controller Manager 설정 ===
# # Controller Manager
# [kube-controller-manager]
# distinguished_name = kube-controller-manager_distinguished_name
# prompt             = no
# req_extensions     = kube-controller-manager_req_extensions
# 
# [kube-controller-manager_req_extensions]
# basicConstraints     = CA:FALSE
# extendedKeyUsage     = clientAuth, serverAuth
# keyUsage             = critical, digitalSignature, keyEncipherment
# nsCertType           = client
# nsComment            = "Kube Controller Manager Certificate"
# subjectAltName       = DNS:kube-controller-manager, IP:127.0.0.1
# subjectKeyIdentifier = hash
# 
# [kube-controller-manager_distinguished_name]
# CN = system:kube-controller-manager         # Common Name: kube-controller-manager 시스템 사용자
# O  = system:kube-controller-manager         # Organization: kube-controller-manager 그룹
# C  = US
# ST = Washington
# L  = Seattle
# 
# 
# === Scheduler 설정 ===
# # Scheduler
# [kube-scheduler]
# distinguished_name = kube-scheduler_distinguished_name
# prompt             = no
# req_extensions     = kube-scheduler_req_extensions
# 
# [kube-scheduler_req_extensions]
# basicConstraints     = CA:FALSE
# extendedKeyUsage     = clientAuth, serverAuth
# keyUsage             = critical, digitalSignature, keyEncipherment
# nsCertType           = client
# nsComment            = "Kube Scheduler Certificate"
# subjectAltName       = DNS:kube-scheduler, IP:127.0.0.1
# subjectKeyIdentifier = hash
# 
# [kube-scheduler_distinguished_name]
# CN = system:kube-scheduler                  # Common Name: kube-scheduler 시스템 사용자
# O  = system:system:kube-scheduler           # Organization: kube-scheduler 그룹
# C  = US
# ST = Washington
# L  = Seattle
# 
# 
# === API Server 설정 ===
# # API Server
# #
# # The Kubernetes API server is automatically assigned the `kubernetes`
# # internal dns name, which will be linked to the first IP address (`10.32.0.1`)
# # from the address range (`10.32.0.0/24`) reserved for internal cluster
# # services.
# # Kubernetes API 서버는 자동으로 `kubernetes` 내부 DNS 이름이 할당되며,
# # 이는 클러스터 내부 서비스를 위한 주소 범위(`10.32.0.0/24`)의 첫 번째 IP(`10.32.0.1`)에 연결됨
# 
# [kube-api-server]
# distinguished_name = kube-api-server_distinguished_name
# prompt             = no
# req_extensions     = kube-api-server_req_extensions
# 
# [kube-api-server_req_extensions]
# basicConstraints     = CA:FALSE
# extendedKeyUsage     = clientAuth, serverAuth      # 클라이언트/서버 인증 모두 지원 (API 서버는 양방향 통신)
# keyUsage             = critical, digitalSignature, keyEncipherment
# nsCertType           = client, server              # 클라이언트 및 서버 인증서 (다른 컴포넌트와 달리 서버도 포함)
# nsComment            = "Kube API Server Certificate"
# subjectAltName       = @kube-api-server_alt_names  # SAN 참조 (아래 alt_names 섹션 참조)
# subjectKeyIdentifier = hash
# 
# [kube-api-server_alt_names]                      # Subject Alternative Names (SAN)
# IP.0  = 127.0.0.1                                 # 로컬호스트 IP
# IP.1  = 10.32.0.1                                 # ServiceCIDR의 첫 번째 IP (kubernetes Service ClusterIP)
# DNS.0 = kubernetes                                # 기본 Kubernetes 서비스 DNS 이름
# DNS.1 = kubernetes.default                        # default 네임스페이스의 kubernetes 서비스
# DNS.2 = kubernetes.default.svc                    # 서비스 FQDN
# DNS.3 = kubernetes.default.svc.cluster            # 클러스터 도메인 포함
# DNS.4 = kubernetes.svc.cluster.local              # 전체 FQDN
# DNS.5 = server.kubernetes.local                   # 컨트롤플레인 서버 호스트명
# DNS.6 = api-server.kubernetes.local               # API 서버 별칭
# 
# [kube-api-server_distinguished_name]
# CN = kubernetes                                  # Common Name: kubernetes (API 서버의 기본 DNS 이름)
# C  = US
# ST = Washington
# L  = Seattle
# 
# 
# === 기본 확장 옵션 (default_req_extensions) ===
# admin, service-accounts 등에서 사용되는 공통 CSR 확장 옵션
# [default_req_extensions]
# basicConstraints     = CA:FALSE                  # CA가 아님
# extendedKeyUsage     = clientAuth                # 클라이언트 인증 용도만 (서버 인증 제외)
# keyUsage             = critical, digitalSignature, keyEncipherment
# nsCertType           = client                    # 클라이언트 인증서
# nsComment            = "Admin Client Certificate"
# subjectKeyIdentifier = hash

Certification Authority

모든 인증 기관은 개인 키와 루트 인증서로 시작합니다. 체 서명 인증 기관을 생성해 봅니다.

# Root CA 개인키 생성 : ca.key
# Root CA 개인키 생성 : ca.key (4096비트 RSA 키)
openssl genrsa -out ca.key 4096

ls -l ca.key
# -rw------- 1 root root 3272 Jan  8 01:37 ca.key
# 권한: 600 (소유자만 읽기/쓰기 가능, 보안상 중요)

# Root CA 인증서 생성 : ca.crt
## -x509 : CSR을 만들지 않고 바로 인증서(X.509) 생성, 즉, Self-Signed Certificate
## -new : 새로운 인증서 요청 생성
## -sha512 : SHA-512 해시 알고리즘 사용
## -noenc : 개인키를 암호화하지 않음, 즉, CA 키(ca.key)에 패스프레이즈 없음
## -key ca.key : 사용할 개인키 파일
## -days 3653 : 인증서 유효기간 10년 (3653일)
## -config ca.conf : 인증서 세부 정보는 설정 파일에서 읽음 
## [req] 섹션 사용됨
## DN 정보 → [req_distinguished_name] 
## CA 확장 → [ca_x509_extensions]
openssl req -x509 -new -sha512 -noenc \
  -key ca.key -days 3653 \
  -config ca.conf \
  -out ca.crt

ls -l ca.crt
# -rw-r--r-- 1 root root 1899 Jan  8 01:37 ca.crt
# 권한: 644 (모든 사용자 읽기 가능, 공개 인증서)

# 인증서 내용 확인 (PEM 형식)
cat ca.crt
# -----BEGIN CERTIFICATE-----
# MIIFTDCCAzSgAwIBAgIUKSZDmHAveOeaxzRjesH7fSFH1r0wDQYJKoZIhvcNAQEN
# BQAwQTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcM
# B1NlYXR0bGUxCzAJBgNVBAMMAkNBMB4XDTI2MDEwNzE2MzcxMVoXDTM2MDEwODE2
# MzcxMVowQTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNV
# BAcMB1NlYXR0bGUxCzAJBgNVBAMMAkNBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
# MIICCgKCAgEA2SEbpcvZM8RVEAQTKNnuYqc5g7vVMoiAK9T05CIhm9OY6IYl1lN/
# 7jqXDMdrC7DT2kAB2CUvSaUdWa8fhbAZBXh+c0r7zN4E04sQVJR+l43KUSrwUyw6
# lN+k2cNqp8y8SKRkkCamhU2C/CMl/h82nE43qE5HKixIuTI/bi7ww5wHbJoq8PH1
# 93wPPYXBwC1vnH/ro09xI1YjEdrWnSmzOdWoYWGLlHVKSUkDt/udvig+8a48idbt
# Hx8nxOD8L6kVig5UzByYtu7G0EyT3zC6dYRAI8kCQnr/m2Z8Oa1ZADGldwuQzFFf
# 5ER5YF6Py71PiBwtbphbkrOirYwWgyJZUhHuMOmOHMae1A0BPQXpNzkDNonDLxLw
# lljbXj+NHiZl0yN3+7nhPSXdl7z+W5Iik+95R+ik0xIUG2DPe0AaJ/FyMHAmyRKm
# eFwaYOWZIkJXmiiT5tmfHXEliHKZM4/+JBnExc6KpoTXNoAOjKJSKFYoAg4lIwCL
# Xi17ZESJSiF2REYq7wgCclcU8vdpUTBFzEd3rLmB2K9ODkwkPGLFqC/9KZ+YIAHb
# 1UVPH2WeaWg53b22dFCspovFkR18/uMHyYRax2gkvBuaMY1IRWayHXdzLs51ylHL
# 2GwtNaRj3slK8KOHJsHTEi7SQlBx+7ostpuyoCIwTgf2sKQ/NLzMQ+UCAwEAAaM8
# MDowDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFIpzc4InWMCq
# RADNA6qkyRVhEFkOMA0GCSqGSIb3DQEBDQUAA4ICAQDM4dCm3cbxspfOsMkoNdSO
# mCp/g3qvuiTvJv2mcC6n09ibknMcHlT5MGHbXhgcSFC97RZaWQ+rJsxygF9I20+Y
# vCO9Li+ndkXOKhQPCSBU7OLLdTpRAguLD7YczO8j/pQKmVhseUwsDSW4shkyDj/X
# HC+6ioJ2wifM0VLKen2G6LUl4oER5QDx2zcthf3W5PmFEAb6Zq9sByssC4zBbiBS
# MDRItRY4UnhFYlJO2VhyBR6biagW5lEXTquwHZWMlFRSlFD/Fxenxe071W+1r4a9
# 5SHWvsBbvHfZOungbJN2vCXrMBtWFu3ppYwMAG4Xjeb0RD+OhjkSVIpHDiaCPgZi
# aR/29nmqh58O30Hmh9SklKnAkc8j/1l/ewSauF4yz9YPdK/U2BKHWOd2E3brFmxX
# y2+IDKQogH7bQBjgwTACE2XJDP3A4gicm5KYwdYoizxDv0qWxGAYnyXLX15xYXLl
# b2lZINXYBYOsewUp28JlADoaOlv0RKiZz8NqIWnJFa+nGUi4aPVXbHzNbvPnnil3
# npwSR20jdivF47eH9RYvEL7NJuZtT7TsUwmy1iCFXm66XzQg0XRi6Q+CoHtTKF2V
# QrQ9G752Y2COnSbBNSj9ejGm3vkklmb86fAu1YJU9egZn6gmvN2q92TcJ0tCtjd7
# WVz9ew/uSn1PVVR8bSws6w==
# -----END CERTIFICATE-----

# 인증서 상세 정보 확인 (텍스트 형식)
openssl x509 -in ca.crt -text -noout
# Certificate:
#     Data:
#         Version: 3 (0x2)                        # X.509 v3 인증서
#         Serial Number:
#             29:26:43:98:70:2f:78:e7:9a:c7:34:63:7a:c1:fb:7d:21:47:d6:bd
#         Signature Algorithm: sha512WithRSAEncryption  # SHA-512 with RSA 서명 알고리즘
#         Issuer: C = US, ST = Washington, L = Seattle, CN = CA  # 발급자 (자체 서명)
#         Validity
#             Not Before: Jan  7 16:37:11 2026 GMT  # 유효기간 시작
#             Not After : Jan  8 16:37:11 2036 GMT  # 유효기간 종료 (10년)
#         Subject: C = US, ST = Washington, L = Seattle, CN = CA  # 주체 (Issuer와 동일, Self-Signed)
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (4096 bit)            # 4096비트 RSA 공개키
#                 Modulus:
#                     00:d9:21:1b:a5:cb:d9:33:c4:55:10:04:13:28:d9:
#                     ... (공개키 모듈러스 값)
#                 Exponent: 65537 (0x10001)        # 공개 지수
#         X509v3 extensions:
#             X509v3 Basic Constraints: 
#                 CA:TRUE                          # CA 인증서임을 명시
#             X509v3 Key Usage: 
#                 Certificate Sign, CRL Sign        # 인증서 서명 및 CRL 서명 용도
#             X509v3 Subject Key Identifier: 
#                 8A:73:73:82:27:58:C0:AA:44:00:CD:03:AA:A4:C9:15:61:10:59:0E
#     Signature Algorithm: sha512WithRSAEncryption
#     Signature Value:
#         cc:e1:d0:a6:dd:c6:f1:b2:97:ce:b0:c9:28:35:d4:8e:98:2a:
#         ... (서명 값)

Create Client and Server Certificates: admin

admin 인증서는 kubectl 클라이언트가 Kubernetes API 서버에 인증하기 위해 사용하는 클라이언트 인증서입니다.
이 인증서는 system:masters 그룹에 속하여 클러스터 관리 권한을 가집니다.

system:masters 그룹이란?

  • Kubernetes RBAC(Role-Based Access Control) 시스템에서 정의된 특별한 그룹입니다
  • 이 그룹에 속한 사용자는 클러스터의 모든 리소스에 대한 모든 권한을 자동으로 부여받습니다
  • 별도의 Role이나 RoleBinding을 설정하지 않아도 슈퍼유저(superuser) 권한을 가집니다
  • 인증서의 O (Organization) 필드에 system:masters를 설정하면, Kubernetes API 서버가 이를 그룹으로 인식합니다
  • 운영 환경에서는 보안상 이 그룹 사용을 최소화하고, 필요한 권한만 부여하는 것이 권장됩니다

인증서 생성 과정:

  1. 개인키 생성 (admin.key)
  2. CSR(Certificate Signing Request) 생성 (admin.csr)
  3. CA로 CSR 서명하여 인증서 생성 (admin.crt)
# Step 1: Admin 개인키 생성 (4096비트 RSA)
openssl genrsa -out admin.key 4096

ls -l admin.key
# -rw------- 1 root root 3268 Jan  8 01:48 admin.key
# 권한: 600 (소유자만 읽기/쓰기 가능)

# Step 2: CSR(Certificate Signing Request) 생성
## -new : 새로운 인증서 요청 생성
## -key admin.key : 사용할 개인키
## -sha256 : SHA-256 해시 알고리즘 사용
## -config ca.conf : 설정 파일 지정
## -section admin : ca.conf 파일의 [admin] 섹션 사용
##   → [admin_distinguished_name]에서 CN=admin, O=system:masters 정보 읽음
##   → [default_req_extensions]에서 클라이언트 인증서 확장 옵션 적용
openssl req -new -key admin.key -sha256 \
  -config ca.conf -section admin \
  -out admin.csr

ls -l admin.csr
# -rw-r--r-- 1 root root 1830 Jan  8 01:48 admin.csr

# Step 3: CSR 내용 확인
openssl req -in admin.csr -text -noout
# Certificate Request:
#     Data:
#         Version: 1 (0x0)
#         Subject: CN = admin, O = system:masters  # 주체 정보: admin 사용자, system:masters 그룹
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (4096 bit)            # 4096비트 RSA 공개키
#                 Modulus:
#                     00:96:2d:1d:15:3a:7d:3a:56:20:92:44:5a:00:a5:
#                     ... (공개키 모듈러스 값)
#                 Exponent: 65537 (0x10001)
#         Attributes:
#             Requested Extensions:
#                 X509v3 Basic Constraints: 
#                     CA:FALSE                     # CA가 아님
#                 X509v3 Extended Key Usage: 
#                     TLS Web Client Authentication # 클라이언트 인증 용도
#                 X509v3 Key Usage: critical
#                     Digital Signature, Key Encipherment  # 디지털 서명 및 키 암호화
#                 Netscape Cert Type: 
#                     SSL Client                   # SSL 클라이언트 인증서
#                 Netscape Comment: 
#                     Admin Client Certificate
#                 X509v3 Subject Key Identifier: 
#                     A6:9B:42:0A:3F:B8:71:04:3E:A7:C3:22:BA:09:14:39:2D:3C:2B:9B
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         6f:38:8c:e5:ba:a6:be:9d:3f:1f:69:41:c6:a0:a3:ff:b2:79:
#         ... (서명 값)

# Step 4: CA로 CSR 서명하여 인증서 생성
## -req : CSR을 입력으로 받음
## -days 3653 : 인증서 유효기간 10년
## -in admin.csr : 입력 CSR 파일
## -copy_extensions copyall : CSR의 모든 확장 옵션을 인증서에 복사
## -sha256 : SHA-256 해시 알고리즘 사용
## -CA ca.crt : CA 인증서
## -CAkey ca.key : CA 개인키
## -CAcreateserial : CA 시리얼 번호 파일 자동 생성 (ca.srl)
## -out admin.crt : 출력 인증서 파일
openssl x509 -req -days 3653 -in admin.csr \
  -copy_extensions copyall \
  -sha256 -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -out admin.crt
# Certificate request self-signature ok
# subject=CN = admin, O = system:masters

ls -l admin.crt
# -rw-r--r-- 1 root root 2021 Jan  8 01:48 admin.crt

# Step 5: 생성된 인증서 상세 정보 확인
openssl x509 -in admin.crt -text -noout
# Certificate:
#     Data:
#         Version: 3 (0x2)                        # X.509 v3 인증서
#         Serial Number:
#             22:7a:39:e8:ca:02:11:b5:89:10:f2:82:a9:4e:ad:4c:95:d6:2f:16
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = US, ST = Washington, L = Seattle, CN = CA  # CN(Common Name)은 인증서의 주체 이름(여기서는 인증기관 이름, CA)을 의미함
#         Validity
#             Not Before: Jan  7 16:48:48 2026 GMT  # 유효기간 시작
#             Not After : Jan  8 16:48:48 2036 GMT  # 유효기간 종료 (10년)
#         Subject: CN = admin, O = system:masters   # 주체: admin 사용자, system:masters 그룹
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (4096 bit)            # 4096비트 RSA 공개키
#                 Modulus:
#                     00:96:2d:1d:15:3a:7d:3a:56:20:92:44:5a:00:a5:
#                     ... (공개키 모듈러스 값)
#                 Exponent: 65537 (0x10001)
#         X509v3 extensions:
#             X509v3 Basic Constraints: 
#                 CA:FALSE                          # CA가 아님
#             X509v3 Extended Key Usage: 
#                 TLS Web Client Authentication     # TLS 클라이언트 인증 용도
#             X509v3 Key Usage: critical
#                 Digital Signature, Key Encipherment
#             Netscape Cert Type: 
#                 SSL Client                        # SSL 클라이언트 인증서
#             Netscape Comment: 
#                 Admin Client Certificate
#             X509v3 Subject Key Identifier: 
#                 A6:9B:42:0A:3F:B8:71:04:3E:A7:C3:22:BA:09:14:39:2D:3C:2B:9B
#             X509v3 Authority Key Identifier:      # CA의 키 식별자 (인증서 체인 확인용)
#                 8A:73:73:82:27:58:C0:AA:44:00:CD:03:AA:A4:C9:15:61:10:59:0E
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         11:ce:c0:d9:a8:d3:22:e1:08:3f:9e:e9:fa:ad:0d:a2:2e:55:
#         ... (CA가 서명한 서명 값)

# 중요: Authority Key Identifier가 CA의 Subject Key Identifier와 일치하여
#       인증서가 올바르게 CA에 의해 서명되었음을 확인할 수 있음

system:masters 그룹은 쿠버네티스에서 내장된 슈퍼유저 그룹으로, 여기에 속한 사용자는 인증만 되면 인가(RBAC/Webhook 등) 검사를 거치지 않고 API 서버의 모든 요청이 허용됩니다.

즉, 클러스터 전체에 대한 무제한 관리자 권한을 가지므로, 가능한 사용을 제한해야 하며 인증서가 탈취될 경우 심각한 보안 위험이 발생할 수 있습니다.
실제 운영에서는최소한의 용도로만 사용하고, 일반 계정에는 cluster-admin 역할을 부여하는 방식(RBAC)으로 사용해야 합니다.

Create Client and Server Certificates

Kubernetes 클러스터의 각 구성요소(node, kube-proxy, kube-scheduler, kube-controller-manager, kube-api-server, service-accounts)에 대한 TLS 인증서를 일괄 생성합니다.

각 인증서의 용도:

  • node-0, node-1: 워커 노드의 Kubelet이 API 서버와 통신할 때 사용
  • kube-proxy: kube-proxy가 API 서버와 통신할 때 사용
  • kube-scheduler: 스케줄러가 API 서버와 통신할 때 사용
  • kube-controller-manager: 컨트롤러 매니저가 API 서버와 통신할 때 사용
  • kube-api-server: API 서버의 서버 인증서 (클라이언트가 API 서버를 인증할 때 사용)
  • service-accounts: Service Account 토큰 서명에 사용
# Step 1: ca.conf 파일에서 kube-scheduler 설정 확인
# (설정 파일에 오타가 있을 수 있으므로 확인)
cat ca.conf | grep system:kube-scheduler
# CN = system:kube-scheduler
# O  = system:system:kube-scheduler  # 오타 발견: system:system: → system: 으로 수정 필요

# Step 2: ca.conf 파일의 오타 수정
# system:system:kube-scheduler → system:kube-scheduler
sed -i 's/system:system:kube-scheduler/system:kube-scheduler/' ca.conf

# Step 3: 수정 확인
cat ca.conf | grep system:kube-scheduler
# CN = system:kube-scheduler
# O  = system:kube-scheduler  # 수정 완료

# Step 4: 생성할 인증서 목록 정의
certs=(
  "node-0" "node-1"
  "kube-proxy" "kube-scheduler"
  "kube-controller-manager"
  "kube-api-server"
  "service-accounts"
)

# Step 5: 인증서 목록 확인
echo ${certs[*]}
# node-0 node-1 kube-proxy kube-scheduler kube-controller-manager kube-api-server service-accounts

# Step 6: 각 인증서를 일괄 생성
# 각 인증서에 대해:
#   1. 개인키 생성 (4096비트 RSA)
#   2. CSR 생성 (ca.conf의 해당 섹션 사용)
#   3. CA로 CSR 서명하여 인증서 생성
for i in ${certs[*]}; do
  openssl genrsa -out "${i}.key" 4096

  openssl req -new -key "${i}.key" -sha256 \
    -config "ca.conf" -section ${i} \
    -out "${i}.csr"

  openssl x509 -req -days 3653 -in "${i}.csr" \
    -copy_extensions copyall \
    -sha256 -CA "ca.crt" \
    -CAkey "ca.key" \
    -CAcreateserial \
    -out "${i}.crt"
done
# Certificate request self-signature ok
# subject=CN = system:node:node-0, O = system:nodes, C = US, ST = Washington, L = Seattle
# Certificate request self-signature ok
# subject=CN = system:node:node-1, O = system:nodes, C = US, ST = Washington, L = Seattle
# Certificate request self-signature ok
# subject=CN = system:kube-proxy, O = system:node-proxier, C = US, ST = Washington, L = Seattle
# Certificate request self-signature ok
# subject=CN = system:kube-scheduler, O = system:kube-scheduler, C = US, ST = Washington, L = Seattle
# Certificate request self-signature ok
# subject=CN = system:kube-controller-manager, O = system:kube-controller-manager, C = US, ST = Washington, L = Seattle
# Certificate request self-signature ok
# subject=CN = kubernetes, C = US, ST = Washington, L = Seattle
# Certificate request self-signature ok
# subject=CN = service-accounts

# Step 7: 생성된 인증서, 키, CSR 파일 목록 확인
ls -1 *.crt *.key *.csr
# admin.crt
# admin.csr
# admin.key
# ca.crt
# ca.key
# kube-api-server.crt
# kube-api-server.csr
# kube-api-server.key
# kube-controller-manager.crt
# kube-controller-manager.csr
# kube-controller-manager.key
# kube-proxy.crt
# kube-proxy.csr
# kube-proxy.key
# kube-scheduler.crt
# kube-scheduler.csr
# kube-scheduler.key
# node-0.crt
# node-0.csr
# node-0.key
# node-1.crt
# node-1.csr
# node-1.key
# service-accounts.crt
# service-accounts.csr
# service-accounts.key

# Step 8: 각 인증서의 상세 정보 확인

# node-0 인증서 확인
openssl x509 -in node-0.crt -text -noout
# Certificate:
#     Data:
#         Version: 3 (0x2)
#         Serial Number:
#             22:7a:39:e8:ca:02:11:b5:89:10:f2:82:a9:4e:ad:4c:95:d6:2f:17
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = US, ST = Washington, L = Seattle, CN = CA
#         Validity
#             Not Before: Jan  7 17:02:22 2026 GMT
#             Not After : Jan  8 17:02:22 2036 GMT
#         Subject: CN = system:node:node-0, O = system:nodes, C = US, ST = Washington, L = Seattle
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (4096 bit)
#                 Modulus:
#                     00:ba:6a:f3:9e:6f:c5:b6:70:ea:67:0a:66:f7:62:
#                     ... (바이너리 데이터 생략)
#                 Exponent: 65537 (0x10001)
#         X509v3 extensions:
#             X509v3 Basic Constraints: 
#                 CA:FALSE
#             X509v3 Extended Key Usage: 
#                 TLS Web Client Authentication, TLS Web Server Authentication
#             X509v3 Key Usage: critical
#                 Digital Signature, Key Encipherment
#             Netscape Cert Type: 
#                 SSL Client
#             Netscape Comment: 
#                 Node-0 Certificate
#             X509v3 Subject Alternative Name: 
#                 DNS:node-0, IP Address:127.0.0.1
#             X509v3 Subject Key Identifier: 
#                 90:45:BD:BF:8E:12:4E:0D:6C:66:11:2C:19:BA:1B:9E:EC:9E:80:2C
#             X509v3 Authority Key Identifier: 
#                 8A:73:73:82:27:58:C0:AA:44:00:CD:03:AA:A4:C9:15:61:10:59:0E
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         b5:b3:ae:5f:30:b3:fd:ee:1b:55:b8:55:54:a0:67:09:d5:0c:
#         ... (바이너리 데이터 생략)

# node-1 인증서 확인
openssl x509 -in node-1.crt -text -noout
# Certificate:
#     Data:
#         Version: 3 (0x2)
#         Serial Number:
#             22:7a:39:e8:ca:02:11:b5:89:10:f2:82:a9:4e:ad:4c:95:d6:2f:18
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = US, ST = Washington, L = Seattle, CN = CA
#         Validity
#             Not Before: Jan  7 17:02:23 2026 GMT
#             Not After : Jan  8 17:02:23 2036 GMT
#         Subject: CN = system:node:node-1, O = system:nodes, C = US, ST = Washington, L = Seattle
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (4096 bit)
#                 Modulus:
#                     00:f7:8e:bb:5b:55:b8:d4:a4:4c:d1:dd:ff:5e:ad:
#                     ... (바이너리 데이터 생략)
#                 Exponent: 65537 (0x10001)
#         X509v3 extensions:
#             X509v3 Basic Constraints: 
#                 CA:FALSE
#             X509v3 Extended Key Usage: 
#                 TLS Web Client Authentication, TLS Web Server Authentication
#             X509v3 Key Usage: critical
#                 Digital Signature, Key Encipherment
#             Netscape Cert Type: 
#                 SSL Client
#             Netscape Comment: 
#                 Node-1 Certificate
#             X509v3 Subject Alternative Name: 
#                 DNS:node-1, IP Address:127.0.0.1
#             X509v3 Subject Key Identifier: 
#                 FD:44:5D:91:65:4F:C6:EB:6D:71:73:54:06:2B:60:C5:03:F8:C3:03
#             X509v3 Authority Key Identifier: 
#                 8A:73:73:82:27:58:C0:AA:44:00:CD:03:AA:A4:C9:15:61:10:59:0E
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         cc:ef:57:78:47:c2:fc:9a:2b:55:bf:ae:2b:ab:00:38:a3:8c:
#         ... (바이너리 데이터 생략)

# kube-proxy 인증서 확인
openssl x509 -in kube-proxy.crt -text -noout
# Certificate:
#     Data:
#         Version: 3 (0x2)
#         Serial Number:
#             22:7a:39:e8:ca:02:11:b5:89:10:f2:82:a9:4e:ad:4c:95:d6:2f:19
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = US, ST = Washington, L = Seattle, CN = CA
#         Validity
#             Not Before: Jan  7 17:02:23 2026 GMT
#             Not After : Jan  8 17:02:23 2036 GMT
#         Subject: CN = system:kube-proxy, O = system:node-proxier, C = US, ST = Washington, L = Seattle
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (4096 bit)
#                 Modulus:
#                     00:c6:93:28:0d:7e:46:8d:ca:90:b1:f4:06:ee:92:
#                     ... (바이너리 데이터 생략)
#                 Exponent: 65537 (0x10001)
#         X509v3 extensions:
#             X509v3 Basic Constraints: 
#                 CA:FALSE
#             X509v3 Extended Key Usage: 
#                 TLS Web Client Authentication, TLS Web Server Authentication
#             X509v3 Key Usage: critical
#                 Digital Signature, Key Encipherment
#             Netscape Cert Type: 
#                 SSL Client
#             Netscape Comment: 
#                 Kube Proxy Certificate
#             X509v3 Subject Alternative Name: 
#                 DNS:kube-proxy, IP Address:127.0.0.1
#             X509v3 Subject Key Identifier: 
#                 25:04:E3:0B:E8:CA:ED:1C:F6:82:D1:51:3E:75:1B:45:0B:09:A8:20
#             X509v3 Authority Key Identifier: 
#                 8A:73:73:82:27:58:C0:AA:44:00:CD:03:AA:A4:C9:15:61:10:59:0E
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         28:64:7c:85:26:05:49:72:cf:35:19:ea:89:0d:1b:b9:f2:37:
#         ... (바이너리 데이터 생략)

# kube-scheduler 인증서 확인
openssl x509 -in kube-scheduler.crt -text -noout
# Certificate:
#     Data:
#         Version: 3 (0x2)
#         Serial Number:
#             22:7a:39:e8:ca:02:11:b5:89:10:f2:82:a9:4e:ad:4c:95:d6:2f:1a
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = US, ST = Washington, L = Seattle, CN = CA
#         Validity
#             Not Before: Jan  7 17:02:24 2026 GMT
#             Not After : Jan  8 17:02:24 2036 GMT
#         Subject: CN = system:kube-scheduler, O = system:kube-scheduler, C = US, ST = Washington, L = Seattle
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (4096 bit)
#                 Modulus:
#                     00:ad:fd:20:26:d3:c3:b4:04:af:92:6d:37:d5:8a:
#                     ... (바이너리 데이터 생략)
#                 Exponent: 65537 (0x10001)
#         X509v3 extensions:
#             X509v3 Basic Constraints: 
#                 CA:FALSE
#             X509v3 Extended Key Usage: 
#                 TLS Web Client Authentication, TLS Web Server Authentication
#             X509v3 Key Usage: critical
#                 Digital Signature, Key Encipherment
#             Netscape Cert Type: 
#                 SSL Client
#             Netscape Comment: 
#                 Kube Scheduler Certificate
#             X509v3 Subject Alternative Name: 
#                 DNS:kube-scheduler, IP Address:127.0.0.1
#             X509v3 Subject Key Identifier: 
#                 4C:60:2B:F6:6C:EE:83:47:31:68:DD:8C:9D:44:35:F9:C0:7E:BE:CD
#             X509v3 Authority Key Identifier: 
#                 8A:73:73:82:27:58:C0:AA:44:00:CD:03:AA:A4:C9:15:61:10:59:0E
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         49:9c:0d:fb:66:1a:55:ee:77:10:3b:a1:b5:67:11:25:2f:54:
#         ... (바이너리 데이터 생략)

# kube-controller-manager 인증서 확인
openssl x509 -in kube-controller-manager.crt -text -noout
# Certificate:
#     Data:
#         Version: 3 (0x2)
#         Serial Number:
#             22:7a:39:e8:ca:02:11:b5:89:10:f2:82:a9:4e:ad:4c:95:d6:2f:1b
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = US, ST = Washington, L = Seattle, CN = CA
#         Validity
#             Not Before: Jan  7 17:02:25 2026 GMT
#             Not After : Jan  8 17:02:25 2036 GMT
#         Subject: CN = system:kube-controller-manager, O = system:kube-controller-manager, C = US, ST = Washington, L = Seattle
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (4096 bit)
#                 Modulus:
#                     00:ee:9e:ec:f3:cc:73:3e:9b:d8:1f:b3:e8:22:99:
#                     ... (바이너리 데이터 생략)
#                 Exponent: 65537 (0x10001)
#         X509v3 extensions:
#             X509v3 Basic Constraints: 
#                 CA:FALSE
#             X509v3 Extended Key Usage: 
#                 TLS Web Client Authentication, TLS Web Server Authentication
#             X509v3 Key Usage: critical
#                 Digital Signature, Key Encipherment
#             Netscape Cert Type: 
#                 SSL Client
#             Netscape Comment: 
#                 Kube Controller Manager Certificate
#             X509v3 Subject Alternative Name: 
#                 DNS:kube-controller-manager, IP Address:127.0.0.1
#             X509v3 Subject Key Identifier: 
#                 D6:4E:9B:4A:DC:B4:F5:C2:FB:A2:17:1F:AE:5B:0E:A5:57:BC:78:51
#             X509v3 Authority Key Identifier: 
#                 8A:73:73:82:27:58:C0:AA:44:00:CD:03:AA:A4:C9:15:61:10:59:0E
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         63:2a:44:be:c4:fc:88:52:dd:0e:47:19:23:db:bf:ea:31:10:
#         ... (바이너리 데이터 생략)

# kube-api-server 인증서 확인 (여러 SAN 포함, SSL Server 인증 지원[타 인증서들에는 없음])
openssl x509 -in kube-api-server.crt -text -noout
# Certificate:
#     Data:
#         Version: 3 (0x2)
#         Serial Number:
#             22:7a:39:e8:ca:02:11:b5:89:10:f2:82:a9:4e:ad:4c:95:d6:2f:1c
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = US, ST = Washington, L = Seattle, CN = CA
#         Validity
#             Not Before: Jan  7 17:02:27 2026 GMT
#             Not After : Jan  8 17:02:27 2036 GMT
#         Subject: CN = kubernetes, C = US, ST = Washington, L = Seattle
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (4096 bit)
#                 Modulus:
#                     00:d3:cc:f9:76:2c:5d:4c:46:db:17:0c:b3:64:1e:
#                     ... (바이너리 데이터 생략)
#                 Exponent: 65537 (0x10001)
#         X509v3 extensions:
#             X509v3 Basic Constraints: 
#                 CA:FALSE
#             X509v3 Extended Key Usage: 
#                 TLS Web Client Authentication, TLS Web Server Authentication
#             X509v3 Key Usage: critical
#                 Digital Signature, Key Encipherment
#             Netscape Cert Type: 
#                 SSL Client, SSL Server  # 클라이언트 및 서버 인증 모두 지원
#             Netscape Comment: 
#                 Kube API Server Certificate
#             X509v3 Subject Alternative Name:  # API 서버는 여러 이름/IP로 접근 가능해야 함(SAN)
#                 IP Address:127.0.0.1, IP Address:10.32.0.1,  # localhost 및 Service CIDR 첫 IP
#                 DNS:kubernetes,  # 기본 서비스 이름
#                 DNS:kubernetes.default,  # default 네임스페이스의 서비스
#                 DNS:kubernetes.default.svc,  # 서비스 FQDN
#                 DNS:kubernetes.default.svc.cluster,  # 클러스터 도메인 포함
#                 DNS:kubernetes.svc.cluster.local,  # 전체 FQDN
#                 DNS:server.kubernetes.local,  # 호스트명
#                 DNS:api-server.kubernetes.local  # 별칭
#             X509v3 Subject Key Identifier: 
#                 16:05:49:17:7C:EA:A1:E6:09:89:C4:FA:DF:24:4C:17:82:86:2F:6E
#             X509v3 Authority Key Identifier: 
#                 8A:73:73:82:27:58:C0:AA:44:00:CD:03:AA:A4:C9:15:61:10:59:0E
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         83:01:6e:0f:52:17:ea:79:e5:2a:6b:f8:cd:7c:59:29:bf:d7:
#         ... (바이너리 데이터 생략)

# service-accounts 인증서 확인
openssl x509 -in service-accounts.crt -text -noout
# Certificate:
#     Data:
#         Version: 3 (0x2)
#         Serial Number:
#             22:7a:39:e8:ca:02:11:b5:89:10:f2:82:a9:4e:ad:4c:95:d6:2f:1d
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = US, ST = Washington, L = Seattle, CN = CA
#         Validity
#             Not Before: Jan  7 17:02:27 2026 GMT
#             Not After : Jan  8 17:02:27 2036 GMT
#         Subject: CN = service-accounts
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (4096 bit)
#                 Modulus:
#                     00:b5:a6:68:8b:54:3b:a5:87:b5:9c:48:58:70:c3:
#                     ... (바이너리 데이터 생략)
#                 Exponent: 65537 (0x10001)
#         X509v3 extensions:
#             X509v3 Basic Constraints: 
#                 CA:FALSE
#             X509v3 Extended Key Usage: 
#                 TLS Web Client Authentication  # 클라이언트 인증만 (서버 인증 없음)
#             X509v3 Key Usage: critical
#                 Digital Signature, Key Encipherment
#             Netscape Cert Type: 
#                 SSL Client
#             Netscape Comment: 
#                 Admin Client Certificate
#             X509v3 Subject Key Identifier: 
#                 28:1D:57:AC:A2:EC:C9:75:5B:56:2A:DE:AE:13:D7:C9:E6:F5:6D:F5
#             X509v3 Authority Key Identifier: 
#                 8A:73:73:82:27:58:C0:AA:44:00:CD:03:AA:A4:C9:15:61:10:59:0E
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         17:fa:36:94:1a:8a:02:61:21:9c:58:76:b2:fe:3a:70:8c:e3:
#         ... (바이너리 데이터 생략)

# 중요 사항:
# - 모든 인증서는 동일한 CA(ca.crt)에 의해 서명됨 (Authority Key Identifier가 모두 동일)
# - 각 인증서의 Subject는 해당 구성요소의 역할을 나타냄
# - node-0, node-1은 system:nodes 그룹에 속하여 Node Authorizer에 의해 인가됨
# - kube-proxy는 system:node-proxier 그룹에 속함
# - kube-api-server 인증서는 가장 많은 SAN(Subject Alternative Name, 대체 이름)을 포함함.  
#   SAN이란 인증서의 또 다른 식별자(추가 DNS 이름, IP 주소 등)로, 다양한 이름이나 IP로 API 서버에 접근할 때 인증서 검증이 올바르게 작동하도록 해줌.
# - service-accounts 인증서는 Service Account 토큰 서명에 사용됨

Distribute the Client and Server Certificates

생성한 인증서를 각 서버들에 배포합니다.

배포 내역:

  • Worker Nodes (node-0, node-1): 각 노드의 개별 인증서 + CA 인증서
    • 배포 위치: /var/lib/kubelet/
    • 파일: ca.crt, kubelet.crt, kubelet.key
  • Control Plane (server): API 서버 관련 인증서 + CA 키/인증서 + Service Account 인증서
    • 배포 위치: /root/
    • 파일: ca.key, ca.crt, kube-api-server.key, kube-api-server.crt, service-accounts.key, service-accounts.crt
# Step 1: Worker Nodes에 인증서 배포 (node-0, node-1)
# - CA 인증서: API 서버의 인증서를 검증하기 위해 필요
# - 노드별 인증서/키: Kubelet이 API 서버에 클라이언트 인증할 때 사용
for host in node-0 node-1; do
  # Kubelet 인증서 저장 디렉토리 생성
  ssh root@${host} mkdir /var/lib/kubelet/

  # CA 인증서 복사 (API 서버 인증서 검증용)
  scp ca.crt root@${host}:/var/lib/kubelet/

  # 노드별 인증서 복사 (예: node-0.crt → kubelet.crt)
  scp ${host}.crt \
    root@${host}:/var/lib/kubelet/kubelet.crt

  # 노드별 개인키 복사 (예: node-0.key → kubelet.key)
  scp ${host}.key \
    root@${host}:/var/lib/kubelet/kubelet.key
done
# ca.crt                                                100% 1899     4.8MB/s   00:00
# node-0.crt                                            100% 2147     4.6MB/s   00:00
# node-0.key                                            100% 3268     7.0MB/s   00:00
# ca.crt                                                100% 1899     4.3MB/s   00:00
# node-1.crt                                            100% 2147     4.0MB/s   00:00
# node-1.key                                            100% 3272     5.7MB/s   00:00

# Step 2: node-0 배포 확인
ssh node-0 ls -l /var/lib/kubelet
# total 12
# -rw-r--r-- 1 root root 1899 Jan  8 02:24 ca.crt
# -rw-r--r-- 1 root root 2147 Jan  8 02:24 kubelet.crt
# -rw------- 1 root root 3268 Jan  8 02:24 kubelet.key

# Step 3: node-1 배포 확인
ssh node-1 ls -l /var/lib/kubelet
# total 12
# -rw-r--r-- 1 root root 1899 Jan  8 02:24 ca.crt
# -rw-r--r-- 1 root root 2147 Jan  8 02:24 kubelet.crt
# -rw------- 1 root root 3272 Jan  8 02:24 kubelet.key

# Step 4: Control Plane (server)에 인증서 배포
# - CA 키/인증서: 새 인증서 서명 및 클라이언트 인증서 검증용
# - API 서버 인증서/키: API 서버의 TLS 서버 인증서
# - Service Account 인증서/키: Service Account 토큰 서명/검증용
scp \
  ca.key ca.crt \
  kube-api-server.key kube-api-server.crt \
  service-accounts.key service-accounts.crt \
  root@server:~/
# ca.key                                                100% 3272     8.4MB/s   00:00
# ca.crt                                                100% 1899     3.9MB/s   00:00
# kube-api-server.key                                   100% 3272     8.9MB/s   00:00
# kube-api-server.crt                                   100% 2354     5.2MB/s   00:00
# service-accounts.key                                  100% 3272     6.3MB/s   00:00
# service-accounts.crt                                  100% 2004     5.5MB/s   00:00

# Step 5: server 배포 확인
ssh server ls -l /root
# total 24
# -rw-r--r-- 1 root root 1899 Jan  8 02:25 ca.crt
# -rw------- 1 root root 3272 Jan  8 02:25 ca.key
# -rw-r--r-- 1 root root 2354 Jan  8 02:25 kube-api-server.crt
# -rw------- 1 root root 3272 Jan  8 02:25 kube-api-server.key
# -rw-r--r-- 1 root root 2004 Jan  8 02:25 service-accounts.crt
# -rw------- 1 root root 3272 Jan  8 02:25 service-accounts.key

# 중요 사항:
# - 개인키(.key) 파일은 권한 600 (소유자만 읽기/쓰기)
# - 인증서(.crt) 파일은 권한 644 (모든 사용자 읽기 가능)
# - CA 개인키(ca.key)는 Control Plane에만 배포 (Worker Node에는 불필요)
# - 각 노드는 자신의 인증서만 보유 (node-0은 node-0.crt, node-1은 node-1.crt)

05 - Generating Kubernetes Configuration Files for Authentication

API Server와 통신을 위한 Client 인증 설정 파일(kubeconfig) 작성합니다.

출처:

  • Kubernetes The Hard Way - Configuration Files
  • Kubernetes Client Configuration Files

Kubeconfig 파일이란?

Kubeconfig 파일은 Kubernetes 클러스터에 접근하기 위한 클라이언트 설정 파일입니다. 이 파일에는 다음 정보가 포함됩니다.

  • 클러스터 정보: API 서버 주소, CA 인증서
  • 사용자 정보: 클라이언트 인증서/키 또는 토큰
  • 컨텍스트: 클러스터와 사용자의 조합

각 Kubernetes 구성요소(kubelet, kube-proxy, kube-controller-manager, kube-scheduler, admin)는 API 서버와 통신하기 위해 각각의 kubeconfig 파일이 필요합니다.

Kubelet Kubeconfig 파일의 특별한 요구사항

Kubelet의 kubeconfig 파일 생성 시 반드시 해당 노드 이름과 일치하는 클라이언트 인증서를 사용해야 합니다.

  • Kubernetes는 Node Authorizer라는 특수한 인가(authorization) 모드를 사용합니다.
  • Node Authorizer는 Kubelet의 요청이 해당 노드 자신의 리소스에 대한 것인지 검증합니다.
  • 인증서의 CN(Common Name)이 system:node:<node-name> 형식이어야 하며, 노드 이름과 일치해야 합니다.
  • 인증서의 O(Organization)가 system:nodes 그룹에 속해야 합니다.

Node Authorization이란?

  • Kubelet이 수행하는 API 요청에 대한 권한을 특별히 부여하는 특수 목적의 인가 모드 입니다.
  • 일반적인 RBAC과 별도로 동작하며, Kubelet이 다음 작업을 수행할 수 있도록 허용합니다.
    • 자신의 노드 정보 읽기/쓰기
    • 자신의 노드에서 실행되는 Pod 정보 읽기
    • 자신의 노드에서 실행되는 Pod의 상태 업데이트
    • 자신의 노드에 할당된 PVC, ConfigMap, Secret 등의 리소스 접근
  • 다른 노드의 리소스에는 접근할 수 없도록 제한합니다 (보안 강화)

예시:

# node-0의 인증서 CN
CN = system:node:node-0
O = system:nodes

# node-0의 kubeconfig는 이 인증서를 사용
# → Node Authorizer가 node-0 관련 리소스에 대한 접근 허용

The kubelet Kubernetes Configuration File

각 Worker Node(node-0, node-1)의 Kubelet이 API 서버와 통신하기 위한 kubeconfig 파일을 생성합니다.

kubeconfig 파일 생성 단계:

  1. set-cluster: 클러스터 정보 설정 (API 서버 주소, CA 인증서)
  2. set-credentials: 사용자 인증 정보 설정 (클라이언트 인증서/키)
  3. set-context: 컨텍스트 생성 (클러스터 + 사용자 조합)
  4. use-context: 기본 컨텍스트로 설정
# 참고: API 서버의 authorization-mode 확인
# Kind 클러스터에서 API 서버가 Node,RBAC 모드를 사용하는지 확인
kubectl describe pod -n kube-system kube-apiserver-myk8s-control-plane
#     Command:
#       kube-apiserver
#       --authorization-mode=Node,RBAC
# (실제 Hardway 클러스터에서는 Node Authorizer가 활성화되어 있음)

# Step 1: 클러스터 정보 설정 (node-0)
# --certificate-authority: CA 인증서 파일 경로
# --embed-certs=true: 인증서를 base64로 인코딩하여 파일에 임베드
# --server: API 서버 주소
# --kubeconfig: 생성할 kubeconfig 파일 경로
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=node-0.kubeconfig && ls -l node-0.kubeconfig && cat node-0.kubeconfig
# Cluster "kubernetes-the-hard-way" set.
# -rw------- 1 root root 2758 Jan  8 02:39 node-0.kubeconfig
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZURENDQXpTZ0F3SUJBZ0lVS1NaRG1IQXZlT2VheHpSamVzSDdmU0ZIMXIwd0RRWUpLb1pJaHZjTkFRRU4KQlFBd1FURUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQWdNQ2xkaGMyaHBibWQwYjI0eEVEQU9CZ05WQkFjTQpCMU5sWVhSMGJHVXhDekFKQmdOVkJBTU1Ba05CTUI0WERUSTJNREV3TnpFMk16Y3hNVm9YRFRNMk1ERXdPREUyCk16Y3hNVm93UVRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlYKQkFjTUIxTmxZWFIwYkdVeEN6QUpCZ05WQkFNTUFrTkJNSUlDSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQWc4QQpNSUlDQ2dLQ0FnRUEyU0VicGN2Wk04UlZFQVFUS05udVlxYzVnN3ZWTW9pQUs5VDA1Q0lobTlPWTZJWWwxbE4vCjdqcVhETWRyQzdEVDJrQUIyQ1V2U2FVZFdhOGZoYkFaQlhoK2Mwcjd6TjRFMDRzUVZKUitsNDNLVVNyd1V5dzYKbE4razJjTnFwOHk4U0tSa2tDYW1oVTJDL0NNbC9oODJuRTQzcUU1SEtpeEl1VEkvYmk3d3c1d0hiSm9xOFBIMQo5M3dQUFlYQndDMXZuSC9ybzA5eEkxWWpFZHJXblNtek9kV29ZV0dMbEhWS1NVa0R0L3VkdmlnKzhhNDhpZGJ0Ckh4OG54T0Q4TDZrVmlnNVV6QnlZdHU3RzBFeVQzekM2ZFlSQUk4a0NRbnIvbTJaOE9hMVpBREdsZHd1UXpGRmYKNUVSNVlGNlB5NzFQaUJ3dGJwaGJrck9pcll3V2d5SlpVaEh1TU9tT0hNYWUxQTBCUFFYcE56a0ROb25ETHhMdwpsbGpiWGorTkhpWmwweU4zKzduaFBTWGRsN3orVzVJaWsrOTVSK2lrMHhJVUcyRFBlMEFhSi9GeU1IQW15UkttCmVGd2FZT1daSWtKWG1paVQ1dG1mSFhFbGlIS1pNNC8rSkJuRXhjNktwb1RYTm9BT2pLSlNLRllvQWc0bEl3Q0wKWGkxN1pFU0pTaUYyUkVZcTd3Z0NjbGNVOHZkcFVUQkZ6RWQzckxtQjJLOU9Ea3drUEdMRnFDLzlLWitZSUFIYgoxVVZQSDJXZWFXZzUzYjIyZEZDc3BvdkZrUjE4L3VNSHlZUmF4MmdrdkJ1YU1ZMUlSV2F5SFhkekxzNTF5bEhMCjJHd3ROYVJqM3NsSzhLT0hKc0hURWk3U1FsQngrN29zdHB1eW9DSXdUZ2Yyc0tRL05Mek1RK1VDQXdFQUFhTTgKTURvd0RBWURWUjBUQkFVd0F3RUIvekFMQmdOVkhROEVCQU1DQVFZd0hRWURWUjBPQkJZRUZJcHpjNEluV01DcQpSQUROQTZxa3lSVmhFRmtPTUEwR0NTcUdTSWIzRFFFQkRRVUFBNElDQVFETTRkQ20zY2J4c3BmT3NNa29OZFNPCm1DcC9nM3F2dWlUdkp2Mm1jQzZuMDlpYmtuTWNIbFQ1TUdIYlhoZ2NTRkM5N1JaYVdRK3JKc3h5Z0Y5STIwK1kKdkNPOUxpK25ka1hPS2hRUENTQlU3T0xMZFRwUkFndUxEN1ljek84ai9wUUttVmhzZVV3c0RTVzRzaGt5RGovWApIQys2aW9KMndpZk0wVkxLZW4yRzZMVWw0b0VSNVFEeDJ6Y3RoZjNXNVBtRkVBYjZacTlzQnlzc0M0ekJiaUJTCk1EUkl0Ulk0VW5oRllsSk8yVmh5QlI2YmlhZ1c1bEVYVHF1d0haV01sRlJTbEZEL0Z4ZW54ZTA3MVcrMXI0YTkKNVNIV3ZzQmJ2SGZaT3VuZ2JKTjJ2Q1hyTUJ0V0Z1M3BwWXdNQUc0WGplYjBSRCtPaGprU1ZJcEhEaWFDUGdaaQphUi8yOW5tcWg1OE8zMEhtaDlTa2xLbkFrYzhqLzFsL2V3U2F1RjR5ejlZUGRLL1UyQktIV09kMkUzYnJGbXhYCnkyK0lES1FvZ0g3YlFCamd3VEFDRTJYSkRQM0E0Z2ljbTVLWXdkWW9penhEdjBxV3hHQVlueVhMWDE1eFlYTGwKYjJsWklOWFlCWU9zZXdVcDI4SmxBRG9hT2x2MFJLaVp6OE5xSVduSkZhK25HVWk0YVBWWGJIek5idlBubmlsMwpucHdTUjIwamRpdkY0N2VIOVJZdkVMN05KdVp0VDdUc1V3bXkxaUNGWG02Nlh6UWcwWFJpNlErQ29IdFRLRjJWClFyUTlHNzUyWTJDT25TYkJOU2o5ZWpHbTN2a2tsbWI4NmZBdTFZSlU5ZWdabjZnbXZOMnE5MlRjSjB0Q3RqZDcKV1Z6OWV3L3VTbjFQVlZSOGJTd3M2dz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
#     server: https://server.kubernetes.local:6443
#   name: kubernetes-the-hard-way
# contexts: null
# current-context: ""
# kind: Config
# preferences: {}
# users: null

# Step 2: 클러스터 정보 설정 (node-1)
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=node-1.kubeconfig && ls -l node-1.kubeconfig && cat node-1.kubeconfig
# Cluster "kubernetes-the-hard-way" set.
# -rw------- 1 root root 2758 Jan  8 02:39 node-1.kubeconfig
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: ... (base64 인코딩된 CA 인증서, 생략)
#     server: https://server.kubernetes.local:6443
#   name: kubernetes-the-hard-way
# contexts: null
# current-context: ""
# kind: Config
# preferences: {}
# users: null

# Step 3: 사용자 인증 정보 설정 (node-0)
# --client-certificate: 클라이언트 인증서 파일
# --client-key: 클라이언트 개인키 파일
# --embed-certs=true: 인증서/키를 base64로 인코딩하여 파일에 임베드
# 사용자 이름: system:node:node-0 (Node Authorizer가 인식하는 형식)
kubectl config set-credentials system:node:node-0 \
  --client-certificate=node-0.crt \
  --client-key=node-0.key \
  --embed-certs=true \
  --kubeconfig=node-0.kubeconfig && cat node-0.kubeconfig
# User "system:node:node-0" set.
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: ... (base64 인코딩된 CA 인증서, 생략)
#     server: https://server.kubernetes.local:6443
#   name: kubernetes-the-hard-way
# contexts: null
# current-context: ""
# kind: Config
# preferences: {}
# users:
# - name: system:node:node-0
#   user:
#     client-certificate-data: ... (base64 인코딩된 클라이언트 인증서, 생략)
#     client-key-data: ... (base64 인코딩된 클라이언트 개인키, 생략)

# Step 4: 사용자 인증 정보 설정 (node-1)
kubectl config set-credentials system:node:node-1 \
  --client-certificate=node-1.crt \
  --client-key=node-1.key \
  --embed-certs=true \
  --kubeconfig=node-1.kubeconfig && cat node-1.kubeconfig
# User "system:node:node-1" set.
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: ... (base64 인코딩된 CA 인증서, 생략)
#     server: https://server.kubernetes.local:6443
#   name: kubernetes-the-hard-way
# contexts: null
# current-context: ""
# kind: Config
# preferences: {}
# users:
# - name: system:node:node-1
#   user:
#     client-certificate-data: ... (base64 인코딩된 클라이언트 인증서, 생략)
#     client-key-data: ... (base64 인코딩된 클라이언트 개인키, 생략)

# Step 5: 컨텍스트 생성 (node-0)
# --cluster: 사용할 클러스터 이름
# --user: 사용할 사용자 이름
# --kubeconfig: 설정할 kubeconfig 파일
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:node:node-0 \
  --kubeconfig=node-0.kubeconfig && cat node-0.kubeconfig
# Context "default" created.
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: ... (base64 인코딩된 CA 인증서, 생략)
#     server: https://server.kubernetes.local:6443
#   name: kubernetes-the-hard-way
# contexts:
# - context:
#     cluster: kubernetes-the-hard-way
#     user: system:node:node-0
#   name: default
# current-context: ""
# kind: Config
# preferences: {}
# users:
# - name: system:node:node-0
#   user:
#     client-certificate-data: ... (base64 인코딩된 클라이언트 인증서, 생략)
#     client-key-data: ... (base64 인코딩된 클라이언트 개인키, 생략)

# Step 6: 컨텍스트 생성 (node-1)
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:node:node-1 \
  --kubeconfig=node-1.kubeconfig && cat node-1.kubeconfig
# Context "default" created.
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: ... (base64 인코딩된 CA 인증서, 생략)
#     server: https://server.kubernetes.local:6443
#   name: kubernetes-the-hard-way
# contexts:
# - context:
#     cluster: kubernetes-the-hard-way
#     user: system:node:node-1
#   name: default
# current-context: ""
# kind: Config
# preferences: {}
# users:
# - name: system:node:node-1
#   user:
#     client-certificate-data: ... (base64 인코딩된 클라이언트 인증서, 생략)
#     client-key-data: ... (base64 인코딩된 클라이언트 개인키, 생략)

# Step 7: 기본 컨텍스트로 설정 (node-0)
# use-context: current-context 필드를 설정하여 기본 컨텍스트 지정
kubectl config use-context default \
  --kubeconfig=node-0.kubeconfig
# Switched to context "default".

# Step 8: 기본 컨텍스트로 설정 (node-1)
kubectl config use-context default \
  --kubeconfig=node-1.kubeconfig
# Switched to context "default".

The kube-proxy Kubernetes Configuration File

kube-proxy 서비스가 API 서버와 통신하기 위한 kubeconfig 파일을 생성합니다.
kube-proxy는 각 Worker Node에서 실행되며, Kubernetes Service의 네트워크 프록시 역할을 수행합니다.
API 서버에서 Service 및 Endpoint 정보를 조회하기 위해 인증이 필요합니다.

# Step 1: 클러스터 정보 설정
# --certificate-authority: CA 인증서 파일
# --embed-certs=true: 인증서를 base64로 인코딩하여 파일에 임베드
# --server: API 서버 주소
# --kubeconfig: 생성할 kubeconfig 파일 경로
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=kube-proxy.kubeconfig
# Cluster "kubernetes-the-hard-way" set.

# Step 2: 사용자 인증 정보 설정
# --client-certificate: kube-proxy 클라이언트 인증서
# --client-key: kube-proxy 클라이언트 개인키
# --embed-certs=true: 인증서/키를 base64로 인코딩하여 파일에 임베드
# 사용자 이름: system:kube-proxy (system:node-proxier 그룹에 속함)
kubectl config set-credentials system:kube-proxy \
  --client-certificate=kube-proxy.crt \
  --client-key=kube-proxy.key \
  --embed-certs=true \
  --kubeconfig=kube-proxy.kubeconfig
# User "system:kube-proxy" set.

# Step 3: 컨텍스트 생성
# --cluster: 사용할 클러스터 이름
# --user: 사용할 사용자 이름
# --kubeconfig: 설정할 kubeconfig 파일
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-proxy \
  --kubeconfig=kube-proxy.kubeconfig
# Context "default" created.

# Step 4: 기본 컨텍스트로 설정
# use-context: current-context 필드를 설정하여 기본 컨텍스트 지정
kubectl config use-context default \
  --kubeconfig=kube-proxy.kubeconfig
# Switched to context "default".

# Step 5: 생성된 kubeconfig 파일 내용 확인
cat kube-proxy.kubeconfig
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: ... (base64 인코딩된 CA 인증서, 생략)
#     server: https://server.kubernetes.local:6443
#   name: kubernetes-the-hard-way
# contexts:
# - context:
#     cluster: kubernetes-the-hard-way
#     user: system:kube-proxy
#   name: default
# current-context: default
# kind: Config
# preferences: {}
# users:
# - name: system:kube-proxy
#   user:
#     client-certificate-data: ... (base64 인코딩된 클라이언트 인증서, 생략)
#     client-key-data: ... (base64 인코딩된 클라이언트 개인키, 생략)

# 생성 완료: kube-proxy.kubeconfig 파일이 준비되었습니다.
# 이 파일은 모든 Worker Node의 kube-proxy 서비스가 API 서버와 통신할 때 사용됩니다.

The kube-controller-manager Kubernetes Configuration File

kube-controller-manager 서비스가 API 서버와 통신하기 위한 kubeconfig 파일을 생성합니다.

kube-controller-manager는 Control Plane 구성요소로, 클러스터의 다양한 컨트롤러(Pod, Service, Deployment 등)를 관리하며 API 서버와 지속적으로 통신해야 합니다.

# Step 1: 클러스터 정보 설정
# --certificate-authority: CA 인증서 파일
# --embed-certs=true: 인증서를 base64로 인코딩하여 파일에 임베드
# --server: API 서버 주소
# --kubeconfig: 생성할 kubeconfig 파일 경로
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=kube-controller-manager.kubeconfig
# Cluster "kubernetes-the-hard-way" set.

# Step 2: 사용자 인증 정보 설정
# --client-certificate: kube-controller-manager 클라이언트 인증서
# --client-key: kube-controller-manager 클라이언트 개인키
# --embed-certs=true: 인증서/키를 base64로 인코딩하여 파일에 임베드
# 사용자 이름: system:kube-controller-manager (RBAC에서 특별한 권한을 가짐)
kubectl config set-credentials system:kube-controller-manager \
  --client-certificate=kube-controller-manager.crt \
  --client-key=kube-controller-manager.key \
  --embed-certs=true \
  --kubeconfig=kube-controller-manager.kubeconfig
# User "system:kube-controller-manager" set.

# Step 3: 컨텍스트 생성
# --cluster: 사용할 클러스터 이름
# --user: 사용할 사용자 이름
# --kubeconfig: 설정할 kubeconfig 파일
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-controller-manager \
  --kubeconfig=kube-controller-manager.kubeconfig
# Context "default" created.

# Step 4: 기본 컨텍스트로 설정
# use-context: current-context 필드를 설정하여 기본 컨텍스트 지정
kubectl config use-context default \
  --kubeconfig=kube-controller-manager.kubeconfig
# Switched to context "default".

# Step 5: 생성된 kubeconfig 파일 내용 확인
cat kube-controller-manager.kubeconfig
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: ... (base64 인코딩된 CA 인증서, 생략)
#     server: https://server.kubernetes.local:6443
#   name: kubernetes-the-hard-way
# contexts:
# - context:
#     cluster: kubernetes-the-hard-way
#     user: system:kube-controller-manager
#   name: default
# current-context: default
# kind: Config
# preferences: {}
# users:
# - name: system:kube-controller-manager
#   user:
#     client-certificate-data: ... (base64 인코딩된 클라이언트 인증서, 생략)
#     client-key-data: ... (base64 인코딩된 클라이언트 개인키, 생략)

# 이 파일은 Control Plane의 kube-controller-manager가 API 서버와 통신할 때 사용됩니다.

The kube-scheduler Kubernetes Configuration File

kube-scheduler 서비스가 API 서버와 통신하기 위한 kubeconfig 파일을 생성합니다.

kube-scheduler는 Control Plane 구성요소로, 새로 생성된 Pod를 적절한 Node에 스케줄링하는 역할을 수행합니다.

API 서버에서 Node 및 Pod 정보를 조회하기 위해 인증이 필요합니다.

# Step 1: 클러스터 정보 설정
# --certificate-authority: CA 인증서 파일
# --embed-certs=true: 인증서를 base64로 인코딩하여 파일에 임베드
# --server: API 서버 주소
# --kubeconfig: 생성할 kubeconfig 파일 경로
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=kube-scheduler.kubeconfig
# Cluster "kubernetes-the-hard-way" set.

# Step 2: 사용자 인증 정보 설정
# --client-certificate: kube-scheduler 클라이언트 인증서
# --client-key: kube-scheduler 클라이언트 개인키
# --embed-certs=true: 인증서/키를 base64로 인코딩하여 파일에 임베드
# 사용자 이름: system:kube-scheduler (RBAC에서 특별한 권한을 가짐)
kubectl config set-credentials system:kube-scheduler \
  --client-certificate=kube-scheduler.crt \
  --client-key=kube-scheduler.key \
  --embed-certs=true \
  --kubeconfig=kube-scheduler.kubeconfig
# User "system:kube-scheduler" set.

# Step 3: 컨텍스트 생성
# --cluster: 사용할 클러스터 이름
# --user: 사용할 사용자 이름
# --kubeconfig: 설정할 kubeconfig 파일
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-scheduler \
  --kubeconfig=kube-scheduler.kubeconfig
# Context "default" created.

# Step 4: 기본 컨텍스트로 설정
# use-context: current-context 필드를 설정하여 기본 컨텍스트 지정
kubectl config use-context default \
  --kubeconfig=kube-scheduler.kubeconfig
# Switched to context "default".

# Step 5: 생성된 kubeconfig 파일 내용 확인
cat kube-scheduler.kubeconfig
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: ... (base64 인코딩된 CA 인증서, 생략)
#     server: https://server.kubernetes.local:6443
#   name: kubernetes-the-hard-way
# contexts:
# - context:
#     cluster: kubernetes-the-hard-way
#     user: system:kube-scheduler
#   name: default
# current-context: default
# kind: Config
# preferences: {}
# users:
# - name: system:kube-scheduler
#   user:
#     client-certificate-data: ... (base64 인코딩된 클라이언트 인증서, 생략)
#     client-key-data: ... (base64 인코딩된 클라이언트 개인키, 생략)

# 생성 완료: kube-scheduler.kubeconfig 파일이 준비되었습니다.
# 이 파일은 Control Plane의 kube-scheduler가 API 서버와 통신할 때 사용됩니다.

The admin Kubernetes Configuration File

admin 사용자가 클러스터를 관리하기 위한 kubeconfig 파일을 생성합니다.

admin kubeconfig는 클러스터 관리자가 kubectl 명령어를 사용하여 클러스터를 관리할 때 사용됩니다.
admin 인증서는 system:masters 그룹에 속하여 모든 권한을 가집니다.

다른 kubeconfig와 달리 서버 주소가 https://127.0.0.1:6443 (localhost)로 설정됩니다.
이는 관리자가 로컬에서 kubectl을 사용할 때 일반적으로 localhost를 통해 API 서버에 접근하기 때문입니다.

# Step 1: 클러스터 정보 설정
# --certificate-authority: CA 인증서 파일
# --embed-certs=true: 인증서를 base64로 인코딩하여 파일에 임베드
# --server: API 서버 주소 (localhost 사용)
# --kubeconfig: 생성할 kubeconfig 파일 경로
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://127.0.0.1:6443 \
  --kubeconfig=admin.kubeconfig
# Cluster "kubernetes-the-hard-way" set.

# Step 2: 사용자 인증 정보 설정
# --client-certificate: admin 클라이언트 인증서
# --client-key: admin 클라이언트 개인키
# --embed-certs=true: 인증서/키를 base64로 인코딩하여 파일에 임베드
# 사용자 이름: admin (system:masters 그룹에 속함)
kubectl config set-credentials admin \
  --client-certificate=admin.crt \
  --client-key=admin.key \
  --embed-certs=true \
  --kubeconfig=admin.kubeconfig
# User "admin" set.

# Step 3: 컨텍스트 생성
# --cluster: 사용할 클러스터 이름
# --user: 사용할 사용자 이름
# --kubeconfig: 설정할 kubeconfig 파일
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=admin \
  --kubeconfig=admin.kubeconfig
# Context "default" created.

# Step 4: 기본 컨텍스트로 설정
# use-context: current-context 필드를 설정하여 기본 컨텍스트 지정
kubectl config use-context default \
  --kubeconfig=admin.kubeconfig
# Switched to context "default".

# Step 5: 생성된 kubeconfig 파일 내용 확인
cat admin.kubeconfig
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: ... (base64 인코딩된 CA 인증서, 생략)
#     server: https://127.0.0.1:6443
#   name: kubernetes-the-hard-way
# contexts:
# - context:
#     cluster: kubernetes-the-hard-way
#     user: admin
#   name: default
# current-context: default
# kind: Config
# preferences: {}
# users:
# - name: admin
#   user:
#     client-certificate-data: ... (base64 인코딩된 클라이언트 인증서, 생략)
#     client-key-data: ... (base64 인코딩된 클라이언트 개인키, 생략)

Distribute the Kubernetes Configuration Files

생성한 kubeconfig 파일들을 각 서버에 배포합니다.

배포 내역:
Worker Nodes (node-0, node-1)

  • 각 노드의 kubelet kubeconfig → /var/lib/kubelet/kubeconfig
  • kube-proxy kubeconfig → /var/lib/kube-proxy/kubeconfig
    Control Plane (server)
  • admin.kubeconfig → /root/
  • kube-controller-manager.kubeconfig → /root/
  • kube-scheduler.kubeconfig → /root/
# Step 1: 생성된 모든 kubeconfig 파일 목록 확인
ls -l *.kubeconfig
# -rw------- 1 root root  9949 Jan  8 03:10 admin.kubeconfig
# -rw------- 1 root root 10305 Jan  8 03:05 kube-controller-manager.kubeconfig
# -rw------- 1 root root 10187 Jan  8 03:02 kube-proxy.kubeconfig
# -rw------- 1 root root 10215 Jan  8 03:07 kube-scheduler.kubeconfig
# -rw------- 1 root root 10157 Jan  8 02:39 node-0.kubeconfig
# -rw------- 1 root root 10161 Jan  8 02:39 node-1.kubeconfig

# Step 2: Worker Nodes에 kubeconfig 파일 배포
# - 각 노드에 디렉토리 생성: /var/lib/kube-proxy, /var/lib/kubelet
# - kube-proxy.kubeconfig → /var/lib/kube-proxy/kubeconfig (모든 노드 동일)
# - node-0.kubeconfig → /var/lib/kubelet/kubeconfig (node-0 전용)
# - node-1.kubeconfig → /var/lib/kubelet/kubeconfig (node-1 전용)
for host in node-0 node-1; do
  # 디렉토리 생성 (kubelet, kube-proxy)
  ssh root@${host} "mkdir -p /var/lib/{kube-proxy,kubelet}"

  # kube-proxy kubeconfig 복사 (모든 노드 동일)
  scp kube-proxy.kubeconfig \
    root@${host}:/var/lib/kube-proxy/kubeconfig \

  # 노드별 kubelet kubeconfig 복사
  scp ${host}.kubeconfig \
    root@${host}:/var/lib/kubelet/kubeconfig
done
# kube-proxy.kubeconfig                                            100%   10KB  13.1MB/s   00:00
# node-0.kubeconfig                                                100%   10KB  18.9MB/s   00:00
# kube-proxy.kubeconfig                                            100%   10KB  15.3MB/s   00:00
# node-1.kubeconfig                                                100%   10KB  16.9MB/s   00:00

# Step 3: node-0 배포 확인
ssh node-0 ls -l /var/lib/*/kubeconfig
# -rw------- 1 root root 10157 Jan  8 03:12 /var/lib/kubelet/kubeconfig
# -rw------- 1 root root 10187 Jan  8 03:12 /var/lib/kube-proxy/kubeconfig

# Step 4: node-1 배포 확인
ssh node-1 ls -l /var/lib/*/kubeconfig
# -rw------- 1 root root 10161 Jan  8 03:12 /var/lib/kubelet/kubeconfig
# -rw------- 1 root root 10187 Jan  8 03:12 /var/lib/kube-proxy/kubeconfig

# Copy the kube-controller-manager and kube-scheduler kubeconfig files to the server machine
scp admin.kubeconfig \
  kube-controller-manager.kubeconfig \
  kube-scheduler.kubeconfig \
  root@server:~/

# 확인
ssh server ls -l /root/*.kubeconfig
-rw------- 1 root root  9949 Jan  8 03:15 /root/admin.kubeconfig
-rw------- 1 root root 10305 Jan  8 03:15 /root/kube-controller-manager.kubeconfig
-rw------- 1 root root 10215 Jan  8 03:15 /root/kube-scheduler.kubeconfig

06 - Generating the Data Encryption Config and Key

이번 실습에서는 ETCD에 Secret을 저장할 때 암호화하여 저장합니다.

# Step 1: 암호화 키 생성
# head -c 32 /dev/urandom: 32바이트(256비트)의 랜덤 바이트 생성
# base64: 바이너리 데이터를 base64 문자열로 인코딩
# AES-256 암호화에 사용될 32바이트(256비트) 키 생성
export ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
echo $ENCRYPTION_KEY
# z2cxMgkkcr5iVKXDYFo+JTQEZUqmV+HFFos3Hv0XKWE=
# (실행마다 다른 랜덤 값이 생성됨)

# Step 2: Encryption Config 템플릿 파일 확인
# configs/encryption-config.yaml: ENCRYPTION_KEY 변수를 포함한 템플릿 파일
# envsubst를 사용하여 ${ENCRYPTION_KEY}를 실제 값으로 치환
cat configs/encryption-config.yaml
# kind: EncryptionConfiguration
# apiVersion: apiserver.config.k8s.io/v1
# resources:
#   - resources:
#       - secrets  # Secret 리소스에 대해 암호화 적용
#     providers:
#       - aescbc:  # AES-CBC 암호화 알고리즘 사용
#           keys:
#             - name: key1
#               secret: ${ENCRYPTION_KEY}  # 환경변수로부터 치환됨
#       - identity: {}  # 암호화되지 않은 경우를 위한 identity provider (fallback)

# 참고: 실제 etcd에 저장될 때 값 앞에 헤더가 붙음
# 형식: k8s:enc:aescbc:v1:key1:<암호화된_텍스트>

# Step 3: 환경변수 치환하여 최종 Encryption Config 파일 생성
# envsubst: 환경변수를 실제 값으로 치환
# < configs/encryption-config.yaml: 템플릿 파일 입력
# > encryption-config.yaml: 치환된 결과 파일 출력
envsubst < configs/encryption-config.yaml > encryption-config.yaml

# Step 4: 생성된 Encryption Config 파일 내용 확인
cat encryption-config.yaml
# kind: EncryptionConfiguration
# apiVersion: apiserver.config.k8s.io/v1
# resources:
#   - resources:
#       - secrets
#     providers:
#       - aescbc:
#           keys:
#             - name: key1
#               secret: z2cxMgkkcr5iVKXDYFo+JTQEZUqmV+HFFos3Hv0XKWE=  # 실제 암호화 키 값
#       - identity: {}

# Step 5: Control Plane (server)에 Encryption Config 파일 배포
# 이 파일은 API 서버가 시작될 때 읽어서 Secret 암호화에 사용됨
scp encryption-config.yaml root@server:~/
# encryption-config.yaml                                            100%  271   507.6KB/s   00:00

# Step 6: server 배포 확인
ssh server ls -l /root/encryption-config.yaml
# -rw-r--r-- 1 root root 271 Jan  9 16:20 /root/encryption-config.yaml

Encryption Key는 보안상 매우 중요하므로 안전하게 관리해야 하고, 이 키를 잃어버리면 기존에 암호화된 Secret을 복호화할 수 없기 때문에 주의해야합니다.


07 - Bootstrapping the etcd Cluster

단일 노드로 구성된 etcd 클러스터를 부트스트랩합니다.

#### Prerequisites: etcd.service 파일 수정

# etcd 클러스터 내에서 각 etcd 멤버는 고유한 이름을 가져야 합니다.
# 현재 호스트명이 server이므로 etcd 이름도 server로 설정해야 합니다.
# 중요: 이 실습에서는 etcd가 HTTP 평문 통신을 사용합니다 (운영 환경에서는 HTTPS 사용 권장).

# Step 1: 기존 etcd.service 파일에서 controller 확인
# (원본 템플릿이 controller로 설정되어 있음)
cat units/etcd.service | grep controller
#   --name controller \
#   --initial-cluster controller=http://127.0.0.1:2380 \

# Step 2: etcd.service 파일을 server 호스트명에 맞게 수정
# ETCD_NAME: etcd 멤버 이름 (호스트명과 일치해야 함)
ETCD_NAME=server
cat > units/etcd.service <<EOF
[Unit]
Description=etcd
Documentation=https://github.com/etcd-io/etcd

[Service]
Type=notify
ExecStart=/usr/local/bin/etcd \\
  --name ${ETCD_NAME} \\
  --initial-advertise-peer-urls http://127.0.0.1:2380 \\
  --listen-peer-urls http://127.0.0.1:2380 \\
  --listen-client-urls http://127.0.0.1:2379 \\
  --advertise-client-urls http://127.0.0.1:2379 \\
  --initial-cluster-token etcd-cluster-0 \\
  --initial-cluster ${ETCD_NAME}=http://127.0.0.1:2380 \\
  --initial-cluster-state new \\
  --data-dir=/var/lib/etcd
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

# Step 3: 수정된 내용 확인
cat units/etcd.service | grep server
#   --name server \
#   --initial-cluster server=http://127.0.0.1:2380 \

# Step 4: etcd 바이너리 및 서비스 파일을 server에 복사
# - etcd: etcd 서버 바이너리
# - etcdctl: etcd 클라이언트 도구
# - etcd.service: systemd 서비스 파일
scp \
  downloads/controller/etcd \
  downloads/client/etcdctl \
  units/etcd.service \
  root@server:~/
# etcd                                                              100%   23MB 158.0MB/s   00:00
# etcdctl                                                           100%   15MB 204.9MB/s   00:00
# etcd.service                                                      100%  564     2.1MB/s   00:00


# ========== server 가상머신에서 실행 ==========

# Step 5: server 가상머신에 SSH 접속
# 이후 명령어들은 server 가상머신에서 실행됨
ssh root@server
# Linux server 6.1.0-40-arm64 #1 SMP Debian 6.1.153-1 (2025-09-20) aarch64
# ...
# root@server:~# 

# Step 6: 현재 디렉토리 확인
pwd
# /root

# Step 7: etcd 바이너리 설치
# etcd 서버와 클라이언트 도구를 /usr/local/bin/에 설치
mv etcd etcdctl /usr/local/bin/

# Step 8: etcd 서버 설정
# /etc/etcd: etcd 설정 파일 및 인증서 저장 디렉토리
# /var/lib/etcd: etcd 데이터 저장 디렉토리
mkdir -p /etc/etcd /var/lib/etcd
chmod 700 /var/lib/etcd  # etcd 데이터 디렉토리 권한 700 (소유자만 접근 가능)

# Step 9: etcd 인증서 복사
# CA 인증서 및 API 서버 인증서/키를 /etc/etcd/에 복사
# (참고: 현재는 HTTP 통신이지만, 추후 TLS 설정을 위해 미리 준비)
cp ca.crt kube-api-server.key kube-api-server.crt /etc/etcd/

# Step 10: etcd.service systemd 유닛 파일 설치
mv etcd.service /etc/systemd/system/

# Step 11: systemd 디렉토리 구조 확인
tree /etc/systemd/system/
# /etc/systemd/system/
# ├── apt-daily.service -> /dev/null
# ├── apt-daily-upgrade.service -> /dev/null
# ├── dbus-org.freedesktop.timesync1.service -> /lib/systemd/system/systemd-timesyncd.service
# ├── etcd.service  # 새로 추가된 etcd 서비스 파일
# ├── getty.target.wants
# │   └── getty@tty1.service -> /lib/systemd/system/getty@.service
# ├── multi-user.target.wants
# │   ├── console-setup.service -> /lib/systemd/system/console-setup.service
# │   ├── cron.service -> /lib/systemd/system/cron.service
# │   ... (기타 시스템 서비스)
# ...

# Step 12: etcd 서비스 시작
# daemon-reload: systemd 설정 파일 변경사항 반영
systemctl daemon-reload
# enable: 부팅 시 자동 시작 설정
systemctl enable etcd
# Created symlink /etc/systemd/system/multi-user.target.wants/etcd.service → /etc/systemd/system/etcd.service.
# start: etcd 서비스 시작
systemctl start etcd

# Step 13: etcd 서비스 상태 확인
systemctl status etcd --no-pager
# ● etcd.service - etcd
#      Loaded: loaded (/etc/systemd/system/etcd.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-01-09 16:25:12 KST; 2s ago
#        Docs: https://github.com/etcd-io/etcd
#    Main PID: 2790 (etcd)
#       Tasks: 9 (limit: 2096)
#      Memory: 8.4M
#         CPU: 29ms
#      CGroup: /system.slice/etcd.service
#              └─2790 /usr/local/bin/etcd --name server --initial-advertise-peer-urls http://127.0.0.1:2380 ...
# 
# Jan 09 16:25:12 server etcd[2790]: {"level":"info","ts":"2026-01-09T16:25:12.694299+0900","caller":"etcdmain/main.go:44","msg":"notifying init daemon"}
# Jan 09 16:25:12 server etcd[2790]: {"level":"info","ts":"2026-01-09T16:25:12.694568+0900","caller":"v3rpc/health.go:63","msg":"grpc service status changed","service":"","status":"SERVING"}
# Jan 09 16:25:12 server systemd[1]: Started etcd.service - etcd.
# Jan 09 16:25:12 server etcd[2790]: {"level":"info","ts":"2026-01-09T16:25:12.695809+0900","caller":"etcdmain/main.go:50","msg":"successfully notified init daemon"}
# Jan 09 16:25:12 server etcd[2790]: {"level":"info","ts":"2026-01-09T16:25:12.696239+0900","caller":"membership/cluster.go:647","msg":"set initial cluster version","cluster-id":"1dda2deacb7d5fb1","local-member-id":"6702b0a34e2cfd39","cluster-version":"3.6"}
# Jan 09 16:25:12 server etcd[2790]: {"level":"info","ts":"2026-01-09T16:25:12.696278+0900","caller":"api/capability.go:76","msg":"enabled capabilities for version","cluster-version":"3.6"}
# Jan 09 16:25:12 server etcd[2790]: {"level":"info","ts":"2026-01-09T16:25:12.696293+0900","caller":"etcdserver/server.go:2383","msg":"cluster version is updated","cluster-version":"3.6"}
# Jan 09 16:25:12 server etcd[2790]: {"level":"info","ts":"2026-01-09T16:25:12.696522+0900","caller":"version/monitor.go:116","msg":"cluster version differs from storage version.","cluster-version":"3.6.0","storage-version":"3.5.0"}
# Jan 09 16:25:12 server etcd[2790]: {"level":"info","ts":"2026-01-09T16:25:12.696540+0900","caller":"schema/migration.go:65","msg":"updated storage version","new-storage-version":"3.6.0"}
# Jan 09 16:25:12 server etcd[2790]: {"level":"info","ts":"2026-01-09T16:25:12.696802+0900","caller":"embed/serve.go:220","msg":"serving client traffic insecurely; this is strongly discouraged!","traffic":"grpc+http","address":"127.0.0.1:2379"}

# Step 14: etcd 포트 리스닝 확인
# 2379: 클라이언트 통신 포트 (API 서버가 etcd에 접근할 때 사용)
# 2380: 피어 통신 포트 (etcd 멤버 간 통신, 단일 노드이므로 사용 안 함)
ss -tnlp | grep etcd
# LISTEN 0      4096       127.0.0.1:2379      0.0.0.0:*    users:(("etcd",pid=2790,fd=6))
# LISTEN 0      4096       127.0.0.1:2380      0.0.0.0:*    users:(("etcd",pid=2790,fd=3))

# Step 15: etcd 클러스터 멤버 목록 확인
etcdctl member list -w table
# +------------------+---------+--------+-----------------------+-----------------------+------------+
# |        ID        | STATUS  |  NAME  |      PEER ADDRS       |     CLIENT ADDRS      | IS LEARNER |
# +------------------+---------+--------+-----------------------+-----------------------+------------+
# | 6702b0a34e2cfd39 | started | server | http://127.0.0.1:2380 | http://127.0.0.1:2379 |      false |
# +------------------+---------+--------+-----------------------+-----------------------+------------+
# 
# 멤버 정보:
# - ID: 6702b0a34e2cfd39 (etcd 멤버의 고유 ID)
# - STATUS: started (정상 실행 중)
# - NAME: server (호스트명과 일치)
# - PEER ADDRS: http://127.0.0.1:2380 (피어 통신 주소)
# - CLIENT ADDRS: http://127.0.0.1:2379 (클라이언트 통신 주소)
# - IS LEARNER: false (정식 멤버, learner 아님)

# Step 16: etcd 엔드포인트 상태 확인
etcdctl endpoint status -w table
# +----------------+------------------+------------+-----------------+---------+--------+-----------------------+-------+-----------+------------+-----------+------------+--------------------+--------+--------------------------+-------------------+
# |    ENDPOINT    |        ID        |  VERSION   | STORAGE VERSION | DB SIZE | IN USE | PERCENTAGE NOT IN USE | QUOTA | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS | DOWNGRADE TARGET VERSION | DOWNGRADE ENABLED |
# +----------------+------------------+------------+-----------------+---------+--------+-----------------------+-------+-----------+------------+-----------+------------+--------------------+--------+--------------------------+-------------------+
# | 127.0.0.1:2379 | 6702b0a34e2cfd39 | 3.6.0-rc.3 |           3.6.0 |   20 kB |  16 kB |                   20% |   0 B |      true |      false |         2 |          4 |                  4 |        |                          |             false |
# +----------------+------------------+------------+-----------------+---------+--------+-----------------------+-------+-----------+------------+-----------+------------+--------------------+--------+--------------------------+-------------------+
# 
# 상태 정보:
# - ENDPOINT: 127.0.0.1:2379 (클라이언트 엔드포인트)
# - ID: 6702b0a34e2cfd39
# - VERSION: 3.6.0-rc.3 (etcd 버전)
# - STORAGE VERSION: 3.6.0 (저장소 버전)
# - DB SIZE: 20 kB (데이터베이스 크기)
# - IS LEADER: true (리더 역할 수행, 단일 노드이므로 항상 리더)
# - RAFT TERM: 2 (Raft 합의 알고리즘의 현재 터미)
# - RAFT INDEX: 4 (Raft 로그 인덱스)
# - RAFT APPLIED INDEX: 4 (적용된 Raft 로그 인덱스)

# Step 17: server 가상머신에서 나가기
exit

08 - Bootstrapping the Kubernetes Control Plane

Kubernetes 컨트롤 플레인을 부트스트랩합니다
| 항목 | 네트워크 대역 or IP |
| --- | --- |
| clusterCIDR | 10.200.0.0/16 |
| → node-0 PodCIDR | 10.200.0.0/24 |
| → node-1 PodCIDR | 10.200.1.0/24 |
| ServiceCIDR | 10.32.0.0/24 |
| → api clusterIP | 10.32.0.1 |

설정파일을 작성하고 server에 전달

api -> kubelet 호출 시 흐름은 다음과 같습니다.
kube-apiserver (client)
|
| (TLS client cert, CN=kubernetes)
↓
kubelet API Server 역할 (/stats, /log, /metrics)
|
↓
RBAC 평가:
User = kubernetes
→ ClusterRoleBinding system:kube-apiserver 매칭
→ ClusterRole system:kube-apiserver-to-kubelet 권한 부여

# Prerequisites: Kubernetes 컨트롤 플레인 구성요소 설정

# Step 1: ca.conf에서 kube-api-server 인증서의 SAN 확인
# service-cluster-ip-range 값은 ca.conf의 [kube-api-server_alt_names]에 설정된 Service IP 범위와 일치해야 함
# 참고: https://github.com/kelseyhightower/kubernetes-the-hard-way/issues/905
cat ca.conf | grep '\[kube-api-server_alt_names' -A2
# [kube-api-server_alt_names]
# IP.0  = 127.0.0.1
# IP.1  = 10.32.0.1
# 
# IP.0 (127.0.0.1): 로컬호스트 접근용
# IP.1 (10.32.0.1): Kubernetes Service Cluster IP (10.32.0.0/24 범위의 첫 번째 IP)

# Step 2: 기존 kube-apiserver.service 파일 확인
cat units/kube-apiserver.service
# Step 3: kube-apiserver.service 파일 생성 (service-cluster-ip-range 추가)
cat << EOF > units/kube-apiserver.service
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-apiserver \\
  --allow-privileged=true \\
  --apiserver-count=1 \\
  --audit-log-maxage=30 \\
  --audit-log-maxbackup=3 \\
  --audit-log-maxsize=100 \\
  --audit-log-path=/var/log/audit.log \\
  --authorization-mode=Node,RBAC \\
  --bind-address=0.0.0.0 \\
  --client-ca-file=/var/lib/kubernetes/ca.crt \\
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \\
  --etcd-servers=http://127.0.0.1:2379 \\
  --event-ttl=1h \\
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \\
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.crt \\
  --kubelet-client-certificate=/var/lib/kubernetes/kube-api-server.crt \\
  --kubelet-client-key=/var/lib/kubernetes/kube-api-server.key \\
  --runtime-config='api/all=true' \\
  --service-account-key-file=/var/lib/kubernetes/service-accounts.crt \\
  --service-account-signing-key-file=/var/lib/kubernetes/service-accounts.key \\
  --service-account-issuer=https://server.kubernetes.local:6443 \\
  --service-cluster-ip-range=10.32.0.0/24 \\
  --service-node-port-range=30000-32767 \\
  --tls-cert-file=/var/lib/kubernetes/kube-api-server.crt \\
  --tls-private-key-file=/var/lib/kubernetes/kube-api-server.key \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

# Step 4: 수정된 kube-apiserver.service 파일 확인
cat units/kube-apiserver.service
# [Unit]
# Description=Kubernetes API Server
# Documentation=https://github.com/kubernetes/kubernetes
# 
# [Service]
# ExecStart=/usr/local/bin/kube-apiserver \
#   --allow-privileged=true \
#   --apiserver-count=1 \
#   --audit-log-maxage=30 \
#   --audit-log-maxbackup=3 \
#   --audit-log-maxsize=100 \
#   --audit-log-path=/var/log/audit.log \
#   --authorization-mode=Node,RBAC \
#   --bind-address=0.0.0.0 \
#   --client-ca-file=/var/lib/kubernetes/ca.crt \
#   --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \
#   --etcd-servers=http://127.0.0.1:2379 \
#   --event-ttl=1h \
#   --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \
#   --kubelet-certificate-authority=/var/lib/kubernetes/ca.crt \
#   --kubelet-client-certificate=/var/lib/kubernetes/kube-api-server.crt \
#   --kubelet-client-key=/var/lib/kubernetes/kube-api-server.key \
#   --runtime-config='api/all=true' \
#   --service-account-key-file=/var/lib/kubernetes/service-accounts.crt \
#   --service-account-signing-key-file=/var/lib/kubernetes/service-accounts.key \
#   --service-account-issuer=https://server.kubernetes.local:6443 \
#   --service-cluster-ip-range=10.32.0.0/24 \
#   --service-node-port-range=30000-32767 \
#   --tls-cert-file=/var/lib/kubernetes/kube-api-server.crt \
#   --tls-private-key-file=/var/lib/kubernetes/kube-api-server.key \
#   --v=2
# Restart=on-failure
# RestartSec=5
# 
# [Install]
# WantedBy=multi-user.target
# 
# 주요 옵션 설명:
# - --apiserver-count=1: API 서버 인스턴스 개수 (단일 노드이므로 1)
# - --authorization-mode=Node,RBAC: Node 및 RBAC 인가 모드 사용
# - --etcd-servers=http://127.0.0.1:2379: etcd 서버 주소
# - --service-cluster-ip-range=10.32.0.0/24: Service Cluster IP 범위
# - --service-node-port-range=30000-32767: NodePort 서비스 포트 범위
# - --kubelet-client-certificate/key: kubelet과 통신 시 사용할 클라이언트 인증서/키
# - --service-account-issuer: Service Account 토큰 발급자 (JWT)


# Step 5: kube-apiserver-to-kubelet RBAC 설정 확인
# kube-apiserver가 kubelet(Node)에 접근할 수 있도록 허용하는 '시스템 내부용 RBAC' 설정
cat configs/kube-apiserver-to-kubelet.yaml ; echo
# apiVersion: rbac.authorization.k8s.io/v1
# kind: ClusterRole
# metadata:
#   annotations:
#     rbac.authorization.kubernetes.io/autoupdate: "true"
#   labels:
#     kubernetes.io/bootstrapping: rbac-defaults
#   name: system:kube-apiserver-to-kubelet
# rules:
#   - apiGroups:
#       - ""
#     resources:
#       - nodes/proxy
#       - nodes/stats
#       - nodes/log
#       - nodes/spec
#       - nodes/metrics
#     verbs:
#       - "*"
# ---
# apiVersion: rbac.authorization.k8s.io/v1
# kind: ClusterRoleBinding
# metadata:
#   name: system:kube-apiserver
#   namespace: ""
# roleRef:
#   apiGroup: rbac.authorization.k8s.io
#   kind: ClusterRole
#   name: system:kube-apiserver-to-kubelet
# subjects:
#   - apiGroup: rbac.authorization.k8s.io
#     kind: User
#     name: kubernetes
# 
# 설명:
# - ClusterRole: kube-apiserver가 kubelet의 /stats, /log, /metrics 엔드포인트에 접근할 수 있는 권한 부여
# - ClusterRoleBinding: 인증서의 CN=kubernetes 사용자에게 위 ClusterRole을 바인딩
# - kube-apiserver는 kubelet의 metrics, logs, stats를 수집하기 위해 kubelet API에 접근해야 함

# Step 6: kube-api-server 인증서의 Subject CN 확인
# kube-apiserver가 kubelet에 접근할 때 사용하는 클라이언트 인증서의 CN이 'kubernetes'여야 함
openssl x509 -in kube-api-server.crt -text -noout
# Certificate:
#     Data:
#         Version: 3 (0x2)
#         Serial Number: ...
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = US, ST = Washington, L = Seattle, CN = CA
#         Validity
#             Not Before: Jan  7 17:02:27 2026 GMT
#             Not After : Jan  8 17:02:27 2036 GMT
#         Subject: CN = kubernetes, C = US, ST = Washington, L = Seattle
#         Subject Public Key Info: ... (생략)
#         X509v3 extensions:
#             X509v3 Basic Constraints: CA:FALSE
#             X509v3 Extended Key Usage: TLS Web Client Authentication, TLS Web Server Authentication
#             X509v3 Key Usage: critical, Digital Signature, Key Encipherment
#             X509v3 Subject Alternative Name: 
#                 IP Address:127.0.0.1, IP Address:10.32.0.1, DNS:kubernetes, DNS:kubernetes.default, 
#                 DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster, 
#                 DNS:kubernetes.svc.cluster.local, DNS:server.kubernetes.local, DNS:api-server.kubernetes.local
#             X509v3 Subject Key Identifier: ...
#             X509v3 Authority Key Identifier: ...
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value: ... (생략)
# 
# 중요 사항:
# - Subject CN = kubernetes: kube-apiserver가 kubelet에 접근할 때 사용하는 사용자 이름
# - SAN에 포함된 IP/DNS:
#   - 127.0.0.1: 로컬호스트 접근
#   - 10.32.0.1: Kubernetes Service Cluster IP
#   - kubernetes, kubernetes.default, kubernetes.default.svc, ...: Kubernetes Service DNS 이름들
#   - server.kubernetes.local, api-server.kubernetes.local: 호스트명

# Step 7: kube-scheduler 서비스 파일 확인
cat units/kube-scheduler.service ; echo
# [Unit]
# Description=Kubernetes Scheduler
# Documentation=https://github.com/kubernetes/kubernetes
# 
# [Service]
# ExecStart=/usr/local/bin/kube-scheduler \
#   --config=/etc/kubernetes/config/kube-scheduler.yaml \
#   --v=2
# Restart=on-failure
# RestartSec=5
# 
# [Install]
# WantedBy=multi-user.target
# 
# 설명:
# - kube-scheduler는 설정 파일을 통해 구성됨 (커맨드라인 옵션 대신)
# - --config: kube-scheduler 설정 파일 경로

# Step 8: kube-scheduler 설정 파일 확인
cat configs/kube-scheduler.yaml ; echo
# apiVersion: kubescheduler.config.k8s.io/v1
# kind: KubeSchedulerConfiguration
# clientConnection:
#   kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig"
# leaderElection:
#   leaderElect: true
# 
# 설명:
# - clientConnection.kubeconfig: API 서버와 통신하기 위한 kubeconfig 파일
# - leaderElection.leaderElect: 리더 선출 활성화 (고가용성 환경에서 중요)

# Step 9: kube-controller-manager 서비스 파일 확인
# cluster-cidr: POD CIDR를 포함하는 대역 (10.200.0.0/16)
# service-cluster-ip-range: apiserver 설정 값과 동일하게 설정 (10.32.0.0/24)
cat units/kube-controller-manager.service ; echo
# [Unit]
# Description=Kubernetes Controller Manager
# Documentation=https://github.com/kubernetes/kubernetes
# 
# [Service]
# ExecStart=/usr/local/bin/kube-controller-manager \
#   --bind-address=0.0.0.0 \
#   --cluster-cidr=10.200.0.0/16 \
#   --cluster-name=kubernetes \
#   --cluster-signing-cert-file=/var/lib/kubernetes/ca.crt \
#   --cluster-signing-key-file=/var/lib/kubernetes/ca.key \
#   --kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \
#   --root-ca-file=/var/lib/kubernetes/ca.crt \
#   --service-account-private-key-file=/var/lib/kubernetes/service-accounts.key \
#   --service-cluster-ip-range=10.32.0.0/24 \
#   --use-service-account-credentials=true \
#   --v=2
# Restart=on-failure
# RestartSec=5
# 
# [Install]
# WantedBy=multi-user.target
# 
# 주요 옵션 설명:
# - --cluster-cidr=10.200.0.0/16: Pod IP 대역 (전체 클러스터 Pod CIDR)
# - --service-cluster-ip-range=10.32.0.0/24: Service Cluster IP 대역 (apiserver와 동일)
# - --cluster-signing-cert-file/key-file: kubelet 인증서 서명용 CA 인증서/키
# - --service-account-private-key-file: Service Account 토큰 서명용 개인키
# - --use-service-account-credentials: 각 컨트롤러가 고유한 Service Account 사용

# Step 10: Kubernetes 바이너리 및 설정 파일을 server에 복사
scp \
  downloads/controller/kube-apiserver \
  downloads/controller/kube-controller-manager \
  downloads/controller/kube-scheduler \
  downloads/client/kubectl \
  units/kube-apiserver.service \
  units/kube-controller-manager.service \
  units/kube-scheduler.service \
  configs/kube-scheduler.yaml \
  configs/kube-apiserver-to-kubelet.yaml \
  root@server:~/
# kube-apiserver                                                           100%   86MB 181.1MB/s   00:00
# kube-controller-manager                                                  100%   80MB 183.5MB/s   00:00
# kube-scheduler                                                           100%   61MB 181.6MB/s   00:00
# kubectl                                                                  100%   53MB 170.1MB/s   00:00
# kube-apiserver.service                                                   100% 1442     3.4MB/s   00:00
# kube-controller-manager.service                                          100%  735     2.3MB/s   00:00
# kube-scheduler.service                                                   100%  281   818.8KB/s   00:00
# kube-scheduler.yaml                                                      100%  191   505.2KB/s   00:00
# kube-apiserver-to-kubelet.yaml                                           100%  727     2.1MB/s   00:00

# Step 11: server에 파일이 정상적으로 복사되었는지 확인
ssh server ls -l /root
# total 286960
# -rw------- 1 root root     9949 Jan  8 03:15 admin.kubeconfig
# -rw-r--r-- 1 root root     1899 Jan  8 02:25 ca.crt
# -rw------- 1 root root     3272 Jan  8 02:25 ca.key
# -rw-r--r-- 1 root root      271 Jan  9 16:20 encryption-config.yaml
# -rwxr-xr-x 1 root root 90243224 Jan  9 16:33 kube-apiserver
# -rw-r--r-- 1 root root     2354 Jan  8 02:25 kube-api-server.crt
# -rw------- 1 root root     3272 Jan  8 02:25 kube-api-server.key
# -rw-r--r-- 1 root root     1442 Jan  9 16:33 kube-apiserver.service
# -rw-r--r-- 1 root root      727 Jan  9 16:33 kube-apiserver-to-kubelet.yaml
# -rwxr-xr-x 1 root root 83427480 Jan  9 16:33 kube-controller-manager
# -rw------- 1 root root    10305 Jan  8 03:15 kube-controller-manager.kubeconfig
# -rw-r--r-- 1 root root      735 Jan  9 16:33 kube-controller-manager.service
# -rwxr-xr-x 1 root root 55836824 Jan  9 16:33 kubectl
# -rwxr-xr-x 1 root root 64225432 Jan  9 16:33 kube-scheduler
# -rw------- 1 root root    10215 Jan  8 03:15 kube-scheduler.kubeconfig
# -rw-r--r-- 1 root root      281 Jan  9 16:33 kube-scheduler.service
# -rw-r--r-- 1 root root      191 Jan  9 16:33 kube-scheduler.yaml
# -rw-r--r-- 1 root root     2004 Jan  8 02:25 service-accounts.crt
# -rw------- 1 root root     3272 Jan  8 02:25 service-accounts.key
# 
# 파일 목록:
# - kube-apiserver, kube-controller-manager, kube-scheduler, kubectl: 바이너리 파일 (실행 권한)
# - *.service: systemd 서비스 파일
# - *.kubeconfig: 각 구성요소의 kubeconfig 파일 (이전에 생성함)
# - kube-scheduler.yaml: kube-scheduler 설정 파일
# - kube-apiserver-to-kubelet.yaml: RBAC 설정 파일
# - ca.crt, ca.key: CA 인증서 및 키
# - kube-api-server.crt, kube-api-server.key: API 서버 인증서 및 키
# - service-accounts.crt, service-accounts.key: Service Account 인증서 및 키
# - encryption-config.yaml: Secret 암호화 설정 파일

Provision the Kubernetes Control Plane : kubectl 확인

# Step 1: server 가상머신에 SSH 접속
ssh root@server

# Step 2: Kubernetes 설정 디렉토리 생성
pwd
# /root
mkdir -p /etc/kubernetes/config

# Step 3: Kubernetes 바이너리 설치
mv kube-apiserver \
  kube-controller-manager \
  kube-scheduler kubectl \
  /usr/local/bin/

# 설치된 바이너리 확인
ls -l /usr/local/bin/kube-*
# -rwxr-xr-x 1 root root 90243224 Jan  9 16:33 /usr/local/bin/kube-apiserver
# -rwxr-xr-x 1 root root 83427480 Jan  9 16:33 /usr/local/bin/kube-controller-manager
# -rwxr-xr-x 1 root root 64225432 Jan  9 16:33 /usr/local/bin/kube-scheduler
# -rwxr-xr-x 1 root root 55836824 Jan  9 16:33 /usr/local/bin/kubectl

# Step 4: Kubernetes API Server 설정
mkdir -p /var/lib/kubernetes/

# 인증서 및 설정 파일을 /var/lib/kubernetes/로 이동
mv ca.crt ca.key \
  kube-api-server.key kube-api-server.crt \
  service-accounts.key service-accounts.crt \
  encryption-config.yaml \
  /var/lib/kubernetes/

# 이동된 파일 확인
ls -l /var/lib/kubernetes/
# -rw-r--r-- 1 root root     1899 Jan  8 02:25 ca.crt
# -rw------- 1 root root     3272 Jan  8 02:25 ca.key
# -rw-r--r-- 1 root root      271 Jan  9 16:20 encryption-config.yaml
# -rw-r--r-- 1 root root     2354 Jan  8 02:25 kube-api-server.crt
# -rw------- 1 root root     3272 Jan  8 02:25 kube-api-server.key
# -rw-r--r-- 1 root root     2004 Jan  8 02:25 service-accounts.crt
# -rw------- 1 root root     3272 Jan  8 02:25 service-accounts.key

# Step 5: kube-apiserver systemd 서비스 파일 설치
mv kube-apiserver.service \
  /etc/systemd/system/kube-apiserver.service

# systemd 디렉토리 구조 확인 (주요 파일만 표시)
tree /etc/systemd/system | grep -E "(etcd|kube-)"
# ├── etcd.service
# ├── kube-apiserver.service
# ... (기타 시스템 서비스)

# Step 6: Kubernetes Controller Manager 설정

# kube-controller-manager kubeconfig 파일 이동
mv kube-controller-manager.kubeconfig /var/lib/kubernetes/

# kube-controller-manager systemd 서비스 파일 설치
mv kube-controller-manager.service /etc/systemd/system/

# Step 7: Kubernetes Scheduler 설정

# kube-scheduler kubeconfig 파일 이동
mv kube-scheduler.kubeconfig /var/lib/kubernetes/

# kube-scheduler 설정 파일 이동
mv kube-scheduler.yaml /etc/kubernetes/config/

# kube-scheduler systemd 서비스 파일 설치
mv kube-scheduler.service /etc/systemd/system/

# Step 8: 컨트롤 플레인 서비스 시작
systemctl daemon-reload
systemctl enable kube-apiserver kube-controller-manager kube-scheduler
# Created symlink /etc/systemd/system/multi-user.target.wants/kube-apiserver.service → /etc/systemd/system/kube-apiserver.service.
# Created symlink /etc/systemd/system/multi-user.target.wants/kube-controller-manager.service → /etc/systemd/system/kube-controller-manager.service.
# Created symlink /etc/systemd/system/multi-user.target.wants/kube-scheduler.service → /etc/systemd/system/kube-scheduler.service.
systemctl start  kube-apiserver kube-controller-manager kube-scheduler

# Step 9: 서비스 상태 확인

# Kubernetes 서비스 포트 리스닝 확인
ss -tlp | grep kube
# LISTEN 0      4096            *:6443            *:*    users:(("kube-apiserver",pid=2938,fd=7))
# 
# 6443: Kubernetes API Server HTTPS 포트

# kube-apiserver 서비스 상태 확인
systemctl is-active kube-apiserver
# active

systemctl status kube-apiserver --no-pager
# ● kube-apiserver.service - Kubernetes API Server
#      Loaded: loaded (/etc/systemd/system/kube-apiserver.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-01-09 16:42:47 KST; 23s ago
#        Docs: https://github.com/kubernetes/kubernetes
#    Main PID: 2938 (kube-apiserver)
#       Tasks: 12 (limit: 2096)
#      Memory: 85.3M
#         CPU: 1.5s
#      CGroup: /system.slice/kube-apiserver.service
#              └─2938 /usr/local/bin/kube-apiserver --allow-privileged=true ...
# 
# 주요 로그:
# - API 그룹 초기화: authentication, authorization, core, apps, networking, storage 등
# - etcd 연결 및 캐시 초기화
# - RBAC 부트스트랩 역할 생성
# - PriorityClass 생성 (system-node-critical, system-cluster-critical)
# - API Priority and Fairness (APF) 설정
# - TLS 인증서 로드 완료
# - HTTPS 포트 6443에서 서비스 시작

# kube-scheduler 서비스 상태 확인
systemctl status kube-scheduler --no-pager
# ● kube-scheduler.service - Kubernetes Scheduler
#      Loaded: loaded (/etc/systemd/system/kube-scheduler.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-01-09 16:42:46 KST; 23s ago
#        Docs: https://github.com/kubernetes/kubernetes
#    Main PID: 2944 (kube-scheduler)
#       Tasks: 8 (limit: 2096)
#      Memory: 15.5M
#         CPU: 260ms
#      CGroup: /system.slice/kube-scheduler.service
#              └─2944 /usr/local/bin/kube-scheduler --config=/etc/kubernetes/config/kube-scheduler.yaml --v=2
# 
# 주요 로그:
# - 캐시 초기화: Pod, Node, PersistentVolumeClaim, StorageClass 등
# - 리더 선출 완료: successfully acquired lease kube-system/kube-scheduler

# kube-controller-manager 서비스 상태 확인
systemctl status kube-controller-manager --no-pager
# ● kube-controller-manager.service - Kubernetes Controller Manager
#      Loaded: loaded (/etc/systemd/system/kube-controller-manager.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-01-09 16:42:46 KST; 26s ago
#        Docs: https://github.com/kubernetes/kubernetes
#    Main PID: 2943 (kube-controller)
#       Tasks: 5 (limit: 2096)
#      Memory: 37.5M
#         CPU: 798ms
#      CGroup: /system.slice/kube-controller-manager.service
#              └─2943 /usr/local/bin/kube-controller-manager --bind-address=0.0.0.0 ...
# 
# 주요 로그:
# - 컨트롤러 캐시 동기화 완료: daemon sets, jobs, statefulsets, replicasets 등
# - 리더 선출 완료: became leader

# Step 10: kubectl을 사용한 클러스터 확인

# 클러스터 정보 확인
kubectl cluster-info --kubeconfig admin.kubeconfig
# Kubernetes control plane is running at https://127.0.0.1:6443
# 
# To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
# 
# 설명:
# - API 서버가 https://127.0.0.1:6443에서 정상 실행 중
# - 컨트롤 플레인이 정상 작동 중임을 확인

# 노드 확인 (아직 워커 노드가 추가되지 않아 비어있음)
kubectl get node --kubeconfig admin.kubeconfig
# No resources found

# Pod 확인 (아직 Pod이 없음)
kubectl get pod -A --kubeconfig admin.kubeconfig
# No resources found

# Service 및 Endpoints 확인
kubectl get service,ep --kubeconfig admin.kubeconfig
# NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
# service/kubernetes   ClusterIP   10.32.0.1    <none>        443/TCP   63s
# 
# NAME                   ENDPOINTS        AGE
# endpoints/kubernetes   10.0.2.15:6443   63s
# 
# 설명:
# - service/kubernetes: Kubernetes 기본 서비스 (ClusterIP: 10.32.0.1)
# - endpoints/kubernetes: API 서버 엔드포인트 (10.0.2.15:6443)
# - 이 서비스는 API 서버를 클러스터 내에서 접근할 수 있도록 하는 기본 서비스

# Step 11: RBAC (Role-Based Access Control) 확인

# ClusterRole 목록 확인
kubectl get clusterroles --kubeconfig admin.kubeconfig
# NAME                                                                   CREATED AT
# admin                                                                  2026-01-09T07:42:48Z
# cluster-admin                                                          2026-01-09T07:42:48Z
# edit                                                                   2026-01-09T07:42:48Z
# system:aggregate-to-admin                                              2026-01-09T07:42:48Z
# system:aggregate-to-edit                                               2026-01-09T07:42:48Z
# system:aggregate-to-view                                               2026-01-09T07:42:48Z
# system:auth-delegator                                                  2026-01-09T07:42:48Z
# system:basic-user                                                      2026-01-09T07:42:48Z
# ... (기타 시스템 ClusterRole들)
# system:kube-controller-manager                                         2026-01-09T07:42:48Z
# system:kube-scheduler                                                  2026-01-09T07:42:48Z
# ... (총 50개 이상의 ClusterRole)

# system:kube-scheduler ClusterRole 상세 정보 확인
kubectl describe clusterroles system:kube-scheduler --kubeconfig admin.kubeconfig
# Name:         system:kube-scheduler
# Labels:       kubernetes.io/bootstrapping=rbac-defaults
# Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
# PolicyRule:
#   Resources                                  Non-Resource URLs  Resource Names    Verbs
#   ---------                                  -----------------  --------------    -----
#   leasecandidates.coordination.k8s.io        []                 []                [create delete deletecollection get list patch update watch]
#   events                                     []                 []                [create patch update]
#   events.events.k8s.io                       []                 []                [create patch update]
#   bindings                                   []                 []                [create]
#   pods/binding                               []                 []                [create]
#   tokenreviews.authentication.k8s.io         []                 []                [create]
#   subjectaccessreviews.authorization.k8s.io  []                 []                [create]
#   leases.coordination.k8s.io                 []                 []                [create]
#   pods                                       []                 []                [delete get list watch]
#   leases.coordination.k8s.io                 []                 [kube-scheduler]  [get list update watch]
#   namespaces                                 []                 []                [get list watch]
#   nodes                                      []                 []                [get list watch]
#   persistentvolumeclaims                     []                 []                [get list watch]
#   persistentvolumes                          []                 []                [get list watch]
#   ... (기타 리소스들)
# 
# 설명:
# - kube-scheduler는 Pod 스케줄링을 위해 다양한 리소스를 읽고 쓸 수 있는 권한을 가짐
# - Pod, Node, PersistentVolume, Namespace 등을 조회하고 Pod 바인딩을 생성할 수 있음
# - Leader Election을 위한 Lease 리소스도 관리함

# ClusterRoleBinding 목록 확인
kubectl get clusterrolebindings --kubeconfig admin.kubeconfig
# NAME                                                            ROLE                                                                        AGE
# cluster-admin                                                   ClusterRole/cluster-admin                                                   81s
# system:basic-user                                               ClusterRole/system:basic-user                                               81s
# ... (기타 ClusterRoleBinding들)
# system:kube-controller-manager                                  ClusterRole/system:kube-controller-manager                                  81s
# system:kube-scheduler                                           ClusterRole/system:kube-scheduler                                           81s
# ... (총 50개 이상의 ClusterRoleBinding)

# system:kube-scheduler ClusterRoleBinding 상세 정보 확인
kubectl describe clusterrolebindings system:kube-scheduler --kubeconfig admin.kubeconfig
# Name:         system:kube-scheduler
# Labels:       kubernetes.io/bootstrapping=rbac-defaults
# Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
# Role:
#   Kind:  ClusterRole
#   Name:  system:kube-scheduler
# Subjects:
#   Kind  Name                   Namespace
#   ----  ----                   ---------
#   User  system:kube-scheduler  
# 
# - system:kube-scheduler 사용자에게 system:kube-scheduler ClusterRole이 바인딩됨
# - kube-scheduler가 사용하는 인증서의 CN이 system:kube-scheduler이므로 이 바인딩이 적용됨

# 중요 사항:
# - Kubernetes 컨트롤 플레인이 성공적으로 부트스트랩되었음
# - API Server, Controller Manager, Scheduler가 모두 정상 실행 중
# - 기본 RBAC 역할 및 바인딩이 자동 생성됨
# - 아직 워커 노드가 추가되지 않아 Node와 Pod이 없는 상태 (정상)
# - 다음 단계는 워커 노드 추가 및 kubelet, kube-proxy 설정

RBAC for Kubelet Authorization

Kubernetes API 서버가 각 작업자 노드에서 Kubelet API에 접근할 수 있도록 RBAC 권한을 구성합니다.

목적:

  • Kubelet API에 대한 접근 권한은 메트릭, 로그를 검색하고 Pod에서 명령을 실행하는 데 필요합니다.
  • kube-apiserver는 kubelet의 /stats, /log, /metrics 엔드포인트에 접근해야 합니다.
  • Kubelet의 --authorization-mode 플래그를 Webhook으로 설정해야 합니다 (워커 노드 설정 시).
  • kube-apiserver가 kubelet에 접근할 때 사용하는 인증서의 CN(Common Name)이 kubernetes여야 합니다.
# ========== server 가상머신에서 실행 ==========
# (이미 server에 SSH 접속된 상태)

# Step 1: kube-apiserver-to-kubelet.yaml 파일 내용 확인
# 이 파일은 kube-apiserver가 kubelet API에 접근할 수 있도록 하는 RBAC 설정입니다.
cat kube-apiserver-to-kubelet.yaml
# apiVersion: rbac.authorization.k8s.io/v1
# kind: ClusterRole
# metadata:
#   annotations:
#     rbac.authorization.kubernetes.io/autoupdate: "true"
#   labels:
#     kubernetes.io/bootstrapping: rbac-defaults
#   name: system:kube-apiserver-to-kubelet
# rules:
#   - apiGroups:
#       - ""
#     resources:
#       - nodes/proxy
#       - nodes/stats
#       - nodes/log
#       - nodes/spec
#       - nodes/metrics
#     verbs:
#       - "*"
# ---
# apiVersion: rbac.authorization.k8s.io/v1
# kind: ClusterRoleBinding
# metadata:
#   name: system:kube-apiserver
#   namespace: ""
# roleRef:
#   apiGroup: rbac.authorization.k8s.io
#   kind: ClusterRole
#   name: system:kube-apiserver-to-kubelet
# subjects:
#   - apiGroup: rbac.authorization.k8s.io
#     kind: User
#     name: kubernetes
# 
# - ClusterRole: kube-apiserver가 kubelet의 nodes/proxy, nodes/stats, nodes/log, nodes/spec, nodes/metrics 리소스에 접근할 수 있는 권한 부여
# - ClusterRoleBinding: 인증서의 CN=kubernetes 사용자에게 위 ClusterRole을 바인딩
# - kube-apiserver는 kubelet의 metrics, logs, stats를 수집하기 위해 kubelet API에 접근해야 함

# Step 2: RBAC 설정 적용
kubectl apply -f kube-apiserver-to-kubelet.yaml --kubeconfig admin.kubeconfig
# clusterrole.rbac.authorization.k8s.io/system:kube-apiserver-to-kubelet created
# clusterrolebinding.rbac.authorization.k8s.io/system:kube-apiserver created

# Step 3: 생성된 ClusterRole 확인
kubectl get clusterroles system:kube-apiserver-to-kubelet --kubeconfig admin.kubeconfig
# NAME                               CREATED AT
# system:kube-apiserver-to-kubelet   2026-01-09T09:20:45Z

# Step 4: 생성된 ClusterRoleBinding 확인
kubectl get clusterrolebindings system:kube-apiserver --kubeconfig admin.kubeconfig
# NAME                    ROLE                                           AGE
# system:kube-apiserver   ClusterRole/system:kube-apiserver-to-kubelet   7s

 

jumpbox 서버세서도 k8s 컨트롤플레인이 정상적으로 작동하는지 확인합니다.

 

curl -s -k --cacert ca.crt https://server.kubernetes.local:6443/version | jq
# {
#   "major": "1",
#   "minor": "32",
#   "gitVersion": "v1.32.3",
#   "gitCommit": "32cc146f75aad04beaaa245a7157eb35063a9f99",
#   "gitTreeState": "clean",
#   "buildDate": "2025-03-11T19:52:21Z",
#   "goVersion": "go1.23.6",
#   "compiler": "gc",
#   "platform": "linux/arm64"
# }

09 - Bootstrapping the Kubernetes Worker Nodes

이번에는 워커노드를 구축합니다.

사전 준비단계

# ========== jumpbox에서 실행 ==========

# Step 1: CNI Bridge 설정 파일 확인
# CNI Bridge 플러그인 설정: Pod 네트워크를 위한 브리지 인터페이스 생성 및 IP 할당
cat configs/10-bridge.conf | jq
# {
#   "cniVersion": "1.0.0",
#   "name": "bridge",
#   "type": "bridge",                    # CNI 플러그인 타입: bridge
#   "bridge": "cni0",                    # 생성할 브리지 인터페이스 이름
#   "isGateway": true,                   # 브리지를 게이트웨이로 사용
#   "ipMasq": true,                      # IP 마스커레이딩 활성화 (외부 통신용)
#   "ipam": {                            # IP 주소 관리 설정
#     "type": "host-local",              # 호스트 로컬 IP 할당 방식
#     "ranges": [
#       [
#         {
#           "subnet": "SUBNET"            # Pod 서브넷 (나중에 각 노드별로 치환됨)
#         }
#       ]
#     ],
#     "routes": [
#       {
#         "dst": "0.0.0.0/0"              # 기본 라우트 (모든 트래픽)
#       }
#     ]
#   }
# }

# Step 2: Kubelet 설정 파일 확인
# KubeletConfiguration: kubelet의 동작을 제어하는 설정
cat configs/kubelet-config.yaml | yq
# kind: KubeletConfiguration
# apiVersion: kubelet.config.k8s.io/v1beta1
# address: "0.0.0.0"                     # kubelet HTTPS 서버 바인딩 주소: 모든 인터페이스에서 10250 포트 수신
# authentication:
#   anonymous:
#     enabled: false                     # 익명 인증 비활성화 (보안 강화)
#   webhook:
#     enabled: true                      # 인증 요청을 kube-apiserver에 위임: ServiceAccount 토큰, bootstrap 토큰 처리 가능
#   x509:                                # kubelet에 접근하는 클라이언트 인증서 검증용 CA
#     clientCAFile: "/var/lib/kubelet/ca.crt"  # 대상: kube-apiserver, metrics-server, kubectl (직접 접근 시)
# authorization:                        
#   mode: Webhook                        # 인가 요청을 kube-apiserver에 위임: Node Authorizer + RBAC 적용됨
# cgroupDriver: systemd                  # cgroup 드라이버: systemd 사용
# containerRuntimeEndpoint: "unix:///var/run/containerd/containerd.sock"  # CRI 엔드포인트: containerd 소켓
# enableServer: true                     # kubelet API 서버 활성화, false면 apiserver가 kubelet 접근 불가
# failSwapOn: false                     # Swap 사용 허용 (실습 환경용)
# maxPods: 16                           # 노드당 최대 파드 수 16개
# memorySwap:
#   swapBehavior: NoSwap                 # Swap 사용 안 함
# port: 10250                            # kubelet HTTPS API 포트: 로그, exec, stats, metrics 접근에 사용
# resolvConf: "/etc/resolv.conf"         # 파드에 전달할 DNS 설정 파일
# registerNode: true                     # kubelet이 API 서버에 Node 객체 자동 등록
# runtimeRequestTimeout: "15m"           # CRI 요청 최대 대기 시간: 이미지 pull, container start 등
# tlsCertFile: "/var/lib/kubelet/kubelet.crt"      # TLS 서버 인증서 (kubelet 자신): kubelet HTTPS 서버의 서버 인증서
# tlsPrivateKeyFile: "/var/lib/kubelet/kubelet.key"
# 
# 참고: clusterDomain, clusterDNS 없어도 smoke test까지 잘됨 -> 실습에서 coredns 미사용

# Step 3: 각 워커 노드별로 SUBNET을 치환하여 설정 파일 생성 및 전송
# node-0: 10.200.0.0/24, node-1: 10.200.1.0/24
for HOST in node-0 node-1; do
  # machines.txt에서 해당 호스트의 PodCIDR 추출 (4번째 필드)
  SUBNET=$(grep ${HOST} machines.txt | cut -d " " -f 4)

  # CNI Bridge 설정 파일에서 SUBNET 플레이스홀더를 실제 서브넷으로 치환
  sed "s|SUBNET|$SUBNET|g" \
    configs/10-bridge.conf > 10-bridge.conf

  # Kubelet 설정 파일에서도 SUBNET 치환 (현재는 사용 안 하지만 일관성을 위해)
  sed "s|SUBNET|$SUBNET|g" \
    configs/kubelet-config.yaml > kubelet-config.yaml

  # 생성된 설정 파일을 워커 노드로 전송
  scp 10-bridge.conf kubelet-config.yaml \
  root@${HOST}:~/
done
# 10-bridge.conf                                                          100%  265   439.0KB/s   00:00
# kubelet-config.yaml                                                     100%  610     1.7MB/s   00:00
# 10-bridge.conf                                                          100%  265   483.4KB/s   00:00
# kubelet-config.yaml                                                     100%  610     1.5MB/s   00:00

# Step 4: 전송된 파일 확인
ssh node-0 ls -l /root
# total 8
# -rw-r--r-- 1 root root 265 Jan  9 18:29 10-bridge.conf
# -rw-r--r-- 1 root root 610 Jan  9 18:29 kubelet-config.yaml

ssh node-1 ls -l /root
# total 8
# -rw-r--r-- 1 root root 265 Jan  9 18:29 10-bridge.conf
# -rw-r--r-- 1 root root 610 Jan  9 18:29 kubelet-config.yaml

# Step 5: 추가 설정 파일 확인 및 전송 준비

# CNI Loopback 설정: 로컬호스트 네트워크 인터페이스 생성
cat configs/99-loopback.conf ; echo
# {
#   "cniVersion": "1.1.0",
#   "name": "lo",
#   "type": "loopback"                    # Loopback 인터페이스 생성
# }

# Containerd 설정: 컨테이너 런타임 설정
cat configs/containerd-config.toml ; echo
# version = 2
# 
# [plugins."io.containerd.grpc.v1.cri"]  # CRI (Container Runtime Interface) 플러그인 설정
#   [plugins."io.containerd.grpc.v1.cri".containerd]
#     snapshotter = "overlayfs"           # 스냅샷 저장 방식: overlayfs
#     default_runtime_name = "runc"       # 기본 런타임: runc
#   [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
#     runtime_type = "io.containerd.runc.v2"
#   [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
#     SystemdCgroup = true                # systemd cgroup 드라이버 사용
# [plugins."io.containerd.grpc.v1.cri".cni]
#   bin_dir = "/opt/cni/bin"              # CNI 플러그인 바이너리 디렉토리
#   conf_dir = "/etc/cni/net.d"           # CNI 설정 파일 디렉토리

# Kube-proxy 설정: 네트워크 프록시 설정
cat configs/kube-proxy-config.yaml ; echo
# kind: KubeProxyConfiguration
# apiVersion: kubeproxy.config.k8s.io/v1alpha1
# clientConnection:
#   kubeconfig: "/var/lib/kube-proxy/kubeconfig"  # API 서버 연결용 kubeconfig
# mode: "iptables"                                 # 프록시 모드: iptables 사용
# clusterCIDR: "10.200.0.0/16"                    # 클러스터 Pod CIDR (전체 Pod IP 범위)

# Step 6: systemd 서비스 파일 확인

# Containerd 서비스: 컨테이너 런타임 서비스
cat units/containerd.service
# [Unit]
# Description=containerd container runtime
# Documentation=https://containerd.io
# After=network.target
# 
# [Service]
# ExecStartPre=/sbin/modprobe overlay     # overlay 모듈 로드
# ExecStart=/bin/containerd               # containerd 실행
# Restart=always
# RestartSec=5
# Delegate=yes                            # cgroup 위임 허용
# KillMode=process
# OOMScoreAdjust=-999                     # OOM 우선순위 조정
# LimitNOFILE=1048576                     # 파일 디스크립터 제한
# LimitNPROC=infinity
# LimitCORE=infinity
# 
# [Install]
# WantedBy=multi-user.target

# Kubelet 서비스: Kubernetes 노드 에이전트
cat units/kubelet.service
# [Unit]
# Description=Kubernetes Kubelet
# Documentation=https://github.com/kubernetes/kubernetes
# After=containerd.service                # containerd 시작 후 실행
# Requires=containerd.service             # containerd 필수 의존성
# 
# [Service]
# ExecStart=/usr/local/bin/kubelet \
#   --config=/var/lib/kubelet/kubelet-config.yaml  # 설정 파일 경로
#   --kubeconfig=/var/lib/kubelet/kubeconfig        # API 서버 연결 설정
#   --v=2                                           # 로그 레벨
# Restart=on-failure
# RestartSec=5
# 
# [Install]
# WantedBy=multi-user.target

# Kube-proxy 서비스: 네트워크 프록시 서비스
cat units/kube-proxy.service
# [Unit]
# Description=Kubernetes Kube Proxy
# Documentation=https://github.com/kubernetes/kubernetes
# 
# [Service]
# ExecStart=/usr/local/bin/kube-proxy \
#   --config=/var/lib/kube-proxy/kube-proxy-config.yaml  # 설정 파일 경로
# Restart=on-failure
# RestartSec=5
# 
# [Install]
# WantedBy=multi-user.target

# Step 7: 워커 노드 바이너리 및 설정 파일 전송
# downloads/worker/*: containerd, runc, kubelet, kube-proxy 등 워커 노드 바이너리
# downloads/client/kubectl: kubectl 클라이언트 도구
for HOST in node-0 node-1; do
  scp \
    downloads/worker/* \
    downloads/client/kubectl \
    configs/99-loopback.conf \
    configs/containerd-config.toml \
    configs/kube-proxy-config.yaml \
    units/containerd.service \
    units/kubelet.service \
    units/kube-proxy.service \
    root@${HOST}:~/
done
# containerd                                                              100%   54MB 195.2MB/s   00:00
# containerd-shim-runc-v2                                                 100% 7808KB 173.2MB/s   00:00
# containerd-stress                                                       100%   21MB 181.2MB/s   00:00
# crictl                                                                  100%   37MB 187.9MB/s   00:00
# ctr                                                                     100%   22MB 194.6MB/s   00:00
# kubelet                                                                 100%   72MB 187.2MB/s   00:00
# kube-proxy                                                              100%   62MB 190.0MB/s   00:00
# runc                                                                    100%   11MB 188.6MB/s   00:00
# kubectl                                                                 100%   53MB 187.9MB/s   00:00
# 99-loopback.conf                                                        100%   65   181.1KB/s   00:00
# containerd-config.toml                                                  100%  470     1.1MB/s   00:00
# kube-proxy-config.yaml                                                  100%  184   530.1KB/s   00:00
# containerd.service                                                      100%  352     1.0MB/s   00:00
# kubelet.service                                                         100%  365     1.0MB/s   00:00
# kube-proxy.service                                                      100%  268   842.8KB/s   00:00
# ... (node-1에도 동일하게 전송)

# Step 8: CNI 플러그인 전송
# CNI 플러그인: Pod 네트워크 관리를 위한 바이너리들
for HOST in node-0 node-1; do
  scp \
    downloads/cni-plugins/* \
    root@${HOST}:~/cni-plugins/
done
# bandwidth                                                               100% 4492KB 108.4MB/s   00:00
# bridge                                                                  100% 5065KB 156.4MB/s   00:00
# dhcp                                                                    100%   12MB 177.2MB/s   00:00
# dummy                                                                   100% 4623KB 192.2MB/s   00:00
# firewall                                                                100% 5088KB 178.4MB/s   00:00
# host-device                                                             100% 4611KB 180.0MB/s   00:00
# host-local                                                              100% 3886KB 178.4MB/s   00:00
# ipvlan                                                                  100% 4628KB 172.9MB/s   00:00
# LICENSE                                                                 100%   11KB  17.5MB/s   00:00
# loopback                                                                100% 4030KB 168.2MB/s   00:00
# macvlan                                                                 100% 4764KB 179.2MB/s   00:00
# portmap                                                                 100% 4513KB 189.4MB/s   00:00
# ptp                                                                     100% 4877KB 184.2MB/s   00:00
# README.md                                                               100% 2343     5.2MB/s   00:00
# sbr                                                                     100% 4216KB 167.2MB/s   00:00
# static                                                                  100% 3498KB 172.3MB/s   00:00
# tap                                                                     100% 4775KB 174.9MB/s   00:00
# tuning                                                                  100% 4054KB 151.3MB/s   00:00
# vlan                                                                    100% 4627KB  82.9MB/s   00:00
# vrf                                                                     100% 4325KB 124.8MB/s   00:00
# ... (node-1에도 동일하게 전송, 총 20개 플러그인)

# Step 9: 전송된 파일 최종 확인

# node-0 파일 확인
ssh node-0 ls -l /root
# total 347860
# -rw-r--r-- 1 root root      265 Jan  9 18:29 10-bridge.conf
# -rw-r--r-- 1 root root       65 Jan  9 18:29 99-loopback.conf
# drwxr-xr-x 2 root root     4096 Jan  9 18:29 cni-plugins
# -rwxr-xr-x 1 root root 56836190 Jan  9 18:29 containerd
# -rw-r--r-- 1 root root      470 Jan  9 18:29 containerd-config.toml
# -rw-r--r-- 1 root root      352 Jan  9 18:29 containerd.service
# -rwxr-xr-x 1 root root  7995576 Jan  9 18:29 containerd-shim-runc-v2
# -rwxr-xr-x 1 root root 22020449 Jan  9 18:29 containerd-stress
# -rwxr-xr-x 1 root root 38808389 Jan  9 18:29 crictl
# -rwxr-xr-x 1 root root 22806881 Jan  9 18:29 ctr
# -rwxr-xr-x 1 root root 55836824 Jan  9 18:29 kubectl
# -rwxr-xr-x 1 root root 75235588 Jan  9 18:29 kubelet
# -rw-r--r-- 1 root root      610 Jan  9 18:29 kubelet-config.yaml
# -rw-r--r-- 1 root root      365 Jan  9 18:29 kubelet.service
# -rwxr-xr-x 1 root root 65274008 Jan  9 18:29 kube-proxy
# -rw-r--r-- 1 root root      184 Jan  9 18:29 kube-proxy-config.yaml
# -rw-r--r-- 1 root root      268 Jan  9 18:29 kube-proxy.service
# -rwxr-xr-x 1 root root 11305168 Jan  9 18:29 runc

# node-1 파일 확인
ssh node-1 ls -l /root
# total 347860
# -rw-r--r-- 1 root root      265 Jan  9 18:29 10-bridge.conf
# -rw-r--r-- 1 root root       65 Jan  9 18:29 99-loopback.conf
# drwxr-xr-x 2 root root     4096 Jan  9 18:29 cni-plugins
# -rwxr-xr-x 1 root root 56836190 Jan  9 18:29 containerd
# ... (node-0와 동일한 파일 목록)

# CNI 플러그인 확인 (node-0)
ssh node-0 ls -l /root/cni-plugins
# total 88164
# -rwxr-xr-x 1 root root  4600029 Jan  9 18:29 bandwidth
# -rwxr-xr-x 1 root root  5186762 Jan  9 18:29 bridge
# -rwxr-xr-x 1 root root 12321657 Jan  9 18:29 dhcp
# -rwxr-xr-x 1 root root  4734102 Jan  9 18:29 dummy
# -rwxr-xr-x 1 root root  5210586 Jan  9 18:29 firewall
# -rwxr-xr-x 1 root root  4721690 Jan  9 18:29 host-device
# -rwxr-xr-x 1 root root  3979580 Jan  9 18:29 host-local
# -rwxr-xr-x 1 root root  4738895 Jan  9 18:29 ipvlan
# -rwxr-xr-x 1 root root    11357 Jan  9 18:29 LICENSE
# -rwxr-xr-x 1 root root  4127141 Jan  9 18:29 loopback
# -rwxr-xr-x 1 root root  4878636 Jan  9 18:29 macvlan
# -rwxr-xr-x 1 root root  4621227 Jan  9 18:29 portmap
# -rwxr-xr-x 1 root root  4994381 Jan  9 18:29 ptp
# -rwxr-xr-x 1 root root     2343 Jan  9 18:29 README.md
# -rwxr-xr-x 1 root root  4317592 Jan  9 18:29 sbr
# -rwxr-xr-x 1 root root  3582221 Jan  9 18:29 static
# -rwxr-xr-x 1 root root  4889353 Jan  9 18:29 tap
# -rwxr-xr-x 1 root root  4150810 Jan  9 18:29 tuning
# -rwxr-xr-x 1 root root  4738028 Jan  9 18:29 vlan
# -rwxr-xr-x 1 root root  4428332 Jan  9 18:29 vrf
# 
# 총 20개 CNI 플러그인: bridge, loopback, host-local 등

# CNI 플러그인 확인 (node-1)
ssh node-1 ls -l /root/cni-plugins
# total 88164
# -rwxr-xr-x 1 root root  4600029 Jan  9 18:29 bandwidth
# ... (node-0와 동일한 플러그인 목록)

# 중요 사항:
# - 모든 워커 노드 바이너리, 설정 파일, CNI 플러그인이 정상적으로 전송됨
# - 각 노드별로 PodCIDR이 다르게 설정됨 (node-0: 10.200.0.0/24, node-1: 10.200.1.0/24)
# - 다음 단계는 워커 노드에서 containerd, kubelet, kube-proxy 서비스 설치 및 시작

Provisioning a Kubernetes Worker Node : node-0, node-1 에 접속해서 실행

워커 노드(node-0, node-1)에 Kubernetes 워커 컴포넌트를 설치하고 구성합니다.

워커 노드 구성요소:

  • containerd: 컨테이너 런타임 (CRI 구현)
  • kubelet: 노드 에이전트 (Pod 생명주기 관리)
  • kube-proxy: 네트워크 프록시 (Service 및 네트워크 정책 관리)
  • CNI 플러그인: Pod 네트워크 관리
# ========== node-0 가상머신에서 실행 ==========

# Step 1: node-0에 SSH 접속
ssh root@node-0

# Step 2: 현재 디렉토리 및 파일 확인
pwd
# /root
ls -l
# total 347860
# -rw-r--r-- 1 root root      265 Jan  9 18:29 10-bridge.conf
# -rw-r--r-- 1 root root       65 Jan  9 18:29 99-loopback.conf
# drwxr-xr-x 2 root root     4096 Jan  9 18:29 cni-plugins
# -rwxr-xr-x 1 root root 56836190 Jan  9 18:29 containerd
# ... (기타 바이너리 및 설정 파일)

# Step 3: OS 의존성 설치
# socat: kubectl port-forward 명령 지원
# conntrack: 연결 추적 (kube-proxy iptables 모드에 필요)
# ipset: IP 세트 관리 (네트워크 정책에 필요)
# kmod: 커널 모듈 관리
# psmisc: 프로세스 관리 도구
# bridge-utils: 브리지 네트워크 관리
apt-get -y install socat conntrack ipset kmod psmisc bridge-utils
# Reading package lists... Done
# Building dependency tree... Done
# Reading state information... Done
# kmod is already the newest version (30+20221128-1).
# The following additional packages will be installed:
#   iptables libip6tc2 libipset13 libnetfilter-conntrack3 libnfnetlink0
# The following NEW packages will be installed:
#   bridge-utils conntrack ipset iptables libip6tc2 libipset13 libnetfilter-conntrack3 libnfnetlink0 psmisc socat
# 0 upgraded, 10 newly installed, 0 to remove and 2 not upgraded.
# Need to get 1,225 kB of archives.
# ... (패키지 다운로드 및 설치)
# Setting up psmisc (23.6-1) ...
# Setting up libip6tc2:arm64 (1.8.9-2) ...
# Setting up socat (1.7.4.4-2) ...
# Setting up bridge-utils (1.7.1-1) ...
# ... (모든 패키지 설치 완료)

# Step 4: Swap 비활성화 확인
# Kubernetes는 Swap을 사용하지 않도록 권장 (성능 및 안정성)
swapon --show
# (출력 없음 - Swap이 비활성화되어 있음)

# Step 5: 설치 디렉토리 생성
# /etc/cni/net.d: CNI 네트워크 설정 파일 디렉토리
# /opt/cni/bin: CNI 플러그인 바이너리 디렉토리
# /var/lib/kubelet: kubelet 데이터 및 설정 디렉토리
# /var/lib/kube-proxy: kube-proxy 설정 디렉토리
# /var/lib/kubernetes: Kubernetes 인증서 및 설정 디렉토리
# /var/run/kubernetes: Kubernetes 런타임 데이터 디렉토리
mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

# Step 6: 워커 바이너리 설치
# Kubernetes 워커 컴포넌트 및 컨테이너 런타임 바이너리를 적절한 위치로 이동
mv crictl kube-proxy kubelet runc /usr/local/bin/  # Kubernetes 및 컨테이너 도구
mv containerd containerd-shim-runc-v2 containerd-stress /bin/  # containerd 런타임
mv cni-plugins/* /opt/cni/bin/  # CNI 플러그인 (bridge, loopback, host-local 등)

# Step 7: CNI 네트워크 설정

# CNI Bridge 및 Loopback 설정 파일 설치
mv 10-bridge.conf 99-loopback.conf /etc/cni/net.d/

# Bridge 설정 파일 확인 (node-0의 PodCIDR: 10.200.0.0/24)
cat /etc/cni/net.d/10-bridge.conf 
# {
#   "cniVersion": "1.0.0",
#   "name": "bridge",
#   "type": "bridge",
#   "bridge": "cni0",                    # 생성할 브리지 인터페이스 이름
#   "isGateway": true,                   # 브리지를 게이트웨이로 사용
#   "ipMasq": true,                      # IP 마스커레이딩 활성화
#   "ipam": {
#     "type": "host-local",              # 호스트 로컬 IP 할당
#     "ranges": [
#       [{"subnet": "10.200.0.0/24"}]    # node-0의 Pod 서브넷
#     ],
#     "routes": [{"dst": "0.0.0.0/0"}]   # 기본 라우트
#   }
# }

# Step 8: br-netfilter 커널 모듈 로드 및 설정
# CNI 브리지 네트워크를 통과하는 트래픽이 iptables에 의해 처리되도록 설정

# 현재 로드된 netfilter 모듈 확인
lsmod | grep netfilter
# (출력 없음 - 아직 로드되지 않음)

# br-netfilter 모듈 로드 (브리지 트래픽을 iptables로 전달)
modprobe br-netfilter

# 부팅 시 자동 로드 설정
echo "br-netfilter" >> /etc/modules-load.d/modules.conf

# 모듈 로드 확인
lsmod | grep netfilter
# br_netfilter           32768  0
# bridge                262144  1 br_netfilter
# 
# 설명:
# - br_netfilter: 브리지 네트워크 트래픽을 netfilter(iptables)로 전달하는 모듈
# - bridge: Linux 브리지 커널 모듈

# sysctl 설정: 브리지 트래픽이 iptables 규칙에 적용되도록 설정
echo "net.bridge.bridge-nf-call-iptables = 1"  >> /etc/sysctl.d/kubernetes.conf  # IPv4 iptables 호출
echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.d/kubernetes.conf   # IPv6 ip6tables 호출

# sysctl 설정 적용
sysctl -p /etc/sysctl.d/kubernetes.conf
# net.bridge.bridge-nf-call-iptables = 1
# net.bridge.bridge-nf-call-ip6tables = 1
# 
# 설명:
# - 이 설정으로 브리지 네트워크(cni0)를 통과하는 Pod 트래픽이 iptables 규칙에 적용됨
# - kube-proxy가 Service 라우팅을 위해 iptables 규칙을 설정할 수 있게 됨

# Step 9: containerd 설정

# containerd 설정 디렉토리 생성
mkdir -p /etc/containerd/

# containerd 설정 파일 설치
mv containerd-config.toml /etc/containerd/config.toml
mv containerd.service /etc/systemd/system/

# containerd 설정 파일 확인
cat /etc/containerd/config.toml ; echo
# version = 2
# 
# [plugins."io.containerd.grpc.v1.cri"]               # CRI 플러그인 활성화: kubelet은 이 플러그인을 통해 containerd와 통신
#   [plugins."io.containerd.grpc.v1.cri".containerd]  # containerd 기본 런타임 설정
#     snapshotter = "overlayfs"                       # 컨테이너 파일시스템 레이어 관리 방식: Linux 표준/성능 최적
#     default_runtime_name = "runc"                   # 기본 OCI 런타임: 파드가 별도 지정 없을 경우 runc 사용
#   [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]  # runc 런타임 상세 설정
#     runtime_type = "io.containerd.runc.v2"                        # containerd 최신 runc shim
#   [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]  # runc 옵션
#     SystemdCgroup = true                                                  # containerd가 cgroup을 systemd로 관리
# [plugins."io.containerd.grpc.v1.cri".cni]           # CNI 설정
#   bin_dir = "/opt/cni/bin"                          # CNI 플러그인 바이너리 위치
#   conf_dir = "/etc/cni/net.d"                       # CNI 네트워크 설정 파일 위치

# kubelet ↔ containerd 연결 Flow
# kubelet
#   ↓ CRI (gRPC)
# unix:///var/run/containerd/containerd.sock
#   ↓
# containerd CRI plugin
#   ↓
# runc
#   ↓
# Linux namespaces / cgroups

# Step 10: Kubelet 설정

# kubelet 설정 파일 및 서비스 파일 설치
mv kubelet-config.yaml /var/lib/kubelet/  # kubelet 설정 파일
mv kubelet.service /etc/systemd/system/  # kubelet systemd 서비스

# Step 11: Kubernetes Proxy 설정

# kube-proxy 설정 파일 및 서비스 파일 설치
mv kube-proxy-config.yaml /var/lib/kube-proxy/  # kube-proxy 설정 파일
mv kube-proxy.service /etc/systemd/system/      # kube-proxy systemd 서비스

# Step 12: 워커 서비스 시작

# systemd 설정 파일 변경사항 반영
systemctl daemon-reload

# 부팅 시 자동 시작 설정
systemctl enable containerd kubelet kube-proxy
# Created symlink /etc/systemd/system/multi-user.target.wants/containerd.service → /etc/systemd/system/containerd.service.
# Created symlink /etc/systemd/system/multi-user.target.wants/kubelet.service → /etc/systemd/system/kubelet.service.
# Created symlink /etc/systemd/system/multi-user.target.wants/kube-proxy.service → /etc/systemd/system/kube-proxy.service.

# 서비스 시작 (containerd → kubelet → kube-proxy 순서로 시작)
systemctl start containerd kubelet kube-proxy

# Step 13: 서비스 상태 확인

# kubelet 서비스 상태 확인
systemctl status kubelet --no-pager
# ● kubelet.service - Kubernetes Kubelet
#      Loaded: loaded (/etc/systemd/system/kubelet.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-01-09 19:02:22 KST; 4s ago
#        Docs: https://github.com/kubernetes/kubernetes
#    Main PID: 3107 (kubelet)
#       Tasks: 11 (limit: 2096)
#      Memory: 24.5M
#         CPU: 134ms
#      CGroup: /system.slice/kubelet.service
#              └─3107 /usr/local/bin/kubelet --config=/var/lib/kubelet/kubelet-config.yaml --kubeconfig=/var/lib/kubelet/kubeconfig --v=2
# 
# 주요 로그:
# Jan 09 19:02:22 node-0 kubelet[3107]: I0109 19:02:22.614618    3107 kubelet_node_status.go:687] "Recording event message for node" node="node-0" event="NodeHasNoDiskPressure"
# Jan 09 19:02:22 node-0 kubelet[3107]: I0109 19:02:22.614624    3107 kubelet_node_status.go:687] "Recording event message for node" node="node-0" event="NodeHasSufficientPID"
# Jan 09 19:02:22 node-0 kubelet[3107]: I0109 19:02:22.614637    3107 kubelet_node_status.go:75] "Attempting to register node" node="node-0"
# Jan 09 19:02:22 node-0 kubelet[3107]: I0109 19:02:22.618254    3107 kubelet_node_status.go:78] "Successfully registered node" node="node-0"
# Jan 09 19:02:22 node-0 kubelet[3107]: I0109 19:02:22.824780    3107 kubelet_node_status.go:687] "Recording event message for node" node="node-0" event="NodeReady"
# Jan 09 19:02:22 node-0 kubelet[3107]: I0109 19:02:22.824839    3107 kubelet_node_status.go:501] "Fast updating node status as it just became ready"
# Jan 09 19:02:23 node-0 kubelet[3107]: I0109 19:02:23.426526    3107 apiserver.go:52] "Watching apiserver"
# Jan 09 19:02:23 node-0 kubelet[3107]: I0109 19:02:23.428363    3107 reflector.go:376] Caches populated for *v1.Pod from pkg/kubelet/config/apiserver.go:66
# Jan 09 19:02:23 node-0 kubelet[3107]: I0109 19:02:23.428401    3107 kubelet.go:2468] "SyncLoop ADD" source="api" pods=[]
# Jan 09 19:02:23 node-0 kubelet[3107]: I0109 19:02:23.430671    3107 desired_state_of_world_populator.go:157] "Finished populating initial desired state of world"


# containerd 서비스 상태 확인
systemctl status containerd --no-pager
# ● containerd.service - containerd container runtime
#      Loaded: loaded (/etc/systemd/system/containerd.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-01-09 19:02:22 KST; 8s ago
#        Docs: https://containerd.io
#    Process: 3096 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS)
#   Main PID: 3103 (containerd)
#       Tasks: 8 (limit: 2096)
#      Memory: 19.5M
#         CPU: 54ms
#      CGroup: /system.slice/containerd.service
#              └─3103 /bin/containerd
# 
# 주요 로그:
# Jan 09 19:02:22 node-0 containerd[3103]: time="2026-01-09T19:02:22.402466220+09:00" level=info msg="Start event monitor"
# Jan 09 19:02:22 node-0 containerd[3103]: time="2026-01-09T19:02:22.402489190+09:00" level=info msg="Start cni network conf syncer for default"
# Jan 09 19:02:22 node-0 containerd[3103]: time="2026-01-09T19:02:22.402506282+09:00" level=info msg="Start streaming server"
# Jan 09 19:02:22 node-0 containerd[3103]: time="2026-01-09T19:02:22.402723932+09:00" level=info msg="Registered namespace \"k8s.io\" with NRI"
# Jan 09 19:02:22 node-0 containerd[3103]: time="2026-01-09T19:02:22.402878634+09:00" level=info msg=serving... address=/run/containerd/containerd.sock
# Jan 09 19:02:22 node-0 containerd[3103]: time="2026-01-09T19:02:22.402892808+09:00" level=info msg="containerd successfully booted in 0.023178s"


# kube-proxy 서비스 상태 확인
systemctl status kube-proxy --no-pager
# ● kube-proxy.service - Kubernetes Kube Proxy
#      Loaded: loaded (/etc/systemd/system/kube-proxy.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-01-09 19:02:22 KST; 12s ago
#        Docs: https://github.com/kubernetes/kubernetes
#   Main PID: 3097 (kube-proxy)
#       Tasks: 5 (limit: 2096)
#      Memory: 15.3M
#         CPU: 104ms
#      CGroup: /system.slice/kube-proxy.service
#              └─3097 /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/kube-proxy-config.yaml
# 
# 주요 로그:
# Jan 09 19:02:23 node-0 kube-proxy[3097]: I0109 19:02:23.642343    3097 server.go:499] "Golang settings" GOGC="" GOMAXPROCS="" GOTRACEBACK=""
# Jan 09 19:02:23 node-0 kube-proxy[3097]: I0109 19:02:23.643585    3097 config.go:199] "Starting service config controller"
# Jan 09 19:02:23 node-0 kube-proxy[3097]: I0109 19:02:23.643598    3097 shared_informer.go:313] Waiting for caches to sync for service config
# Jan 09 19:02:23 node-0 kube-proxy[3097]: I0109 19:02:23.643609    3097 config.go:105] "Starting endpoint slice config controller"
# Jan 09 19:02:23 node-0 kube-proxy[3097]: I0109 19:02:23.643864    3097 config.go:329] "Starting node config controller"
# Jan 09 19:02:23 node-0 kube-proxy[3097]: I0109 19:02:23.744405    3097 shared_informer.go:320] Caches are synced for endpoint slice config
# Jan 09 19:02:23 node-0 kube-proxy[3097]: I0109 19:02:23.744410    3097 shared_informer.go:320] Caches are synced for node config
# Jan 09 19:02:23 node-0 kube-proxy[3097]: I0109 19:02:23.744419    3097 shared_informer.go:320] Caches are synced for service config


# Step 14: node-0에서 나가기
exit
# logout
# Connection to node-0 closed.

# ========== jumpbox에서 실행 ==========

# Step 15: server에서 node-0 정보 확인 (상세)
# Node 객체의 상세 정보 확인 (YAML 형식)
ssh server "kubectl get nodes node-0 -o yaml --kubeconfig admin.kubeconfig" | yq
# {
#   "apiVersion": "v1",
#   "kind": "Node",
#   "metadata": {
#     "name": "node-0",
#     "labels": {
#       "kubernetes.io/arch": "arm64",
#       "kubernetes.io/hostname": "node-0",
#       "kubernetes.io/os": "linux"
#     },
#     "uid": "f7cd0235-5a49-403e-9498-96ec5ecc47a7"
#   },
#   "status": {
#     "addresses": [
#       {
#         "address": "192.168.10.101",    # node-0의 내부 IP
#         "type": "InternalIP"
#       },
#       {
#         "address": "node-0",             # 호스트명
#         "type": "Hostname"
#       }
#     ],
#     "allocatable": {                     # 할당 가능한 리소스
#       "cpu": "2",
#       "memory": "1791292Ki",
#       "pods": "16"                       # maxPods 설정값과 일치
#     },
#     "capacity": {                        # 노드 전체 리소스
#       "cpu": "2",
#       "memory": "1893692Ki",
#       "pods": "16"
#     },
#     "conditions": [                      # 노드 상태 조건
#       {
#         "type": "MemoryPressure",
#         "status": "False",               # 메모리 압박 없음
#         "reason": "KubeletHasSufficientMemory"
#       },
#       {
#         "type": "DiskPressure",
#         "status": "False",               # 디스크 압박 없음
#         "reason": "KubeletHasNoDiskPressure"
#       },
#       {
#         "type": "PIDPressure",
#         "status": "False",               # PID 압박 없음
#         "reason": "KubeletHasSufficientPID"
#       },
#       {
#         "type": "Ready",
#         "status": "True",                # 노드가 Ready 상태
#         "reason": "KubeletReady",
#         "message": "kubelet is posting ready status"
#       }
#     ],
#     "daemonEndpoints": {
#       "kubeletEndpoint": {
#         "Port": 10250                    # kubelet HTTPS API 포트
#       }
#     },
#     "nodeInfo": {
#       "architecture": "arm64",
#       "containerRuntimeVersion": "containerd://2.1.0-beta.0",
#       "kernelVersion": "6.1.0-40-arm64",
#       "kubeProxyVersion": "v1.32.3",
#       "kubeletVersion": "v1.32.3",
#       "operatingSystem": "linux",
#       "osImage": "Debian GNU/Linux 12 (bookworm)"
#     }
#   }
# }

# Step 16: 노드 목록 확인 (간단)
ssh server "kubectl get nodes -owide --kubeconfig admin.kubeconfig"
# NAME     STATUS   ROLES    AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION   CONTAINER-RUNTIME
# node-0   Ready    <none>   33s   v1.32.3   192.168.10.101   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0
# 
# 설명:
# - STATUS: Ready (노드가 정상 작동 중)
# - ROLES: <none> (워커 노드, 컨트롤 플레인 역할 없음)
# - AGE: 33s (노드 등록 후 경과 시간)
# - VERSION: v1.32.3 (Kubernetes 버전)
# - INTERNAL-IP: 192.168.10.101 (노드 내부 IP)
# - CONTAINER-RUNTIME: containerd://2.1.0-beta.0

# Step 17: Pod 확인 (아직 Pod이 없음)
ssh server "kubectl get pod -A --kubeconfig admin.kubeconfig"
# No resources found
# 
# 아직 클러스터에 Pod이 없음 (정상)

node-1 접속 후 실행

# ========== node-1 가상머신에서 실행 ==========
# node-0와 동일한 절차를 node-1에서도 수행합니다.

# Step 1: node-1에 SSH 접속
ssh root@node-1
# Linux node-1 6.1.0-40-arm64 #1 SMP Debian 6.1.153-1 (2025-09-20) aarch64
# ...

# Step 2: OS 의존성 설치
# node-0와 동일한 패키지 설치 (socat, conntrack, ipset, kmod, psmisc, bridge-utils)
apt-get -y install socat conntrack ipset kmod psmisc bridge-utils
# Reading package lists... Done
# Building dependency tree... Done
# Reading state information... Done
# kmod is already the newest version (30+20221128-1).
# The following additional packages will be installed:
#   iptables libip6tc2 libipset13 libnetfilter-conntrack3 libnfnetlink0
# The following NEW packages will be installed:
#   bridge-utils conntrack ipset iptables libip6tc2 libipset13 libnetfilter-conntrack3 libnfnetlink0 psmisc socat
# 0 upgraded, 10 newly installed, 0 to remove and 2 not upgraded.
# Need to get 1,225 kB of archives.
# ... (패키지 다운로드 및 설치)
# Setting up psmisc (23.6-1) ...
# Setting up libip6tc2:arm64 (1.8.9-2) ...
# Setting up socat (1.7.4.4-2) ...
# Setting up bridge-utils (1.7.1-1) ...
# ... (모든 패키지 설치 완료)

# Step 3: 설치 디렉토리 생성
# node-0와 동일한 디렉토리 구조 생성
mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

# Step 4: 워커 바이너리 설치
# Kubernetes 워커 컴포넌트 및 컨테이너 런타임 바이너리 설치
mv crictl kube-proxy kubelet runc /usr/local/bin/  # Kubernetes 및 컨테이너 도구
mv containerd containerd-shim-runc-v2 containerd-stress /bin/  # containerd 런타임
mv cni-plugins/* /opt/cni/bin/  # CNI 플러그인

# Step 5: CNI 네트워크 설정

# CNI Bridge 및 Loopback 설정 파일 설치
mv 10-bridge.conf 99-loopback.conf /etc/cni/net.d/

# Bridge 설정 파일 확인 (node-1의 PodCIDR: 10.200.1.0/24)
cat /etc/cni/net.d/10-bridge.conf 
# {
#   "cniVersion": "1.0.0",
#   "name": "bridge",
#   "type": "bridge",
#   "bridge": "cni0",
#   "isGateway": true,
#   "ipMasq": true,
#   "ipam": {
#     "type": "host-local",
#     "ranges": [
#       [{"subnet": "10.200.1.0/24"}]    # node-1의 Pod 서브넷 (node-0와 다름)
#     ],
#     "routes": [{"dst": "0.0.0.0/0"}]
#   }
# }
# 
# 참고: node-0는 10.200.0.0/24, node-1은 10.200.1.0/24를 사용

# Step 6: br-netfilter 커널 모듈 로드 및 설정
# CNI 브리지 네트워크를 통과하는 트래픽이 iptables에 의해 처리되도록 설정
modprobe br-netfilter  # br-netfilter 모듈 로드
echo "br-netfilter" >> /etc/modules-load.d/modules.conf  # 부팅 시 자동 로드 설정

# 모듈 로드 확인
lsmod | grep netfilter
# br_netfilter           32768  0
# bridge                262144  1 br_netfilter

# sysctl 설정: 브리지 트래픽이 iptables 규칙에 적용되도록 설정
echo "net.bridge.bridge-nf-call-iptables = 1"  >> /etc/sysctl.d/kubernetes.conf  # IPv4 iptables 호출
echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.d/kubernetes.conf   # IPv6 ip6tables 호출

# sysctl 설정 적용
sysctl -p /etc/sysctl.d/kubernetes.conf
# net.bridge.bridge-nf-call-iptables = 1
# net.bridge.bridge-nf-call-ip6tables = 1

# Step 7: containerd 설정
# containerd 설정 디렉토리 생성 및 설정 파일 설치
mkdir -p /etc/containerd/
mv containerd-config.toml /etc/containerd/config.toml
mv containerd.service /etc/systemd/system/

# Step 8: Kubelet 설정
# kubelet 설정 파일 및 서비스 파일 설치
mv kubelet-config.yaml /var/lib/kubelet/
mv kubelet.service /etc/systemd/system/

# Step 9: Kubernetes Proxy 설정
# kube-proxy 설정 파일 및 서비스 파일 설치
mv kube-proxy-config.yaml /var/lib/kube-proxy/
mv kube-proxy.service /etc/systemd/system/

# Step 10: 워커 서비스 시작
# systemd 설정 파일 변경사항 반영
systemctl daemon-reload

# 부팅 시 자동 시작 설정
systemctl enable containerd kubelet kube-proxy
# Created symlink /etc/systemd/system/multi-user.target.wants/containerd.service → /etc/systemd/system/containerd.service.
# Created symlink /etc/systemd/system/multi-user.target.wants/kubelet.service → /etc/systemd/system/kubelet.service.
# Created symlink /etc/systemd/system/multi-user.target.wants/kube-proxy.service → /etc/systemd/system/kube-proxy.service.

# 서비스 시작
systemctl start containerd kubelet kube-proxy

# Step 11: 서비스 상태 확인

# kubelet 서비스 상태 확인 (초기 시작 시 실패할 수 있음)
systemctl status kubelet --no-pager
# ● kubelet.service - Kubernetes Kubelet
#      Loaded: loaded (/etc/systemd/system/kubelet.service; enabled; preset: enabled)
#      Active: activating (auto-restart) (Result: exit-code) since Fri 2026-01-09 19:15:05 KST; 2s ago
#        Docs: https://github.com/kubernetes/kubernetes
#    Process: 3100 ExecStart=/usr/local/bin/kubelet ... (code=exited, status=1/FAILURE)
#   Main PID: 3100 (code=exited, status=1/FAILURE)
# 
# Jan 09 19:15:05 node-1 systemd[1]: kubelet.service: Main process exited, code=exited, status=1/FAILURE
# Jan 09 19:15:05 node-1 systemd[1]: kubelet.service: Failed with result 'exit-code'.

# containerd 서비스 상태 확인
systemctl status containerd --no-pager
# ● containerd.service - containerd container runtime
#      Loaded: loaded (/etc/systemd/system/containerd.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-01-09 19:15:05 KST; 9s ago
#        Docs: https://containerd.io
#    Process: 3093 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS)
#   Main PID: 3099 (containerd)
#       Tasks: 8 (limit: 2096)
#      Memory: 20.1M
#         CPU: 58ms
#      CGroup: /system.slice/containerd.service
#              └─3099 /bin/containerd
# 
# 주요 로그:
# Jan 09 19:15:05 node-1 containerd[3099]: time="2026-01-09T19:15:05.930777128+09:00" level=info msg="Start cni network conf syncer for default"
# Jan 09 19:15:05 node-1 containerd[3099]: time="2026-01-09T19:15:05.930781297+09:00" level=info msg="Start streaming server"
# Jan 09 19:15:05 node-1 containerd[3099]: time="2026-01-09T19:15:05.930842036+09:00" level=info msg="containerd successfully booted in 0.044249s"

# kube-proxy 서비스 상태 확인
systemctl status kube-proxy --no-pager
# ● kube-proxy.service - Kubernetes Kube Proxy
#      Loaded: loaded (/etc/systemd/system/kube-proxy.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-01-09 19:15:05 KST; 29s ago
#        Docs: https://github.com/kubernetes/kubernetes
#   Main PID: 3094 (kube-proxy)
#       Tasks: 5 (limit: 2096)
#      Memory: 14.8M
#         CPU: 107ms
#      CGroup: /system.slice/kube-proxy.service
#              └─3094 /usr/local/bin/kube-proxy --config=/var/lib/kube-proxy/kube-proxy-config.yaml
# 
# 주요 로그:
# Jan 09 19:15:14 node-1 kube-proxy[3094]: I0109 19:15:14.093778    3094 config.go:199] "Starting service config controller"
# Jan 09 19:15:14 node-1 kube-proxy[3094]: I0109 19:15:14.194593    3094 shared_informer.go:320] Caches are synced for node config
# Jan 09 19:15:14 node-1 kube-proxy[3094]: I0109 19:15:14.194593    3094 shared_informer.go:320] Caches are synced for service config
# Jan 09 19:15:14 node-1 kube-proxy[3094]: I0109 19:15:14.194613    3094 shared_informer.go:320] Caches are synced for endpoint slice config

# kubelet 재시작 후 상태 확인 (자동 재시작으로 정상 작동)
systemctl start containerd kubelet kube-proxy  # 재시작 (필요시)
systemctl status kubelet --no-pager
# ● kubelet.service - Kubernetes Kubelet
#      Loaded: loaded (/etc/systemd/system/kubelet.service; enabled; preset: enabled)
#      Active: active (running) since Fri 2026-01-09 19:15:10 KST; 15s ago
#        Docs: https://github.com/kubernetes/kubernetes
#   Main PID: 3139 (kubelet)
#       Tasks: 10 (limit: 2096)
#      Memory: 24.4M
#         CPU: 206ms
#      CGroup: /system.slice/kubelet.service
#              └─3139 /usr/local/bin/kubelet --config=/var/lib/kubelet/kubelet-config.yaml --kubeconfig=/var/lib/kubelet/kubeconfig --v=2
# 
# 주요 로그:
# Jan 09 19:15:11 node-1 kubelet[3139]: I0109 19:15:11.219044    3139 kubelet_node_status.go:75] "Attempting to register node" node="node-1"
# Jan 09 19:15:11 node-1 kubelet[3139]: I0109 19:15:11.223387    3139 kubelet_node_status.go:78] "Successfully registered node" node="node-1"
# Jan 09 19:15:11 node-1 kubelet[3139]: I0109 19:15:11.533702    3139 kubelet_node_status.go:687] "Recording event message for node" node="node-1" event="NodeReady"
# Jan 09 19:15:11 node-1 kubelet[3139]: I0109 19:15:11.533762    3139 kubelet_node_status.go:501] "Fast updating node status as it just became ready"
# Jan 09 19:15:12 node-1 kubelet[3139]: I0109 19:15:12.033352    3139 apiserver.go:52] "Watching apiserver"
# Jan 09 19:15:12 node-1 kubelet[3139]: I0109 19:15:12.035223    3139 reflector.go:376] Caches populated for *v1.Pod from pkg/kubelet/config/apiserver.go:66
# Jan 09 19:15:12 node-1 kubelet[3139]: I0109 19:15:12.035318    3139 kubelet.go:2468] "SyncLoop ADD" source="api" pods=[]
# Jan 09 19:15:12 node-1 kubelet[3139]: I0109 19:15:12.041271    3139 desired_state_of_world_populator.go:157] "Finished populating initial desired state of world"


# Step 12: node-1에서 나가기
exit
# logout
# Connection to node-1 closed.

# ========== jumpbox에서 실행 ==========

# Step 13: 두 워커 노드 모두 확인
# server에서 두 노드의 상태 확인
ssh server "kubectl get nodes -owide --kubeconfig admin.kubeconfig"
# NAME     STATUS   ROLES    AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION   CONTAINER-RUNTIME
# node-0   Ready    <none>   13m   v1.32.3   192.168.10.101   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0
# node-1   Ready    <none>   44s   v1.32.3   192.168.10.102   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0

# Step 14: Pod 확인 (아직 Pod이 없음)
ssh server "kubectl get pod -A --kubeconfig admin.kubeconfig"
# No resources found

 

node-0와 node-1 두 워커 노드는 모두 클러스터에 성공적으로 등록되어 Ready 상태입니다.

 

각 노드는 node-0는 10.200.0.0/24, node-1은 10.200.1.0/24와 같이 고유한 PodCIDR을 할당받았습니다.

 

\모든 워커 노드에서 containerd, kubelet, kube-proxy 서비스가 정상적으로 실행되고 있음을 확인할 수 있습니다.


10 - Configuring kubectl for Remote Access

관리자 자격 증명을 기반으로 커맨드 접속용 kubeconfig 파일을 생성합니다

# ========== jumpbox에서 실행 ==========
# jumpbox에서 원격으로 Kubernetes 클러스터에 접근하기 위한 kubeconfig 파일을 생성합니다.
# 이전에 생성한 admin 인증서를 사용하여 관리자 권한으로 클러스터에 접근할 수 있습니다.

# Step 1: API 서버 연결 확인
# /etc/hosts에 등록된 server.kubernetes.local DNS 엔트리를 기반으로 API 서버에 접근 가능한지 확인
curl -s --cacert ca.crt https://server.kubernetes.local:6443/version | jq
# {
#   "major": "1",
#   "minor": "32",
#   "gitVersion": "v1.32.3",
#   "gitCommit": "32cc146f75aad04beaaa245a7157eb35063a9f99",
#   "gitTreeState": "clean",
#   "buildDate": "2025-03-11T19:52:21Z",
#   "goVersion": "go1.23.6",
#   "compiler": "gc",
#   "platform": "linux/arm64"
# }

# Step 2: 클러스터 설정 추가
# kubeconfig 파일에 클러스터 정보를 추가합니다.
# --embed-certs=true: CA 인증서를 base64로 인코딩하여 kubeconfig 파일에 포함
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443
# Cluster "kubernetes-the-hard-way" set.

# Step 3: 사용자 자격 증명 설정
# admin 사용자의 클라이언트 인증서와 키를 설정합니다.
# 주의: --embed-certs 옵션을 사용하지 않으므로 인증서 파일 경로가 저장됨
kubectl config set-credentials admin \
  --client-certificate=admin.crt \
  --client-key=admin.key
# User "admin" set.

# Step 4: 컨텍스트 생성
# 클러스터와 사용자를 연결하는 컨텍스트를 생성합니다.
kubectl config set-context kubernetes-the-hard-way \
  --cluster=kubernetes-the-hard-way \
  --user=admin
# Context "kubernetes-the-hard-way" created.

# Step 5: 컨텍스트 활성화
# 생성한 컨텍스트를 기본 컨텍스트로 설정합니다.
kubectl config use-context kubernetes-the-hard-way
# Switched to context "kubernetes-the-hard-way".
# 
# 설명:
# - 기본 컨텍스트가 kubernetes-the-hard-way로 설정됨
# - 이후 kubectl 명령어는 이 컨텍스트를 자동으로 사용함
# - ~/.kube/config 파일이 생성/수정됨

# Step 6: 클러스터 버전 확인
# 원격 Kubernetes 클러스터의 버전을 확인합니다.
kubectl version
# Client Version: v1.32.3
# Kustomize Version: v5.5.0
# Server Version: v1.32.3

# Step 7: 노드 목록 조회 (상세 로그 포함)
# -v=6 옵션으로 kubectl의 상세 로그를 확인하면서 노드 목록을 조회합니다.
kubectl get nodes -v=6
# I0109 19:18:11.796526    3402 loader.go:402] Config loaded from file:  /root/.kube/config
# I0109 19:18:11.796996    3402 envvar.go:172] "Feature gate default state" feature="InformerResourceVersion" enabled=false
# I0109 19:18:11.797009    3402 envvar.go:172] "Feature gate default state" feature="WatchListClient" enabled=false
# I0109 19:18:11.797012    3402 envvar.go:172] "Feature gate default state" feature="ClientsAllowCBOR" enabled=false
# I0109 19:18:11.797014    3402 envvar.go:172] "Feature gate default state" feature="ClientsPreferCBOR" enabled=false
# I0109 19:18:11.797112    3402 cert_rotation.go:140] Starting client certificate rotation controller
# I0109 19:18:11.811497    3402 round_trippers.go:560] GET https://server.kubernetes.local:6443/api?timeout=32s 200 OK in 14 milliseconds
# I0109 19:18:11.813475    3402 round_trippers.go:560] GET https://server.kubernetes.local:6443/apis?timeout=32s 200 OK in 0 milliseconds
# I0109 19:18:11.819024    3402 round_trippers.go:560] GET https://server.kubernetes.local:6443/api/v1/nodes?limit=500 200 OK in 1 milliseconds
# NAME     STATUS   ROLES    AGE    VERSION
# node-0   Ready    <none>   15m    v1.32.3
# node-1   Ready    <none>   3m1s   v1.32.3

# Step 8: 생성된 kubeconfig 파일 확인
# ~/.kube/config 파일의 내용을 확인합니다.
cat /root/.kube/config
# apiVersion: v1
# clusters:
# - cluster:
#     certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZURENDQXpTZ0F3SUJBZ0lVS1NaRG1IQXZlT2VheHpSamVzSDdmU0ZIMXIwd0RRWUpLb1pJaHZjTkFRRU4KQlFBd1FURUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQWdNQ2xkaGMyaHBibWQwYjI0eEVEQU9CZ05WQkFjTQpCMU5sWVhSMGJHVXhDekFKQmdOVkJBTU1Ba05CTUI0WERUSTJNREV3TnpFMk16Y3hNVm9YRFRNMk1ERXdPREUyCk16Y3hNVm93UVRFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01DbGRoYzJocGJtZDBiMjR4RURBT0JnTlYKQkFjTUIxTmxZWFIwYkdVeEN6QUpCZ05WQkFNTUFrTkJNSUlDSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQWc4QQpNSUlDQ2dLQ0FnRUEyU0VicGN2Wk04UlZFQVFUS05udVlxYzVnN3ZWTW9pQUs5VDA1Q0lobTlPWTZJWWwxbE4vCjdqcVhETWRyQzdEVDJrQUIyQ1V2U2FVZFdhOGZoYkFaQlhoK2Mwcjd6TjRFMDRzUVZKUitsNDNLVVNyd1V5dzYKbE4razJjTnFwOHk4U0tSa2tDYW1oVTJDL0NNbC9oODJuRTQzcUU1SEtpeEl1VEkvYmk3d3c1d0hiSm9xOFBIMQo5M3dQUFlYQndDMXZuSC9ybzA5eEkxWWpFZHJXblNtek9kV29ZV0dMbEhWS1NVa0R0L3VkdmlnKzhhNDhpZGJ0Ckh4OG54T0Q4TDZrVmlnNVV6QnlZdHU3RzBFeVQzekM2ZFlSQUk4a0NRbnIvbTJaOE9hMVpBREdsZHd1UXpGRmYKNUVSNVlGNlB5NzFQaUJ3dGJwaGJrck9pcll3V2d5SlpVaEh1TU9tT0hNYWUxQTBCUFFYcE56a0ROb25ETHhMdwpsbGpiWGorTkhpWmwweU4zKzduaFBTWGRsN3orVzVJaWsrOTVSK2lrMHhJVUcyRFBlMEFhSi9GeU1IQW15UkttCmVGd2FZT1daSWtKWG1paVQ1dG1mSFhFbGlIS1pNNC8rSkJuRXhjNktwb1RYTm9BT2pLSlNLRllvQWc0bEl3Q0wKWGkxN1pFU0pTaUYyUkVZcTd3Z0NjbGNVOHZkcFVUQkZ6RWQzckxtQjJLOU9Ea3drUEdMRnFDLzlLWitZSUFIYgoxVVZQSDJXZWFXZzUzYjIyZEZDc3BvdkZrUjE4L3VNSHlZUmF4MmdrdkJ1YU1ZMUlSV2F5SFhkekxzNTF5bEhMCjJHd3ROYVJqM3NsSzhLT0hKc0hURWk3U1FsQngrN29zdHB1eW9DSXdUZ2Yyc0tRL05Mek1RK1VDQXdFQUFhTTgKTURvd0RBWURWUjBUQkFVd0F3RUIvekFMQmdOVkhROEVCQU1DQVFZd0hRWURWUjBPQkJZRUZJcHpjNEluV01DcQpSQUROQTZxa3lSVmhFRmtPTUEwR0NTcUdTSWIzRFFFQkRRVUFBNElDQVFETTRkQ20zY2J4c3BmT3NNa29OZFNPCm1DcC9nM3F2dWlUdkp2Mm1jQzZuMDlpYmtuTWNIbFQ1TUdIYlhoZ2NTRkM5N1JaYVdRK3JKc3h5Z0Y5STIwK1kKdkNPOUxpK25ka1hPS2hRUENTQlU3T0xMZFRwUkFndUxEN1ljek84ai9wUUttVmhzZVV3c0RTVzRzaGt5RGovWApIQys2aW9KMndpZk0wVkxLZW4yRzZMVWw0b0VSNVFEeDJ6Y3RoZjNXNVBtRkVBYjZacTlzQnlzc0M0ekJiaUJTCk1EUkl0Ulk0VW5oRllsSk8yVmh5QlI2YmlhZ1c1bEVYVHF1d0haV01sRlJTbEZEL0Z4ZW54ZTA3MVcrMXI0YTkKNVNIV3ZzQmJ2SGZaT3VuZ2JKTjJ2Q1hyTUJ0V0Z1M3BwWXdNQUc0WGplYjBSRCtPaGprU1ZJcEhEaWFDUGdaaQphUi8yOW5tcWg1OE8zMEhtaDlTa2xLbkFrYzhqLzFsL2V3U2F1RjR5ejlZUGRLL1UyQktIV09kMkUzYnJGbXhYCnkyK0lES1FvZ0g3YlFCamd3VEFDRTJYSkRQM0E0Z2ljbTVLWXdkWW9penhEdjBxV3hHQVlueVhMWDE1eFlYTGwKYjJsWklOWFlCWU9zZXdVcDI4SmxBRG9hT2x2MFJLaVp6OE5xSVduSkZhK25HVWk0YVBWWGJIek5idlBubmlsMwpucHdTUjIwamRpdkY0N2VIOVJZdkVMN05KdVp0VDdUc1V3bXkxaUNGWG02Nlh6UWcwWFJpNlErQ29IdFRLRjJWClFyUTlHNzUyWTJDT25TYkJOU2o5ZWpHbTN2a2tsbWI4NmZBdTFZSlU5ZWdabjZnbXZOMnE5MlRjSjB0Q3RqZDcKV1Z6OWV3L3VTbjFQVlZSOGJTd3M2dz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
#     server: https://server.kubernetes.local:6443
#   name: kubernetes-the-hard-way
# contexts:
# - context:
#     cluster: kubernetes-the-hard-way
#     user: admin
#   name: kubernetes-the-hard-way
# current-context: kubernetes-the-hard-way
# kind: Config
# preferences: {}
# users:
# - name: admin
#   user:
#     client-certificate: /root/kubernetes-the-hard-way/admin.crt
#     client-key: /root/kubernetes-the-hard-way/admin.key

# Step 9: 노드 상세 정보 확인
# 노드의 상세 정보를 확인합니다.
kubectl get nodes -owide
# NAME     STATUS   ROLES    AGE    VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION   CONTAINER-RUNTIME
# node-0   Ready    <none>   15m    v1.32.3   192.168.10.101   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0
# node-1   Ready    <none>   3m9s   v1.32.3   192.168.10.102   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0
# 
# - node-0: Ready 상태, AGE 15m, INTERNAL-IP 192.168.10.101
# - node-1: Ready 상태, AGE 3m9s, INTERNAL-IP 192.168.10.102
# - 두 노드 모두 동일한 버전 (v1.32.3) 및 컨테이너 런타임 (containerd://2.1.0-beta.0)
# - OS: Debian GNU/Linux 12 (bookworm), Kernel: 6.1.0-40-arm64

# Step 10: 모든 네임스페이스의 Pod 확인
# 클러스터에 배포된 모든 Pod을 확인합니다.
kubectl get pod -A
# No resources found

 

jumpbox에서 kubectl을 이용해 원격 Kubernetes 클러스터에 접근할 수 있으며, 이때 생성된 ~/.kube/config 파일이 기본

 

kubeconfig로 사용되어 --kubeconfig 옵션 없이도 kubectl 명령어를 실행할 수 있습니다.

 

또한 admin 인증서를 사용하므로 system:masters 그룹의 모든 권한을 갖습니다.


11 - Provisioning Pod Network Routes

이제 각 노드에서 Pod들이 통신을 할 수 있도록, OS 커널에 수동으로 라우팅 설정을 수행합니다.

왜 라우팅 설정이 필요한가?

Kubernetes 클러스터에서 각 워커 노드는 고유한 PodCIDR(Pod IP 대역)을 할당받습니다.

  • node-0: PodCIDR 10.200.0.0/24 (10.200.0.1 ~ 10.200.0.254)
  • node-1: PodCIDR 10.200.1.0/24 (10.200.1.1 ~ 10.200.1.254)

문제 상황:

  • node-0의 Pod(예: 10.200.0.10)가 node-1의 Pod(예: 10.200.1.20)로 패킷을 보내려고 할 때
  • 각 노드의 라우팅 테이블에는 자신의 PodCIDR만 로컬 네트워크로 인식됨
  • 다른 노드의 PodCIDR에 대한 라우팅 정보가 없으면 패킷이 전달되지 않음

해결 방법:

  • 각 노드의 라우팅 테이블에 다른 노드의 PodCIDR에 대한 정적 라우트를 추가
  • 목적지 PodCIDR로 가는 패킷을 해당 노드의 물리적 IP(192.168.10.x)로 전달하도록 설정
  • 이를 통해 노드 간 Pod 통신이 가능해짐

네트워크 구조

항목 네트워크 대역 or IP 설명
clusterCIDR 10.200.0.0/16 전체 클러스터 Pod IP 대역
→ node-0 PodCIDR 10.200.0.0/24 node-0에 할당된 Pod IP 대역
→ node-1 PodCIDR 10.200.1.0/24 node-1에 할당된 Pod IP 대역
ServiceCIDR 10.32.0.0/24 Kubernetes Service IP 대역
→ api clusterIP 10.32.0.1 API 서버의 ClusterIP

 

물리적 네트워크:

  • server: 192.168.10.100
  • node-0: 192.168.10.101
  • node-1: 192.168.10.102
# ========== jumpbox에서 실행 ==========
# 각 노드의 IP 주소와 PodCIDR 정보를 추출하여 라우팅 설정에 사용합니다.

# Step 1: machines.txt 파일에서 각 노드의 정보 추출
# machines.txt 형식: IP FQDN HOSTNAME PodCIDR
SERVER_IP=$(grep server machines.txt | cut -d " " -f 1)      # server의 IP 주소 추출
NODE_0_IP=$(grep node-0 machines.txt | cut -d " " -f 1)      # node-0의 IP 주소 추출
NODE_0_SUBNET=$(grep node-0 machines.txt | cut -d " " -f 4)  # node-0의 PodCIDR 추출
NODE_1_IP=$(grep node-1 machines.txt | cut -d " " -f 1)      # node-1의 IP 주소 추출
NODE_1_SUBNET=$(grep node-1 machines.txt | cut -d " " -f 4)  # node-1의 PodCIDR 추출

# 추출한 정보 확인
echo $SERVER_IP $NODE_0_IP $NODE_0_SUBNET $NODE_1_IP $NODE_1_SUBNET
# 192.168.10.100 192.168.10.101 10.200.0.0/24 192.168.10.102 10.200.1.0/24
# 
# 설명:
# - SERVER_IP: 192.168.10.100 (컨트롤 플레인 노드)
# - NODE_0_IP: 192.168.10.101 (워커 노드 0)
# - NODE_0_SUBNET: 10.200.0.0/24 (node-0의 PodCIDR)
# - NODE_1_IP: 192.168.10.102 (워커 노드 1)
# - NODE_1_SUBNET: 10.200.1.0/24 (node-1의 PodCIDR)

# Step 2: server 노드에 라우팅 설정
# server(컨트롤 플레인)는 두 워커 노드의 PodCIDR에 접근할 수 있어야 합니다.
# 예: server에서 node-0의 Pod(10.200.0.x)나 node-1의 Pod(10.200.1.x)로 접근 가능

# server의 현재 라우팅 테이블 확인
ssh server ip -c route
# default via 10.0.2.2 dev eth0 
# 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 
# 192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.100 
# 
# 설명:
# - default: 기본 게이트웨이 (10.0.2.2 via eth0)
# - 10.0.2.0/24: Vagrant NAT 네트워크 (eth0)
# - 192.168.10.0/24: 클러스터 내부 네트워크 (eth1)
# - 아직 PodCIDR에 대한 라우트가 없음

# server에 두 워커 노드의 PodCIDR 라우트 추가
# 라우트 형식: "목적지 PodCIDR → 해당 노드의 물리적 IP로 전달"
ssh root@server <<EOF
  ip route add ${NODE_0_SUBNET} via ${NODE_0_IP}  # 10.200.0.0/24 → 192.168.10.101로 전달
  ip route add ${NODE_1_SUBNET} via ${NODE_1_IP}  # 10.200.1.0/24 → 192.168.10.102로 전달
EOF
# Pseudo-terminal will not be allocated because stdin is not a terminal.
# Linux server 6.1.0-40-arm64 #1 SMP Debian 6.1.153-1 (2025-09-20) aarch64
# ...
# 
# - ip route add: 정적 라우트 추가
# - via: 다음 홉(Next Hop) 주소, 즉 패킷을 전달할 게이트웨이 주소

# server의 라우팅 테이블 재확인 (라우트 추가 확인)
ssh server ip -c route
# default via 10.0.2.2 dev eth0 
# 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 
# 10.200.0.0/24 via 192.168.10.101 dev eth1    # ← 새로 추가된 라우트
# 10.200.1.0/24 via 192.168.10.102 dev eth1    # ← 새로 추가된 라우트
# 192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.100 
# 
# - 10.200.0.0/24 via 192.168.10.101 dev eth1: node-0의 PodCIDR 라우트 추가됨
# - 10.200.1.0/24 via 192.168.10.102 dev eth1: node-1의 PodCIDR 라우트 추가됨
# - 이제 server에서 두 워커 노드의 Pod로 패킷을 전달할 수 있음

# Step 3: node-0에 라우팅 설정
# node-0는 node-1의 PodCIDR에 접근할 수 있어야 합니다.
# 예: node-0의 Pod(10.200.0.x)가 node-1의 Pod(10.200.1.x)로 통신 가능

# node-0의 현재 라우팅 테이블 확인
ssh node-0 ip -c route
# default via 10.0.2.2 dev eth0 
# 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 
# 192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.101 
# 
# - 기본 라우팅 정보만 있음
# - 아직 node-1의 PodCIDR(10.200.1.0/24)에 대한 라우트가 없음

# node-0에 node-1의 PodCIDR 라우트 추가
ssh root@node-0 <<EOF
  ip route add ${NODE_1_SUBNET} via ${NODE_1_IP}  # 10.200.1.0/24 → 192.168.10.102로 전달
EOF
# Pseudo-terminal will not be allocated because stdin is not a terminal.
# Linux node-0 6.1.0-40-arm64 #1 SMP Debian 6.1.153-1 (2025-09-20) aarch64
# ...
# 
# - node-1의 PodCIDR(10.200.1.0/24)로 가는 패킷을 node-1의 물리적 IP(192.168.10.102)로 전달
# - 이를 통해 node-0의 Pod에서 node-1의 Pod로 통신 가능

# node-0의 라우팅 테이블 재확인
ssh node-0 ip -c route
# default via 10.0.2.2 dev eth0 
# 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 
# 10.200.1.0/24 via 192.168.10.102 dev eth1    # ← 새로 추가된 라우트
# 192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.101 
# 
# - 10.200.1.0/24 via 192.168.10.102 dev eth1: node-1의 PodCIDR 라우트 추가됨
# - node-0에서 node-1의 Pod로 패킷을 전달할 수 있음

# Step 4: node-1에 라우팅 설정
# node-1는 node-0의 PodCIDR에 접근할 수 있어야 합니다.
# 예: node-1의 Pod(10.200.1.x)가 node-0의 Pod(10.200.0.x)로 통신 가능

# node-1의 현재 라우팅 테이블 확인
ssh node-1 ip -c route
# default via 10.0.2.2 dev eth0 
# 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 
# 192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.102 
# 
# - 기본 라우팅 정보만 있음
# - 아직 node-0의 PodCIDR(10.200.0.0/24)에 대한 라우트가 없음

# node-1에 node-0의 PodCIDR 라우트 추가
ssh root@node-1 <<EOF
  ip route add ${NODE_0_SUBNET} via ${NODE_0_IP}  # 10.200.0.0/24 → 192.168.10.101로 전달
EOF
# Pseudo-terminal will not be allocated because stdin is not a terminal.
# Linux node-1 6.1.0-40-arm64 #1 SMP Debian 6.1.153-1 (2025-09-20) aarch64
# ...
# 
# - node-0의 PodCIDR(10.200.0.0/24)로 가는 패킷을 node-0의 물리적 IP(192.168.10.101)로 전달
# - 이를 통해 node-1의 Pod에서 node-0의 Pod로 통신 가능

# node-1의 라우팅 테이블 재확인
ssh node-1 ip -c route
# default via 10.0.2.2 dev eth0 
# 10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 
# 10.200.0.0/24 via 192.168.10.101 dev eth1    # ← 새로 추가된 라우트
# 192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.102 
# 
# - 10.200.0.0/24 via 192.168.10.101 dev eth1: node-0의 PodCIDR 라우트 추가됨
# - node-1에서 node-0의 Pod로 패킷을 전달할 수 있음

# ========== 라우팅 설정 완료 ==========
# 
# 최종 라우팅 구조:
# 
# server (192.168.10.100):
#   - 10.200.0.0/24 → 192.168.10.101 (node-0)
#   - 10.200.1.0/24 → 192.168.10.102 (node-1)
# 
# node-0 (192.168.10.101):
#   - 10.200.1.0/24 → 192.168.10.102 (node-1)
#   - 10.200.0.0/24 → 로컬 네트워크 (CNI 브리지)
# 
# node-1 (192.168.10.102):
#   - 10.200.0.0/24 → 192.168.10.101 (node-0)
#   - 10.200.1.0/24 → 로컬 네트워크 (CNI 브리지)
# 
# 중요 사항:
# - 이 라우팅 설정은 재부팅 시 사라지는 임시 설정입니다.
# - 프로덕션 환경에서는 네트워크 관리 도구(예: BGP, OSPF)나 CNI 플러그인(예: Calico, Flannel)을 사용하여 자동으로 라우팅을 관리합니다.
# - 이 실습에서는 Kubernetes The Hard Way의 이해를 위해 수동으로 라우팅을 설정합니다.
# - 각 노드의 CNI 플러그인(여기서는 bridge)은 자신의 PodCIDR 내에서 Pod 간 통신을 처리합니다.
# - 노드 간 Pod 통신은 위에서 설정한 정적 라우트를 통해 이루어집니다.

12 - Smoke Test

이제 구축된 클러스터를 테스트 해 봅니다.

시크릿 저장시 ETCD 데이터 암호화 테스트

Kind 환경에서는 다음과 같이 Secret 생성시 key, value값이 노출되는 형태로 저장이 됩니다.


# Create a generic secret
kubectl create secret generic kubernetes-the-hard-way --from-literal="mykey=mydata"

root@myk8s-control-plane:/# etcdctl get /registry/secrets/default/kubernetes-the-hard-way | hexdump -C
00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret|
00000010  73 2f 64 65 66 61 75 6c  74 2f 6b 75 62 65 72 6e  |s/default/kubern|
00000020  65 74 65 73 2d 74 68 65  2d 68 61 72 64 2d 77 61  |etes-the-hard-wa|
00000030  79 0a 6b 38 73 00 0a 0c  0a 02 76 31 12 06 53 65  |y.k8s.....v1..Se|
00000040  63 72 65 74 12 db 01 0a  bf 01 0a 17 6b 75 62 65  |cret........kube|
00000050  72 6e 65 74 65 73 2d 74  68 65 2d 68 61 72 64 2d  |rnetes-the-hard-|
00000060  77 61 79 12 00 1a 07 64  65 66 61 75 6c 74 22 00  |way....default".|
00000070  2a 24 33 34 65 39 35 37  61 36 2d 36 38 34 62 2d  |*$34e957a6-684b-|
00000080  34 61 63 64 2d 39 36 65  30 2d 37 34 66 36 34 32  |4acd-96e0-74f642|
00000090  34 32 32 65 35 63 32 00  38 00 42 08 08 e8 bb 83  |422e5c2.8.B.....|
000000a0  cb 06 10 00 8a 01 62 0a  0e 6b 75 62 65 63 74 6c  |......b..kubectl|
000000b0  2d 63 72 65 61 74 65 12  06 55 70 64 61 74 65 1a  |-create..Update.|
000000c0  02 76 31 22 08 08 e8 bb  83 cb 06 10 00 32 08 46  |.v1".........2.F|
000000d0  69 65 6c 64 73 56 31 3a  2e 0a 2c 7b 22 66 3a 64  |ieldsV1:..,{"f:d|
000000e0  61 74 61 22 3a 7b 22 2e  22 3a 7b 7d 2c 22 66 3a  |ata":{".":{},"f:|
000000f0  6d 79 6b 65 79 22 3a 7b  7d 7d 2c 22 66 3a 74 79  |mykey":{}},"f:ty|
00000100  70 65 22 3a 7b 7d 7d 42  00 12 0f 0a 05 6d 79 6b  |pe":{}}B.....myk|
00000110  65 79 12 06 6d 79 64 61  74 61 1a 06 4f 70 61 71  |ey..mydata..Opaq|
00000120  75 65 1a 00 22 00 0a                              |ue.."..|
00000127

 

하지만 hardway 실습에서는 ETCD에 Secret을 암호화 하여 저장하도록 환경을 구성하였기 때문에 다음과 같이 암호화된 secret을 확인할 수 있었습니다.

# ========== jumpbox에서 실행 ==========
# Kubernetes Secret의 암호화 저장을 검증하기 위한 테스트를 수행합니다.
# 이전에 설정한 EncryptionConfiguration이 정상적으로 작동하는지 확인합니다.

# Step 1: Secret 생성
# --from-literal: 키-값 쌍을 직접 지정하여 Secret 생성
# mykey=mydata: 테스트용 간단한 데이터
kubectl create secret generic kubernetes-the-hard-way --from-literal="mykey=mydata"
# secret/kubernetes-the-hard-way created

# Step 2: Secret 기본 정보 확인
# Secret의 기본 정보(이름, 타입, 데이터 개수, 생성 시간) 확인
kubectl get secret kubernetes-the-hard-way
# NAME                      TYPE     DATA   AGE
# kubernetes-the-hard-way   Opaque   1      5s

# Step 3: Secret 상세 정보 확인 (YAML 형식)
# Secret의 전체 YAML 정의 확인 (base64로 인코딩된 데이터 포함)
kubectl get secret kubernetes-the-hard-way -o yaml
# apiVersion: v1
# data:
#   mykey: bXlkYXRh
# kind: Secret
# metadata:
#   creationTimestamp: "2026-01-09T10:39:12Z"
#   name: kubernetes-the-hard-way
#   namespace: default
#   resourceVersion: "13328"
#   uid: 8fd9e91b-cba7-4147-a9ed-0e56dbabd3c4
# type: Opaque

# Step 4: Secret 데이터 추출 (base64 인코딩된 형태)
# jsonpath를 사용하여 특정 필드만 추출
kubectl get secret kubernetes-the-hard-way -o jsonpath='{.data.mykey}' ; echo
# bXlkYXRh

kubectl get secret kubernetes-the-hard-way -o jsonpath='{.data.mykey}' | base64 -d ; echo
# mydata

ssh root@server \
    'etcdctl get /registry/secrets/default/kubernetes-the-hard-way | hexdump -C'
# 00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret| # etcd key 이름 (평문)
# 00000010  73 2f 64 65 66 61 75 6c  74 2f 6b 75 62 65 72 6e  |s/default/kubern|
# 00000020  65 74 65 73 2d 74 68 65  2d 68 61 72 64 2d 77 61  |etes-the-hard-wa|
# 00000030  79 0a 6b 38 73 3a 65 6e  63 3a 61 65 73 63 62 63  |y.k8s:enc:aescbc|
# 00000040  3a 76 31 3a 6b 65 79 31  3a 87 6a 8a ee 13 08 d1  |:v1:key1:.j.....|
# 00000050  36 be ac d5 9b 53 20 40  c0 38 5e 7b ce f0 6a 6a  |6....S @.8^{..jj|
# 00000060  fd a0 8e 24 a7 83 3b a1  77 e1 4d 80 0c 57 71 eb  |...$..;.w.M..Wq.|
# 00000070  84 19 c1 bd 63 bc 6d ec  81 9c c3 de d7 eb e5 99  |....c.m.........|
# 00000080  1e 28 e2 ea ae 40 47 1b  26 12 c9 5f 18 6a 3c 29  |.(...@G.&.._.j<)|
# 00000090  b7 0c 4d 53 6c 11 85 6e  97 e2 c8 30 21 05 89 6d  |..MSl..n...0!..m|
# 000000a0  3e 70 45 35 fd dd f1 dd  b7 cc b1 6b 04 fc b5 18  |>pE5.......k....|
# 000000b0  af 23 4a d5 01 47 7d 14  7a c7 c4 a1 bb 5e fc f0  |.#J..G}.z....^..|
# 000000c0  d1 04 de 3b 2a b8 dd 9d  3f c7 cc f0 eb de 40 17  |...;*...?.....@.|
# 000000d0  4d 61 d5 73 6e 2b db 62  93 78 3a d3 f0 ef b4 c6  |Ma.sn+.b.x:.....|
# 000000e0  3a 5d dd e2 3a d0 b0 fe  e3 30 7d 82 0a 22 9b e7  |:]..:....0}.."..|
# 000000f0  c7 ad ac a0 c5 52 bd 63  18 93 63 3b 02 4d a7 69  |.....R.c..c;.M.i|
# 00000100  d2 0b 37 0f ce 9e c9 b6  9f 73 3d e8 1a 9e a0 bf  |..7......s=.....|
# 00000110  09 e6 0e 80 32 df 27 1c  68 2c b1 8a ff 2a 2b 18  |....2.'.h,...*+.|
# 00000120  c4 05 d6 67 95 91 c7 f6  7b b2 6e c3 ee 54 9e 65  |...g....{..n..T.e|
# 00000130  5b d0 1d 80 ab 3f dd d2  fb b5 17 e6 39 b1 f8 e0  |[....?......9...|
# 00000140  60 f0 11 be 82 23 2b 41  dc 72 f1 de 36 c4 41 f0  |`....#+A.r..6.A.|
# 00000150  cc 16 e
# 
# 1. etcd key 이름 (평문):
#    - 00000000-0000002f: /registry/secrets/default/kubernetes-the-hard-way
#    - etcd key 이름은 항상 평문으로 저장됨 (어떤 리소스인지 식별 가능)
#    - 0a: 줄바꿈 문자 (\n)
# 
# 2. 암호화 헤더 (평문):
#    - 00000030-00000047: k8s:enc:aescbc:v1:key1:
#    - k8s:enc: Kubernetes 암호화 포맷 식별자
#    - aescbc: 암호화 알고리즘 (AES-CBC)
#    - v1: encryption provider 버전
#    - key1: 사용된 encryption key 이름 (encryption-config.yaml에서 설정한 key1)
# 
# 3. 암호화된 데이터 (바이너리):
#    - 00000048 이후: 암호화된 Secret 데이터
#    - 이 데이터는 AES-CBC 알고리즘으로 암호화되어 있어 평문으로 읽을 수 없음
#    - "mydata"라는 원본 데이터가 암호화되어 저장됨
# 
# 중요 사항:
# - Kubernetes Secret이 etcd에 AES-CBC 방식으로 정상 암호화되어 저장되고 있음을 증명
# - etcd에 직접 접근해도 Secret 데이터를 평문으로 읽을 수 없음
# - 암호화 헤더를 통해 어떤 알고리즘과 키를 사용했는지 확인 가능
# - 이는 이전에 설정한 EncryptionConfiguration이 정상적으로 작동하고 있음을 의미

Deployments, Port Forwarding, Log, Exec, Service (NodePort)

# ========== jumpbox에서 실행 ==========
# Kubernetes 클러스터의 기본 기능들을 테스트합니다.
# Deployment, Pod, Service, Port Forwarding, Logs, Exec 등을 확인합니다.

# ========== Deployments ==========

# Step 1: 현재 Pod 상태 확인
kubectl get pod
# No resources found in default namespace.
# 
# - default 네임스페이스에 Pod이 없음 (정상)

# Step 2: nginx Deployment 생성
# nginx 웹 서버를 위한 Deployment 생성
kubectl create deployment nginx --image=nginx:latest
# deployment.apps/nginx created
# 
# - Deployment 리소스 생성 성공
# - 이미지: nginx:latest
# - 기본 replicas: 1

# Step 3: Deployment 스케일링
# nginx Deployment를 2개의 replica로 확장
kubectl scale deployment nginx --replicas=2
# deployment.apps/nginx scaled
# 
# - Deployment가 2개의 Pod로 스케일링됨
# - 각 Pod는 서로 다른 노드에 스케줄링될 수 있음

# Step 4: Pod 상태 확인 (초기)
kubectl get pod -owide
# NAME                     READY   STATUS              RESTARTS   AGE   IP       NODE     NOMINATED NODE   READINESS GATES
# nginx-54c98b4f84-fz86r   0/1     ContainerCreating   0          6s    <none>   node-0   <none>           <none>
# nginx-54c98b4f84-x7bx5   0/1     ContainerCreating   0          3s    <none>   node-1   <none>           <none>
# 
# - 두 Pod 모두 ContainerCreating 상태 (컨테이너 생성 중)
# - node-0와 node-1에 각각 하나씩 스케줄링됨
# - 아직 IP가 할당되지 않음

# Step 5: Pod 상태 확인 (진행 중)
kubectl get pod -owide
# NAME                     READY   STATUS              RESTARTS   AGE   IP           NODE     NOMINATED NODE   READINESS GATES
# nginx-54c98b4f84-fz86r   1/1     Running             0          12s   10.200.0.2   node-0   <none>           <none>
# nginx-54c98b4f84-x7bx5   0/1     ContainerCreating   0          9s    <none>       node-1   <none>           <none>
# 
# - node-0의 Pod: Running 상태, IP 10.200.0.2 할당됨
# - node-1의 Pod: 아직 ContainerCreating 상태

# Step 6: Pod 상태 확인 (최종)
kubectl get pod -owide
# NAME                     READY   STATUS    RESTARTS   AGE   IP           NODE     NOMINATED NODE   READINESS GATES
# nginx-54c98b4f84-fz86r   1/1     Running   0          14s   10.200.0.2   node-0   <none>           <none>
# nginx-54c98b4f84-x7bx5   1/1     Running   0          11s   10.200.1.2   node-1   <none>           <none>
# 
# - 두 Pod 모두 Running 상태
# - node-0: IP 10.200.0.2 (PodCIDR 10.200.0.0/24)
# - node-1: IP 10.200.1.2 (PodCIDR 10.200.1.0/24)
# - 각 Pod는 해당 노드의 PodCIDR에서 IP를 할당받음

# Step 7: 컨테이너 확인 (node-0)
# crictl을 사용하여 node-0에서 실행 중인 컨테이너 확인
ssh node-0 crictl ps
# time="2026-01-09T19:40:40+09:00" level=warning msg="Config \"/etc/crictl.yaml\" does not exist, trying next: \"/usr/local/bin/crictl.yaml\""
# time="2026-01-09T19:40:40+09:00" level=warning msg="runtime connect using default endpoints: [unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock]. As the default settings are now deprecated, you should set the endpoint instead."
# time="2026-01-09T19:40:40+09:00" level=warning msg="Image connect using default endpoints: [unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock]. As the default settings are now deprecated, you should set the endpoint instead."
# CONTAINER           IMAGE               CREATED             STATE               NAME                ATTEMPT             POD ID              POD                      NAMESPACE
# b36b452e70a99       759581db3b0c2       9 seconds ago       Running             nginx               0                   54379db4350f0       nginx-54c98b4f84-fz86r   default
# 
# - crictl: CRI(Container Runtime Interface) 클라이언트 도구
# - nginx 컨테이너가 node-0에서 정상 실행 중
# - Pod ID: 54379db4350f0
# - 경고 메시지는 설정 파일 경로 관련 (기능에는 영향 없음)

# Step 8: 컨테이너 확인 (node-1)
ssh node-1 crictl ps
# time="2026-01-09T19:40:43+09:00" level=warning msg="Config \"/etc/crictl.yaml\" does not exist, trying next: \"/usr/local/bin/crictl.yaml\""
# time="2026-01-09T19:40:43+09:00" level=warning msg="runtime connect using default endpoints: [unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock]. As the default settings are now deprecated, you should set the endpoint instead."
# time="2026-01-09T19:40:43+09:00" level=warning msg="Image connect using default endpoints: [unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock]. As the default settings are now deprecated, you should set the endpoint instead."
# CONTAINER           IMAGE               CREATED             STATE               NAME                ATTEMPT             POD ID              POD                      NAMESPACE
# 0baec4fa0b20b       759581db3b0c2       9 seconds ago       Running             nginx               0                   9e1680023fbbf       nginx-54c98b4f84-x7bx5   default
# 
# - nginx 컨테이너가 node-1에서 정상 실행 중
# - Pod ID: 9e1680023fbbf

# Step 9: 프로세스 트리 확인 (node-0)
# pstree를 사용하여 node-0의 프로세스 계층 구조 확인
ssh node-0 pstree -ap
# systemd,1
#   |-containerd,3103
#   |   |-{containerd},3104
#   |   ...
#   |-containerd-shim,3636 -namespace k8s.io -id 54379db4350f0333aba3aef43b79a9b6011d27b76bef8692742e1444e49c9d34 -address/ru
#   |   |-nginx,3690
#   |   |   |-nginx,3723
#   |   |   `-nginx,3724
#   |   |-pause,3661
#   |   ...
#   |-kube-proxy,3097 --config=/var/lib/kube-proxy/kube-proxy-config.yaml
#   |-kubelet,3107 --config=/var/lib/kubelet/kubelet-config.yaml --kubeconfig=/var/lib/kubelet/kubeconfig --v=2
#   ...
# 
# - containerd: 컨테이너 런타임
# - containerd-shim: Pod별 샌드박스 관리 프로세스
#   - nginx: 실제 애플리케이션 컨테이너 (워커 프로세스 포함)
#   - pause: Pod의 네트워크 네임스페이스를 유지하는 인프라 컨테이너
# - kubelet: 노드 에이전트
# - kube-proxy: 네트워크 프록시

# Step 10: 프로세스 트리 확인 (node-1)
ssh node-1 pstree -ap
# systemd,1
#   |-containerd,3099
#   |   ...
#   |-containerd-shim,3561 -namespace k8s.io -id 9e1680023fbbfcf527c47d7ed97b670bdd7192c429844df8331ccaac8d60be2f -address/ru
#   |   |-nginx,3614
#   |   |   |-nginx,3650
#   |   |   `-nginx,3651
#   |   |-pause,3585
#   |   ...
#   |-kube-proxy,3094 --config=/var/lib/kube-proxy/kube-proxy-config.yaml
#   |-kubelet,3139 --config=/var/lib/kubelet/kubelet-config.yaml --kubeconfig=/var/lib/kubelet/kubeconfig --v=2
#   ...
# 
# - node-1도 동일한 구조로 컨테이너가 실행 중

# Step 11: 브리지 네트워크 확인 (node-0)
# brctl을 사용하여 CNI 브리지 네트워크 확인
ssh node-0 brctl show
# bridge name     bridge id               STP enabled     interfaces
# cni0            8000.5e07d0f6227f       no              veth569e4701
#
# - cni0: CNI 플러그인이 생성한 브리지 인터페이스
# - veth569e4701: Pod의 veth 인터페이스 (가상 이더넷 쌍의 한쪽)
# - STP: Spanning Tree Protocol (비활성화됨)

# Step 12: 브리지 네트워크 확인 (node-1)
ssh node-1 brctl show
# bridge name     bridge id               STP enabled     interfaces
# cni0            8000.3a4fc43898b5       no              veth8d15c9d5
# 
# - node-1도 동일하게 cni0 브리지가 생성됨
# - veth8d15c9d5: node-1의 Pod veth 인터페이스

# Step 13: 네트워크 인터페이스 확인 (node-0)
# ip addr를 사용하여 네트워크 인터페이스 상세 정보 확인
ssh node-0 ip addr
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
#     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
#     inet 127.0.0.1/8 scope host lo
# ...
# 4: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
#     link/ether 5e:07:d0:f6:22:7f brd ff:ff:ff:ff:ff:ff
#     inet 10.200.0.1/24 brd 10.200.0.255 scope global cni0
#     valid_lft forever preferred_lft forever
# ...
# 5: veth569e4701@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master cni0 state UP group default qlen 1000
#     link/ether ce:96:0d:58:39:fc brd ff:ff:ff:ff:ff:ff link-netns cni-a8fab2ba-e2fc-1e74-d382-79e4a37a28ed
#     inet6 fe80::cc96:dff:fe58:39fc/64 scope link 
#     valid_lft forever preferred_lft forever
# 
# - cni0: IP 10.200.0.1/24 (브리지 게이트웨이)
# - veth569e4701: Pod의 veth 인터페이스
#   - master cni0: cni0 브리지에 연결됨
#   - link-netns: Pod의 네트워크 네임스페이스와 연결됨
# - 각 Pod마다 고유한 veth 인터페이스가 생성됨

# Step 14: 네트워크 인터페이스 확인 (node-1)
ssh node-1 ip addr
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
#     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
#     inet 127.0.0.1/8 scope host lo
# ...
# 4: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
#     link/ether 3a:4f:c4:38:98:b5 brd ff:ff:ff:ff:ff:ff
#     inet 10.200.1.1/24 brd 10.200.1.255 scope global cni0
#     valid_lft forever preferred_lft forever
# ...
# 5: veth8d15c9d5@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master cni0 state UP group default qlen 1000
#     link/ether da:1d:1f:f9:ba:8d brd ff:ff:ff:ff:ff:ff link-netns cni-57bcacb8-76a4-f333-9c0f-309cea38fa23
#     inet6 fe80::d81d:1fff:fef9:ba8d/64 scope link 
#     valid_lft forever preferred_lft forever
# 
# - cni0: IP 10.200.1.1/24 (node-1의 브리지 게이트웨이)
# - veth8d15c9d5: node-1의 Pod veth 인터페이스

# Step 15: Pod IP로 직접 접근 테스트 (node-1 Pod)
# server 노드에서 node-1의 Pod IP로 직접 HTTP 요청
ssh server curl -s 10.200.1.2 | grep title
# <title>Welcome to nginx!</title>

# Step 16: Pod IP로 직접 접근 테스트 (node-0 Pod)
ssh server curl -s 10.200.0.2 | grep title
# <title>Welcome to nginx!</title>

# ========== Port Forwarding ==========

# Step 17: Pod 이름 추출
# nginx Pod의 이름을 변수에 저장
POD_NAME=$(kubectl get pods -l app=nginx -o jsonpath="{.items[0].metadata.name}")
echo $POD_NAME
# nginx-54c98b4f84-fz86r
# 
# - -l app=nginx: app=nginx 레이블을 가진 Pod 선택
# - jsonpath: 첫 번째 Pod의 이름 추출
# - 결과: nginx-54c98b4f84-fz86r

# Step 18: Port Forwarding 설정
# 로컬 8080 포트를 Pod의 80 포트로 포워딩
# & : 백그라운드 실행
kubectl port-forward $POD_NAME 8080:80 &
# [1] 3518
# Forwarding from 127.0.0.1:8080 -> 80
# Forwarding from [::1]:8080 -> 80

# - 포트 포워딩이 백그라운드에서 시작됨
# - 로컬 8080 포트 → Pod 80 포트
# - 프로세스 ID: 3518

# Step 19: Port Forwarding 프로세스 확인
ps -ef | grep kubectl
# root        3518    2080  0 19:42 pts/1    00:00:00 kubectl port-forward nginx-54c98b4f84-fz86r 8080:80
# root        3526    2080  0 19:43 pts/1    00:00:00 grep kubectl

# Step 20: Port Forwarding 테스트
# 로컬 8080 포트로 HTTP 요청
curl --head http://127.0.0.1:8080
# Handling connection for 8080
# HTTP/1.1 200 OK
# Server: nginx/1.29.4
# Date: Fri, 09 Jan 2026 10:43:28 GMT
# Content-Type: text/html
# Content-Length: 615
# Last-Modified: Tue, 09 Dec 2025 18:28:10 GMT
# Connection: keep-alive
# ETag: "69386a3a-267"
# Accept-Ranges: bytes
# 
# - Port Forwarding이 정상 작동
# - 로컬 8080 포트로 요청하면 Pod의 80 포트로 전달됨
# - nginx 서버가 정상 응답 (HTTP 200 OK)

# ========== Logs ==========

# Step 21: Pod 로그 확인
# nginx Pod의 로그 출력
kubectl logs $POD_NAME
# /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
# /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
# ...
# 2026/01/09 10:40:30 [notice] 1#1: start worker processes
# 2026/01/09 10:40:30 [notice] 1#1: start worker process 28
# 2026/01/09 10:40:30 [notice] 1#1: start worker process 29
# 192.168.10.100 - - [09/Jan/2026:10:42:48 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.88.1" "-"
# 127.0.0.1 - - [09/Jan/2026:10:43:28 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.88.1" "-"
# 
# - nginx 컨테이너 시작 로그
# - nginx 버전: 1.29.4
# - 접근 로그: server(192.168.10.100)와 localhost(127.0.0.1)에서의 요청 기록

# Step 22: 추가 요청 후 로그 재확인
curl --head http://127.0.0.1:8080
# Handling connection for 8080
# HTTP/1.1 200 OK
# ...
kubectl logs $POD_NAME
# ... (이전 로그)
# 127.0.0.1 - - [09/Jan/2026:10:43:37 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.88.1" "-"
# 
# - 새로운 요청이 로그에 추가됨
# - 로그는 실시간으로 누적됨

# Step 23: Port Forwarding 종료
# kubectl port-forward 프로세스 종료
kill -9 $(pgrep kubectl)
# [1]+  Killed                  kubectl port-forward $POD_NAME 8080:80

# ========== Exec ==========

# Step 24: Pod 내부에서 명령어 실행
# nginx 컨테이너 내부에서 nginx 버전 확인
kubectl exec -ti $POD_NAME -- nginx -v
# nginx version: nginx/1.29.4

# ========== Service (NodePort) ==========

# Step 25: NodePort Service 생성
# nginx Deployment를 NodePort 타입의 Service로 노출
kubectl expose deployment nginx \
  --port 80 --type NodePort
# service/nginx exposed
# 
# - expose: Deployment를 Service로 노출
# - --port 80: Service 포트 80
# - --type NodePort: NodePort 타입 (모든 노드의 특정 포트로 접근 가능)

# Step 26: Service 및 Endpoints 확인
kubectl get service,ep nginx
# NAME            TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
# service/nginx   NodePort   10.32.0.149   <none>        80:30441/TCP   10s
# 
# NAME              ENDPOINTS                     AGE
# endpoints/nginx   10.200.0.2:80,10.200.1.2:80   10s
# 
# - Service:
#   - TYPE: NodePort
#   - CLUSTER-IP: 10.32.0.149 (클러스터 내부 IP)
#   - PORT(S): 80:30441/TCP (Service 포트 80, NodePort 30441)
# - Endpoints:
#   - 10.200.0.2:80: node-0의 Pod
#   - 10.200.1.2:80: node-1의 Pod
#   - Service는 두 Pod를 모두 백엔드로 사용

# Step 27: NodePort 추출
# Service에 할당된 NodePort 번호 추출
NODE_PORT=$(kubectl get svc nginx --output=jsonpath='{range .spec.ports[0]}{.nodePort}')
echo $NODE_PORT
# 30441

# Step 28: NodePort를 통한 접근 테스트 (node-0)
# node-0의 NodePort를 통해 Service 접근
curl -s -I http://node-0:${NODE_PORT}
# HTTP/1.1 200 OK
# Server: nginx/1.29.4
# Date: Fri, 09 Jan 2026 11:24:00 GMT
# Content-Type: text/html
# Content-Length: 615
# Last-Modified: Tue, 09 Dec 2025 18:28:10 GMT
# Connection: keep-alive
# ETag: "69386a3a-267"
# Accept-Ranges: bytes
# 
# - node-0:30441로 접근 성공
# - kube-proxy가 요청을 백엔드 Pod로 전달
# - HTTP 200 OK 응답

# Step 29: NodePort를 통한 접근 테스트 (node-1)
curl -s -I http://node-1:${NODE_PORT}
# HTTP/1.1 200 OK
# Server: nginx/1.29.4
# Date: Fri, 09 Jan 2026 11:24:06 GMT
# Content-Type: text/html
# Content-Length: 615
# Last-Modified: Tue, 09 Dec 2025 18:28:10 GMT
# Connection: keep-alive
# ETag: "69386a3a-267"
# Accept-Ranges: bytes
# 
# - node-1:30441로 접근 성공
# - 모든 노드에서 동일한 NodePort로 Service에 접근 가능
# - kube-proxy가 로드 밸런싱을 수행 (두 Pod 중 하나로 요청 전달)

마치며

이번 스터디에서는 Kubernetes The Hard Way 실습을 통해 쿠버네티스 클러스터를 처음부터 끝까지 수동으로 구축하는 과정을 배웠습니다.

매번 kubespray나 kubeadm 같은 도구를 사용하여 편하게 클러스터를 설치하다 보니, 내부적으로 각 구성요소들이 어떻게 설치되고 설정되는지에 대한 깊이 있는 이해가 부족했습니다.

이번 실습을 통해 각 구성요소의 역할과 상호작용을 직접 경험하며 이해할 수 있었고, 인증서 기반 인증과 RBAC의 실제 동작 방식을 확인할 수 있었습니다.

또한 네트워크 구성이 얼마나 복잡하고 중요한지 체감할 수 있었고, 컨테이너 런타임과 CNI가 어떻게 통합되어 작동하는지 알 수 있었습니다.

 

 

특히, etcd에 저장된 Secret 데이터가 실제로 암호화되어 있는지 직접 확인하고, Pod 네트워크가 어떻게 구성되는지 네트워크 인터페이스 레벨에서 확인한 경험은 매우 유익했습니다.

Kubernetes The Hard Way 실습은 시간이 많이 걸리고 복잡하지만, 쿠버네티스의 내부 동작 원리를 깊이 이해하는 데 매우 유용한 방법이라고 생각되네요..

이번 경험을 바탕으로 향후 클러스터 운영이나 트러블슈팅 시 더 나은 이해를 바탕으로 접근할 수 있을 것 같습니다.

다음 스터디 포스팅에서 뵙겠습니다.

 

긴 글 읽어주셔서 감사합니다! :)

반응형
    devlos
    devlos
    안녕하세요, Devlos 입니다. 새로 공부 중인 지식들을 공유하고, 명확히 이해하고자 블로그를 개설했습니다 :) 여러 DEVELOPER 분들과 자유롭게 지식을 공유하고 싶어요! 방문해 주셔서 감사합니다 😀 - DEVLOS -

    티스토리툴바