끄적끄적 코딩
728x90

들어가며

안녕하세요.
On-Premise K8S 배포 Hands-on Study 1기에 참여하며 진행한 내용을 정리한 글입니다.

이번 스터디에서는 Kubernetes를 더 깊이 이해하기 위해 kubeadm, kubespray 같은 자동화 도구를 사용하지 않고, 클러스터를 수동으로 설치하는 과정(Kubernetes The Hard Way)을 직접 실습했습니다.

또한 동일한 클러스터를 kind(Kubernetes in Docker)로도 구성해 비교하면서, 쿠버네티스 설치가 어떤 순서로 이루어지는지, 내부 구성 요소들이 어떤 역할을 하는지 직접 확인했습니다. 이를 통해 자동화 도구가 내부적으로 어떤 작업을 대신 수행하는지도 이해하는 것을 목표로 했습니다.

실습 환경은 아래 두 가지로 구성했습니다.

  • kind 설치: Windows 10 기반 WSL2 환경의 Ubuntu 24.04
  • 수동 설치(The Hard Way): Windows 환경에서 VirtualBox + Vagrant로 구성한 Debian 12 VM

Kubernetes The Hard Way 전체 흐름 살펴보기

이제 Kubernetes The Hard Way가 어떤 단계로 구성되어 있는지 전체 흐름을 먼저 정리하고 넘어가겠습니다.

Kubernetes The Hard Way는 kubeadm이나 kubespray 같은 자동화 도구를 사용하지 않고, Kubernetes 클러스터를 구성하는 모든 구성 요소를 하나씩 직접 설치하고 연결해보는 방식입니다.
이 과정의 목적은 “빠른 설치”가 아니라, 인증서, 네트워크, Control Plane, Worker Node가 어떤 순서와 관계로 구성되는지를 명확히 이해하는 데 있습니다.

실습 환경 준비

가장 먼저 Kubernetes를 구성하기 위한 기본 환경을 준비합니다.

  • 실습에 필요한 CLI 도구 설치
  • 모든 작업을 수행할 Jumpbox 구성
  • Control Plane과 Worker Node로 사용할 가상 머신 준비

이 단계에서는 아직 Kubernetes 자체를 다루지는 않지만, 이후 모든 작업의 기반이 되는 환경을 만드는 과정이기 때문에 IP 구성, 호스트 이름, 네트워크 설정이 중요합니다.

인증과 보안: Kubernetes의 시작점

Kubernetes는 처음부터 끝까지 인증서 기반 보안 구조를 사용합니다.
따라서 클러스터를 구성하기 전에 가장 먼저 해야 할 일은 CA(Certificate Authority)를 구성하고 각 컴포넌트에 필요한 인증서를 발급하는 것입니다.

이 과정에서 다음과 같은 것들을 직접 생성하게 됩니다.

  • Kubernetes CA
  • API Server, kubelet, controller-manager, scheduler 인증서
  • etcd 통신을 위한 인증서

이 단계를 통해 Kubernetes가 왜 “보안 중심적으로 설계된 시스템”인지 자연스럽게 이해할 수 있습니다.

인증 정보와 암호화 설정

인증서 생성 이후에는, 각 컴포넌트가 어떤 자격으로 API Server에 접근할지 정의하는 kubeconfig 파일들을 직접 생성합니다.

또한 Kubernetes가 etcd에 저장하는 데이터 중 Secret과 같은 민감 정보를 보호하기 위해 데이터 암호화 설정(Encryption Config) 도 함께 구성합니다.

이 과정은 평소 자동화 도구를 사용할 때는 거의 의식하지 않지만, 실제로는 Kubernetes 보안에서 매우 중요한 부분입니다.

etcd 클러스터 구성

다음 단계에서는 Kubernetes의 핵심 저장소인 etcd 클러스터를 구성합니다.

  • 다중 노드 etcd 클러스터
  • TLS 기반 통신
  • systemd를 통한 서비스 등록

Kubernetes의 모든 상태 정보는 etcd에 저장되기 때문에, 이 단계가 정상적으로 구성되지 않으면 이후 Kubernetes Control Plane은 정상적으로 동작할 수 없습니다.

Control Plane 부트스트랩

etcd가 준비되면, 드디어 Kubernetes Control Plane 컴포넌트들을 하나씩 실행합니다.

  • kube-apiserver
  • kube-controller-manager
  • kube-scheduler

각 컴포넌트는 모두 독립적인 바이너리로 실행되며, 인증서와 kubeconfig를 기반으로 서로 통신합니다.

이 단계를 완료하면 비로소 “Kubernetes 클러스터가 살아 있다”라고 말할 수 있는 상태가 됩니다.

Worker Node 구성

Control Plane이 준비된 이후에는 실제 파드가 실행될 Worker Node를 구성합니다.

  • container runtime 설치 (containerd)
  • kubelet 설정 및 실행
  • kube-proxy 구성
  • CNI 플러그인 설치

이 과정을 통해 Worker Node가 Control Plane에 등록되고, 실제 워크로드를 실행할 수 있는 상태가 됩니다.

네트워크 구성과 검증

마지막으로, 파드 간 통신을 위한 네트워크 라우팅을 구성하고 간단한 애플리케이션을 배포하여 클러스터가 정상적으로 동작하는지 확인합니다.

이 단계에서는 다음과 같은 것들을 검증합니다.

  • Node 간 Pod 통신
  • Service 동작 여부
  • 기본적인 Kubernetes API 동작 확인

kind 기반 Kubernetes 구성

Kubernetes The Hard Way를 진행하기 전에, 동일한 쿠버네티스 구성 요소들이 kind 환경에서는 어떻게 자동으로 준비되는지를 먼저 확인했습니다.

kind는 Kubernetes 노드를 Docker 컨테이너로 생성해 클러스터를 구성하는 도구입니다.
Control Plane과 Worker Node가 모두 컨테이너 형태로 실행되며, kubeadm을 기반으로 인증서, kubeconfig, 네트워크 설정까지 자동으로 구성됩니다.
빠르게 클러스터를 띄워 내부 구조를 관찰하거나, 실습용 비교 환경으로 사용하기에 적합합니다.

이번 kind 실습은 Windows 10 기반 WSL2 Ubuntu 24.04 환경에서 진행했으며, 아래 흐름으로 구성했습니다.

  • WSL2 설치 및 Ubuntu 24.04 준비
  • WSL2 Ubuntu에 Docker 설치 및 동작 확인
  • kind/kubectl/helm 등 관리 도구 설치
  • kind 기본 클러스터 생성/검증

멀티노드 + 버전 고정 + 포트 매핑 구성 및 관찰

kind 설치 및 기본 환경 준비

WSL2 설치

WSL2(Windows Subsystem for Linux 2)는 Windows에서 Linux 커널 기반으로 리눅스를 실행할 수 있도록 지원하는 기능입니다. WSL1 대비 커널 호환성과 파일/네트워크 성능이 개선되어 Docker 같은 리눅스 의존 도구를 별도 VM 없이 안정적으로 구동할 수 있습니다.

아래는 WSL2 환경을 준비하기 위해 실행한 명령입니다.

# WSL 기능 활성화
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

# WSL2를 위한 VirtualMachinePlatform 활성화
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

# WSL 설치 (기본 Ubuntu 포함)
wsl --install

# 기본 WSL 버전: 2
wsl --set-default-version 2

# WSL 업데이트
wsl --update

Ubuntu 24.04 설치

Ubuntu는 WSL2에서 가장 널리 사용되는 배포판이고, apt 기반으로 필요한 도구를 빠르게 준비할 수 있어 실습 환경으로 적합합니다.

# 설치 가능한 배포판 확인
wsl --list --online

# Ubuntu 24.04 설치
wsl --install Ubuntu-24.04

설치 후 Ubuntu에 진입해 기본 상태를 확인하고, 실습에 필요한 패키지를 설치했습니다.

hostnamectl
whoami
id
pwd

sudo apt update
sudo apt install -y jq htop curl wget ca-certificates net-tools

ifconfig eth0
ping -c 1 8.8.8.8

exit

배포판 실행 상태 및 WSL 버전 확인 후 재진입합니다.

wsl -l -v
wsl

Docker 설치 및 확인

kind는 Kubernetes “노드”를 Docker 컨테이너로 구성하기 때문에, Docker가 먼저 준비되어야 합니다.
실습에서는 Docker 공식 자동 설치 스크립트를 사용했습니다.

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh

설치 로그에서 아래 메시지가 보이면 Docker 데몬이 정상 활성화된 상태입니다.

INFO: Docker daemon enabled and started

Docker 설치 확인

docker info
docker ps
sudo systemctl status docker
cat /etc/group | grep docker
  • 출력에서 확인한 포인트는 다음과 같습니다.
    • Docker 데몬이 active (running) 상태로 동작
    • Cgroup Driver: systemd, Cgroup Version: 2 적용
    • docker 그룹에 사용자(jetty)가 포함된 상태

kind 및 관리 도구 설치

kind 실습을 위해 kind/kubectl/helm과 편의 도구들을 설치했습니다.

# 기본 사용자 디렉터리 이동
cd $PWD
pwd

# AppArmor 비활성화
sudo systemctl stop apparmor && sudo systemctl disable apparmor

# 필수 패키지 설치
sudo apt update && sudo apt-get install bridge-utils net-tools jq tree unzip kubectx kubecolor -y

# Install Kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
kind --version

# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv ./kubectl /usr/bin
sudo kubectl version --client=true

# Install Helm
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
helm version

# Source the completion
source <(kubectl completion bash)
echo 'source <(kubectl completion bash)' >> ~/.bashrc

# Alias kubectl to k
echo 'alias k=kubectl' >> ~/.bashrc
echo 'complete -o default -F __start_kubectl k' >> ~/.bashrc

# Install Kubeps & Setting PS1
git clone https://github.com/jonmosco/kube-ps1.git
echo -e "source $PWD/kube-ps1/kube-ps1.sh" >> ~/.bashrc
cat <<"EOT" >> ~/.bashrc
KUBE_PS1_SYMBOL_ENABLE=true
function get_cluster_short() {
  echo "$1" | cut -d . -f1
}
KUBE_PS1_CLUSTER_FUNCTION=get_cluster_short
KUBE_PS1_SUFFIX=') '
PS1='$(kube_ps1)'$PS1
EOT

# .bashrc 적용을 위해서 logout 후 터미널 다시 접속
exit

kind 클러스터 생성 및 기본 확인

기본 클러스터 생성 및 상태 확인

기본 kind 클러스터를 생성하고, Kubernetes 구성요소가 어떻게 올라오는지 확인했습니다.

# 클러스터 배포 전 확인
docker ps

# Create a cluster with kind
kind create cluster

# 클러스터 배포 확인
kind get clusters
kind get nodes
kubectl cluster-info

# 노드 정보 확인
kubectl get node -o wide

# 파드 정보 확인
kubectl get pod -A
kubectl get componentstatuses

# 컨트롤플레인 (컨테이너) 확인
docker ps
docker images

# kube config 파일 확인
cat ~/.kube/config

워크로드 스케줄링 확인(nginx)

sudo apt update
sudo apt install bridge-utils net-tools jq tree unzip kubectx kubecolor -y

그리고 노드의 Taint를 확인했습니다.

kubectl describe node | grep Taints

아래처럼 Taint가 없어서 control-plane 노드에도 일반 파드가 스케줄링되는 것을 확인했습니다.

Taints: <none>

클러스터 삭제

kind delete cluster

멀티 노드 kind 클러스터 구성

이번에는 클러스터 이름/버전을 고정하고, worker 노드를 포함한 멀티노드 클러스터를 구성했습니다. 또한 포트 매핑 설정을 포함해 kind가 Docker 포트를 어떻게 연결하는지도 관찰했습니다.

kind create cluster --name myk8s --image kindest/node:v1.32.8 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
- role: worker
EOF

생성 후 확인

kind get nodes --name myk8s
kubens default

docker network ls
docker inspect kind | jq

kubectl cluster-info
kubectl get node -o wide
kubectl get pod -A -o wide
kubectl get namespaces

docker ps
docker images
docker exec -it myk8s-control-plane ss -tnlp

kubectl get pod -v6
cat ~/.kube/config
ls -l ~/.kube/config

Docker 네트워크 구조 확인

kind는 클러스터 생성 시 kind라는 별도 Docker 브리지 네트워크를 만들었고, IPAM 설정에서 172.18.0.0/16 대역을 사용하는 것을 확인했습니다.

"Name": "kind"
...
"Subnet": "172.18.0.0/16"
...
"Containers": {
  "myk8s-control-plane": { "IPv4Address": "172.18.0.2/16" },
  "myk8s-worker": { "IPv4Address": "172.18.0.3/16" }
}

API Server 접근 방식 확인

kubectl cluster-info 및 kubeconfig에서 API 서버 주소가 127.0.0.1:PORT로 잡히는 것을 확인했습니다.

Kubernetes control plane is running at https://127.0.0.1:39505
  • Kubernetes API 서버는 컨테이너 내부에 존재하지만
  • Docker 포트 포워딩을 통해 로컬 127.0.0.1:PORT로 접근 가능하며
  • kind가 kubeconfig 구성까지 자동으로 처리합니다.

컨트롤플레인 컨테이너 내부 프로세스 확인

컨트롤플레인 컨테이너 내부에서 리스닝 포트를 확인해, etcd/kube-apiserver/kubelet 등이 컨테이너 내부 프로세스로 실행됨을 관찰했습니다.

docker exec -it myk8s-control-plane ss -tnlp

출력 예시

LISTEN ... 172.18.0.2:2379  users:(("etcd",...))
LISTEN ... *:6443          users:(("kube-apiserver",...))
LISTEN ... *:10250         users:(("kubelet",...))

클러스터 삭제

kind delete cluster --name myk8s
docker ps
cat ~/.kube/config

사전 준비

On-Premise Kubernetes 실습을 위한 가상화 환경 준비

kind 기반 Kubernetes는 로컬에서 빠르게 클러스터를 구성하고 구조를 이해하는 데 유용하지만, 실제 On-Premise 환경에서는 가상 머신 기반으로 여러 노드를 구성하고 네트워크/컴포넌트를 직접 제어하는 방식이 일반적입니다.

이번 실습에서는 VirtualBox + Vagrant 조합으로, Kubernetes The Hard Way를 위한 VM 환경을 구성했습니다.

  • VirtualBox: 로컬에서 여러 대의 VM을 실행할 수 있는 하이퍼바이저
  • Vagrant: VM 생성/설정/네트워크 구성을 코드로 재현 가능하게 해주는 도구

VirtualBox 설치

VirtualBox는 Oracle에서 제공하는 가상화 소프트웨어로, 로컬 PC에서 여러 대의 VM을 실행할 수 있습니다.

주요 특징은 다음과 같습니다.

  • 여러 대의 Linux 서버를 동시에 실행 가능
  • NAT / Bridged / Host-Only 네트워크 구성 지원
  • Kubernetes Control Plane과 Worker Node 분리 구성 가능

설치는 공식 사이트에서 진행합니다.

https://www.virtualbox.org/wiki/Downloads

Vagrant 설치

Vagrant는 VirtualBox 위에서 동작하는 가상 머신 환경 자동화 도구입니다.
VM 생성부터 네트워크 설정, 초기 구성까지를 코드(Vagrantfile)로 관리할 수 있습니다.

Vagrant를 사용하는 이유는 다음과 같습니다.

  • Vagrantfile 하나로 동일한 VM 환경 재현 가능
  • 여러 노드를 동일한 설정으로 빠르게 생성
  • OS 설치 및 기본 설정 자동화
  • 환경을 팀 단위로 동일하게 유지 가능

설치는 아래 링크를 참고합니다.

https://developer.hashicorp.com/vagrant/downloads

실습용 가상 머신 구성

이번 실습에서는 Jumpbox(관리용) 1대, Control Plane 1대, Worker 2대로 구성했습니다.

NAME Description CPU RAM NIC1 NIC2 HOSTNAME
jumpbox Administration host 2 1536MB 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
node-0 Kubernetes worker 2 2GB 10.0.2.15 192.168.10.101 node-0.kubernetes.local
node-1 Kubernetes worker 2 2GB 10.0.2.15 192.168.10.102 node-1.kubernetes.local
  • NIC1: NAT (외부 인터넷 접근)
  • NIC2: Host-Only (노드 간 통신)

사용한 OS 이미지는 아래와 같습니다.

# OS: Debian 12
# 제공자: bento
# 이미지 버전: 202510.26.0
# 아래 Vagrantfile에 포함되어 있음
Vagrant.configure("2") do |config|
  config.vm.box = "bento/debian-12"
  config.vm.box_version = "202510.26.0"
end

Kubernetes 가상머신 환경 배포 및 검증

VM 배포

# 작업용 디렉터리 생성
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 running (virtualbox)
# server  running (virtualbox)
# node-0  running (virtualbox)
# node-1  running (virtualbox)

jumpbox 접속 및 기본 상태 점검

관리용 노드(jumpbox)에 접속해 사용자/경로/OS 정보를 확인했습니다.

# vagrant jumpbox 가상머신 접속
vagrant ssh jumpbox

whoami
pwd

cat /etc/os-release
AppArmor 상태와 서비스 활성 여부도 점검했습니다.
 
aa-status
systemctl is-active apparmor

마지막으로 노드 간 이름 해석이 가능한지 /etc/hosts 구성을 확인했습니다.

cat /etc/hosts

/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

정리: Vagrant로 배포된 jumpbox에 접속해 OS/보안(AppArmor)/호스트 이름 해석(/etc/hosts)을 점검했고, 이후 단계에서 모든 작업의 기준점이 되는 “관리 노드 준비”를 완료했습니다.


관리 노드(jumpbox) 준비

Kubernetes The Hard Way 실습을 위한 바이너리 준비

이후 작업의 기준점이 되는 관리 노드(jumpbox)를 먼저 준비합니다.
jumpbox는 클러스터 관리 작업을 수행하기 위한 중앙 관리 노드입니다.
Kubernetes The Hard Way에서는 인증서 생성, kubeconfig 생성, 각 노드로의 파일 배포와 원격 실행이 모두 jumpbox를 기준으로 이루어집니다.

root 계정 확인 및 저장소 클론

# root 계정 확인
whoami
root

# kubernetes-the-hard-way 저장소 클론
git clone --depth 1 https://github.com/kelseyhightower/kubernetes-the-hard-way.git
cd kubernetes-the-hard-way

디렉터리 구조/아키텍처 확인

# 디렉터리 구조 확인
tree
---
# (configs, docs, downloads-*.txt 등 확인)
---
# CPU 아키텍처 확인
dpkg --print-architecture
amd64

# 아키텍처에 맞는 다운로드 목록 확인
cat downloads-$(dpkg --print-architecture).txt
---
# (kubectl, kube-apiserver, kubelet, etcd, containerd 등 URL 목록)
---

Kubernetes 구성 요소 다운로드 및 정리

# Kubernetes 구성 요소 다운로드
wget -q --show-progress \
  --https-only \
  --timestamping \
  -P downloads \
  -i downloads-$(dpkg --print-architecture).txt

# 다운로드 파일 확인
ls -oh downloads
---
# (약 500MB 규모 바이너리 파일들)
---
# 디렉터리 구조 생성
mkdir -p downloads/{client,cni-plugins,controller,worker}

# 바이너리 압축 해제
ARCH=$(dpkg --print-architecture)

tar -xvf downloads/crictl-v1.32.0-linux-${ARCH}.tar.gz -C downloads/worker/
tar -xvf downloads/containerd-2.1.0-beta.0-linux-${ARCH}.tar.gz \
  --strip-components 1 -C downloads/worker/
tar -xvf downloads/cni-plugins-linux-${ARCH}-v1.6.2.tgz -C downloads/cni-plugins/
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}/etcd \
  etcd-v3.6.0-rc.3-linux-${ARCH}/etcdctl

---
# (중간 tree / ls 출력 생략)
---
# 역할별 바이너리 정리
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
# 압축 파일 제거
rm -rf downloads/*gz

# 실행 권한 부여
chmod +x downloads/{client,cni-plugins,controller,worker}/*

# 소유자 정리
chown root:root downloads/client/etcdctl
chown root:root downloads/controller/etcd
chown root:root downloads/worker/crictl

 

kubectl 설치 및 확인

# kubectl 설치
cp downloads/client/kubectl /usr/local/bin/

# kubectl 확인
kubectl version --client
Client Version: v1.32.3
Kustomize Version: v5.5.0

노드/SSH 접속 준비

SSH 접속 환경 설정

이 단계에서는 클러스터 노드 정보를 machines.txt로 정리하고, jumpbox에서 각 노드로 root 무비밀번호(SSH 키 기반) 접속이 가능하도록 설정합니다. 또한 호스트명/FQDN과 /etc/hosts 구성을 점검해 노드 간 통신 준비를 완료합니다.

노드 정보 정의 및 machines.txt 파싱 확인

먼저 클러스터 노드 정보를 machines.txt로 정의했습니다. (IP / FQDN / Hostname / Pod CIDR)

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

파일이 의도대로 파싱되는지 간단히 확인했습니다.

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

SSH 서버 설정 확인

SSH 접속 정책(특히 root 로그인/비밀번호 인증)이 어떤 상태인지 확인했습니다

grep "^[^#]" /etc/ssh/sshd_config
---
# PermitRootLogin yes
# PasswordAuthentication yes
---

. SSH 키 생성 (jumpbox)

jumpbox에서 사용할 SSH 키를 생성했습니다.

ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa

ls -l /root/.ssh
---
# id_rsa / id_rsa.pub 생성 확인
---

각 노드에 SSH 공개키 배포

초기에는 비밀번호 기반 접속이 필요하므로 sshpass + ssh-copy-id로 각 노드에 공개키를 배포했습니다.

while read IP FQDN HOST SUBNET; do
  sshpass -p 'qwe123' ssh-copy-id -o StrictHostKeyChecking=no root@${IP}
done < machines.txt

SSH 키 배포 확인

각 노드에 authorized_keys가 정상 등록되었는지 확인했습니다.

while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} cat /root/.ssh/authorized_keys
done < machines.txt
---
# 동일한 공개키가 등록되어 있는지 확인
---

노드 기본 정보 검증 (Hostname / FQDN / hosts)

각 노드의 hostname, FQDN, /etc/hosts 상태를 점검했습니다.

# 각 노드 hostname 확인
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} hostname
done < machines.txt
# FQDN 설정 확인
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} hostname --fqdn
done < machines.txt
# /etc/hosts 동기화 상태 확인
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} cat /etc/hosts
done < machines.txt
---
# 모든 노드에 동일한 hosts 엔트리 존재
---

호스트명 기반 SSH 접속 확인

IP 대신 hostname으로 SSH 접속이 가능한지 확인했습니다.

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

마지막으로 각 노드의 OS/아키텍처/노드명을 확인했습니다.

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

CA 구성 및 TLS 인증서 생성

Kubernetes The Hard Way에서는 kubeadm 같은 자동화 도구가 대신 생성해주던 CA(신뢰 루트)와 각 컴포넌트용 TLS 인증서를 직접 구성합니다. 이 단계에서 생성한 인증서는 이후 etcd, API Server, kubelet, controller-manager, scheduler 등 모든 통신의 기반이 됩니다.

 

  • CN(Common Name): 인증서의 주체(사용자 또는 컴포넌트 정체성)
  • O(Organization): Kubernetes RBAC에서 그룹으로 해석되는 값
  • SAN(Subject Alternative Name): 인증서가 유효한 IP/DNS 목록
    Kubernetes에서는 CN과 O 조합이 실제 권한 결정에 중요한 역할을 합니다.

 

인증서 설계 및 기준값

생성 대상 및 식별 정보(CN/O) 정의

대상 개인키 CSR 인증서 참고
Root CA ca.key X ca.crt 클러스터 신뢰 루트
admin admin.key admin.csr admin.crt CN=admin, O=system:masters
node-0 node-0.key node-0.csr node-0.crt CN=system:node:node-0, O=system:nodes
node-1 node-1.key node-1.csr node-1.crt CN=system:node:node-1, O=system:nodes
kube-proxy kube-proxy.key kube-proxy.csr kube-proxy.crt CN=system:kube-proxy, O=system:node-proxier
kube-scheduler kube-scheduler.key kube-scheduler.csr kube-scheduler.crt CN=system:kube-scheduler, O=system:kube-scheduler
kube-controller-manager kube-controller-manager.key kube-controller-manager.csr kube-controller-manager.crt CN=system:kube-controller-manager, O=system:kube-controller-manager
kube-api-server kube-api-server.key kube-api-server.csr kube-api-server.crt CN=kubernetes, SAN 포함
service-accounts service-accounts.key service-accounts.csr service-accounts.crt CN=service-accounts

네트워크 기준값

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

kind 환경에서 인증서 관련 구조 확인

먼저 kind(kubeadm)가 자동 생성하는 인증서와 디렉터리 구조를 확인해, 이후 Hard Way에서 “직접 만들 것들”이 무엇인지 감을 잡았습니다.

# kind 컨트롤 플레인 컨테이너 접속 전, 필요한 도구 설치
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree yq jq -y'

# kubeadm이 자동 생성한 인증서 만료 정보 확인
docker exec -i myk8s-control-plane kubeadm certs check-expiration

# kubeadm이 생성한 Kubernetes 인증서 및 매니페스트 구조 확인
docker exec -it myk8s-control-plane tree /etc/kubernetes

이 결과로 /etc/kubernetes/pki 아래에 CA, apiserver, etcd, front-proxy, service-account 키 등이 자동 구성되는 것을 확인했습니다.

ca.conf 확인

Hard Way에서는 인증서의 CN(사용자/컴포넌트 정체성), O(그룹), EKU(clientAuth/serverAuth), SAN(IP/DNS) 같은 값을 ca.conf로 정의하고, 이를 기준으로 CSR/인증서를 생성했습니다.

  • [req]: OpenSSL 요청 기본 동작
  • [ca_*]: CA 인증서
  • [admin]: 관리자(kubectl)
  • [service-accounts]: ServiceAccount 토큰 서명
  • [node-*]: 워커 노드(kubelet)
  • [kube-proxy]: kube-proxy
  • [kube-controller-manager]: controller-manager
  • [kube-scheduler]: scheduler
  • [kube-api-server]: apiserver (SAN 포함)
  • [default_req_extensions]: 공통 CSR 옵션

Root CA 생성

클러스터 전체의 신뢰 루트가 되는 Root CA 개인키/인증서를 생성했습니다.

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

# Root CA 인증서 생성 (Self-Signed)
openssl req -x509 -new -sha512 -noenc \
  -key ca.key \
  -days 3653 \
  -config ca.conf \
  -out ca.crt

# CA 인증서 전체 정보 확인
openssl x509 -in ca.crt -text -noout
  • ca.key: CA 개인키(외부 유출 방지 중요)
  • ca.crt: Self-Signed Root CA(약 10년 유효)
  • 이후 생성되는 모든 컴포넌트 인증서는 이 CA로 서명됩니다.

kind 환경의 CA와 비교

kind(kubeadm)가 생성한 CA 인증서를 확인해 Hard Way와 차이를 비교했습니다.

docker exec -i myk8s-control-plane \
  cat /etc/kubernetes/pki/ca.crt | openssl x509 -text -noout
  • kubeadm은 기본적으로 2048bit RSA + SHA-256 기반 CA를 자동 생성
  • Hard Way에서는 4096bit RSA + SHA-512로 CA를 직접 생성

클라이언트/컴포넌트 인증서 생성

admin 인증서 생성

Hard Way에서 admin 클라이언트 인증서 생성

관리자(admin) 인증서를 생성해 kubectl/API 호출에 사용할 수 있도록 구성했습니다.

# 1) 개인키 생성
openssl genrsa -out admin.key 4096

# 2) CSR 생성
openssl req -new -key admin.key -sha256 \
  -config ca.conf -section admin \
  -out admin.csr

# 3) CA로 CSR 서명하여 인증서 발급
openssl x509 -req -days 3653 -in admin.csr \
  -copy_extensions copyall \
  -sha256 -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -out admin.crt

# 4) 인증서 확인
openssl x509 -in admin.crt -text -noout
  • CN=admin은 Kubernetes 사용자 정체성
  • O=system:masters는 관리자 그룹으로 매핑되는 핵심 값입니다.

kind 환경의 admin 관련 인증서 확인

kind에서는 super-admin.conf, admin.conf에 인증서가 포함되어 있고(base64), 이를 디코딩해 내용을 확인했습니다.

docker exec -i myk8s-control-plane \
  cat /etc/kubernetes/super-admin.conf

docker exec -i myk8s-control-plane \
  cat /etc/kubernetes/super-admin.conf \
  | grep client-certificate-data \
  | cut -d ':' -f2 \
  | tr -d ' ' \
  | base64 -d \
  | openssl x509 -text -noout

kubectl describe clusterrolebindings cluster-admin
kubectl describe clusterroles cluster-admin

docker exec -i myk8s-control-plane \
  cat /etc/kubernetes/admin.conf

docker exec -i myk8s-control-plane \
  cat /etc/kubernetes/admin.conf \
  | grep client-certificate-data \
  | cut -d ':' -f2 \
  | tr -d ' ' \
  | base64 -d \
  | openssl x509 -text -noout

kubectl describe clusterrolebindings kubeadm:cluster-admins
  • Kubernetes 인증서는 CN(사용자) + O(그룹)로 신원을 표현합니다.
  • 실제 권한은 CN이 아니라, O(Group)가 어떤 RBAC 바인딩에 연결되는지로 결정됩니다.
  • Hard Way에서 만든 admin.crt(CN=admin, O=system:masters)는 kind의 system:masters 계열(super-admin)과 같은 권한 레벨로 해석할 수 있습니다.

system:masters 그룹의 특징

system:masters는 Kubernetes 내장 슈퍼유저 그룹으로, 이 그룹에 속한 사용자는 API 서버에 대해 매우 강력한 권한을 가집니다.

노드 및 Control Plane 컴포넌트 인증서 일괄 생성

kube-scheduler의 대상 목록을 정의하고 인증서를 일괄 생성했습니다.

# kube-scheduler 인증서의 Organization 값 수정
sed -i 's/system:system:kube-scheduler/system:kube-scheduler/' ca.conf
grep system:kube-scheduler ca.conf

# 생성 대상 정의
certs=(
  "node-0" "node-1"
  "kube-proxy" "kube-scheduler"
  "kube-controller-manager"
  "kube-api-server"
  "service-accounts"
)

# 인증서 일괄 생성
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

# 생성 결과 확인
ls -1 *.crt *.csr *.key

생성된 인증서 예시는 아래처럼 확인했습니다.

openssl x509 -in node-0.crt -text -noout
openssl x509 -in kube-api-server.crt -text -noout
openssl x509 -in service-accounts.crt -text -noout

kind 환경의 관련 구성 추가 확인

kind(control-plane/worker)에서 kubelet 인증서 구조, kube-proxy 인증 방식, scheduler/controller-manager 인증서, apiserver 인증서 SAN 등을 확인했습니다.

docker exec -it myk8s-control-plane tree /etc/kubernetes
docker exec -it myk8s-control-plane tree /var/lib/kubelet/pki
docker exec -i myk8s-control-plane \
  cat /var/lib/kubelet/pki/kubelet.crt | openssl x509 -text -noout

docker exec -i myk8s-worker \
  cat /var/lib/kubelet/pki/kubelet.crt | openssl x509 -text -noout

kubectl get cm -n kube-system kube-proxy -o yaml

docker exec -i myk8s-control-plane cat /etc/kubernetes/scheduler.conf
docker exec -i myk8s-control-plane cat /etc/kubernetes/controller-manager.conf
docker exec -i myk8s-control-plane \
  cat /etc/kubernetes/pki/apiserver.crt | openssl x509 -text -noout
docker exec -i myk8s-control-plane \
  cat /etc/kubernetes/pki/apiserver-kubelet-client.crt | openssl x509 -text -noout

docker exec -it myk8s-control-plane ls -l /etc/kubernetes/pki/sa.{key,pub}
  • kubeadm(kind)은 클러스터 생성 시 CA를 중심으로 서버/클라이언트/SA 서명 키를 명확히 분리해 구성합니다.
  • kubelet은 서버/클라이언트 역할이 모두 존재하며, 인증서/CA 구조가 노드 단위로 분리되어 보입니다.
  • kube-proxy는 ConfigMap에서 확인했듯 인증서가 아닌 ServiceAccount 토큰 기반으로 동작하는 구성이 확인됩니다.

인증서 배포

마지막으로 Hard Way에서 생성한 인증서를 worker 노드와 control-plane(server)에 수동 배포했습니다.

worker 노드 배포

for host in node-0 node-1; do
  ssh root@${host} mkdir -p /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

server 배포

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

인증을 위한 kubeconfig 생성

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

각 구성 요소(kubelet, kube-proxy, controller-manager, scheduler, admin)가 API Server에 접근할 때 사용할 kubeconfig 파일을 생성합니다.
kubeconfig는 크게 아래 정보를 묶어둔 인증 설정 파일입니다.

  • cluster: API Server 주소(server) + 신뢰할 CA(certificate-authority-data)
  • user: 클라이언트 인증서/키(client-certificate-data, client-key-data)
  • context: (cluster + user) 조합
  • current-context: 기본으로 사용할 context

The kubelet Kubernetes Configuration File

kind 환경에서 kube-apiserver 실행 형태 확인

kubectl describe pod -n kube-system kube-apiserver-myk8s-control-plane
  • API Server는 myk8s-control-plane(172.18.0.2) 노드에서 실행
  • kubernetes.io/config.source: file → static pod 형태
  • 주요 플래그
    • --advertise-address=172.18.0.2
    • --secure-port=6443
    • --authorization-mode=Node,RBAC
    • --etcd-servers=https://127.0.0.1:2379
    • --client-ca-file=/etc/kubernetes/pki/ca.crt
    • --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
    • --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
    • --service-cluster-ip-range=10.96.0.0/16
  • /etc/kubernetes/pki 아래 인증서들이 마운트되어 사용됨

node-0 / node-1 kubeconfig 생성

각 워커 노드(kubelet)가 API Server에 접근할 수 있도록 노드 전용 kubeconfig를 생성했습니다.

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

생성 직후 kubeconfig를 확인하면 cluster만 정의된 상태입니다.

cat node-0.kubeconfig
# users: null
# contexts: null

kubelet 사용자(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
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

context 생성 및 활성화

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

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

최종 생성 파일 목록

ls -l *.kubeconfig
-rw------- ... node-0.kubeconfig
-rw------- ... node-1.kubeconfig

The kube-proxy Kubernetes Configuration File

kube-proxy가 system:kube-proxy 사용자로 API Server에 인증하도록 kubeconfig를 생성했습니다.

kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=kube-proxy.kubeconfig

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

The kube-controller-manager Kubernetes Configuration File

kube-controller-manager가 system:kube-controller-manager 사용자로 API Server에 접근하도록 kubeconfig를 생성했습니다.

kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=kube-controller-manager.kubeconfig

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

The kube-scheduler Kubernetes Configuration File

kube-scheduler가 system:kube-scheduler 사용자로 API Server에 접근하도록 kubeconfig를 생성했습니다.

kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=kube-scheduler.kubeconfig

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

The admin Kubernetes Configuration File

관리자(admin)가 로컬 API Server에 접근할 수 있도록 kubeconfig를 생성했습니다.
여기서는 API Server 주소를 https://127.0.0.1:6443로 지정했습니다.

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

Distribute the Kubernetes Configuration Files

생성된 kubeconfig 파일 목록

ls -l *.kubeconfig

worker 노드(node-0, node-1) 배포

kubelet/kube-proxy용 디렉터리를 생성하고 kubeconfig를 배포했습니다.

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

control-plane(server) 배포

controller-manager/scheduler/admin kubeconfig를 server로 전달했습니다.

scp admin.kubeconfig \
  kube-controller-manager.kubeconfig \
  kube-scheduler.kubeconfig \
  root@server:~/

배포 확인

ssh server ls -l /root/*.kubeconfig

 

데이터 암호화 설정(EncryptionConfig) 생성

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

Kubernetes API Server가 etcd에 저장하는 Secret 리소스를 AES-CBC로 암호화하도록 EncryptionConfiguration을 생성하고, 컨트롤 플레인(server)로 전달합니다.

암호화 키 생성

export ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)

echo $ENCRYPTION_KEY
cBZnBglJ64tucD4dIMacPMXlMKwoNhnD9bz2UV7YJwc=

encryption-config 템플릿 확인 및 값 치환

cat configs/encryption-config.yaml
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: ${ENCRYPTION_KEY}
      - identity: {}
envsubst < configs/encryption-config.yaml > encryption-config.yaml
cat encryption-config.yaml

치환된 결과

kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: cBZnBglJ64tucD4dIMacPMXlMKwoNhnD9bz2UV7YJwc=
      - identity: {}

server로 전달 및 배치 확인

scp encryption-config.yaml root@server:~/
ssh server ls -l /root/encryption-config.yaml

etcd 구성

Kubernetes의 상태 저장소인 etcd를 구성하고 systemd 서비스로 기동합니다.
etcd가 정상 동작해야 Control Plane이 클러스터 상태를 저장/조회할 수 있습니다.

etcd는 Kubernetes 클러스터의 모든 상태 정보를 저장하는 분산 키-값 저장소입니다.
노드 정보, 파드 상태, 설정 값 등이 모두 etcd에 기록되며, Control Plane은 이 데이터를 기준으로 클러스터를 제어합니다.

server 노드에 etcd 서비스 기동

단일 노드 etcd를 systemd 서비스로 등록하고 실행합니다.

기존 etcd 유닛 설정 확인

cat units/etcd.service | grep controller

etcd 멤버 이름(server) 반영하여 유닛 파일 재작성

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

바이너리/유닛 파일 전송

scp \
  downloads/controller/etcd \
  downloads/client/etcdctl \
  units/etcd.service \
  root@server:~/

server에서 etcd 설치 및 실행

ssh root@server

mv etcd etcdctl /usr/local/bin/
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/
mv etcd.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable etcd
systemctl start etcd

상태/포트 및 멤버 상태 확인

systemctl status etcd --no-pager
ss -tnlp | grep etcd
  • 127.0.0.1:2379 (client)
  • 127.0.0.1:2380 (peer)
etcdctl member list
etcdctl member list -w table
etcdctl endpoint status -w table

kind 환경의 etcd TLS 구성 확인

kind에서는 etcd가 static pod로 실행되며, client/peer 통신에 TLS가 적용되어 있습니다.

etcd Pod 실행 옵션 확인

kubectl describe pod -n kube-system etcd-myk8s-control-plane
kubectl describe pod -n kube-system etcd-myk8s-control-plane | sed -n '/Command:/,/State:/p'
  • peer/client URL이 https://... 기반
  • --trusted-ca-file, --peer-trusted-ca-file로 CA 지정
  • --client-cert-auth=true, --peer-client-cert-auth=true로 상호 인증 활성화

etcd 인증서 디렉터리 구조 확인

docker exec -it myk8s-control-plane tree /etc/kubernetes/pki/etcd

etcd 인증서 만료 확인

docker exec -i myk8s-control-plane kubeadm certs check-expiration

kube-apiserver의 etcd client 인증서 확인

docker exec -i myk8s-control-plane \
  cat /etc/kubernetes/pki/apiserver-etcd-client.crt | openssl x509 -text -noout
  • Subject: CN = kube-apiserver-etcd-client
  • Issuer: CN = etcd-ca
  • Extended Key Usage: TLS Web Client Authentication

컨트롤 플레인(Control Plane) 부트스트랩

server 노드에 kube-apiserver / kube-scheduler / kube-controller-manager 서비스 기동

etcd 이후 kube-apiserver, kube-controller-manager, kube-scheduler를 server 노드에서 실행해 Control Plane을 구성합니다.
이 단계가 완료되면 API 접근이 가능해지고, 클러스터가 “동작하는 상태”에 들어갑니다.

 

  • kube-apiserver: 모든 요청의 진입점
  • kube-controller-manager: 클러스터 상태를 원하는 상태로 유지
  • kube-scheduler: 파드를 실행할 노드 선택

이 세 컴포넌트가 함께 동작하면서 Kubernetes 클러스터의 제어 로직을 담당합니다.

 

  • Pod CIDR(클러스터): 10.200.0.0/16
    • node-0: 10.200.0.0/24
    • node-1: 10.200.1.0/24
  • Service CIDR: 10.32.0.0/24
    • kubernetes 서비스 ClusterIP: 10.32.0.1

설정 파일 작성 후 server에 전달

kube-apiserver 인증서 SAN 확인

cat ca.conf | grep '\[kube-api-server_alt_names' -A2
  • 127.0.0.1
  • 10.32.0.1

kube-apiserver 유닛 파일 재작성

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

kube-apiserver → kubelet 접근 RBAC 정의 확인

cat configs/kube-apiserver-to-kubelet.yaml

kube-apiserver 인증서 정보 확인

openssl x509 -in kube-api-server.crt -text -noout
 
scheduler / controller-manager 유닛 및 설정 확인
cat units/kube-scheduler.service
cat configs/kube-scheduler.yaml

cat units/kube-controller-manager.service

server로 바이너리/유닛/설정 전송

scp \
  downloads/controller/kube-apiserver \
  downloads/controller/kube-controller-manager \
  downloads/controller/kube-scheduler \
  downloads/client/kubectl \
  units/kube-apiserver.service \
  units/kube-controller-manager.service \
  units/kube-scheduler.service \
  configs/kube-scheduler.yaml \
  configs/kube-apiserver-to-kubelet.yaml \
  root@server:~/

Kubernetes Control Plane 구성 및 기동(server)

디렉터리 준비 및 바이너리 설치

ssh root@server

mkdir -p /etc/kubernetes/config /var/lib/kubernetes/

mv kube-apiserver \
   kube-controller-manager \
   kube-scheduler \
   kubectl \
   /usr/local/bin/

인증서/키/암호화 설정 배치

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/

systemd 유닛 및 kubeconfig 배치

mv kube-apiserver.service /etc/systemd/system/
mv kube-controller-manager.service /etc/systemd/system/
mv kube-scheduler.service /etc/systemd/system/

mv kube-controller-manager.kubeconfig /var/lib/kubernetes/
mv kube-scheduler.kubeconfig /var/lib/kubernetes/
mv kube-scheduler.yaml /etc/kubernetes/config/

서비스 활성화 및 시작

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

systemctl is-active kube-apiserver
systemctl status kube-apiserver --no-pager
systemctl status kube-scheduler --no-pager
systemctl status kube-controller-manager --no-pager

API 접근 및 기본 오브젝트 확인

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

kubectl get service,ep --kubeconfig admin.kubeconfig
kubectl get clusterroles --kubeconfig admin.kubeconfig
kubectl get clusterrolebindings --kubeconfig admin.kubeconfig

scheduler RBAC 확인

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

RBAC for Kubelet Authorization

RBAC 적용

kubectl apply -f kube-apiserver-to-kubelet.yaml --kubeconfig admin.kubeconfig
확인
kubectl get clusterroles system:kube-apiserver-to-kubelet --kubeconfig admin.kubeconfig
kubectl get clusterrolebindings system:kube-apiserver --kubeconfig admin.kubeconfig

API Server 버전 확인(jumpbox)

curl -s -k --cacert ca.crt https://server.kubernetes.local:6443/version | jq

워커 노드(Worker Node) 부트스트랩

node-0 / node-1에 runc, CNI, containerd, kubelet, kube-proxy 설치

실제 파드가 실행될 Worker Node에 containerd, kubelet, kube-proxy, CNI 플러그인을 설치하고 기동합니다.
이 구성이 끝나면 Worker Node가 Control Plane에 등록되고 워크로드를 실행할 수 있습니다.

사전 파일 확인 및 노드별 설정 배포(jumpbox)

CNI bridge 설정 템플릿 확인

cat configs/10-bridge.conf | jq

kubelet 설정 템플릿 확인

cat configs/kubelet-config.yaml | yq

노드별 SUBNET 치환 후 배포

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

기타 설정 및 유닛/바이너리 배포

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

CNI 플러그인 배포

for HOST in node-0 node-1; do
  ssh root@${HOST} "mkdir -p /root/cni-plugins"
  scp downloads/cni-plugins/* root@${HOST}:/root/cni-plugins/
done

node-0에서 설치/기동

ssh root@node-0

apt-get -y install socat conntrack ipset kmod psmisc bridge-utils
swapon --show

mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

mv crictl kube-proxy kubelet runc /usr/local/bin/
mv containerd containerd-shim-runc-v2 containerd-stress /bin/
mv cni-plugins/* /opt/cni/bin/

mv 10-bridge.conf 99-loopback.conf /etc/cni/net.d/

modprobe br-netfilter
echo "br-netfilter" >> /etc/modules-load.d/modules.conf

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

mkdir -p /etc/containerd/
mv containerd-config.toml /etc/containerd/config.toml
mv containerd.service /etc/systemd/system/

mv kubelet-config.yaml /var/lib/kubelet/
mv kube-proxy.service /etc/systemd/system/

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

컨트롤 플레인에서 노드 상태 확인

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

node-1에서 설치/기동

node-0와 동일하게 진행합니다(대상 호스트만 node-1).

ssh root@node-1

apt-get -y install socat conntrack ipset kmod psmisc bridge-utils
swapon --show

mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

mv crictl kube-proxy kubelet runc /usr/local/bin/
mv containerd containerd-shim-runc-v2 containerd-stress /bin/
mv cni-plugins/* /opt/cni/bin/

mv 10-bridge.conf 99-loopback.conf /etc/cni/net.d/

modprobe br-netfilter
echo "br-netfilter" >> /etc/modules-load.d/modules.conf

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

mkdir -p /etc/containerd/
mv containerd-config.toml /etc/containerd/config.toml
mv containerd.service /etc/systemd/system/

mv kubelet-config.yaml /var/lib/kubelet/
mv kube-proxy.service /etc/systemd/system/

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

최종 확인

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

원격 접근을 위한 kubectl 설정

jumpbox에서 admin 자격증명 기반으로 기본 kubeconfig(~/.kube/config)를 구성해 원격으로 클러스터를 제어합니다.
이후부터는 매번 --kubeconfig 옵션 없이도 kubectl로 편하게 확인/작업할 수 있습니다.

jumpbox에서 admin 자격증명으로 kubectl 설정

API Server 버전 확인(CA 검증)

curl -s --cacert ca.crt https://server.kubernetes.local:6443/version | jq

kubectl 클러스터/자격증명/컨텍스트 구성

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 version
kubectl get nodes
kubectl get nodes -owide
kubectl get pod -A

kubeconfig 내용 확인

cat /root/.kube/config

파드 네트워크 라우팅 구성

각 노드는 자신에게 할당된 PodCIDR 범위를 가지고 있으며, 서로 다른 노드의 파드 간 통신을 위해서는 OS 라우팅 테이블에 해당 경로가 필요합니다.
자동화된 CNI 환경에서는 숨겨지지만, The Hard Way에서는 이 라우팅을 직접 구성합니다.

PodCIDR 라우팅 수동 구성

machines.txt 기반 변수 로드

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

server 라우팅 확인 및 PodCIDR route 추가

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

node-0 라우팅 확인 및 node-1 PodCIDR 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

node-1 라우팅 확인 및 node-0 PodCIDR 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

Smoke Test

마지막으로 클러스터가 정상 동작하는지 스모크 테스트로 확인합니다.
Secret 암호화 저장, Deployment/Service(NodePort), port-forward, logs/exec 등 핵심 기능이 기대대로 동작하는지 점검합니다.

기본 동작 검증

Secret 암호화 저장 확인

Secret 생성

kubectl create secret generic kubernetes-the-hard-way \
  --from-literal="mykey=mydata"

Secret 확인

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

etcd 저장 데이터 확인(암호화 흔적 확인)

ssh root@server \
  'etcdctl get /registry/secrets/default/kubernetes-the-hard-way | hexdump -C'

kind에서 etcd 확인

  • kind는 etcd가 TLS로 구성되어 있어 ETCDCTL_CACERT/ETCDCTL_CERT/ETCDCTL_KEY 설정이 필요합니다.
  • Secret을 만들고 etcdctl get /registry/... | hexdump -C 로 확인하는 방식은 동일합니다.

Deployment 및 파드 동작 확인

Deployment 생성 및 확인

kubectl create deployment nginx --image=nginx:latest
kubectl scale deployment nginx --replicas=2
kubectl get pod -owide

Pod 직접 접근(server)

ssh server curl -s 10.200.0.2 | grep title
ssh server curl -s 10.200.1.3 | grep title

접근 및 네트워크 검증

Port-forward

POD_NAME=$(kubectl get pods -l app=nginx -o jsonpath="{.items[0].metadata.name}")
kubectl port-forward $POD_NAME 8080:80 &
curl --head http://127.0.0.1:8080

Logs / Exec

kubectl logs $POD_NAME
kubectl exec -ti $POD_NAME -- nginx -v

Service(NodePort) 및 접근

kubectl expose deployment nginx --type=NodePort --port=80
kubectl get service,ep nginx

NODE_PORT=$(kubectl get svc nginx -o jsonpath='{.spec.ports[0].nodePort}')
curl -s -I http://node-0:${NODE_PORT}
curl -s -I http://node-1:${NODE_PORT}

정리

Vagrant 환경 삭제

vagrant destroy -f
rmdir .vagrant

kind 클러스터 삭제

kind delete cluster --name myk8s

마무리하며

이번 실습에서는 Kubernetes The Hard Way 방식을 따라가며, kubeadm이나 kind 같은 자동화 도구 없이 Kubernetes 클러스터를 처음부터 끝까지 직접 구성해 보았습니다. 인증서 생성부터 etcd, Control Plane, Worker Node, 네트워크 라우팅까지 모든 단계를 수동으로 진행하면서, 각 구성 요소가 어떤 역할을 맡고 어떤 순서로 연결되는지를 확인할 수 있었습니다.

또한 kind 환경과 비교해보며, 자동화 도구가 내부적으로 어떤 작업을 대신 수행해 주는지도 자연스럽게 이해할 수 있었습니다. 평소에는 잘 드러나지 않던 인증, 보안, 네트워크 설정들이 실제 클러스터 동작에 얼마나 중요한지 체감할 수 있는 과정이었습니다.

이번 실습을 통해 Kubernetes 클러스터를 “설치한다”기보다는, 여러 독립적인 구성 요소를 하나의 시스템으로 “조립한다”는 관점에서 바라보게 되었습니다. 이후 자동화 도구를 사용하더라도, 내부 동작을 더 명확히 이해한 상태에서 클러스터를 운영하고 문제를 바라볼 수 있을 것이라 생각합니다.

검색 태그