티스토리 뷰

 

https://github.com/kelseyhightower/kubernetes-the-hard-way/의 리포지토리 내용을 기반으로

kubeadm과 같은 도구 없이  k8s를 손수 설치하는 내용이다.

해당 스터디를 통해 우리가 기대하는 바는 EKS 및 OCP와 같은 관리형 쿠버네티스, 바닐라 쿠버네티스로 구성된 시스템의

트러블슈팅을 원할하고 수월하게 하기 위해서 k8s의 동작 원리를 깊게 이해하는 데 초점을 맞춘 내용이다.

 

스터디장 기시다님의 가이드 및 선구자 역할로서 많은 도움을 받았다.

특히 vagrant 스크립트 내용이 있어서 수월하게 실습을 할 수 있었다.

 

과거에 gcp위에 인스턴스를 4개 배포해서 master node 1대 , worker node 3대를 kubeadm을 이용해서 k8s를설치했던 경험이

있는데  그건 정말 쉽게 하는거구나를 금주 스터디때 깨달았다.....

 

금주에  건강 이슈 및 주말 지방 원정으로 시간이 부족하여 스터디장님이 기반을 잘 잡아준 vagrant script 도움을 받아서 단순하게 실습을 완주하고 해당 스크릡트의 내용을 완전히 이해를 하는데 부족했던 점이 많아서 차근차근 다시 돌려보고 도전 과제를 할 필요가 있을것 같다.

 

kind 외에 Vagrant를 처음 사용해봤는데 컴퓨터 스펙만 되면 해당 도구를 잘 이용하면 좋겠다는 생각이 든다.

 

윈도우와 Mac 2개가 다 있어서 우선 윈도우를 기준으로 실습하고 향후 Mac에서도 실습 및 도전과제를 할 예정이다.

01 - Pre requisites

실습 최종 아키텍처

 

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

 

Vagrantfile은 Kubernetes the Hard Way 실습을 위해 필요한 가상 머신(VM) 인프라를 Vagrant와 VirtualBox를 이용해 자동으로 구축해주는 설정 파일입니다. 이 파일은 앞서 보았던 아키텍처 다이어그램(Control Plane 1대 + Worker Node 2대)에 관리용 서버(Jumpbox) 1대를 추가하여 총 4대의 VM을 생성합니다.

1. 공통 설정 (Global Configuration)

  • 운영체제 이미지 (Box): bento/debian-12 (Debian 12 버전)을 사용 안정성이 높고 가벼워 쿠버네티스 실습용
  • 버전: 202510.26.0 

2. 가상 머신(VM) 별 상세 설정

총 4대의 VM(jumpbox, server, node-0, node-1)이 정의되어 있습니다.

(1) Jumpbox (jumpbox) - 관리용 서버

  • 역할: 클러스터 구성원이 아니며, 관리자가 접속하여 kubectl 명령을 날리거나 인증서를 생성하고 배포하는 작업용 베이스캠프입니다.​
  • IP: 192.168.10.10
  • SSH 포트 포워딩: 호스트(내 PC)의 60010 포트로 접속하면 VM의 22번 포트로 연결
  • 스펙: CPU 2개, RAM 1.5GB 

(2) Server (server) - Control Plane (Master Node)

  • 역할: 쿠버네티스 Control Plane 역할을 수행합니다 (API 서버, 스케줄러 등 실행).
  • IP: 192.168.10.100
  • SSH 포트: 호스트 60100 -> VM 22
  • 스펙: CPU 2개, RAM 2GB. Control Plane

(3) Worker Nodes (node-0, node-1)

  • 역할: 실제 컨테이너(Pod)가 실행되는 Worker Node
  • IP:
    • node-0: 192.168.10.101 (호스트 포트 60101)
    • node-1: 192.168.10.102 (호스트 포트 60102)
  • 스펙: CPU 2개, RAM 2GB. 실제 워크로드를 감당해야 하므로 Server와 동일한 스펙

3. 주요 설정 포인트 (Technical Details)

  • Provider 설정 (virtualbox):
    • vb.customize ["modifyvm", :id, "--groups", "/Hardway-Lab"]: VirtualBox 관리 화면에서 보기 좋게 /Hardway-Lab이라는 그룹으로 묶어줍니다.
    • vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]: **무차별 모드(Promiscuous Mode)**를 허용합니다. 이는 쿠버네티스 네트워킹(파드 간 통신)이나 브릿지 통신을 위해 패킷을 필터링 없이 모두 받도록 하는 중요한 설정입니다.
  • 네트워크 (private_network):
    • 모든 노드가 192.168.10.0/24 대역의 고정 IP를 할당받아, 서로 내부 통신이 가능
  • 프로비저닝 (shell):
    • init_cfg.sh: VM이 처음 켜질 때 실행되는 쉘 스크립트입니다. 아마도 기본 패키지 설치(curl, git 등)나 방화벽 해제 같은 초기화 작업이 들어있음
# Base Image : https://portal.cloud.hashicorp.com/vagrant/discover/bento/debian-12
BOX_IMAGE = "bento/debian-12"
BOX_VERSION = "202510.26.0"

Vagrant.configure("2") do |config|
# jumpbox
  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"]
      vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
      vb.name = "jumpbox"
      vb.cpus = 2
      vb.memory = 1536 # 2048 2560 3072 4096
      vb.linked_clone = true
    end
    subconfig.vm.host_name = "jumpbox"
    subconfig.vm.network "private_network", ip: "192.168.10.10"
    subconfig.vm.network "forwarded_port", guest: 22, host: 60010, auto_correct: true, id: "ssh"
    subconfig.vm.synced_folder "./", "/vagrant", disabled: true
    subconfig.vm.provision "shell", path: "init_cfg.sh"
  end

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

 

초기 설정 init_cfg.sh

#!/usr/bin/env bash

echo ">>>> Initial Config Start <<<<"

echo "[TASK 1] Setting Profile & Bashrc"
echo "sudo su -" >> /home/vagrant/.bashrc
echo 'alias vi=vim' >> /etc/profile
ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime # Change Timezone

echo "[TASK 2] Disable AppArmor"
systemctl stop apparmor && systemctl disable apparmor >/dev/null 2>&1

echo "[TASK 3] Disable and turn off SWAP"
swapoff -a && sed -i '/swap/s/^/#/' /etc/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

echo "[TASK 6] Setting Sshd Config"
cat << EOF >> /etc/ssh/sshd_config
PasswordAuthentication yes
PermitRootLogin yes
EOF
systemctl restart sshd  >/dev/null 2>&1

echo "[TASK 7] Setting Local DNS Using Hosts file"
sed -i '/^127\.0\.\(1\|2\)\.1/d' /etc/hosts
cat << EOF >> /etc/hosts
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 <<<<"

 

 init_cfg.sh는 Vagrant로 띄운 각 VM을 쿠버네티스 실습(예: the hard way) 하기 좋은 상태로 만들기 위해, 시간대/보안 모듈/스왑/필수 패키지/SSH/로컬 DNS를 한 번에 초기화하는 스크립트

전체 흐름

스크립트는 “Initial Config Start/End” 로그를 찍고, TASK 1~7을 순서대로 실행
대부분은 “실습 편의성”을 높이기 위한 설정이지만, 일부는 보안상 위험한 설정(루트 비밀번호 고정, 루트 SSH 로그인 허용 등)도 포함

TASK 1–2 (쉘 환경/시간대, AppArmor)

  • TASK 1은 /home/vagrant/.bashrc에 sudo su -를 추가하고, /etc/profile에 alias vi=vim을 추가하며, 시스템 타임존을 Asia/Seoul로 고정
  • 특히 .bashrc에 sudo su -를 넣으면 vagrant 사용자로 로그인할 때마다 루트 전환을 시도하게 되어(비밀번호/TTY 상황에 따라) 의도치 않은 동작이나 혼란을 만들 수 있음
  • TASK 2는 AppArmor 서비스를 중지/비활성화하여 보안 정책 적용을 끄는데, 실습 중 권한/프로파일 이슈를 피하려는 목적이지만 운영환경에서는 권장되지 않음

TASK 3–4 (SWAP 비활성화, 패키지 설치)

  • TASK 3은 swapoff -a로 즉시 스왑을 끄고, /etc/fstab에서 swap 라인을 주석 처리해 재부팅 후에도 유지되게 함
  • TASK 4는 apt update  tree/git/jq/yq/unzip/vim/sshpass 등을 설치해, 설정 파일 편집/JSON-YAML 처리/원격 작업을 쉽게 만들어줌

TASK 5–6 (루트 비밀번호, SSH 설정)

  • TASK 5는 루트 비밀번호를 qwe123으로 고정 설정
  • TASK 6는 /etc/ssh/sshd_config에 PasswordAuthentication yes, PermitRootLogin yes를 **추가(append)**하고 sshd를 재시작하여 “루트 계정 비밀번호 로그인”을 가능하게 해줌
  • 이 조합은 실습 접근성은 좋아지지만, 동일 네트워크/포트포워딩 환경에서 매우 위험할 수 있으니 학습 후에는 반드시 원복하는 편이 안전

TASK 7 (hosts 기반 로컬 DNS)

  • TASK 7은 /etc/hosts에서 127.0.1.1 또는 127.0.2.1 형태의 라인을 제거한 뒤, jumpbox/server/node-0/node-1의 IP와 호스트명을 /etc/hosts에 추가
  • 그 결과 각 VM에서 server.kubernetes.local, node-0.kubernetes.local 같은 이름으로 내부 통신이 가능해져, 인증서 CN/SAN 구성이나 설정 파일 작성 시 IP 대신 이름을 쓰기 쉬워짐

 

위위 두 스크립트를 기반으로 실습용 가상머신을 생성

# 작업용 디렉터리 생성
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

 

jumpbox 가상머신 접속: vagrant ssh jumpbox

# 사용자 확인
whoami
pwd

# 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
systemctl is-active apparmor
inactive
     
# /etc/hosts 파일 내용 확인
# https://www.debian.org/doc/manuals/debian-reference/ch05.en.html#_the_hostname_resolution
cat /etc/hosts

 

 

02 - Set Up The 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

# Sync GitHub Repository
## --depth 1 : 최신 커밋만 가져오는 shallow clone을 의미한다. 전체 git 히스토리가 필요 없으므로 이 옵션을 사용하면 다운로드 시간과 용량을 절약할 수 있다.
## 출처 멤버 작성 글 : https://sirzzang.github.io/kubernetes/Kubernetes-Cluster-The-Hard-Way-02/
pwd
git clone --depth 1 https://github.com/kelseyhightower/kubernetes-the-hard-way.git
cd kubernetes-the-hard-way
tree
pwd


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

# CPU 아키텍처 확인
dpkg --print-architecture
arm64   # macOS 사용자
amd64   # Windows 사용자

# CPU 아키텍처 별 다운로드 목록 정보 다름
ls -l downloads-*
-rw-r--r-- 1 root root 839 Jan  4 10:30 downloads-amd64.txt
-rw-r--r-- 1 root root 839 Jan  4 10:30 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 Size 정도
wget -q --show-progress \
  --https-only \
  --timestamping \
  -P downloads \
  -i downloads-$(dpkg --print-architecture).txt

# 확인
ls -oh downloads
total 544M
-rw-r--r-- 1 root 48M Jan  7  2025 cni-plugins-linux-arm64-v1.6.2.tgz
-rw-r--r-- 1 root 34M Mar 18  2025 containerd-2.1.0-beta.0-linux-arm64.tar.gz
-rw-r--r-- 1 root 17M Dec  9  2024 crictl-v1.32.0-linux-arm64.tar.gz
-rw-r--r-- 1 root 21M Mar 28  2025 etcd-v3.6.0-rc.3-linux-arm64.tar.gz
-rw-r--r-- 1 root 87M Mar 12  2025 kube-apiserver
-rw-r--r-- 1 root 80M Mar 12  2025 kube-controller-manager
-rw-r--r-- 1 root 54M Mar 12  2025 kubectl
-rw-r--r-- 1 root 72M Mar 12  2025 kubelet
-rw-r--r-- 1 root 63M Mar 12  2025 kube-proxy
-rw-r--r-- 1 root 62M Mar 12  2025 kube-scheduler
-rw-r--r-- 1 root 11M Mar  4  2025 runc.arm64

# Extract the component binaries from the release archives and organize them under the downloads directory.
ARCH=$(dpkg --print-architecture)
echo $ARCH

mkdir -p downloads/{client,cni-plugins,controller,worker}
tree -d downloads
downloads
├── client
├── cni-plugins
├── controller
└── worker

# 압축 풀기
tar -xvf downloads/crictl-v1.32.0-linux-${ARCH}.tar.gz \
  -C downloads/worker/ && tree -ug downloads

tar -xvf downloads/containerd-2.1.0-beta.0-linux-${ARCH}.tar.gz \
  --strip-components 1 \
  -C downloads/worker/ && tree -ug downloads

tar -xvf downloads/cni-plugins-linux-${ARCH}-v1.6.2.tgz \
  -C downloads/cni-plugins/ && tree -ug downloads

## --strip-components 1 : etcd-v3.6.0-rc.3-linux-amd64/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 -ug downloads

# 확인
tree downloads/worker/
tree downloads/cni-plugins
ls -l downloads/{etcd,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/
tree downloads/controller/
tree downloads/worker/

# 불필요한 압축 파일 제거
ls -l downloads/*gz
rm -rf downloads/*gz

# Make the binaries executable.
ls -l downloads/{client,cni-plugins,controller,worker}/*
chmod +x downloads/{client,cni-plugins,controller,worker}/*
ls -l downloads/{client,cni-plugins,controller,worker}/*

# 일부 파일 소유자 변경
tree -ug downloads # cat /etc/passwd | grep vagrant && cat /etc/group | grep vagrant
chown root:root downloads/client/etcdctl
chown root:root downloads/controller/etcd
chown root:root downloads/worker/crictl
tree -ug downloads


# kubernetes client 도구인 kubectl를 설치
ls -l downloads/client/kubectl
cp downloads/client/kubectl /usr/local/bin/

# can be verified by running the kubectl command:
kubectl version --client
Client Version: v1.32.3
Kustomize Version: v5.5.0

 

이 스크립트는 Kubernetes the Hard Way 실습을 위한 필수 바이너리 파일들을 다운로드하고, 사용하기 좋게 분류 및 정리하는 준비 과정

 


1. 초기 환경 확인 및 소스 코드 가져오기

  • 루트 권한 확인: root 계정으로 작업을 시작합니다. whoami와 .bashrc 확인 과정은 현재 사용자가 루트 권한을 가지고 있는지 체크하는 것
  • 깃허브 저장소 클론: git clone --depth 1 ...을 통해 Kelsey Hightower의 오리지널 kubernetes-the-hard-way 저장소를 가져와서. --depth 1을 쓴 이유는 전체 변경 이력은 필요 없고, 최신 파일만 빠르게 받기 위함

2. 다운로드 목록 준비 (CPU 아키텍처 대응)

  • 아키텍처 확인: dpkg --print-architecture 명령어로 현재 서버의 CPU가 amd64(인텔/AMD)인지 arm64(맥 실리콘 등)인지 확인
  • 목록 선택: 아키텍처에 맞는 다운로드 목록 파일(downloads-amd64.txt 또는 downloads-arm64.txt)을 읽어와서 이 파일 안에는 쿠버네티스 컴포넌트들의 URL이 들어있음

3. 바이너리 파일 다운로드 (wget)

  • 일괄 다운로드: wget 명령어를 사용해 목록 파일(-i)에 있는 모든 URL의 파일을 downloads 폴더에 한 번에 받음
  • 다운로드되는 파일들:
    • Kubernetes Core: kube-apiserver, controller-manager, scheduler, kubelet, kube-proxy, kubectl
    • Container Runtime: containerd, runc, crictl
    • Network: cni-plugins (컨테이너 네트워크 플러그인)
    • Storage: etcd (데이터 저장소)

4. 파일 정리 (압축 해제 및 분류)

다운로드 받은 파일들은 압축파일(tar.gz)과 실행 파일이 섞여 있어, 이를 용도별로 깔끔하게 정리

  • 폴더 생성: 4개의 하위 폴더를 만듭니다.
    • client: 관리자용 도구 (kubectl, etcdctl)
    • controller: 마스터 노드용 (apiserver, scheduler, etcd 등)
    • worker: 워커 노드용 (kubelet, proxy, containerd, runc)
    • cni-plugins: 네트워크 플러그인 모음
  • 압축 해제: tar 명령어로 압축을 풀면서 필요한 파일만 (--strip-components 1 같은 옵션은 폴더 껍질을 벗기고 알맹이만 꺼내는 기술)
  • 파일 이동 (mv): 압축 푼 파일과 다운로드 받은 바이너리를 각자의 역할에 맞는 폴더(client, controller, worker)로 옮김

5. 마무리 (권한 설정 및 설치)

  • 실행 권한 부여: chmod +x로 모든 바이너리 파일에 실행 권한을 줍니다. 이게 없으면 실행이 안됨
  • 소유권 정리: etcdctl, crictl 등의 소유자를 root:root로 명확히 함
  • kubectl 설치: 관리자가 바로 명령어를 쓸 수 있게 downloads/client/kubectl을 /usr/local/bin/으로 복사 어디서든 kubectl version을 치면 동작

03 - Provisioning Compute Resources

# 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

while read IP FQDN HOST SUBNET; do
  echo "${IP} ${FQDN} ${HOST} ${SUBNET}"
done < machines.txt


# Configuring SSH Access 설정
 
# sshd config 설정 파일 확인 : 이미 암호 기반 인증 접속 설정 되어 있음
grep "^[^#]" /etc/ssh/sshd_config
...
PasswordAuthentication yes
PermitRootLogin yes

# Generate a new SSH key
ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa
ls -l /root/.ssh
-rw------- 1 root root 2602 Jan  2 21:07 id_rsa
-rw-r--r-- 1 root root  566 Jan  2 21:07 id_rsa.pub

# Copy the SSH public key to each machine
while read IP FQDN HOST SUBNET; do
  sshpass -p 'qwe123' ssh-copy-id -o StrictHostKeyChecking=no root@${IP}
done < machines.txt

while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} cat /root/.ssh/authorized_keys
done < machines.txt

# Once each key is added, verify SSH public key access is working
# 아래는 IP 기반으로 접속 확인
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} hostname
done < machines.txt


# Hostnames 설정

# 확인 : init_cfg.sh 로 이미 설정되어 있음
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} cat /etc/hosts
done < machines.txt

while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} hostname --fqdn
done < machines.txt

# 아래는 hostname 으로 ssh 접속 확인
cat /etc/hosts
while read IP FQDN HOST SUBNET; do
  sshpass -p 'qwe123' ssh -n -o StrictHostKeyChecking=no root@${HOST} hostname
done < machines.txt

while read IP FQDN HOST SUBNET; do
  sshpass -p 'qwe123' ssh -n root@${HOST} uname -o -m -n
done < machines.txt

 

이 스크립트는 jumpbox(또는 현재 작업 머신)에서 **서버/노드 목록을 파일로 관리(machines.txt)**하고, 그 목록을 기반으로 루트 SSH 키 기반 접속을 일괄 설정한 뒤 접속/호스트네임 설정이 정상인지 검증하는 자동화 절차

1) machines.txt 생성/확인

  • machines.txt에 각 머신의 IPV4_ADDRESS FQDN HOSTNAME POD_SUBNET 정보를 저장
  • server(192.168.10.100)는 Control Plane 용도로 보고 POD_SUBNET을 비워두고, node-0/node-1만 10.200.0.0/24, 10.200.1.0/24 같은 파드 CIDR을 적음
  • while read IP FQDN HOST SUBNET; do ... done < machines.txt 루프는 파일을 한 줄씩 읽어 파싱이 잘 되는지(필드가 원하는 형태로 들어오는지) 단순 출력으로 확인

2) SSH 접속 방식 준비(암호 → 키 기반)

  • 먼저 sshd_config에서 PasswordAuthentication yes, PermitRootLogin yes가 활성화되어 루트 비밀번호 로그인이 가능한 상태임을 확인
  • ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa는 루트 계정에 비밀번호 없는 RSA 키를 생성(자동화 목적).​
  • sshpass -p 'qwe123' ssh-copy-id ... root@${IP}는 각 머신에 루트 비밀번호(qwe123)로 접속해, 생성한 공개키를 원격의 /root/.ssh/authorized_keys에 등록해 키 기반 무비밀번호 SSH로 전환

3) 키 배포 및 접속 검증

  • ssh -n root@${IP} cat /root/.ssh/authorized_keys로 각 머신에 공개키가 실제로 들어갔는지 확인
  • ssh -n root@${IP} hostname는 이제 비밀번호 입력 없이도 접속이 되는지(키 기반 인증 성공 여부)를 검증

4) 호스트네임/hosts 기반 접속 검증

  • ssh -n root@${IP} cat /etc/hosts  hostname --fqdn는 각 머신의 hosts 설정과 FQDN 인식이 정상인지 확인
  • 이후에는 IP 대신 ${HOST}(예: node-0, server)로 접속을 시도하는데, 이 방식은 로컬 /etc/hosts에 node-0 → 192.168.10.101 같은 매핑이 있어야 성공
  • 마지막 uname -o -m -n은 운영체제/아키텍처/노드명까지 한 번에 찍어 “원하는 머신에 붙었는지”를 빠르게 확인하는 단계

5) 실습 관점 주의점

  • StrictHostKeyChecking=no, 루트 로그인 허용, 고정 비밀번호 사용은 실습 편의성은 높지만 보안상 매우 위험한 조합이라 실습 환경에서만 쓰는 게 안전
  • 특히 키 배포가 끝나면 PasswordAuthentication no, PermitRootLogin prohibit-password(또는 no)처럼 되돌리는 방식이 흔

04 - Provisioning a CA and Generating TLS Certificates

CA설정 및 TLS 인증서 생성

항목개인키CSR인증서참고 정보X509v3 Extended Key Usage

Root CA ca.key X ca.crt    
admin admin.key admin.csr admin.crt CN = admin, O = system:masters TLS Web Client Authentication
node-0 node-0.key node-0.csr node-0.crt CN = system:node:node-0, O = system:nodes TLS Web Server / Client Authentication
node-1 node-1.key node-1.csr node-1.crt CN = system:node:node-1, O = system:nodes TLS Web Server / Client Authentication
kube-proxy kube-proxy.key kube-proxy.csr kube-proxy.crt CN = system:kube-proxy, O = system:node-proxier TLS Web Server / Client Authentication
kube-scheduler kube-scheduler.key kube-scheduler kube-scheduler.crt CN = system:kube-scheduler, O = system:kube-scheduler TLS Web Server / Client Authentication
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 TLS Web Server / Client Authentication
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,..) TLS Web Server / Client Authentication
service-accounts service-accounts.key service-accounts.csr service-accounts.crt CN = service-accounts TLS Web Client Authentication

 

항목 네트워크 대역 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
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 옵션
[req]
distinguished_name = req_distinguished_name
prompt             = no                      # CSR 생성 시 대화형 입력 없음
x509_extensions    = ca_x509_extensions      # CA 인증서 생성 시 사용할 확장

[ca_x509_extensions]                         # CA 인증서 설정 (Root of Trust)
basicConstraints = CA:TRUE                   # CA 권한 인증서
keyUsage         = cRLSign, keyCertSign      # 다른 인증서를 서명 가능, Kubernetes 모든 인증의 신뢰 루트

[req_distinguished_name]
C   = US
ST  = Washington
L   = Seattle
CN  = CA                                     # 클러스터 CA

[admin]                                      # Admin 사용자 (kubectl)
distinguished_name = admin_distinguished_name
prompt             = no
req_extensions     = default_req_extensions

[admin_distinguished_name]
CN = admin                                   # [K8S] CN → user
O  = system:masters                          # [K8S] O → group , system:masters - Kubernetes 슈퍼유저 그룹, 모든 RBAC 인가 우회

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

[service-accounts]                           # Service Account 서명자
distinguished_name = service-accounts_distinguished_name
prompt             = no
req_extensions     = default_req_extensions

[service-accounts_distinguished_name] 
CN = service-accounts                        # controller-manager가 사용하는 ServiceAccount 토큰 서명용 인증서 , apiserver에서 --service-account-key-file 로 사용

# 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-0]                                    # Worker Node 인증서 (kubelet)
distinguished_name = node-0_distinguished_name
prompt             = no
req_extensions     = node-0_req_extensions

[node-0_req_extensions]
basicConstraints     = CA:FALSE
extendedKeyUsage     = clientAuth, serverAuth  # clientAuth: apiserver → kubelet & serverAuth: kubelet HTTPS 서버(10250)
keyUsage             = critical, digitalSignature, keyEncipherment
nsCertType           = client
nsComment            = "Node-0 Certificate"
subjectAltName       = DNS:node-0, IP:127.0.0.1
subjectKeyIdentifier = hash

[node-0_distinguished_name]
CN = system:node:node-0                     # kubelet 사용자 , CN = system:node:<nodeName>
O  = system:nodes                           # Node Authorizer 그룹  ,O = system: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
O  = system:nodes
C  = US
ST = Washington
L  = Seattle


# Kube Proxy Section
[kube-proxy]                                # 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
O  = system:node-proxier                    # system:node-proxier ClusterRoleBinding 존재 , 서비스 네트워크 제어 가능
C  = US
ST = Washington
L  = Seattle


# 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] # 클러스터 상태 관리, Node, ReplicaSet, SA 토큰 등 관리
CN = system:kube-controller-manager
O  = system:kube-controller-manager
C  = US
ST = Washington
L  = Seattle


# 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
O  = system:kube-scheduler                  # Pod 스케줄링 전용 권한
C  = US
ST = Washington
L  = Seattle


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

[kube-api-server]                           # 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
keyUsage             = critical, digitalSignature, keyEncipherment
nsCertType           = client, server
nsComment            = "Kube API Server Certificate"
subjectAltName       = @kube-api-server_alt_names
subjectKeyIdentifier = hash

[kube-api-server_alt_names]                 # SAN (Subject Alternative Name) : 모든 내부/외부 접근 주소
IP.0  = 127.0.0.1
IP.1  = 10.32.0.1
DNS.0 = kubernetes
DNS.1 = kubernetes.default
DNS.2 = kubernetes.default.svc
DNS.3 = kubernetes.default.svc.cluster
DNS.4 = kubernetes.svc.cluster.local
DNS.5 = server.kubernetes.local
DNS.6 = api-server.kubernetes.local

[kube-api-server_distinguished_name]
CN = kubernetes
C  = US
ST = Washington
L  = Seattle


[default_req_extensions]                    # 공통 CSR 확장 : 대부분 클라이언트 인증서 -> kubelet / apiserver만 serverAuth 추가
basicConstraints     = CA:FALSE
extendedKeyUsage     = clientAuth
keyUsage             = critical, digitalSignature, keyEncipherment
nsCertType           = client
nsComment            = "Admin Client Certificate"
subjectKeyIdentifier = hash

 

Certificate Authority

# Generate the CA configuration file, certificate, and private key

# Root CA 개인키 생성 : ca.key
openssl genrsa -out ca.key 4096
ls -l ca.key
-rw------- 1 root root 3272 Jan  3 09:41 ca.key

cat ca.key
openssl rsa -in ca.key -text -noout # 개인키 구조 확인

# Root CA 인증서 생성 : ca.crt
## -x509 : CSR을 만들지 않고 바로 인증서(X.509) 생성, 즉, Self-Signed Certificate
## -noenc : 개인키를 암호화하지 않음, 즉, CA 키(ca.key)에 패스프레이즈 없음
## -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  3 09:46 ca.crt

# ca.conf 관련 내용
cat ca.conf
-------------------------------------------
[req]
distinguished_name = req_distinguished_name
prompt             = no
x509_extensions    = ca_x509_extensions

[ca_x509_extensions]
basicConstraints = CA:TRUE                    # 이 인증서는 CA 역할 가능
keyUsage         = cRLSign, keyCertSign       # cRLSign: 인증서 폐기 목록(CRL) 서명 가능, keyCertSign: 다른 인증서를 서명할 수 있음

[req_distinguished_name]
C   = US
ST  = Washington
L   = Seattle
CN  = CA
-------------------------------------------

cat ca.crt
openssl x509 -in ca.crt -text -noout # 인증서 전체 내용 확인
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            56:fb:42:82:5e:2f:96:cf:f5:83:2e:78:46:98:6e:3f:08:ee:99:67
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: C = US, ST = Washington, L = Seattle, CN = CA
        Validity
            Not Before: Jan  3 00:46:22 2026 GMT
            Not After : Jan  4 00:46:22 2036 GMT
        Subject: C = US, ST = Washington, L = Seattle, CN = CA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (4096 bit)
                Modulus:
                    00:ae:ce:95:7e:db:50:a2:4a:71:8b:99:54:0d:b4:
                    ce:1e:6f:13:3c:a6:54:30:0f:5b:0a:76:56:8c:44:
                    75:98:58:a6:57:7d:d2:38:e8:05:3c:cc:a5:e9:86:
                    57:73:98:c5:17:52:7c:7e:c8:48:6c:6b:86:13:1c:
                    7a:72:5d:10:3a:15:72:8d:66:35:e3:55:06:3e:f7:
                    44:7f:1b:fc:9e:4a:2b:4a:28:dd:2c:34:63:8d:26:
                    cc:39:50:3b:44:e5:f8:fe:68:c8:c0:a5:94:ba:b1:
                    d5:e3:55:1d:d9:98:0c:03:23:3f:9d:d9:a0:79:2c:
                    e9:ce:c9:92:b8:1f:6e:83:cb:08:1e:e6:28:cf:55:
                    29:b3:f3:19:1b:fe:c2:d8:30:6e:ee:68:7e:80:c3:
                    9a:53:77:d1:ae:2a:21:ee:82:94:d7:b5:f3:8f:a3:
                    98:f8:85:c6:c9:94:72:f3:1e:61:45:84:97:e4:25:
                    69:c8:5e:11:2c:75:2a:85:a6:b4:75:50:5f:a1:6c:
                    0a:54:1e:78:ab:25:a3:2e:04:18:21:68:86:11:3d:
                    90:09:95:02:aa:fc:32:2c:c5:ed:ac:d1:14:3c:d7:
                    fc:c3:a3:9f:dd:52:07:eb:2f:a7:fc:22:5e:2c:23:
                    ad:f6:f5:1e:90:db:3f:32:eb:10:38:34:c3:f0:40:
                    5d:c9:0b:d0:01:fd:78:73:0b:80:92:75:0c:24:76:
                    c1:6d:93:42:86:4b:a0:6d:99:7b:72:46:b6:52:b1:
                    2f:47:90:a0:ed:d8:93:71:23:c4:20:c8:63:04:a1:
                    f6:b6:d8:6e:6b:20:1a:2b:56:43:02:47:5e:77:ae:
                    4e:00:d5:ec:05:f6:e8:a4:ab:aa:8b:14:8b:b9:da:
                    d4:a6:e6:c6:c2:35:5a:fd:24:51:7d:29:bb:3c:d3:
                    fd:a7:bf:a9:5b:77:5a:e1:b1:7b:51:ab:29:a4:15:
                    e7:ac:f6:2a:1e:38:68:bb:f6:6f:60:e4:26:34:cc:
                    45:08:2b:e0:71:9a:8e:67:e3:0d:d4:67:63:0b:76:
                    27:bd:ff:8d:9c:78:e5:b8:55:f0:ce:c1:35:b7:b6:
                    e7:44:60:60:25:ae:f1:0f:3d:c6:7e:25:03:a3:c8:
                    87:f8:3d:cd:4b:06:1b:d1:94:63:31:50:33:5b:3f:
                    3c:66:a8:4d:df:2a:b4:76:a4:fa:54:73:43:09:ac:
                    6c:21:0b:9c:35:e9:14:ca:25:cd:f1:72:c1:fe:0f:
                    aa:56:59:1d:ea:45:a7:ab:f5:41:a5:d1:50:3d:da:
                    f0:71:ff:8b:d2:3b:04:0a:d2:80:e9:17:d6:9a:a3:
                    1a:5f:19:9b:a0:ef:08:36:d4:88:65:2b:50:42:10:
                    14:e1:a9
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:TRUE
            X509v3 Key Usage: 
                Certificate Sign, CRL Sign
            X509v3 Subject Key Identifier: 
                B3:5D:82:13:B6:1C:44:59:8C:0A:4E:DB:2B:18:98:77:0D:7A:2F:5B

 

이 스크립트는 자체 서명된 루트 인증 기관(Root CA)을 생성하는 과정입니다. 이 CA는 앞으로 만들 모든 쿠버네티스 컴포넌트(API Server, Kubelet 등)의 인증서를 발급하고 보증하는 최상위 신뢰 앵커(Trust Anchor) 역할

1. Root CA 개인키 생성 (ca.key)

 
openssl genrsa -out ca.key 4096
  • 동작: 4096비트 길이의 RSA 개인키를 생성합니다.
  • 의미: 이 키는 절대 유출되면 안 되는 가장 중요한 열쇠입니다. 이 키만 있으면 누구나 "나는 신뢰할 수 있는 서버다"라고 주장하는 가짜 인증서를 만들 수 있기 때문입니다.
  • 확인: openssl rsa ... 명령어로 키가 정상적으로 생성되었는지 구조를 확인합니다.

2. Root CA 인증서 설정 파일 준비 (ca.conf)

 
text
[ca_x509_extensions] basicConstraints = CA:TRUE keyUsage = cRLSign, keyCertSign
  • 중요 설정:
    • basicConstraints = CA:TRUE: "이 인증서는 다른 인증서를 발급할 수 있는 **진짜 CA(인증 기관)**이다"라는 표식을 남깁니다. 일반 웹사이트 인증서에는 이 값이 없습니다.
    • keyUsage = cRLSign, keyCertSign: "이 키로 인증서 서명(발급)과 폐기 목록(CRL) 서명을 할 수 있다"고 권한을 명시합니다.

3. Root CA 인증서 생성 (ca.crt)

openssl req -x509 -new -sha512 -noenc \ -key ca.key -days 3653 \ -config ca.conf \ -out ca.crt
  • 옵션 해석:
    • -x509: CSR(인증서 서명 요청) 단계를 건너뛰고 바로 Self-Signed(자체 서명) 인증서를 만듬. (Root CA는 상위 기관이 없으므로 스스로 보증합니다)
    • -days 3653: 유효 기간을 약 10년으로 설정. Root CA는 자주 교체하기 어렵기 때문에 길게 잡습니다.
    • -config ca.conf: 앞서 만든 설정 파일의 내용(CA 권한 등)을 인증서에 넣음

4. 생성된 인증서 검증 (openssl x509 ...)

출력된 텍스트 결과를 보면 정상적으로 만들어졌음을 알 수 있습니다.

  • Issuer & Subject: CN = CA (발급자와 대상이 같으므로 자체 서명 인증서임)
  • Validity: Not After : Jan 4 ... 2036 (2036년까지 유효함)
  • X509v3 Basic Constraints: CA:TRUE (CA 권한이 정상적으로 들어감)

이제 **"CA"**라는 이름의 사설 인증 기관이 생성됨
앞으로 API 서버나 Kubelet이 사용할 인증서를 만들 때, 방금 만든 ca.key로 도장(서명)을 찍어주면, 클러스터 내의 모든 컴포넌트가 "아, 이건 우리 대장(Root CA)이 보증한 거니까 믿을 수 있어!"라고 신뢰하게 됩니다. 이것이 쿠버네티스 보안 통신(TLS)의 기초

Create Client and Server Certificates : admin

이 절차는 Kubernetes에서 kubectl로 클러스터를 관리할 **admin 클라이언트 인증서(admin.crt)**를, 앞에서 만든 Root CA(ca.crt/ca.key)로 서명해서 발급하는 과정
결과물은 admin.key(개인키) + admin.crt(CA가 서명한 클라이언트 인증서) 조합이며, 보통 kubeconfig에 넣어 관리자 권한으로 API 서버에 인증함

무엇을 만드는가

  • admin.key: 관리자용 RSA 개인키(4096bit)로, 인증서 생성 및 이후 TLS 클라이언트 인증에 사용
  • admin.csr: “이 키의 소유자는 CN=admin이고 O=system:masters 그룹이다”라는 내용을 담은 인증서 서명 요청서
  • admin.crt: CA가 admin.csr를 검증/서명해서 발급한 최종 인증서입니다(자체 서명 아님).

ca.conf의 admin 섹션 의미

  • CN = admin: Kubernetes에서 인증서 기반 인증 시 보통 **사용자명(username)**으로 매핑되는 값으로 사용
  • O = system:masters: Kubernetes에서 보통 **그룹(group)**으로 매핑되는 값으로 사용되며, system:masters는 관례적으로 “클러스터 관리자 그룹”으로 취급되는 경우가 많아 매우 강한 권한을 얻음
  • [default_req_extensions]에서 basicConstraints = CA:FALSE는 “이 인증서는 CA가 아니다”를 뜻하고, extendedKeyUsage = clientAuth는 “클라이언트 인증용”임을 명시

CSR 생성(요청서)

openssl req -new -key admin.key -sha256 -config ca.conf -section admin -out admin.csr
  • -section admin 때문에 ca.conf의 [admin]/[admin_distinguished_name] 설정(CN/O)이 CSR에 그대로 들어갑니다.
  • -sha256은 CSR 서명 해시로 SHA-256을 사용한다는 의미입니다.
  • openssl req -in admin.csr -text -noout 출력에서 Subject가 CN=admin, O=system:masters로 들어갔는지 확인하는 단계입니다.

CA 서명으로 인증서 발급

openssl x509 -req -days 3653 -in admin.csr -copy_extensions copyall \ -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -out admin.crt
  • -req: CSR을 입력으로 받아 인증서를 “발급”하는 모드입니다(자체 서명 루트 인증서 만들 때 쓰는 -x509와 반대 성격).
  • -CA ca.crt -CAkey ca.key: 루트 CA 인증서/개인키로 CSR에 서명해 최종 인증서를 만듭니다.
  • -copy_extensions copyall: CSR에 포함된 확장(예: clientAuth, keyUsage)을 인증서에도 복사합니다.
  • -CAcreateserial: CA 시리얼 파일(보통 ca.srl)을 자동 생성/관리해, 다음 인증서 발급 시 일련번호를 이어가게 합니다.

출력에서 확인할 포인트(정상 여부)

  • Issuer가 CN = CA로 나오면 “CA가 서명했다”는 뜻이고, Subject가 CN = admin, O = system:masters면 의도한 관리자 신원이 들어간 것입니다.
  • X509v3 Basic Constraints: CA:FALSE + Extended Key Usage: TLS Web Client Authentication가 보이면 “클라이언트 인증서로 적합”하게 발급된 상태입니다.
  • admin.key는 유출되면 곧바로 관리자 권한 탈취로 이어질 수 있으니(특히 system:masters 그룹) 실습이 끝나면 폐기하거나 권한을 최소화한 계정으로 다시 발급하는 편이 안전

Create Client and Server Certificates : 나머지 전부

# ca.conf 수정
cat ca.conf | grep system:kube-scheduler
CN = system:kube-scheduler
O  = system:system:kube-scheduler
sed -i 's/system:system:kube-scheduler/system:kube-scheduler/' ca.conf
cat ca.conf | grep system:kube-scheduler
CN = system:kube-scheduler
O  = system:kube-scheduler


# 변수 지정
certs=(
  "node-0" "node-1"
  "kube-proxy" "kube-scheduler"
  "kube-controller-manager"
  "kube-api-server"
  "service-accounts"
)

# 확인
echo ${certs[*]}
node-0 node-1 kube-proxy kube-scheduler kube-controller-manager kube-api-server service-accounts

# 개인키 생성, 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

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


# 인증서 정보 확인
openssl x509 -in node-0.crt -text -noout
        Issuer: C = US, ST = Washington, L = Seattle, CN = CA
        Validity
            Not Before: Jan  3 02:41:09 2026 GMT
            Not After : Jan  4 02:41:09 2036 GMT
        Subject: CN = system:node:node-0, O = system:nodes, C = US, ST = Washington, L = Seattle
        ...
        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

openssl x509 -in node-1.crt -text -noout
        Subject: CN = system:node:node-1, O = system:nodes, C = US, ST = Washington, L = Seattle
            X509v3 Subject Alternative Name: 
                DNS:node-1, IP Address:127.0.0.1

openssl x509 -in kube-proxy.crt -text -noout
        Issuer: C = US, ST = Washington, L = Seattle, CN = CA
        Validity
            Not Before: Jan  3 02:41:13 2026 GMT
            Not After : Jan  4 02:41:13 2036 GMT
        Subject: CN = system:kube-proxy, O = system:node-proxier, C = US, ST = Washington, L = Seattle
        ...
        X509v3 extensions:
            Netscape Cert Type: 
                SSL Client
            Netscape Comment: 
                Kube Proxy Certificate
            X509v3 Subject Alternative Name: 
                DNS:kube-proxy, IP Address:127.0.0.1

openssl x509 -in kube-scheduler.crt -text -noout
        Issuer: C = US, ST = Washington, L = Seattle, CN = CA
        Validity
            Not Before: Jan  3 02:41:13 2026 GMT
            Not After : Jan  4 02:41:13 2036 GMT
        Subject: CN = system:kube-scheduler, O = system:kube-scheduler, C = US, ST = Washington, L = Seattle
            Netscape Cert Type: 
                SSL Client
            Netscape Comment: 
                Kube Scheduler Certificate
            X509v3 Subject Alternative Name: 
                DNS:kube-scheduler, IP Address:127.0.0.1

openssl x509 -in kube-controller-manager.crt -text -noout
        Validity
            Not Before: Jan  3 02:41:16 2026 GMT
            Not After : Jan  4 02:41:16 2036 GMT
        Subject: CN = system:kube-controller-manager, O = system:kube-controller-manager, C = US, ST = Washington, L = Seattle
            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

# api-server : SAN 정보에 10.32.0.1 은 kubernetes (Service) ClusterIP. 다른 인증서와 다르게 SSL Server 역할 추가 확인
openssl x509 -in kube-api-server.crt -text -noout
        Issuer: C = US, ST = Washington, L = Seattle, CN = CA
        Validity
            Not Before: Jan  3 02:41:17 2026 GMT
            Not After : Jan  4 02:41:17 2036 GMT
        Subject: CN = kubernetes, C = US, ST = Washington, L = Seattle
            Netscape Cert Type: 
                SSL Client, SSL Server
            Netscape Comment: 
                Kube API Server Certificate
            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

# service-accounts
openssl x509 -in service-accounts.crt -text -noout
        Issuer: C = US, ST = Washington, L = Seattle, CN = CA
        Validity
            Not Before: Jan  3 02:41:17 2026 GMT
            Not After : Jan  4 02:41:17 2036 GMT
        Subject: CN = service-accounts
        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

이 스크립트는 쿠버네티스 클러스터 운영에 필요한 7종류의 컴포넌트용 인증서를 일괄 생성하고 그 내용을 검증하는 절차
각 인증서는 ca.conf의 특정 섹션 설정을 따라 만들어지며, 컴포넌트별로 **주체(Subject)**와 **용도(Usage/SAN)**가 다르게 설정

1. 생성 대상 및 목적

배열 certs에 정의된 7개 대상의 인증서를 반복문(for)으로 생성

  1. Node 인증서 (node-0, node-1): Kubelet이 API 서버와 통신할 때 사용
    • CN=system:node:<노드명>, O=system:nodes로 설정되어 RBAC에서 노드 권한을 받음.
    • 클라이언트/서버 양쪽 용도(clientAuth, serverAuth)를 모두 가집니다(API 서버가 Kubelet 호출 시 서버 역할 필요).
  2. 클라이언트 전용 (kube-proxy, scheduler, controller-manager): 각 컴포넌트가 API 서버에 명령을 내릴 때 쓰는 신분증
    • CN과 O가 각각의 시스템 컴포넌트 이름으로 매핑
    • SSL Client 타입으로만 동작
  3. API 서버 (kube-api-server): 클러스터의 핵심 서버가 사용할 인증서
    • 가장 복잡한 **SAN(Subject Alternative Name)**을 가집니다. 10.32.0.1(Service CIDR 첫 IP), kubernetes.default... 등 내부 DNS 이름을 모두 포함해야 파드들이 API 서버를 신뢰할 수 있음
    • SSL Server와 SSL Client 역할을 모두 수행
  4. 서비스 계정 (service-accounts): API 서버가 토큰을 서명할 때 쓰는 키 쌍
    • CN=service-accounts로 단순하게 설정됨

2. 생성 과정 자동화

 
bash
for i in ${certs[*]}; do ... done
  • 반복문 안에서 openssl genrsa(키 생성) → openssl req(CSR 생성, -section ${i}로 설정 분리) → openssl x509(인증서 발급) 과정을 수행
  • -section ${i} 옵션 덕분에 ca.conf 파일 하나에 정의된 여러 섹션([node-0], [kube-api-server] 등)을 동적으로 불러와 각기 다른 설정을 적용 가능

3. 검증 포인트 (해석)

  • Node 인증서: Subject: CN = system:node:node-0, O = system:nodes 확인. 이 O=system:nodes 그룹에 속해야만 노드로서 인정받음
  • API Server 인증서: X509v3 Subject Alternative Name에 IP Address:10.32.0.1과 다양한 DNS 이름이 포함되었는지 확인 필수입니다. 이게 빠지면 클러스터 내부 통신 에러가 발생함
  • Service Account: 일반적인 통신용이 아니라 토큰 서명용이라 구성이 비교적 단순

4. 특이사항 (ca.conf 수정)

스크립트 초반에 sed 명령어로 ca.conf의 오타(system:system:kube-scheduler)를 수정하는 부분이 있는데 이는 설정 파일의 실수로 인해 O 필드가 잘못 들어가는 것을 방지하기 위함

이 과정이 끝나면 작업 디렉터리에 .key, .csr, .crt 파일들이 가득 차게 되며, 이 파일들을 각 서버(controller, worker)의 적절한 위치(/var/lib/kubernetes/ 등)로 복사해주는 다음 단계가 필요함

Distribute the Client and Server Certificates

# Copy the appropriate certificates and private keys to the node-0 and node-1 machines
for host in node-0 node-1; do
  ssh root@${host} mkdir /var/lib/kubelet/

  scp ca.crt root@${host}:/var/lib/kubelet/

  scp ${host}.crt \
    root@${host}:/var/lib/kubelet/kubelet.crt

  scp ${host}.key \
    root@${host}:/var/lib/kubelet/kubelet.key
done

# 확인
ssh node-0 ls -l /var/lib/kubelet
ssh node-1 ls -l /var/lib/kubelet


# Copy the appropriate certificates and private keys to the server machine
scp \
  ca.key ca.crt \
  kube-api-server.key kube-api-server.crt \
  service-accounts.key service-accounts.crt \
  root@server:~/

# 확인
ssh server ls -l /root

 

이 스크립트는 로컬(점프박스)에서 생성한 인증서와 키 파일들을 실제 서버들(Worker Nodes, Control Plane)의 정해진 위치로 배포(복사)하는 과정

각 역할에 필요한 파일만 선별해서 보내는 것이 핵심

1. 워커 노드 (node-0, node-1) 배포

 
bash
for host in node-0 node-1; do ... done
  • 디렉터리 생성: 각 노드에 접속해 /var/lib/kubelet/ 디렉터리를 만듭니다. Kubelet이 인증서를 찾을 표준 경로
  • 파일 복사 (scp):
    • ca.crt: 클러스터 통신 신뢰를 위해 루트 CA 인증서는 필수입니다.
    • ${host}.crt  kubelet.crt: 각 노드 이름으로 된 인증서(예: node-0.crt)를 kubelet.crt라는 통일된 이름으로 변경해 저장
    • ${host}.key  kubelet.key: 개인키도 마찬가지로 kubelet.key로 이름을 통일
  • 이유: 이렇게 이름을 통일해두면, 나중에 Kubelet 설정 파일에서 노드마다 다른 파일명을 지정할 필요 없이 /var/lib/kubelet/kubelet.crt로 고정해서 설정할 수 있어 관리가 편해짐

2. 컨트롤 플레인 (server) 배포

 
bash
scp ca.key ca.crt kube-api-server.key ... root@server:~/
  • 대상 파일:
    • ca.key, ca.crt: API 서버가 클라이언트 인증서를 검증하거나 새 인증서를 발급할 때 필요
    • (특히 ca.key는 컨트롤 플레인에만 있어야 합니다).
    • kube-api-server.key/crt: API 서버 자신의 신분증
    • service-accounts.key/crt: 서비스 어카운트 토큰(JWT)을 서명하고 검증하는 데 사용
  • 경로: 일단 루트 홈 디렉터리(~/)로 보냅니다. 보통 이후 단계에서 /var/lib/kubernetes/ 같은 시스템 경로로 옮기고 권한을 설정하는 추가 작업이 이어집니다.

3. 검증 (ls -l)

  • ssh node-0 ls -l ... 등으로 원격 파일 목록을 조회하여, 파일이 누락 없이 잘 도착했는지, 이름 변경(rename)은 잘 되었는지 확인

요약:
"워커 노드에는 **자기 신분증(Kubelet 인증서)**만 주고, 컨트롤 플레인(Server)에는 **클러스터 전체 관리용 도장(CA Key)**과 서버용 신분증을 몰아주는 작업"입니다. 보안상 워커 노드에는 절대로 ca.key나 다른 노드의 키가 들어가면 안 됨

05 - Generating Kubernetes Configuration Files for Authentication

API Server와 통신을 위한 Client 인증 설정 파일 작성

공식문서: https://v1-32.docs.kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/

The kubelet Kubernetes Configuration File

워커 노드(Kubelet)가 API 서버와 통신할 때 사용할 kubeconfig 파일을 생성하는 과정입니다.
API 서버 주소, 인증용 키(인증서), 접속 컨텍스트 정보를 하나의 파일로 묶어주는 작업

최종적으로 생성된 node-0.kubeconfig, node-1.kubeconfig 파일은 API 서버 주소, CA 인증서, 클라이언트 인증서/키가 모두 포함된 완전체 접속 설정 파일
이후 이 파일들을 각 워커 노드의 /var/lib/kubelet/kubeconfig 위치로 복사해주면, Kubelet이 시작될 때 이 파일을 읽고 "아, 내가 node-0이구나, 저기 있는 API 서버에 이 키를 내밀고 접속하면 되겠네"라고 인식하게 됩니다.

# apiserver 파드 args 정보
kubectl describe pod -n kube-system kube-apiserver-myk8s-control-plane
    Command:
      kube-apiserver
      --authorization-mode=Node,RBAC  


# Generate a kubeconfig file for the node-0 and node-1 worker nodes

# config set-cluster
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

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

# config set-credentials
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

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
  
# set-context : default 추가
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:node:node-0 \
  --kubeconfig=node-0.kubeconfig && cat node-0.kubeconfig

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:node:node-1 \
  --kubeconfig=node-1.kubeconfig && cat node-1.kubeconfig
...
contexts:
- context:
    cluster: kubernetes-the-hard-way
    user: system:node:node-0
  name: default

# use-context : current-context 에 default 추가
kubectl config use-context default \
  --kubeconfig=node-0.kubeconfig

kubectl config use-context default \
  --kubeconfig=node-1.kubeconfig
...
current-context: default

#
ls -l *.kubeconfig
-rw------- 1 root root 10157 Jan  3 14:55 node-0.kubeconfig
-rw------- 1 root root 10068 Jan  3 14:50 node-1.kubeconfig

 

The kube-proxy Kubernetes Configuration File

 Kube-Proxy 컴포넌트가 API 서버와 통신할 때 사용할 kube-proxy.kubeconfig 파일을 생성하는 과정
앞서 설명해드린 노드용(kubelet) kubeconfig 생성 과정과 구조는 거의 똑같지만, **사용자(Identity)**가 다름

 kube-proxy.kubeconfig 파일은 나중에 모든 워커 노드(Node-0, Node-1)에 배포
각 노드에서 실행되는 kube-proxy 데몬은 이 파일을 읽고 API 서버에 접속해서, **"지금 서비스(Service)나 엔드포인트(Endpoint) 바뀐 거 있어? 있으면 내가 iptables 규칙 업데이트할게"**라고 주기적으로 확인(Watch)하는 용도로 사용

# Generate a kubeconfig file for the kube-proxy service
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

kubectl config set-credentials system:kube-proxy \
  --client-certificate=kube-proxy.crt \
  --client-key=kube-proxy.key \
  --embed-certs=true \
  --kubeconfig=kube-proxy.kubeconfig

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-proxy \
  --kubeconfig=kube-proxy.kubeconfig

kubectl config use-context default \
  --kubeconfig=kube-proxy.kubeconfig

# 확인
cat kube-proxy.kubeconfig

The kube-controller-manager Kubernetes Configuration File

이 스크립트는 쿠버네티스 컨트롤 플레인의 핵심 컴포넌트인 Controller Manager가 사용할 kube-controller-manager.kubeconfig 파일을 생성하는 과정

Control Plane 서버의 /var/lib/kubernetes/ 같은 경로에 위치하게 되며, kube-controller-manager 데몬이 실행될 때 --kubeconfig 옵션으로 이 파일을 참조
이를 통해 컨트롤러 매니저는 API 서버에 접속해서 "지금 파드 죽은 거 있나? 레플리카 개수 안 맞나?" 하고 끊임없이 감시하고 조치하는 루프(Loop)를 돌게 됨

# Generate a kubeconfig file for the kube-controller-manager service
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

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

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-controller-manager \
  --kubeconfig=kube-controller-manager.kubeconfig

kubectl config use-context default \
  --kubeconfig=kube-controller-manager.kubeconfig

# 확인
cat kube-controller-manager.kubeconfig

The kube-scheduler Kubernetes Configuration File

 Kube-Scheduler가 API 서버와 통신할 때 사용할 kube-scheduler.kubeconfig 파일을 생성하는 과정입니다.​

컨트롤러 매니저 설정과 구조적으로 완전히 동일하며, **사용자(Identity)**만 스케줄러용으로 바뀐 것

이 파일도 Control Plane 서버에 배치됩니다. kube-scheduler 프로세스는 이 파일을 들고 API 서버에 로그인하여, **"새로 생긴 파드 없나요? 제가 빈자리 찾아서 넣어드릴게요"**라고 지속적으로 모니터링하고 배치 결정을 내리는 작업을 수행

# Generate a kubeconfig file for the kube-scheduler service
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

kubectl config set-credentials system:kube-scheduler \
  --client-certificate=kube-scheduler.crt \
  --client-key=kube-scheduler.key \
  --embed-certs=true \
  --kubeconfig=kube-scheduler.kubeconfig

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-scheduler \
  --kubeconfig=kube-scheduler.kubeconfig

kubectl config use-context default \
  --kubeconfig=kube-scheduler.kubeconfig

# 확인
cat kube-scheduler.kubeconfig

The admin Kubernetes Configuration File

클러스터 **최고 관리자(root)**가 사용할 admin.kubeconfig 파일을 생성하는 과정
우리가 보통 kubectl 명령어를 칠 때 사용하는 바로 그 파일

이 파일은 생성 후 Control Plane 서버의 ~/.kube/config 위치로 복사되어, 관리자가 kubectl 명령어로 클러스터를 제어하고 상태를 확인하는 마스터 키 역할을 하게 됨

# Generate a kubeconfig file for the admin user
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

kubectl config set-credentials admin \
  --client-certificate=admin.crt \
  --client-key=admin.key \
  --embed-certs=true \
  --kubeconfig=admin.kubeconfig

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=admin \
  --kubeconfig=admin.kubeconfig

kubectl config use-context default \
  --kubeconfig=admin.kubeconfig

# 확인
cat admin.kubeconfig

Distribute the Kubernetes Configuration Files

#
ls -l *.kubeconfig
-rw------- 1 root root  9949 Jan  3 15:00 admin.kubeconfig
-rw------- 1 root root 10305 Jan  3 14:59 kube-controller-manager.kubeconfig
-rw------- 1 root root 10187 Jan  3 14:58 kube-proxy.kubeconfig
-rw------- 1 root root 10231 Jan  3 14:59 kube-scheduler.kubeconfig
-rw------- 1 root root 10157 Jan  3 14:55 node-0.kubeconfig
-rw------- 1 root root 10068 Jan  3 14:50 node-1.kubeconfig

# Copy the kubelet and kube-proxy kubeconfig files to the node-0 and node-1 machines
for host in node-0 node-1; do
  ssh root@${host} "mkdir -p /var/lib/{kube-proxy,kubelet}"

  scp kube-proxy.kubeconfig \
    root@${host}:/var/lib/kube-proxy/kubeconfig \

  scp ${host}.kubeconfig \
    root@${host}:/var/lib/kubelet/kubeconfig
done

# 확인
ssh node-0 ls -l /var/lib/*/kubeconfig
ssh node-1 ls -l /var/lib/*/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

모든 kubeconfig 파일들을 각자의 주인이 있는 서버로 배달(복사)하는 과정
인증서 배포 때와 마찬가지로, **"누가 어떤 파일을 써야 하는가"**를 정확히 구분해서 보내는 것이 핵심

이제 모든 서버가 **"나는 누구고(인증서), 어디에 있는(서버 주소) API 서버랑 통신해야 하는지(kubeconfig)"**를 알게 됨.

통신 준비 끝

06 - Generating the Data Encryption Config and Key

ETCD 에 Secret 저장 시, 암호화 저장 설정

쿠버네티스 시크릿(Secret) 데이터의 암호화 설정(Encryption Configuration)**을 준비하고 배포하는 과정입니다.
기본적으로 etcd에 저장되는 시크릿 정보는 평문(Plaintext)이지만, 보안 강화를 위해 이를 암호화해서 저장하도록 만드는 중요한 보안 절차

공식문서: https://v1-32.docs.kubernetes.io/docs/tasks/administer-cluster/encrypt-data/

# The Encryption Key

# Generate an encryption key
export ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
echo $ENCRYPTION_KEY
JMnUP1PUUORZE9iadPdzYifnvPVIniSzOW6NUoMofVc=


# The Encryption Config File

# Create the encryption-config.yaml encryption config file
# (참고) 실제 etcd 값에 기록되는 헤더 : k8s:enc:aescbc:v1:key1:<ciphertext>
cat configs/encryption-config.yaml
kind: EncryptionConfiguration           # kube-apiserver가 etcd에 저장할 리소스를 어떻게 암호화할지 정의
apiVersion: apiserver.config.k8s.io/v1  # --encryption-provider-config 플래그로 참조
resources:
  - resources:
      - secrets                         # 암호화 대상 Kubernetes 리소스 : 여기서는 Secret 리소스만 암호화
    providers:                          # 지원 providers(위 부터 적용됨) : identity, aescbc, aesgcm, kms v2, secretbox
      - aescbc:                         # etcd에 저장될 Secret을 AES-CBC 방식으로 암호화
          keys:
            - name: key1                # 키 식별자 (etcd 데이터에 기록됨)
              secret: ${ENCRYPTION_KEY}
      - identity: {}                    # 암호화하지 않음 (Plaintext), 주로 하위 호환성 / 점진적 암호화 목적
# aescbc를 첫 번째에, identity를 두 번째에 배치하는 것은 "새로운 데이터는 무조건 암호화해서 저장하되, 이전에 평문으로 저장되었던 데이터도 문제없이 읽어 들이겠다"는 하위 호환성 전략을 의미
# 설명 출처 : https://yyeon2.medium.com/bootstrap-kubernetes-the-hard-way-48644e868550

envsubst < configs/encryption-config.yaml > encryption-config.yaml
cat encryption-config.yaml

# Copy the encryption-config.yaml encryption config file to each controller instance:
scp encryption-config.yaml root@server:~/
ssh server ls -l /root/encryption-config.yaml

 

07 - Bootstrapping the etcd Cluster

server 노드에 etcd 서비스 기동

쿠버네티스의 데이터 저장소인 etcd 클러스터를 단일 노드(Control Plane)에 구축하고 실행하는 과정
실습의 단순화를 위해 TLS 보안 설정(HTTPS) 대신 로컬 평문 통신(HTTP)을 사용하고, 클러스터링도 로컬호스트(127.0.0.1) 기반으로 구성한 점이 특징

1. etcd 서비스 파일 생성 (etcd.service)

systemd가 etcd를 데몬으로 관리할 수 있도록 서비스 정의 파일을 만듬

  • --name ${ETCD_NAME}: etcd 멤버 이름을 server로 지정합니다. 이 이름은 클러스터 내에서 유일해야 함
  • 주소 설정 (모두 HTTP & 127.0.0.1):
    • --listen-peer-urls: 다른 etcd 멤버와 통신하는 주소 (2380 포트).
    • --listen-client-urls: API 서버 등이 데이터를 읽고 쓸 때 접속하는 주소 (2379 포트).
    • 실습용이라 외부 통신을 고려하지 않고 **로컬 루프백(127.0.0.1)**만 열어둠. 보안상 외부에서 접근이 불가능해 안전하지만, 다중 마스터 구성은 불가능한 설정
  • --initial-cluster ...: "클러스터에 나 혼자(server)밖에 없다"라고 초기 구성을 선언

2. 파일 배포 (scp)

  • etcd(서버 바이너리), etcdctl(CLI 도구), etcd.service(설정 파일) 3가지를 server 머신의 홈 디렉터리로 보냄

3. 서버 내부 설정 및 실행 (ssh root@server)

이제 server 안에 직접 들어가서 설치를 마무리

  1. 설치: 바이너리를 /usr/local/bin/으로 옮겨서 어디서든 실행 가능하게 만듬
  2. 설정:
    • 데이터 저장소(/var/lib/etcd)를 만들고 권한을 700(루트만 접근 가능)으로 조여 보안을 강화
    • /etc/etcd/에 인증서들을 복사해 두는데, 현재 설정 파일(etcd.service)에서는 이 인증서들을 쓰지 않고(옵션이 없음) HTTP로만 통신
  3. 실행: systemctl enable --now etcd로 서비스를 켜고 부팅 시 자동 실행되게 등록

4. 검증 (verification)

  • systemctl status: "Active: active (running)" 상태인지 확인.
  • ss -tnlp: 2379(클라이언트용), 2380(피어용) 포트가 127.0.0.1에서 잘 열려있는지 확인.
  • etcdctl member list: "내 이름은 server고, 상태는 started다"라는 응답이 오면 정상 구축된 것

 쿠버네티스의 데이터 저장소가 살아났습니다. API 서버가 깨어나면 이곳에다가 "파드 A를 실행해라", "서비스 B를 만들어라" 같은 정보를 기록하게 될 것

# Prerequisites

# hostname 변경 : controller -> server
# http 평문 통신!
# Each etcd member must have a unique name within an etcd cluster. 
# Set the etcd name to match the hostname of the current compute instance:
cat units/etcd.service | grep controller

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
cat units/etcd.service | grep server

# Copy etcd binaries and systemd unit files to the server machine
scp \
  downloads/controller/etcd \
  downloads/client/etcdctl \
  units/etcd.service \
  root@server:~/


# 아래는 server 가상머신 접속 후 명령 실행
# The commands in this lab must be run on the server machine. Login to the server machine using the ssh command. Example:
ssh root@server
-------------------------------------------------------------------
# Bootstrapping an etcd Cluster

# Install the etcd Binaries
# Extract and install the etcd server and the etcdctl command line utility
pwd
mv etcd etcdctl /usr/local/bin/

# Configure the etcd Server
mkdir -p /etc/etcd /var/lib/etcd
chmod 700 /var/lib/etcd
cp ca.crt kube-api-server.key kube-api-server.crt /etc/etcd/

# Create the etcd.service systemd unit file:
mv etcd.service /etc/systemd/system/
tree /etc/systemd/system/

# Start the etcd Server
systemctl daemon-reload
systemctl enable etcd
systemctl start etcd

# 확인
systemctl status etcd --no-pager
ss -tnlp | grep etcd
LISTEN 0      4096       127.0.0.1:2380      0.0.0.0:*    users:(("etcd",pid=2829,fd=3))                          
LISTEN 0      4096       127.0.0.1:2379      0.0.0.0:*    users:(("etcd",pid=2829,fd=6)) 

# List the etcd cluster members
etcdctl member list
702b0a34e2cfd39, started, server, http://127.0.0.1:2380, http://127.0.0.1:2379, false

etcdctl member list -w table
etcdctl endpoint status -w table

exit
-------------------------------------------------------------------

08 - Bootstrapping the Kubernetes Control Plane

server 노드에 ‘api server, scheduler, kcm’ 서비스 기동

 

항목 네트워크 대역 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

vagrant ssh jumpbox

설정 파일 작성 후 server 에 전달

API Server의 필수 누락 설정을 수정하고, Kubelet 접속 권한(RBAC)을 구성하며, 최종적으로 Control Plane 실행에 필요한 모든 파일과 설정을 서버로 전송하는 **"버그 수정 및 배포 단계

# Prerequisites

# kube-apiserver.service 수정 : service-cluster-ip-range 추가
# https://github.com/kelseyhightower/kubernetes-the-hard-way/issues/905
# service-cluster-ip 값은 ca.conf 에 설정한 [kube-api-server_alt_names] 항목의 Service IP 범위
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

cat units/kube-apiserver.service
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
cat units/kube-apiserver.service


# 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"  # Kubernetes가 업그레이드 시 자동 관리
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:kube-apiserver-to-kubelet
rules:
  - apiGroups:
      - ""                                               # Core API group (v1) : Node 관련 서브리소스는 core group에 속함
    resources:                                           # 아래 처럼, kubelet API 대부분을 포괄
      - nodes/proxy                                      ## apiserver → kubelet 프록시 통신
      - nodes/stats                                      ## 노드/파드 리소스 통계 (cAdvisor)
      - nodes/log                                        ## metrics-server / top 명령
      - nodes/spec                                       ## kubectl logs
      - nodes/metrics                                    ## metrics-server / top 명령
    verbs:
      - "*"                                              # 대상은 “nodes 하위 리소스”로 한정 + 모든 동작 허용 (get, list, watch, create, proxy 등)
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: system:kube-apiserver                            # 누가 이 권한을 쓰는가? → 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                         # 사용자 kubernetes ,이 사용자는 kube-apiserver가 사용하는 클라이언트 인증서의 CN

# api-server : Subject CN 확인
openssl x509 -in kube-api-server.crt -text -noout
        Subject: CN = kubernetes,

# api -> kubelet 호출 시 Flow
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 권한 부여


# kube-scheduler
cat units/kube-scheduler.service ; echo
cat configs/kube-scheduler.yaml ; echo


# kube-controller-manager : cluster-cidr 는 POD CIDR 포함하는 대역, service-cluster-ip-range 는 apiserver 설정 값 동일 설정.
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


# Connect to the jumpbox and copy Kubernetes binaries and systemd unit files to the server machine
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:~/

# 확인
ssh server ls -l /root

Provision the Kubernetes Control Plane : kubectl 확인

 스크립트는 쿠버네티스 컨트롤 플레인(Control Plane)의 3대장(API Server, Scheduler, Controller Manager)을 서버에 설치하고 실행하여 클러스터를 깨우는 과정입니다.
그리고 마지막에 kubectl 명령어로 클러스터가 정상적으로 살아났는지, 권한(RBAC)은 잘 동작하는지 검증

이제 Control Plane이 완전히 깨어났고, 신경망(RBAC)도 연결되었습니다. 하지만 아직 팔다리(Worker Node)가 없어서 실제 파드를 실행할 수는 없는 상태입니다. 다음 단계는 워커 노드를 설정하는 것

ssh root@server
---------------------------------------------------------------
# Create the Kubernetes configuration directory:
pwd
mkdir -p /etc/kubernetes/config


# Install the Kubernetes binaries:
mv kube-apiserver \
  kube-controller-manager \
  kube-scheduler kubectl \
  /usr/local/bin/
ls -l /usr/local/bin/kube-*


# Configure the Kubernetes API Server
mkdir -p /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/

## Create the kube-apiserver.service systemd unit file:
mv kube-apiserver.service \
  /etc/systemd/system/kube-apiserver.service
tree /etc/systemd/system


# Configure the Kubernetes Controller Manager

## Move the kube-controller-manager kubeconfig into place:
mv kube-controller-manager.kubeconfig /var/lib/kubernetes/

## Create the kube-controller-manager.service systemd unit file:
mv kube-controller-manager.service /etc/systemd/system/


# Configure the Kubernetes Scheduler

## Move the kube-scheduler kubeconfig into place:
mv kube-scheduler.kubeconfig /var/lib/kubernetes/

## Create the kube-scheduler.yaml configuration file:
mv kube-scheduler.yaml /etc/kubernetes/config/

## Create the kube-scheduler.service systemd unit file:
mv kube-scheduler.service /etc/systemd/system/


# Start the Controller Services : Allow up to 10 seconds for the Kubernetes API Server to fully initialize.
systemctl daemon-reload
systemctl enable kube-apiserver kube-controller-manager kube-scheduler
systemctl start  kube-apiserver kube-controller-manager kube-scheduler

# 확인
ss -tlp | grep kube
LISTEN 0      4096               *:6443              *:*    users:(("kube-apiserver",pid=3071,fd=3))                
LISTEN 0      4096               *:10257             *:*    users:(("kube-controller",pid=3072,fd=3))               
LISTEN 0      4096               *:10259             *:*    users:(("kube-scheduler",pid=3073,fd=3))  

systemctl is-active kube-apiserver
systemctl status kube-apiserver --no-pager
journalctl -u kube-apiserver --no-pager

systemctl status kube-scheduler --no-pager
systemctl status kube-controller-manager --no-pager

# Verify this using the kubectl command line tool:
kubectl cluster-info dump --kubeconfig admin.kubeconfig
kubectl cluster-info --kubeconfig admin.kubeconfig
Kubernetes control plane is running at https://127.0.0.1:6443

kubectl get node --kubeconfig admin.kubeconfig
kubectl get pod -A --kubeconfig admin.kubeconfig

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   110m

NAME                   ENDPOINTS        AGE
endpoints/kubernetes   10.0.2.15:6443   110m

# clusterroles 확인
kubectl get clusterroles --kubeconfig admin.kubeconfig
NAME                                                                   CREATED AT
admin                                                                  2026-01-03T08:42:54Z
cluster-admin                                                          2026-01-03T08:42:54Z
edit                                                                   2026-01-03T08:42:54Z
system:aggregate-to-admin                                              2026-01-03T08:42:54Z
system:aggregate-to-edit                                               2026-01-03T08:42:54Z
system:aggregate-to-view                                               2026-01-03T08:42:54Z
system:auth-delegator                                                  2026-01-03T08:42:54Z
system:basic-user                                                      2026-01-03T08:42:54Z
system:certificates.k8s.io:certificatesigningrequests:nodeclient       2026-01-03T08:42:54Z
system:certificates.k8s.io:certificatesigningrequests:selfnodeclient   2026-01-03T08:42:54Z
system:certificates.k8s.io:kube-apiserver-client-approver              2026-01-03T08:42:54Z
system:certificates.k8s.io:kube-apiserver-client-kubelet-approver      2026-01-03T08:42:54Z
system:certificates.k8s.io:kubelet-serving-approver                    2026-01-03T08:42:54Z
system:certificates.k8s.io:legacy-unknown-approver                     2026-01-03T08:42:54Z
system:controller:attachdetach-controller                              2026-01-03T08:42:54Z
system:controller:certificate-controller                               2026-01-03T08:42:54Z
system:controller:clusterrole-aggregation-controller                   2026-01-03T08:42:54Z
system:controller:cronjob-controller                                   2026-01-03T08:42:54Z
system:controller:daemon-set-controller                                2026-01-03T08:42:54Z
system:controller:deployment-controller                                2026-01-03T08:42:54Z
system:controller:disruption-controller                                2026-01-03T08:42:54Z
system:controller:endpoint-controller                                  2026-01-03T08:42:54Z
system:controller:endpointslice-controller                             2026-01-03T08:42:54Z
system:controller:endpointslicemirroring-controller                    2026-01-03T08:42:54Z
system:controller:ephemeral-volume-controller                          2026-01-03T08:42:54Z
system:controller:expand-controller                                    2026-01-03T08:42:54Z
system:controller:generic-garbage-collector                            2026-01-03T08:42:54Z
system:controller:horizontal-pod-autoscaler                            2026-01-03T08:42:54Z
system:controller:job-controller                                       2026-01-03T08:42:54Z
system:controller:legacy-service-account-token-cleaner                 2026-01-03T08:42:54Z
system:controller:namespace-controller                                 2026-01-03T08:42:54Z
system:controller:node-controller                                      2026-01-03T08:42:54Z
system:controller:persistent-volume-binder                             2026-01-03T08:42:54Z
system:controller:pod-garbage-collector                                2026-01-03T08:42:54Z
system:controller:pv-protection-controller                             2026-01-03T08:42:54Z
system:controller:pvc-protection-controller                            2026-01-03T08:42:54Z
system:controller:replicaset-controller                                2026-01-03T08:42:54Z
system:controller:replication-controller                               2026-01-03T08:42:54Z
system:controller:resourcequota-controller                             2026-01-03T08:42:54Z
system:controller:root-ca-cert-publisher                               2026-01-03T08:42:54Z
system:controller:route-controller                                     2026-01-03T08:42:54Z
system:controller:service-account-controller                           2026-01-03T08:42:54Z
system:controller:service-controller                                   2026-01-03T08:42:54Z
system:controller:statefulset-controller                               2026-01-03T08:42:54Z
system:controller:ttl-after-finished-controller                        2026-01-03T08:42:54Z
system:controller:ttl-controller                                       2026-01-03T08:42:54Z
system:controller:validatingadmissionpolicy-status-controller          2026-01-03T08:42:54Z
system:discovery                                                       2026-01-03T08:42:54Z
system:heapster                                                        2026-01-03T08:42:54Z
system:kube-aggregator                                                 2026-01-03T08:42:54Z
system:kube-controller-manager                                         2026-01-03T08:42:54Z
system:kube-dns                                                        2026-01-03T08:42:54Z
system:kube-scheduler                                                  2026-01-03T08:42:54Z
system:kubelet-api-admin                                               2026-01-03T08:42:54Z
system:monitoring                                                      2026-01-03T08:42:54Z
system:node                                                            2026-01-03T08:42:54Z
system:node-bootstrapper                                               2026-01-03T08:42:54Z
system:node-problem-detector                                           2026-01-03T08:42:54Z
system:node-proxier                                                    2026-01-03T08:42:54Z
system:persistent-volume-provisioner                                   2026-01-03T08:42:54Z
system:public-info-viewer                                              2026-01-03T08:42:54Z
system:service-account-issuer-discovery                                2026-01-03T08:42:54Z
system:volume-scheduler                                                2026-01-03T08:42:54Z
view                                                                   2026-01-03T08:42:54Z

kubectl describe clusterroles system:kube-scheduler --kubeconfig admin.kubeconfig


# kube-scheduler subject 확인
kubectl get clusterrolebindings --kubeconfig admin.kubeconfig
kubectl describe clusterrolebindings system:kube-scheduler --kubeconfig admin.kubeconfig
Role:
  Kind:  ClusterRole
  Name:  system:kube-scheduler
Subjects:
  Kind  Name                   Namespace
  ----  ----                   ---------
  User  system:kube-scheduler 

---------------------------------------------------------------

RBAC for Kubelet Authorization

  • 이 섹션에서는 Kubernetes API 서버가 각 작업자 노드에서 Kubelet API에 액세스할 수 있도록 RBAC 권한을 구성합니다.
  • Kubelet API에 대한 액세스 권한은 메트릭, 로그를 검색하고 포드에서 명령을 실행하는 데 필요합니다.
  • 이 튜토리얼에서는 Kubelet --authorization-mode 플래그를 Webhook으로 설정합니다.
  • Webhook 모드에서는 SubjectAccessReview API를 사용하여 권한을 결정합니다.

jumpbox 서버에서 k8s controlplane 정상 동작 확인

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

node-0/1 노드에 ‘runccontainer networking pluginscontainerdkubelet, and kube-proxy’ 설치

vagrant ssh jumpbox: Prerequisites 사전 준비

워커 노드(Worker Node)의 핵심 컴포넌트인 kubelet, containerd, kube-proxy, 그리고 네트워크 플러그인(CNI)을 설정하고 배포하는 과정

1. CNI 및 Kubelet 설정 파일 생성 및 수정

  • 10-bridge.conf (CNI 설정): 컨테이너 네트워크 인터페이스(CNI)의 브릿지 설정 파일
    • SUBNET 치환: 각 노드(node-0, node-1)마다 할당된 파드 CIDR(10.200.0.0/24 등)이 다르므로, sed 명령어를 통해 각 노드에 맞는 서브넷 값으로 바꿔치기해서 저장​
  • kubelet-config.yaml (Kubelet 설정):
    • address: "0.0.0.0" / port: 10250: 모든 인터페이스에서 10250 포트로 API 요청을 받음​
    • authentication: 익명 접속을 막고(anonymous.enabled: false), Webhook 방식 인증을 켬. API 서버가 인증 주체​
    • authorization: Webhook 모드를 사용. 즉, "이 요청 허용해도 되나요?"라고 API 서버(SAR)에게 물어봐서 결정
    • cgroupDriver: systemd: 컨테이너 리소스 관리를 systemd에 위임하여 안정성을 높임
    • containerRuntimeEndpoint: 컨테이너 런타임으로 containerd를 지정​
    • podCIDR: 이 파일에는 없지만, 나중에 Kubelet 실행 시 인자나 자동 감지로 설정 여기서는 registerNode: true로 API 서버에 스스로 등록하면서 할당받게 됨

2. 기타 설정 및 서비스 파일 준비

  • 99-loopback.conf: CNI 루프백 플러그인 설정
  • containerd-config.toml: containerd 런타임의 상세 설정 파일
  • kube-proxy-config.yaml:
    • mode: "iptables": 리눅스 커널의 iptables를 사용해 서비스 트래픽을 파드로 부하 분산
    • clusterCIDR: 전체 파드 네트워크 대역(10.200.0.0/16)을 알려줌
  • Systemd 서비스 파일들: containerd.service, kubelet.service, kube-proxy.service는 각 데몬을 리눅스 서비스로 등록하기 위한 파일

3. 파일 전송 (배포)

  • 설정 및 실행 파일: scp를 통해 준비된 설정 파일들과 바이너리(kubelet, kubectl, kube-proxy 등)를 각 워커 노드(node-0, node-1)의 홈 디렉터리(~/)로 보냄​
  • CNI 플러그인: cni-plugins 폴더에 있는 실행 파일들(bridge, loopback, host-local 등)도 별도로 전송. 이 파일들은 나중에 /opt/cni/bin/ 등으로 옮겨질 것

4. 확인

  • ssh ... ls -l ...: 전송이 잘 되었는지 파일 목록을 확인

요약:
"워커 노드가 **컨테이너를 실행(containerd)**하고, **API 서버와 통신(kubelet)**하며, **네트워크 트래픽을 처리(kube-proxy/CNI)**할 수 있도록, 각자의 '업무 매뉴얼(설정 파일)'과 '장비(바이너리)'를 배급하는 과정

# cni(bridge) 파일과 kubelet-config 파일 작성 및 node-0/1에 전달
cat configs/10-bridge.conf | jq
cat configs/kubelet-config.yaml | yq # clusterDomain , clusterDNS 없어도 smoke test 까지 잘됨 -> 실습에서 coredns 미사용
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
containerRuntimeEndpoint: "unix:///var/run/containerd/containerd.sock"  # CRI 엔드포인트
enableServer: true                            # kubelet API 서버 활성화 , false면 apiserver가 kubelet 접근 불가
failSwapOn: false
maxPods: 16                                   # 노드당 최대 파드 수 16개
memorySwap:
  swapBehavior: NoSwap
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"


for HOST in node-0 node-1; do
  SUBNET=$(grep ${HOST} machines.txt | cut -d " " -f 4)
  sed "s|SUBNET|$SUBNET|g" \
    configs/10-bridge.conf > 10-bridge.conf

  sed "s|SUBNET|$SUBNET|g" \
    configs/kubelet-config.yaml > kubelet-config.yaml

  scp 10-bridge.conf kubelet-config.yaml \
  root@${HOST}:~/
done

# 확인
ssh node-0 ls -l /root
ssh node-1 ls -l /root


# 파일 확인 및 node-0/1에 전달
cat configs/99-loopback.conf ; echo
cat configs/containerd-config.toml ; echo
cat configs/kube-proxy-config.yaml ; echo
kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
  kubeconfig: "/var/lib/kube-proxy/kubeconfig"
mode: "iptables"
clusterCIDR: "10.200.0.0/16"

cat units/containerd.service
cat units/kubelet.service
cat units/kube-proxy.service

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

for HOST in node-0 node-1; do
  scp \
    downloads/cni-plugins/* \
    root@${HOST}:~/cni-plugins/
done

# 확인
ssh node-0 ls -l /root
ssh node-1 ls -l /root
ssh node-0 ls -l /root/cni-plugins
ssh node-1 ls -l /root/cni-plugins
Provisioning a Kubernetes Worker Node : node-0, node-1 에 접속해서 실행
#
ssh root@node-0
-----------------------------------------------------------
pwd
ls -l

# Install the OS dependencies : The socat binary enables support for the kubectl port-forward command.
apt-get -y install socat conntrack ipset kmod psmisc bridge-utils

# Disable Swap : Verify if swap is disabled:
swapon --show

# Create the installation directories
mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

# Install the worker binaries:
mv crictl kube-proxy kubelet runc /usr/local/bin/
mv containerd containerd-shim-runc-v2 containerd-stress /bin/
mv cni-plugins/* /opt/cni/bin/


# Configure CNI Networking

## Create the bridge network configuration file:
mv 10-bridge.conf 99-loopback.conf /etc/cni/net.d/
cat /etc/cni/net.d/10-bridge.conf 

## To ensure network traffic crossing the CNI bridge network is processed by iptables, load and configure the br-netfilter kernel module:
lsmod | grep netfilter
modprobe br-netfilter
echo "br-netfilter" >> /etc/modules-load.d/modules.conf
lsmod | grep netfilter

echo "net.bridge.bridge-nf-call-iptables = 1"  >> /etc/sysctl.d/kubernetes.conf
echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.d/kubernetes.conf
sysctl -p /etc/sysctl.d/kubernetes.conf


# Configure containerd : Install the containerd configuration files:
mkdir -p /etc/containerd/
mv containerd-config.toml /etc/containerd/config.toml
mv containerd.service /etc/systemd/system/
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


# Configure the Kubelet : Create the kubelet-config.yaml configuration file:
mv kubelet-config.yaml /var/lib/kubelet/
mv kubelet.service /etc/systemd/system/


# Configure the Kubernetes Proxy
mv kube-proxy-config.yaml /var/lib/kube-proxy/
mv kube-proxy.service /etc/systemd/system/


# Start the Worker Services
systemctl daemon-reload
systemctl enable containerd kubelet kube-proxy
systemctl start containerd kubelet kube-proxy


# 확인
systemctl status kubelet --no-pager
systemctl status containerd --no-pager
systemctl status kube-proxy --no-pager

exit
-----------------------------------------------------------

# jumpbox 에서 server 접속하여 kubectl node 정보 확인
ssh server "kubectl get nodes node-0 -o yaml --kubeconfig admin.kubeconfig" | yq
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>   2m48s   v1.32.3   192.168.10.101   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0

ssh server "kubectl get pod -A --kubeconfig admin.kubeconfig"

 

node-0 접속 후 실행

워커 노드(Worker Node) 하나(node-0)에 직접 들어가서, 쿠버네티스 실행 환경을 바닥부터 완성하고 실행하는 과정
작업이 끝나면 kubectl get nodes 명령어로 노드가 "Ready" 상태로 올라오는 것을 확인

1. 노드 내부 설정 (ssh root@node-0)

(1) 필수 패키지 및 환경 준비

  • 패키지 설치: socat(포트 포워딩용), conntrack(연결 추적용), ipset(방화벽 규칙용), bridge-utils(네트워크용) 등 쿠버네티스 네트워킹과 프록시 동작에 필수적인 리눅스 도구들을 설치
  • 스왑 확인: swapon --show로 스왑이 꺼져있는지 다시 확인(필수).
  • 디렉터리 생성: 설정 파일과 바이너리가 위치할 표준 경로들(/etc/cni/net.d, /var/lib/kubelet 등)을 생성

(2) 바이너리 배치

  • CRI/Kubelet/Proxy: crictl, kube-proxy, kubelet, runc는 /usr/local/bin/으로, containerd 관련 파일은 /bin/으로, CNI 플러그인은 /opt/cni/bin/으로 각각 옮겨서 실행 준비를 마침

(3) 네트워크(CNI) 및 커널 설정

  • 설정 파일: 미리 만들어둔 10-bridge.conf 등을 /etc/cni/net.d/로 옮깁니다. Kubelet과 Containerd가 나중에 이 파일을 읽어서 파드 네트워크를 구성
  • 커널 모듈 & Sysctl:
    • modprobe br-netfilter: 브릿지 네트워크를 통과하는 패킷이 iptables 규칙의 적용을 받도록 커널 모듈을 로드함
    • net.bridge.bridge-nf-call-iptables = 1: 이것이 설정되어야 kube-proxy가 설정한 iptables 규칙(서비스 부하분산 등)이 정상 작동함

(4) Containerd 설정 (config.toml)

  • CRI 플러그인: containerd가 쿠버네티스 런타임(CRI) 역할을 하도록 플러그인을 활성화
  • SystemdCgroup = true: 리눅스 자원 관리(cgroups)의 주체를 systemd로 통일합니다. Kubelet 설정과 이것이 일치해야 안정적
  • CNI 경로: 네트워크 플러그인을 어디서 찾을지 지정

(5) 서비스 실행 (Kubelet, Proxy, Containerd)

  • 배치: 각 컴포넌트의 설정 파일(.yaml, .toml)과 서비스 파일(.service)을 제자리에 둠
  • 실행: systemctl enable --now로 3대장을 동시에 깨웁니다.
    • containerd: 컨테이너를 실행할 준비를 함
    • kubelet: API 서버에 "저 node-0입니다, 일할 준비 됐습니다(Register)"라고 신고하고, 할당된 파드가 있는지 물어봄
    • kube-proxy: 네트워크 규칙(iptables)을 세팅

2. 외부 확인 (ssh server ...)

워커 노드 설정이 끝나고, 점프박스나 마스터 노드에서 확인하는 단계

  • kubectl get nodes: node-0이 목록에 뜨고 상태가 Ready여야 성공
  • INTERNAL-IP: 192.168.10.101로 잘 인식되었는지 확인
  • CONTAINER-RUNTIME: containerd://2.1.0... 버전이 맞는지 확인

요약:
이제 node-0은 "명령만 내리십시오(Ready)" 상태가 되었습니다. 동일한 과정을 node-1에도 수행하면 2개의 노드를 가진 완전한 클러스터가 됨

#
ssh root@node-0
-----------------------------------------------------------
pwd
ls -l

# Install the OS dependencies : The socat binary enables support for the kubectl port-forward command.
apt-get -y install socat conntrack ipset kmod psmisc bridge-utils

# Disable Swap : Verify if swap is disabled:
swapon --show

# Create the installation directories
mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

# Install the worker binaries:
mv crictl kube-proxy kubelet runc /usr/local/bin/
mv containerd containerd-shim-runc-v2 containerd-stress /bin/
mv cni-plugins/* /opt/cni/bin/


# Configure CNI Networking

## Create the bridge network configuration file:
mv 10-bridge.conf 99-loopback.conf /etc/cni/net.d/
cat /etc/cni/net.d/10-bridge.conf 

## To ensure network traffic crossing the CNI bridge network is processed by iptables, load and configure the br-netfilter kernel module:
lsmod | grep netfilter
modprobe br-netfilter
echo "br-netfilter" >> /etc/modules-load.d/modules.conf
lsmod | grep netfilter

echo "net.bridge.bridge-nf-call-iptables = 1"  >> /etc/sysctl.d/kubernetes.conf
echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.d/kubernetes.conf
sysctl -p /etc/sysctl.d/kubernetes.conf


# Configure containerd : Install the containerd configuration files:
mkdir -p /etc/containerd/
mv containerd-config.toml /etc/containerd/config.toml
mv containerd.service /etc/systemd/system/
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


# Configure the Kubelet : Create the kubelet-config.yaml configuration file:
mv kubelet-config.yaml /var/lib/kubelet/
mv kubelet.service /etc/systemd/system/


# Configure the Kubernetes Proxy
mv kube-proxy-config.yaml /var/lib/kube-proxy/
mv kube-proxy.service /etc/systemd/system/


# Start the Worker Services
systemctl daemon-reload
systemctl enable containerd kubelet kube-proxy
systemctl start containerd kubelet kube-proxy


# 확인
systemctl status kubelet --no-pager
systemctl status containerd --no-pager
systemctl status kube-proxy --no-pager

exit
-----------------------------------------------------------

# jumpbox 에서 server 접속하여 kubectl node 정보 확인
ssh server "kubectl get nodes node-0 -o yaml --kubeconfig admin.kubeconfig" | yq
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>   2m48s   v1.32.3   192.168.10.101   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0

ssh server "kubectl get pod -A --kubeconfig admin.kubeconfig"

node-1 접속 후 실행

#
ssh root@node-1
-----------------------------------------------------------
# Install the OS dependencies : The socat binary enables support for the kubectl port-forward command.
apt-get -y install socat conntrack ipset kmod psmisc bridge-utils

# Create the installation directories
mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

# Install the worker binaries:
mv crictl kube-proxy kubelet runc /usr/local/bin/
mv containerd containerd-shim-runc-v2 containerd-stress /bin/
mv cni-plugins/* /opt/cni/bin/


# Configure CNI Networking

## Create the bridge network configuration file:
mv 10-bridge.conf 99-loopback.conf /etc/cni/net.d/
cat /etc/cni/net.d/10-bridge.conf 

## To ensure network traffic crossing the CNI bridge network is processed by iptables, load and configure the br-netfilter kernel module:
modprobe br-netfilter
echo "br-netfilter" >> /etc/modules-load.d/modules.conf
lsmod | grep netfilter

echo "net.bridge.bridge-nf-call-iptables = 1"  >> /etc/sysctl.d/kubernetes.conf
echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.d/kubernetes.conf
sysctl -p /etc/sysctl.d/kubernetes.conf


# Configure containerd : Install the containerd configuration files:
mkdir -p /etc/containerd/
mv containerd-config.toml /etc/containerd/config.toml
mv containerd.service /etc/systemd/system/


# Configure the Kubelet : Create the kubelet-config.yaml configuration file:
mv kubelet-config.yaml /var/lib/kubelet/
mv kubelet.service /etc/systemd/system/


# Configure the Kubernetes Proxy
mv kube-proxy-config.yaml /var/lib/kube-proxy/
mv kube-proxy.service /etc/systemd/system/


# Start the Worker Services
systemctl daemon-reload
systemctl enable containerd kubelet kube-proxy
systemctl start containerd kubelet kube-proxy


# 확인
systemctl status kubelet --no-pager
systemctl status containerd --no-pager
systemctl status kube-proxy --no-pager

exit
-----------------------------------------------------------

# jumpbox 에서 server 접속하여 kubectl node 정보 확인
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>   93s   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>   15s   v1.32.3   192.168.10.102   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0

ssh server "kubectl get pod -A --kubeconfig admin.kubeconfig"

10 - Configuring kubectl for Remote Access

jumpbox 노드에서 kubectl 을 ‘admin 자격증명’으로 사용을 위한 설정

실습을 진행 중인 작업 머신(Jumpbox)에서 kubectl 명령어를 편하게 쓰기 위해 로컬 kubeconfig를 설정하는 과정.
지금까지는 admin.kubeconfig 파일을 따로 만들어서 마스터 노드(server)로 보냈지만, 이번에는 Jumpbox 자체를 관리자 터미널로 만드는 작업, 클러스터의 모든 상태를 조회하고 명령을 내릴 수 있음

# The Admin Kubernetes Configuration File

# You should be able to ping server.kubernetes.local based on the /etc/hosts DNS entry from a previous lab.
curl -s --cacert ca.crt https://server.kubernetes.local:6443/version | jq

# Generate a kubeconfig file suitable for authenticating as the admin user:
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443

kubectl config set-credentials admin \
  --client-certificate=admin.crt \
  --client-key=admin.key

kubectl config set-context kubernetes-the-hard-way \
  --cluster=kubernetes-the-hard-way \
  --user=admin

kubectl config use-context kubernetes-the-hard-way

# 위 명령어를 실행한 결과 kubectl 명령줄 도구에서 사용하는 기본 위치 ~/.kube/config에 kubectl 파일이 생성됩니다. 
# 이는 또한 구성을 지정하지 않고도 kubectl 명령어를 실행할 수 있음을 의미합니다.

# Check the version of the remote Kubernetes cluster:
kubectl version

# List the nodes in the remote Kubernetes cluster
kubectl get nodes -v=6
I0104 16:27:17.687800    2735 loader.go:402] Config loaded from file:  /root/.kube/config

cat /root/.kube/config
kubectl get nodes -owide
kubectl get pod -A

11 - Provisioning Pod Network Routes

node-0/1에 PodCIDR과 통신을 위한 OS 커널에 (수동) 라우팅 설정

항목네트워크  네트워크 대역 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

 

vagrant ssh jumpbox

 각 노드(Server, Node-0, Node-1)에 정적 라우팅(Static Route) 규칙을 수동으로 추가하여 파드 간 통신을 가능하게 만드는 과정

쿠버네티스 네트워킹의 핵심 원칙인 **"모든 파드는 NAT 없이 서로 통신할 수 있어야 한다"**를 구현하는 가장 원시적인(Hard Way) 방법

1. 변수 준비

  • machines.txt 파일에서 각 노드의 물리 IP(192.168.10.x)와 파드 네트워크 대역(Subnet)(10.200.x.0/24) 정보를 읽어와 변수에 저장
    • node-0: 10.200.0.0/24를 책임짐.
    • node-1: 10.200.1.0/24를 책임짐.

2. Server(Control Plane) 라우팅 설정

ip route add 10.200.0.0/24 via 192.168.10.101 ip route add 10.200.1.0/24 via 192.168.10.102
  • 의미: API 서버에게 "혹시 10.200.0.x (Node-0의 파드들)로 갈 패킷이 있으면, 192.168.10.101 (Node-0)로 던져라"라고 알려주는 것입니다. Node-1 대역도 마찬가지
  • 이게 없으면 API 서버(kubectl exec/logs 등)가 파드 네트워크를 찾지 못해 접속할 수 없음

3. Node-0 라우팅 설정

ip route add 10.200.1.0/24 via 192.168.10.102
  • 의미: Node-0에 있는 파드가 Node-1에 있는 파드(10.200.1.x)와 통신하려고 할 때, "그 패킷은 192.168.10.102 (Node-1)로 보내라"고 길을 알려줌

4. Node-1 라우팅 설정

ip route add 10.200.0.0/24 via 192.168.10.101
  • 의미: 반대로 Node-1에서 Node-0 대역(10.200.0.x)으로 가는 길을 알려줌

보통 클라우드(AWS, GCP)나 온프레미스에서는 Calico, Flannel 같은 CNI 플러그인이 BGP나 VXLAN 등을 이용해 이 길 찾기(라우팅)를 자동으로 해줌
하지만 "Hard Way"에서는 CNI의 마법 없이 리눅스 기본 라우팅 테이블만으로 통신 구조를 이해하기 위해 이렇게 수동으로 한 땀 한 땀 길을 뚫어주는 것

결과:
이제 node-0의 파드가 node-1의 파드 IP로 핑을 날리면, 패킷이 이 라우팅 테이블을 타고 건너편 노드로 잘 전달되게 함

# The Routing Table
# In this section you will gather the information required to create routes in the kubernetes-the-hard-way VPC network.

# Print the internal IP address and Pod CIDR range for each worker instance:
SERVER_IP=$(grep server machines.txt | cut -d " " -f 1)
NODE_0_IP=$(grep node-0 machines.txt | cut -d " " -f 1)
NODE_0_SUBNET=$(grep node-0 machines.txt | cut -d " " -f 4)
NODE_1_IP=$(grep node-1 machines.txt | cut -d " " -f 1)
NODE_1_SUBNET=$(grep node-1 machines.txt | cut -d " " -f 4)
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

ssh server ip -c route
ssh root@server <<EOF
  ip route add ${NODE_0_SUBNET} via ${NODE_0_IP}
  ip route add ${NODE_1_SUBNET} via ${NODE_1_IP}
EOF
ssh server ip -c route

ssh node-0 ip -c route
ssh root@node-0 <<EOF
  ip route add ${NODE_1_SUBNET} via ${NODE_1_IP}
EOF
ssh node-0 ip -c route

ssh node-1 ip -c route
ssh root@node-1 <<EOF
  ip route add ${NODE_0_SUBNET} via ${NODE_0_IP}
EOF
ssh node-1 ip -c route

12 - Smoke Test

실습 최종 구성도

Data Encrytion

쿠버네티스 시크릿(Secret) 데이터가 etcd에 실제로 암호화되어 저장되는지(Encryption at Rest) 검증하는 과정
앞서 encryption-config.yaml을 적용했던 것이 제대로 동작하는지 눈으로 확인하는 단계

1. 시크릿 생성 및 조회 (kubectl)

  • 생성: mykey=mydata라는 간단한 내용을 담은 kubernetes-the-hard-way라는 이름의 시크릿 생성
  • 조회: kubectl get secret ... 명령어로 확인하면, mydata가 bXlkYXRh(base64) 형태로 인코딩되어 조회
  • 주의: 여기서 보이는 base64 인코딩은 암호화가 아니라 단순 변환입니다. kubectl은 권한이 있는 사용자에게는 항상 복호화된(평문) 상태를 보여줍니다. 즉, API 서버 레벨에서는 투명하게 보임

2. etcd 조회 (etcdctl)

진짜 암호화 여부를 확인하려면, API 서버를 거치지 않고 DB(etcd)를 직접 열어봐야 함

ssh root@server 'etcdctl get /registry/secrets/default/kubernetes-the-hard-way | hexdump -C'
  • 경로: 쿠버네티스는 시크릿을 /registry/secrets/<namespace>/<name> 경로에 저장
  • 결과 분석:
    • 00000030 ... k8s:enc:aescbc:v1:key1:...
    • 이 헤더가 보인다면 성공
    • k8s:enc:aescbc: "이 데이터는 쿠버네티스 암호화(enc)되었고, 알고리즘은 AES-CBC다."
    • key1: "암호화할 때 key1이라는 이름의 키(설정 파일에 있던 그 키)를 썼다."
    • 그 뒤에 이어지는 데이터(44 61 dc ...)는 사람이 읽을 수 없는 암호문(Ciphertext)

결론

만약 암호화 설정이 안 되어 있었다면, hexdump 결과에 mykey나 mydata 같은 글자가 그대로 보였을 것
하지만 지금은 etcd를 통째로 털려도, 암호화 키(key1)가 없으면 해커가 내용을 볼 수 없는 안전한 상태임이 증명됨

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

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


# Print a hexdump of the kubernetes-the-hard-way secret stored in etcd
## etcdctl get … : etcd 내부 key 직접 조회, kubernetes API 우회(매우 강력한 접근)
## Secret 리소스의 etcd 실제 저장 경로: /registry/<resource>/<namespace>/<name> -> /registry/secrets/default/kubernetes-the-hard-way
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 44 61 dc 08 37 97 eb  |:v1:key1:Da..7..|
00000050  d4 d0 5b 14 39 23 a5 74  1b 3c a4 56 e4 a1 d1 17  |..[.9#.t.<.V....|
... # Kubernetes Secret이 etcd에 AES-CBC 방식으로 정상 암호화되어 저장되고 있음을 증명하는 출력
# k8s:enc	: Kubernetes 암호화 포맷
# aescbc	: 암호화 알고리즘 (AES-CBC)
# v1	    : encryption provider 버전
# key1	  : 사용된 encryption key 이름
# 이후 데이터는 암호화된 데이터

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

클러스터가 실제로 잘 동작하는지 "종합 테스트"를 하는 과정
파드 생성, 스케일링, 로그 조회, 내부 접속, 외부 노출(Service) 등 쿠버네티스의 핵심 기능들이 정상 작동하는지 하나씩 검증

1. 파드 배포 및 확인 (Deployment)

  • kubectl create deployment nginx ...: Nginx 웹서버를 배포
  • scale ... --replicas=2: 파드를 2개로 늘림
  • 결과 확인: node-0과 node-1에 각각 하나씩(10.200.0.2, 10.200.1.2) 예쁘게 분산되어 Running 상태가 되면 성공
  • 노드 내부 확인: crictl ps, pstree, brctl, ip addr 등을 통해, 실제로 노드 안에서 컨테이너 프로세스가 떴고, 가상 네트워크 인터페이스(veth)가 브릿지에 연결되었는지 "시스템 레벨"에서 교차 검증

2. 네트워크 연결성 확인 (curl from server)

ssh server curl -s 10.200.1.2 ...
  • 의미: Control Plane(Server)에서 워커 노드의 파드 IP로 직접 통신을 시도
  • 성공 조건: 앞서 수동으로 설정했던 **라우팅 테이블(ip route add ...)**이 제대로 동작해야만 이 curl이 성공

3. 포트 포워딩 (port-forward)

  • 기능: 내 PC(Jumpbox)의 8080 포트를 원격지 파드의 80 포트로 터널링
  • 의미: 외부에서 접근 불가능한 파드에 개발자가 디버깅 목적으로 접속할 수 있는지 확인. socat이 노드에 잘 설치되어 있어야 작동함

4. 로그 및 실행 (logs, exec)

  • kubectl logs: 애플리케이션 로그(Access Log 등)를 긁어옵니다. Kubelet이 로그 파일 위치를 잘 관리하고 있다는 뜻
  • kubectl exec: 실행 중인 컨테이너 안에 들어가 명령(nginx -v)을 실행합니다. API 서버와 Kubelet 간의 양방향 통신(RBAC)이 완벽해야 가능

5. 서비스 노출 (NodePort)

  • expose ... NodePort: 서비스를 만들어 클러스터 외부로 포트를 염
  • 결과: 31410 같은 고포트가 할당됨
  • 검증: http://node-0:31410으로 요청을 보내면, **kube-proxy(iptables)**가 이를 받아서 뒤단에 있는 파드(10.200.x.x)로 트래픽을 토스해주는지 확인. 이것이 성공하면 쿠버네티스 네트워킹 구축 완료
# Deployments

# Create a deployment for the nginx web server:
kubectl get pod
kubectl create deployment nginx --image=nginx:latest
kubectl scale deployment nginx --replicas=2
kubectl get pod -owide
NAME                     READY   STATUS    RESTARTS   AGE    IP           NODE     NOMINATED NODE   READINESS GATES
nginx-54c98b4f84-pxp6c   1/1     Running   0          108s   10.200.1.2   node-1   <none>           <none>
nginx-54c98b4f84-qxpbn   1/1     Running   0          12s    10.200.0.2   node-0   <none>           <none>

ssh node-0 crictl ps
ssh node-1 crictl ps
ssh node-0 pstree -ap
ssh node-1 pstree -ap
ssh node-0 brctl show
ssh node-1 brctl show
ssh node-0 ip addr # 파드 별 veth 인터페이스 생성 확인
ssh node-1 ip addr # 파드 별 veth 인터페이스 생성 확인


# server 노드에서 파드 IP로 호출 확인
ssh server curl -s 10.200.1.2 | grep title
ssh server curl -s 10.200.0.2 | grep title


# Port Forwarding
# Retrieve the full name of the nginx pod:
POD_NAME=$(kubectl get pods -l app=nginx -o jsonpath="{.items[0].metadata.name}")
echo $POD_NAME

# Forward port 8080 on your local machine to port 80 of the nginx pod:
kubectl port-forward $POD_NAME 8080:80 &
ps -ef | grep kubectl

# In a new terminal make an HTTP request using the forwarding address:
curl --head http://127.0.0.1:8080


# Log
# Print the nginx pod logs
kubectl logs $POD_NAME
curl --head http://127.0.0.1:8080
kubectl logs $POD_NAME

# 확인 후 port-forward Killed
kill -9 $(pgrep kubectl)


# Exec
# Print the nginx version by executing the nginx -v command in the nginx container:
kubectl exec -ti $POD_NAME -- nginx -v


# Service
# Expose the nginx deployment using a NodePort service:
cur

# 확인
kubectl get service,ep nginx
NAME            TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
service/nginx   NodePort   10.32.0.149   <none>        80:31410/TCP   10s

NAME              ENDPOINTS                     AGE
endpoints/nginx   10.200.0.2:80,10.200.1.2:80   10s

# Retrieve the node port assigned to the nginx service:
NODE_PORT=$(kubectl get svc nginx --output=jsonpath='{range .spec.ports[0]}{.nodePort}')
echo $NODE_PORT

# Make an HTTP request using the IP address and the nginx node port:
curl -s -I http://node-0:${NODE_PORT}
curl -s -I http://node-1:${NODE_PORT}

도전과제 (To_be_next_challange)

  • 도전과제1 가상머신 Image 를 Rocky LinuxUbuntu 처럼 다른 Linux OS 환경에서 실습 진행해보기
  • 도전과제2 실습 가이드의 K8S 버전 1.32 대신 1.33 ~ 1.35 등 상위 K8S 버전 환경으로 실습 진행해보기
  • 도전과제3 [netpple]님의 K8s hard?way 가이드를 직접 실습 환경을 구성하여 따라해보기 - Github
  • 도전과제4 ‘zerotay 추천글 - Link’ 을 1주차 스터티에서 다룬 k8s 관련 기본 이론과 함께 정리해보기
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함