Network & Server Factory

개인 공부 기록

DevOps/Kubernetes

[Kubernetes] kubeadm으로 클러스터 구성 정리

1nfra 2026. 10. 29. 08:15
kubeadm은 Kubernetes 공식 클러스터 설치 도구로, 노드를 준비한 뒤 init과 join으로 클러스터를 만듭니다. 마스터를 여러 대 두려면 처음부터 공통 API 주소를 정해야 하고, 네트워크(CNI)와 외부 노출에 필요한 구성 요소는 따로 설치합니다. 이 글에서는 Ubuntu 24.04 노드 5대로 마스터 3대 클러스터를 만들고, kube-vip로 API 가상 IP를, Cilium으로 kube-proxy 없는 네트워크를, MetalLB와 Gateway API로 외부 노출을 구성한 과정을 명령과 함께 정리합니다. 컨테이너부터 Kubernetes까지 시리즈 12편이며, 이전 글은 [Kubernetes] 리소스 requests, limits와 HPA 정리입니다. 이 클러스터를 홈랩에서 어떻게 설계하고 운영했는지는 홈랩 재구성 시리즈에서 다루었습니다.

1. 구성

 

노드 사양과 IP
마스터 3대 vCPU 2개, 메모리 4GB, 디스크 40GB, 10.10.0.21~23
워커 2대 vCPU 4개, 메모리 12GB, 디스크 80GB, 10.10.0.24~25
API 가상 IP 10.10.0.20 (kube-vip)
LoadBalancer 대역 10.10.0.240~249 (MetalLB)

 

IP는 예시 대역입니다. kube-vip와 MetalLB의 L2(ARP) 방식은 노드가 모두 같은 서브넷에 있어야 동작합니다.

API 주소는 kube-vip 가상 IP로, 네트워크는 Cilium으로, 외부 노출은 MetalLB와 Gateway API로 구성했습니다

 

구성 요소 버전
OS Ubuntu 24.04.5 LTS, 커널 6.8
Kubernetes 1.36.5
containerd 2.2.1 (Ubuntu 기본 저장소)
kube-vip 1.2.4
Cilium 1.20.2
MetalLB 0.16.1
Gateway API CRD 1.6.1
cert-manager 1.21.2
Argo CD 3.5.5 (차트 10.10.3)

 

버전은 공식 문서상 서로 지원하는 조합으로 골랐습니다. Cilium 1.20이 Kubernetes 1.36까지만 지원하므로 1.36.5를 씁니다.

2. 노드 준비

kubelet은 기본적으로 스왑이 켜져 있으면 시작하지 않으므로, Ubuntu autoinstall 단계에서 스왑을 껐습니다(storage.swap.size: 0). 그다음 모든 노드에서 커널 모듈과 sysctl, containerd, kubeadm을 준비합니다.

# 1) 커널 모듈과 sysctl
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.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sudo sysctl --system

# 2) containerd (Ubuntu 24.04 기본 저장소에 2.x가 있음) + systemd cgroup 드라이버
sudo apt-get update
sudo apt-get install -y containerd apt-transport-https ca-certificates curl gpg
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

# 3) kubeadm, kubelet, kubectl (마이너 버전 저장소 고정)
K8S_MINOR=v1.36
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/$K8S_MINOR/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:/$K8S_MINOR/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl cri-tools
echo 'runtime-endpoint: unix:///run/containerd/containerd.sock' | sudo tee /etc/crictl.yaml
sudo apt-mark hold kubelet kubeadm kubectl

 

항목 이유
SystemdCgroup = true cgroup v2와 systemd를 쓰는 Ubuntu 24.04에 맞춰 kubelet과 containerd의 cgroup 드라이버를 systemd로 통일
cri-tools kubeadm 사전 점검에 crictl이 필요하지만 kubeadm 패키지가 함께 설치하지 않아 따로 설치
apt-mark hold apt upgrade로 버전이 바뀌지 않게 고정

 

이 단계에서 kubelet이 멈춰 있거나 재시작을 반복하는 것은 정상입니다. init이나 join으로 설정이 생기면 실행됩니다.

3. kube-vip로 API 가상 IP 만들기

kube-vip는 마스터 노드에서 정적 Pod로 실행되고, 마스터끼리 뽑은 리더가 가상 IP를 ARP로 광고합니다. 별도 로드밸런서 없이 API 주소를 이중화할 수 있습니다. 첫 번째 마스터에서 매니페스트를 만듭니다.

sudo ctr -n k8s.io image pull ghcr.io/kube-vip/kube-vip:v1.2.4
sudo ctr -n k8s.io run --rm --net-host ghcr.io/kube-vip/kube-vip:v1.2.4 vipgen /kube-vip manifest pod \
  --interface ens192 --address 10.10.0.20 --controlplane --arp --leaderElection \
  | sudo tee /etc/kubernetes/manifests/kube-vip.yaml

# 첫 init 동안에만 super-admin.conf를 쓴다
sudo sed -i 's#path: /etc/kubernetes/admin.conf#path: /etc/kubernetes/super-admin.conf#' /etc/kubernetes/manifests/kube-vip.yaml

마지막 줄이 핵심입니다. kubeadm 1.29부터 admin.conf는 kubeadm:cluster-admins 그룹 권한으로 동작하는데, 첫 init 도중에는 이 그룹에 아직 권한이 없습니다. 그래서 kube-vip가 리더를 선출하지 못해 가상 IP가 활성화되지 않고, init은 시간 초과로 실패합니다. 첫 번째 마스터만 init 동안 super-admin.conf를 쓰고, 끝나면 되돌립니다.

4. kubeadm init

# kubeadm-init.yaml
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 10.10.0.21
  bindPort: 6443
nodeRegistration:
  criSocket: unix:///run/containerd/containerd.sock
skipPhases:
  - addon/kube-proxy          # Cilium이 kube-proxy를 대신한다
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.36.5
clusterName: homelab
controlPlaneEndpoint: 10.10.0.20:6443   # 처음부터 가상 IP로 지정
networking:
  podSubnet: 10.244.0.0/16
  serviceSubnet: 10.96.0.0/12
apiServer:
  certSANs:
    - 10.10.0.20
controllerManager:
  extraArgs:
    - name: bind-address
      value: 0.0.0.0     # 클러스터 안의 모니터링이 지표를 가져갈 수 있게
scheduler:
  extraArgs:
    - name: bind-address
      value: 0.0.0.0

 

설정 의미
controlPlaneEndpoint 모든 노드와 인증서가 쓸 API 주소, 나중에 바꾸기 어려워 처음부터 가상 IP로
skipPhases: addon/kube-proxy kube-proxy를 설치하지 않음, Cilium의 kubeProxyReplacement가 대신함
podSubnet Cilium의 ipam.mode=kubernetes가 이 대역에서 노드별 Pod 대역을 받아 씀
bind-address 컨트롤러 매니저와 스케줄러는 기본값 127.0.0.1에서만 지표를 제공해 다른 노드에서 수집할 수 없음

 

sudo kubeadm config validate --config kubeadm-init.yaml
sudo kubeadm config images pull --kubernetes-version v1.36.5   # 마스터 3대에서 미리 받아 둠
sudo kubeadm init --config kubeadm-init.yaml --upload-certs

# init이 끝나면 kube-vip를 admin.conf로 되돌린다
sudo sed -i 's#super-admin.conf#admin.conf#' /etc/kubernetes/manifests/kube-vip.yaml

bind-address를 노드 IP로 두면 127.0.0.1로 확인하는 liveness probe가 실패해 컴포넌트가 계속 다시 시작되므로 0.0.0.0으로 둡니다. 이미 만든 클러스터라면 마스터마다 정적 Pod 설정을 고치고, kubeadm-config ConfigMap에도 같은 값을 넣어야 upgrade 뒤에도 유지됩니다.

 

--upload-certs는 마스터용 인증서를 암호화해 올려 두어, 다른 마스터가 join할 때 certificate-key로 받게 합니다. 출력된 토큰과 키는 인증 정보이므로 기록에 남기지 않습니다.

5. 노드 join

# 마스터 2, 3
sudo kubeadm join 10.10.0.20:6443 --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash> \
  --control-plane --certificate-key <key> \
  --apiserver-advertise-address 10.10.0.22
# join이 끝나면 첫 번째 마스터의 kube-vip.yaml(admin.conf 버전)을 /etc/kubernetes/manifests/에 복사한다

# 워커
sudo kubeadm join 10.10.0.20:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>
kubectl label node k8s-worker-1 k8s-worker-2 node-role.kubernetes.io/worker=

토큰은 기본 24시간 뒤 만료되므로, 나중에 노드를 추가할 때는 새로 만듭니다.

sudo kubeadm token create --ttl 30m --print-join-command

etcd 멤버가 3개인지 확인합니다.

sudo crictl exec $(sudo crictl ps --name etcd -q) etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key member list -w table

6. Cilium 설치

이 단계 전까지 노드는 CNI가 없어 NotReady입니다.

# cilium-values.yaml
kubeProxyReplacement: true
k8sServiceHost: 10.10.0.20   # kube-proxy가 없으므로 API 서버 주소를 직접 알려 준다
k8sServicePort: 6443
ipam:
  mode: kubernetes
helm install cilium cilium --repo https://helm.cilium.io --version 1.20.2 \
  -n kube-system -f cilium-values.yaml --wait --timeout 10m
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg status

kube-proxy가 없으면 Service IP 트래픽은 Cilium이 처리합니다. 그런데 Cilium이 시작되는 시점에는 이 처리가 아직 준비되지 않아, Cilium은 kubernetes.default Service IP로 API 서버에 접속할 수 없습니다. 그래서 k8sServiceHost에 가상 IP를 직접 적습니다. 상태에 KubeProxyReplacement: True와 노드 수만큼의 Cluster health가 나오면 정상입니다.

7. 클러스터 검증

 

항목 방법 결과
노드 kubectl get nodes 5대 모두 Ready
시스템 Pod kubectl get pods -A 모두 Running, kube-proxy 없음
Pod 네트워크 워커 2대에 nginx를 띄우고 Service로 5번 호출 모두 200
DNS kubernetes.default.svc.cluster.local 조회 10.96.0.1
가상 IP 전환 VIP를 가진 마스터에서 kube-vip 매니페스트를 다른 폴더로 옮긴 뒤 매초 /readyz 호출 다른 마스터로 넘어감, 실패 0번

 

가상 IP 전환은 마스터가 실제로 멈추기 전에 꼭 시험해 봅니다.

# VIP를 가진 마스터에서
sudo mv /etc/kubernetes/manifests/kube-vip.yaml /tmp/
# 다른 PC에서
for i in $(seq 30); do curl -sk -o /dev/null -w '%{http_code}\n' https://10.10.0.20:6443/readyz; sleep 1; done
# 확인 후 되돌림
sudo mv /tmp/kube-vip.yaml /etc/kubernetes/manifests/

8. MetalLB

클라우드가 아닌 환경에서는 LoadBalancer Service에 IP를 줄 주체가 없어 MetalLB를 씁니다.

# metallb-values.yaml (0.16부터 BGP용 frr-k8s가 기본으로 켜지므로 L2만 쓸 때는 끈다)
frrk8s:
  enabled: false
---
# metallb-pool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: lan-pool
  namespace: metallb-system
spec:
  addresses:
    - 10.10.0.240-10.10.0.249
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: lan-l2
  namespace: metallb-system
spec:
  ipAddressPools:
    - lan-pool
  interfaces:
    - ens192
helm install metallb metallb --repo https://metallb.github.io/metallb --version 0.16.1 \
  -n metallb-system --create-namespace -f metallb-values.yaml --wait
kubectl apply -f metallb-pool.yaml

kubeadm이 마스터에 node.kubernetes.io/exclude-from-external-load-balancers 라벨을 붙이므로, IP 광고는 워커만 합니다.

9. Gateway API

Cilium은 Gateway API를 직접 구현하므로 별도 ingress 컨트롤러가 필요 없습니다. CRD를 먼저 설치한 뒤 Cilium에서 기능을 켭니다.

B=https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.6.1/config/crd/standard
for c in gatewayclasses gateways httproutes referencegrants grpcroutes backendtlspolicies tlsroutes; do
  kubectl apply --server-side -f $B/gateway.networking.k8s.io_$c.yaml
done
# cilium-values.yaml에 추가
gatewayAPI:
  enabled: true
helm upgrade cilium cilium --repo https://helm.cilium.io --version 1.20.2 -n kube-system -f cilium-values.yaml --wait
kubectl -n kube-system rollout restart deploy/cilium-operator ds/cilium
kubectl get gatewayclass   # cilium, ACCEPTED=True

Cilium 1.20은 standard 채널의 TLSRoute CRD까지 있어야 Gateway 컨트롤러가 시작됩니다. Gateway를 만들면 Cilium이 cilium-gateway-<이름> LoadBalancer Service를 만들고 MetalLB가 IP를 줍니다. 특정 IP는 Gateway의 spec.infrastructure.annotations에 metallb.io/loadBalancerIPs로 지정합니다.

 

서비스는 포트가 아니라 호스트 이름으로 나누는 것이 좋습니다. Cilium은 호스트 이름 없는 HTTP 리스너를 포트와 상관없이 하나의 라우팅 표로 합치므로 서비스가 섞일 수 있습니다.

10. cert-manager

# cert-manager-values.yaml
crds:
  enabled: true
config:
  apiVersion: controller.config.cert-manager.io/v1alpha1
  kind: ControllerConfiguration
  enableGatewayAPI: true
helm install cert-manager cert-manager --repo https://charts.jetstack.io --version v1.21.2 \
  -n cert-manager --create-namespace -f cert-manager-values.yaml --wait

Gateway API CRD가 먼저 있어야 cert-manager의 Gateway 연동(gateway-shim)이 켜집니다. 순서가 바뀌면 cert-manager를 다시 시작해야 합니다. 연동이 켜지면 Gateway에 cert-manager.io/cluster-issuer 어노테이션만 붙여도 HTTPS 리스너의 certificateRefs에 맞춰 인증서를 만듭니다.

11. Argo CD

# argocd-values.yaml
global:
  domain: argocd.homelab.example
configs:
  params:
    server.insecure: true   # TLS는 Gateway에서 끝낸다
dex:
  enabled: false
helm install argocd argo-cd --repo https://argoproj.github.io/argo-helm --version 10.10.3 \
  -n argocd --create-namespace -f argocd-values.yaml --wait

server.insecure를 켜지 않으면 Gateway가 HTTP로 넘긴 요청을 argocd-server가 다시 HTTPS로 리다이렉트해 리다이렉트가 끝없이 반복됩니다. Gateway에서 TLS를 종료하면 argocd-server는 HTTP로 요청을 처리합니다. 초기 admin 비밀번호는 argocd-initial-admin-secret에 있습니다.

12. 정리

  • 마스터를 여러 대 두려면 controlPlaneEndpoint를 처음부터 가상 IP나 DNS 이름으로 지정합니다.
  • kube-vip는 첫 init 동안만 super-admin.conf를 쓰고, init 뒤에는 admin.conf로 되돌립니다.
  • kube-proxy를 빼고 Cilium을 쓸 때는 k8sServiceHost에 API 주소를 직접 적습니다.
  • MetalLB 0.16은 L2만 쓸 때 frr-k8s를 끄고, Gateway API는 CRD를 먼저 설치한 뒤 Cilium과 cert-manager를 설정합니다.
  • 설치 뒤에는 가상 IP 전환과 Pod 네트워크를 직접 시험해 봅니다.
  • 구성 요소는 서로 지원하는 버전 조합인지 문서로 확인하고 설치합니다.

참고

728x90
서울
--:--:--
-전체 글
-카테고리
누적 조회수