kubeadm으로 Kubernetes 설치 실습: Ubuntu, containerd, Flannel
Ubuntu 24.04 서버 3대로 kubeadm 기반 Kubernetes 클러스터를 직접 올려보는 실습 기록입니다. containerd, kubeadm init, CNI, worker join, nginx 배포 검증, 자주 막히는 지점까지 한 번에 정리했습니다.
Kubernetes를 처음 설치할 때 가장 많이 막히는 부분은 의외로 Kubernetes 자체가 아닙니다. swap, containerd 설정, cgroup driver, Pod CIDR, CNI 적용 순서 같은 기본값에서 많이 걸립니다. 그래서 이번 실습은 화려한 구성을 빼고, Ubuntu 서버 3대에 kubeadm으로 클러스터를 직접 올린다는 기준으로 정리했습니다.
실습 목표는 단순합니다. control-plane 1대와 worker 2대를 만들고, nginx Deployment를 배포한 뒤 NodePort로 접속까지 확인합니다. 운영용 HA 구성은 아니지만, Kubernetes 설치 흐름을 몸에 익히기에는 이 정도가 제일 깔끔합니다.
실습 환경
| 노드 | 호스트명 | 예시 IP | 역할 |
|---|---|---|---|
| 1 | k8s-cp1 | 192.168.56.10 | control-plane |
| 2 | k8s-w1 | 192.168.56.11 | worker |
| 3 | k8s-w2 | 192.168.56.12 | worker |
OS는 Ubuntu 24.04 기준으로 작성했습니다. Kubernetes 패키지는 공식 문서 기준 v1.36 저장소를 사용했습니다. 다른 버전을 쓸 경우 `v1.36` 부분만 원하는 minor 버전에 맞춰 바꾸면 됩니다.
0. 설치 전에 먼저 정리할 것
처음부터 명령어를 치기 전에 세 가지를 먼저 정해둡니다. 첫째, 노드 IP가 바뀌지 않게 고정합니다. 둘째, hostname이 겹치지 않게 잡습니다. 셋째, Pod CIDR을 정합니다. 여기서는 Flannel 기본값에 맞춰 `10.244.0.0/16`을 씁니다.
# 모든 노드에서 hostname 확인
hostnamectl
ip -br addr
ip route
# 예시: control-plane 노드
sudo hostnamectl set-hostname k8s-cp1
# 예시: worker 노드
sudo hostnamectl set-hostname k8s-w1
sudo hostnamectl set-hostname k8s-w2실습망에서 DNS가 없다면 `/etc/hosts`에 노드 이름을 넣어둡니다. 운영 환경이면 DNS로 처리하는 편이 낫습니다.
sudo tee -a /etc/hosts >/dev/null <<'EOF'
192.168.56.10 k8s-cp1
192.168.56.11 k8s-w1
192.168.56.12 k8s-w2
EOF1. 모든 노드 공통 준비
아래 작업은 control-plane과 worker 모두에서 실행합니다. Kubernetes 노드는 swap이 켜져 있으면 kubelet preflight에서 막힐 수 있으니 먼저 끕니다.
sudo swapoff -a
sudo sed -i.bak '/ swap / s/^/#/' /etc/fstab
free -h
grep -n ' swap ' /etc/fstab || truecontainerd에서 overlay와 br_netfilter를 쓸 수 있게 커널 모듈과 sysctl을 잡습니다. 이 부분을 빼먹으면 Pod 네트워크가 올라온 뒤에도 통신이 이상하게 보일 수 있습니다.
cat <<'EOF' | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
cat <<'EOF' | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
lsmod | egrep 'overlay|br_netfilter'
sysctl net.bridge.bridge-nf-call-iptables net.ipv4.ip_forward2. containerd 설치와 cgroup 설정
kubeadm 실습에서는 Docker Engine을 직접 붙이는 방식보다 containerd를 쓰는 쪽이 정리가 쉽습니다. 설치 후 기본 설정 파일을 만들고, systemd cgroup을 사용하도록 바꿉니다.
sudo apt-get update
sudo apt-get install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd
sudo systemctl status containerd --no-pager여기서 `SystemdCgroup = true`가 들어갔는지 꼭 확인합니다. kubelet과 container runtime의 cgroup driver가 어긋나면 노드 상태가 멀쩡해 보여도 나중에 이상한 증상으로 돌아옵니다.
grep -n 'SystemdCgroup' /etc/containerd/config.toml
sudo crictl info | grep -i cgroup -A3 || true3. kubelet, kubeadm, kubectl 설치
Kubernetes 패키지는 예전 `apt.kubernetes.io`가 아니라 `pkgs.k8s.io` 저장소를 씁니다. 아래 예시는 v1.36 기준입니다.
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/deb/ /' \
| sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
sudo systemctl enable --now kubelet
kubeadm version
kubectl version --client이 시점의 kubelet은 정상적으로 기동했다가도 아직 클러스터에 join되지 않았기 때문에 에러 로그를 낼 수 있습니다. 이건 자연스러운 상태입니다. 중요한 건 바이너리 설치와 서비스 등록이 끝났는지입니다.
4. control-plane 초기화
이제 control-plane 노드에서만 `kubeadm init`을 실행합니다. Flannel을 붙일 예정이라 `--pod-network-cidr=10.244.0.0/16`을 같이 넣습니다. 노드가 여러 NIC를 가진 경우에는 `--apiserver-advertise-address`도 명시합니다.
sudo kubeadm init \
--apiserver-advertise-address=192.168.56.10 \
--pod-network-cidr=10.244.0.0/16성공하면 마지막에 `kubeadm join ...` 명령이 나옵니다. 이 명령은 worker를 붙일 때 필요합니다. 토큰이 포함되어 있으니 외부에 공개하지 말고 따로 기록합니다.
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
kubectl get nodes -o wide
kubectl get pods -A아직 CNI가 없으므로 control-plane 노드는 `NotReady`로 보일 수 있습니다. 여기서 당황해서 kubeadm reset을 치기보다 CNI를 먼저 붙입니다.
5. CNI 설치
실습용으로는 Flannel이 빠릅니다. Flannel 기본 네트워크가 `10.244.0.0/16`이기 때문에 앞에서 kubeadm init에도 같은 CIDR을 넣었습니다.
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
kubectl -n kube-flannel get pods -o wide
kubectl get nodes -o wide
kubectl get pods -A몇 분 안에 노드 상태가 `Ready`로 바뀌어야 합니다. 계속 `NotReady`이면 kubelet 로그와 CNI Pod 로그를 먼저 봅니다.
sudo journalctl -u kubelet -n 120 --no-pager
kubectl -n kube-flannel logs -l app=flannel --tail=120
kubectl describe node k8s-cp16. worker 노드 join
control-plane 초기화 때 출력된 join 명령을 worker 노드에서 root 권한으로 실행합니다. 토큰을 잃어버렸다면 control-plane에서 다시 만들 수 있습니다.
# control-plane에서 join 명령 재생성
kubeadm token create --print-join-command
# worker 노드에서 실행
sudo kubeadm join 192.168.56.10:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash>worker가 붙으면 control-plane에서 상태를 확인합니다.
kubectl get nodes -o wide
kubectl get pods -A -o wide7. nginx 배포로 실제 동작 확인
설치가 끝났다고 바로 믿지 말고, Pod 생성, Service 연결, NodePort 접속까지 확인합니다. 이 단계에서 DNS, kube-proxy, CNI, node routing 문제가 어느 정도 드러납니다.
kubectl create namespace lab
kubectl -n lab create deployment nginx --image=nginx:1.27
kubectl -n lab scale deployment nginx --replicas=2
kubectl -n lab expose deployment nginx --port=80 --type=NodePort
kubectl -n lab get deploy,rs,pod,svc -o wide
kubectl -n lab rollout status deployment/nginx --timeout=120sNodePort 번호를 확인한 뒤 노드 IP로 접속합니다.
NODE_PORT=$(kubectl -n lab get svc nginx -o jsonpath='{.spec.ports[0].nodePort}')
echo "${NODE_PORT}"
curl -I http://192.168.56.10:${NODE_PORT}
curl -I http://192.168.56.11:${NODE_PORT}
curl -I http://192.168.56.12:${NODE_PORT}8. DNS와 Service 통신 확인
겉에서 nginx가 열려도 클러스터 내부 DNS가 깨져 있으면 실제 애플리케이션 운영에서 바로 막힙니다. 테스트 Pod 하나를 띄워서 Service 이름으로 접근해봅니다.
kubectl -n lab run netcheck --rm -it \
--image=registry.k8s.io/e2e-test-images/agnhost:2.53 \
--restart=Never -- /bin/sh
# Pod 안에서 실행
nslookup nginx.lab.svc.cluster.local
wget -S -O- http://nginx.lab.svc.cluster.local
exit9. 설치 후 바로 남겨둘 점검 명령
설치 실습을 한 번만 하고 끝내면 나중에 어디까지 정상인지 기억이 안 납니다. 아래 출력 정도는 캡처하거나 텍스트로 남겨두면 좋습니다.
kubectl cluster-info
kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl get svc -A
kubectl -n kube-system get configmap kubeadm-config -o yaml
kubectl -n kube-system get daemonset
kubectl -n kube-flannel get pods -o wide
sudo crictl ps
sudo crictl images | head자주 막히는 지점
| 증상 | 먼저 볼 곳 | 흔한 원인 |
|---|---|---|
| `kubeadm init` preflight 실패 | `free -h`, `swapoff`, 포트 사용 여부 | swap 활성화, 6443/10250 포트 충돌 |
| 노드가 계속 NotReady | `journalctl -u kubelet`, CNI Pod 로그 | CNI 미설치, Pod CIDR 불일치 |
| Pod가 ContainerCreating에서 멈춤 | `kubectl describe pod`, `crictl images` | 이미지 pull 실패, registry 접근 불가 |
| Service 이름 접근 실패 | `coredns` Pod, `nslookup` | CoreDNS 미기동, CNI 문제 |
| NodePort 접속 실패 | 노드 방화벽, Service endpoint | 방화벽, endpoint 없음, 잘못된 노드 IP |
초기화가 필요할 때
실습 중 꼬였을 때는 무작정 파일을 지우기보다 kubeadm reset으로 정리합니다. 운영 클러스터에서는 절대 가볍게 치면 안 되고, 실습 VM에서만 사용합니다.
sudo kubeadm reset -f
sudo rm -rf /etc/cni/net.d
sudo rm -rf $HOME/.kube
sudo systemctl restart containerd kubelet
# 필요하면 iptables 규칙도 확인
sudo iptables -S | sed -n '1,80p'
sudo iptables -t nat -S | sed -n '1,80p'정리
kubeadm 설치 실습은 명령어를 외우는 작업이라기보다 Kubernetes가 어떤 순서로 살아나는지 보는 작업입니다. OS 준비, container runtime, kubeadm init, kubeconfig, CNI, worker join, workload 배포, Service 검증까지 순서가 맞아야 합니다. 특히 Pod CIDR과 CNI manifest가 어긋나면 설치는 된 것처럼 보여도 노드가 계속 NotReady로 남습니다.
이 구성을 한 번 손으로 올려두면 이후에는 HA control-plane, external etcd, private registry, offline mirror, Ingress Controller, CSI, monitoring 같은 주제로 넘어가기가 훨씬 편합니다. 기본 설치 흐름을 알고 있어야 장애가 났을 때도 어느 층을 먼저 봐야 하는지 감이 잡힙니다.