본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로인프라 › 컨테이너 › Kubernetes

Kubernetes 테스트 환경 설정

목차

Kubernetes 테스트 환경 설정

VirtualBox와 Vagrant로 컨트롤 플레인 1대 + 워커 1대를 만들고 kubeadm으로 클러스터를 초기화한다. 원래 예제의 VM 이름(m-k8s, w1-k8s)과 사설 주소를 유지했다. VM만 띄우는 것으로는 Kubernetes가 설치되지 않으므로, 아래 install_pkg.sh와 초기화·네트워크 설치 단계까지 순서대로 실행한다.

이 글은 로컬 실습용이다. 단일 컨트롤 플레인은 고가용성이 없고, 아래의 사설망과 관리자 kubeconfig를 운영 환경에 그대로 적용하면 안 된다. 예제에서 사용하는 Kubernetes 저장소 경로는 v1.37 기준이다. 설치 시점에 지원 버전과 해당 버전의 설치 문서를 먼저 확인한다.

flowchart LR
    H[호스트 PC] --> V[Vagrant와 VirtualBox]
    V --> M[m-k8s 192.168.56.10]
    V --> W[w1-k8s 192.168.56.101]
    M --> I[kubeadm init]
    I --> F[Flannel Pod 네트워크]
    F --> J[kubeadm join]
    J --> R[노드와 Pod 검증]

Vagrant가 두 VM을 만들고 동일한 패키지 준비 스크립트를 실행한다. 컨트롤 플레인에서 초기화한 뒤 Pod 네트워크를 설치하고 워커를 조인한다. Kubernetes 공식 문서도 컨테이너 런타임·kubeadm 설치와 클러스터 초기화·CNI 설치를 별도 단계로 안내한다.

1. 준비 사항

  • 호스트에 VirtualBox와 Vagrant를 설치한다. 사용하는 CPU 아키텍처에 맞는 VirtualBox용 Vagrant box가 필요하다.
  • VM 두 대에 각각 최소 2GiB RAM을 할당할 수 있어야 한다. 아래 설정은 컨트롤 플레인 3GiB, 워커 2GiB 및 각각 2 vCPU를 사용한다. 호스트 운영체제에 필요한 여유도 남긴다.
  • 192.168.56.0/24가 호스트의 다른 네트워크 및 Pod CIDR 10.244.0.0/16과 충돌하지 않는지 확인한다.
  • 이미 실행 중인 VM이 같은 이름이나 IP를 사용하지 않아야 한다.

이 예제는 Vagrant의 사설 네트워크로 VM끼리 통신한다. bento/ubuntu-24.04 box는 HashiCorp의 Vagrant 학습 자료에도 사용된다. box와 VirtualBox의 CPU 아키텍처가 맞지 않으면 vagrant up 단계에서 실패할 수 있다.

2. Vagrantfile 작성

빈 디렉터리에 아래 두 파일을 만든다. 워커를 늘리려면 IP와 호스트 항목·호스트 자원을 함께 조정해야 한다.

[Vagrantfile]

Vagrant.configure("2") do |config|
  config.vm.box = "bento/ubuntu-24.04"

  nodes = {
    "m-k8s" => ["192.168.56.10", 3072],
    "w1-k8s" => ["192.168.56.101", 2048]
  }

  nodes.each do |name, (address, memory)|
    config.vm.define name do |node|
      node.vm.hostname = name
      node.vm.network "private_network", ip: address
      node.vm.provider "virtualbox" do |vb|
        vb.name = name
        vb.cpus = 2
        vb.memory = memory
      end
      node.vm.provision "shell", path: "install_pkg.sh"
    end
  end
end

예전 예제의 cfg.vm.box = "base"를 고치는 방식보다 사용할 box를 처음부터 명시하는 편이 분명하다. VM의 기본 NAT 인터페이스는 인터넷 패키지 다운로드에, 사설 인터페이스는 노드 사이의 통신에 사용한다.

3. 모든 VM에 설치할 패키지 준비

[install_pkg.sh] Vagrant의 shell provisioner는 이 파일을 VM 안에서 root로 실행한다. K8S_MINOR를 바꾸면 아래 패키지 저장소의 minor 버전도 함께 바뀐다. 클러스터의 모든 노드에서 같은 minor를 사용한다.

#!/usr/bin/env bash
set -euo pipefail

K8S_MINOR="v1.37"

# kubelet의 기본 설정은 swap이 켜져 있으면 시작을 거부한다.
swapoff -a
sed -ri '/[[:space:]]swap[[:space:]]/s/^/#/' /etc/fstab

cat >/etc/modules-load.d/k8s.conf <<'EOF'
overlay
br_netfilter
EOF
modprobe overlay
modprobe br_netfilter

cat >/etc/sysctl.d/k8s.conf <<'EOF'
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system >/dev/null

apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl gpg containerd

mkdir -p /etc/containerd
containerd config default >/etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
grep -q 'SystemdCgroup = true' /etc/containerd/config.toml
systemctl enable --now containerd
systemctl restart containerd

install -d -m 0755 /etc/apt/keyrings
curl -fsSL "https://pkgs.k8s.io/core:/stable:/${K8S_MINOR}/deb/Release.key" |
  gpg --dearmor --yes -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
printf 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/%s/deb/ /\n' "${K8S_MINOR}" \
  >/etc/apt/sources.list.d/kubernetes.list
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl

# Vagrant의 NAT 주소 대신 노드마다 다른 사설 IP를 kubelet에 지정한다.
case "$(hostname)" in
  m-k8s) NODE_IP="192.168.56.10" ;;
  w1-k8s) NODE_IP="192.168.56.101" ;;
  *) echo "알 수 없는 노드 이름" >&2; exit 1 ;;
esac
printf 'KUBELET_EXTRA_ARGS=--node-ip=%s\n' "${NODE_IP}" >/etc/default/kubelet

# /etc/hosts에는 IP와 호스트 이름만 적는다. nameserver는 DNS 설정 항목이다.
grep -qF '192.168.56.10 m-k8s' /etc/hosts ||
  printf '192.168.56.10 m-k8s\n' >>/etc/hosts
grep -qF '192.168.56.101 w1-k8s' /etc/hosts ||
  printf '192.168.56.101 w1-k8s\n' >>/etc/hosts

/etc/hosts에는 192.168.56.10 m-k8s처럼 주소와 이름을 한 줄씩 적는다. nameserver 1.1.1.1 같은 행과 줄 끝의 역슬래시는 이 파일의 형식이 아니다. DNS 서버를 바꾸는 작업이 필요하면 Ubuntu의 네트워크 관리자 설정을 따로 확인한다.

containerd와 kubelet의 cgroup 드라이버가 맞아야 한다. 위 스크립트는 생성된 containerd 설정의 SystemdCgroup을 켜며, 예상한 설정 항목을 찾지 못하면 중단한다. containerd 1.x와 2.x의 설정 경로가 달라질 수 있으므로 다른 배포판이나 패키지 버전으로 바꾼다면 컨테이너 런타임 문서를 기준으로 설정을 다시 확인한다. /etc/default/kubelet의 KUBELET_EXTRA_ARGS는 Vagrant NAT 주소가 각 VM에서 같게 잡히는 문제를 피하기 위해 사설 IP를 고정한다(kubelet 설정 문서).

4. VM 시작과 사전 확인

호스트 터미널에서 실행한다.

vagrant up
vagrant status
vagrant ssh m-k8s

컨트롤 플레인 VM 안에서 다음을 확인한다. getent 결과는 두 노드의 사설 주소여야 한다.

getent hosts m-k8s w1-k8s
systemctl is-active containerd
kubeadm version
ip -br -4 addr

kubelet은 아직 클러스터에 등록되지 않았으므로 이 단계에서 재시작되거나 대기하는 모습이 정상일 수 있다. containerd가 active가 아니거나 VM끼리 사설 주소로 통신하지 못하면 초기화 전에 원인을 해결한다.

5. 컨트롤 플레인 초기화

m-k8s VM에서 실행한다.

sudo kubeadm init \
  --apiserver-advertise-address=192.168.56.10 \
  --pod-network-cidr=10.244.0.0/16 \
  --cri-socket=unix:///run/containerd/containerd.sock

mkdir -p "$HOME/.kube"
sudo cp /etc/kubernetes/admin.conf "$HOME/.kube/config"
sudo chown "$(id -u):$(id -g)" "$HOME/.kube/config"

kubeadm init의 마지막에 출력되는 kubeadm join ... 명령을 안전한 장소에 보관한다. 토큰과 CA 해시가 포함되므로 블로그나 공개 이슈에 붙이지 않는다. 잃어버렸다면 컨트롤 플레인에서 sudo kubeadm token create --print-join-command로 새 명령을 얻을 수 있다. admin.conf도 관리자 권한을 주는 파일이므로 호스트 공유 폴더나 저장소에 복사하지 않는다.

6. Pod 네트워크 설치

kubeadm은 Pod 네트워크를 자동 설치하지 않는다. 예제에서는 Flannel의 기본 CIDR 10.244.0.0/16을 사용한다. 컨트롤 플레인 VM에서 실행한다.

kubectl apply -f https://github.com/flannel-io/flannel/releases/download/v0.28.9/kube-flannel.yml

Flannel 공식 저장소는 릴리스에 첨부된 manifest를 사용할 것을 안내한다. URL의 버전은 이 글의 예제 버전이므로 설치 시점에 Flannel 릴리스와 Kubernetes 호환성을 확인한다.

Vagrant VM에는 NAT와 사설 인터페이스가 함께 있다. 두 VM에서 ip -br -4 addr로 192.168.56.x가 붙은 인터페이스 이름을 확인한다. 두 VM의 이름이 동일할 때만 다음 명령의 enp0s8을 실제 이름으로 바꿔 적용한다. Flannel이 NAT 인터페이스를 선택하면 노드 간 Pod 통신이 실패할 수 있다(Flannel 문제 해결 문서).

kubectl -n kube-flannel patch daemonset kube-flannel-ds --type=json \
  -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--iface=enp0s8"}]'
kubectl -n kube-flannel rollout status daemonset/kube-flannel-ds --timeout=180s

두 VM의 사설 인터페이스 이름이 다르면 위 단일 DaemonSet 인자로 동일하게 지정할 수 없다. 그 경우 노드별 Flannel 인터페이스 설정을 공식 설정 문서에 맞춰 별도로 구성한다. 이 시점에 CoreDNS가 Pending이어도 다음 워커 조인 단계로 진행한다. 기본 컨트롤 플레인에는 일반 Pod를 배치하지 않으므로, 워커가 합류하기 전에는 CoreDNS가 스케줄되지 않을 수 있다(kubeadm 클러스터 구성).

7. 워커 조인과 검증

호스트 터미널의 다른 창에서 vagrant ssh w1-k8s로 들어간다. 컨트롤 플레인에서 받은 kubeadm join 명령 앞에 sudo를 붙여 실행한다. API 주소가 192.168.56.10:6443인지 확인한다.

컨트롤 플레인 VM으로 돌아와 결과를 확인한다.

kubectl get nodes -o wide
kubectl get pods -A
kubectl -n kube-flannel get pods -o wide
kubectl -n kube-system rollout status deployment/coredns --timeout=180s

두 노드가 Ready이고 INTERNAL-IP가 각각 192.168.56.10, 192.168.56.101이며 CoreDNS가 Running/Ready이면 기본 연결이 성립한 것이다. 워커 조인 이후에도 시스템 Pod가 계속 Pending이거나 Flannel이 재시작된다면 kubectl describe node w1-k8s, kubectl -n kube-flannel logs daemonset/kube-flannel-ds, VM의 journalctl -u kubelet을 확인한다.

작은 워크로드도 배치해 본다.

kubectl create deployment web-smoke --image=nginx:stable-alpine
kubectl rollout status deployment/web-smoke --timeout=180s
kubectl get pods -o wide
kubectl delete deployment web-smoke

단일 컨트롤 플레인은 기본적으로 일반 워크로드를 받지 않으므로, 테스트 Pod가 워커에 생성되는지 볼 수 있다. 이 명령은 컨테이너 실행·스케줄링 확인이며 외부에서 서비스에 접속할 수 있는지까지 검증하지는 않는다.

참고 문서

같은 카테고리의 글