들어가며
안녕하세요.
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
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
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 클러스터를 “설치한다”기보다는, 여러 독립적인 구성 요소를 하나의 시스템으로 “조립한다”는 관점에서 바라보게 되었습니다. 이후 자동화 도구를 사용하더라도, 내부 동작을 더 명확히 이해한 상태에서 클러스터를 운영하고 문제를 바라볼 수 있을 것이라 생각합니다.
'DevOps' 카테고리의 다른 글
| [3주차 - On-Premise K8S 배포 Hands-on Study] K8S Upgrade (26.01.18) (1) | 2026.01.24 |
|---|---|
| [3주차 - On-Premise K8S 배포 Hands-on Study] Kubeadm (26.01.18) (0) | 2026.01.24 |
| [2주차 - On-Premise K8S 배포 Hands-on Study] Ansible 기초 (26.01.11) (0) | 2026.01.16 |