런타임과 패키지가 준비됐다면 컨트롤 플레인을 구성할 차례입니다. kubeadm이 인증서와 설정을 만들고 kubelet이 구성 요소를 실행합니다. 이어서 Pod 네트워크를 붙이고 워커를 추가해요. 진행 순서는 init → CNI → join → 통신 확인입니다.
목차
1. NetworkManager의 Calico 제외 설정
세 노드에 아래 파일을 만들어 NetworkManager가 Calico 가상 인터페이스를 관리하지 않도록 합니다. 물리 NIC인 ens160은 계속 NetworkManager가 관리합니다. 노드별로 물리 NIC 이름을 통일하는 작업은 필요하지 않습니다.
sudo tee /etc/NetworkManager/conf.d/calico.conf > /dev/null <<'EOF'
[keyfile]
unmanaged-devices=interface-name:cali*;interface-name:tunl*;interface-name:vxlan.calico;interface-name:wireguard.cali
EOF
sudo systemctl reload NetworkManager
2. cp01의 kubeadm init
mkdir -p ~/k8s-lab
cd ~/k8s-lab
cat > kubeadm-init.yaml <<'EOF'
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
advertiseAddress: 192.168.80.10
bindPort: 6443
nodeRegistration:
name: cp01
criSocket: unix:///run/containerd/containerd.sock
kubeletExtraArgs:
- name: node-ip
value: 192.168.80.10
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.36.5
controlPlaneEndpoint: k8s-api.lab:6443
apiServer:
certSANs:
- k8s-api.lab
- 192.168.80.10
networking:
podSubnet: 10.244.0.0/16
serviceSubnet: 10.96.0.0/12
dnsDomain: cluster.local
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: iptables
EOF
sudo kubeadm config validate --config kubeadm-init.yaml
sudo kubeadm config images pull --config kubeadm-init.yaml
sudo kubeadm init --config kubeadm-init.yaml
3편에서 선택한 패치를 kubernetesVersion에도 맞춥니다. 먼저 이미지를 받으면 저장소 연결과 부트스트랩 오류를 나눠 보기 편해요. Pod 대역을 적는 것만으로 CNI가 설치되지는 않습니다. 여기서는 패킷 경로를 일관되게 살펴보려고 kube-proxy를 iptables 모드로 지정합니다.
# cp01의 labadmin 계정
mkdir -p "$HOME/.kube"
sudo cp /etc/kubernetes/admin.conf "$HOME/.kube/config"
sudo chown "$(id -u):$(id -g)" "$HOME/.kube/config"
chmod 600 "$HOME/.kube/config"
kubectl get nodes
kubectl -n kube-system get pods -o wide
admin.conf에는 관리자 자격증명이 들어 있습니다. Git이나 블로그에 공개하지 않습니다. CNI가 없는 상태에서는 cp01의 NotReady와 CoreDNS의 Pending이 나타날 수 있습니다. API 서버 자체가 응답하지 않는 문제와는 나눠 확인합니다.
3. static Pod와 컨트롤 플레인
sudo ls -l /etc/kubernetes/manifests
kubectl get --raw='/readyz?verbose'
API 서버는 요청을 검증하고 상태를 갱신하며 etcd가 그 상태를 보관합니다. 컨트롤러는 선언과 실제 상태의 차이를 줄이고, 스케줄러는 아직 배치되지 않은 Pod의 노드를 고릅니다. 각 노드의 kubelet은 컨테이너 실행을 맞춥니다. 위 디렉터리의 제어부 매니페스트는 kubelet이 static Pod로 실행하므로 잘못 편집하면 제어 기능이 멈출 수 있습니다.
4. Calico 3.33.0 VXLAN 설치
아래 명령은 cp01에서 실행합니다. Calico 3.33 문서는 CRD를 먼저 설치하도록 안내합니다. 예전의 operator 단일 파일 절차와 섞지 않습니다. 배포 파일은 버전을 지정해 받아 보관합니다.
cd ~/k8s-lab
CALICO_VERSION=v3.33.0
curl -fL -o calico-crds.yaml \
"https://raw.githubusercontent.com/projectcalico/calico/${CALICO_VERSION}/manifests/v3_projectcalico_org.yaml"
curl -fL -o tigera-operator.yaml \
"https://raw.githubusercontent.com/projectcalico/calico/${CALICO_VERSION}/manifests/tigera-operator.yaml"
sha256sum calico-crds.yaml tigera-operator.yaml > calico-manifests.sha256
kubectl create -f calico-crds.yaml
kubectl create -f tigera-operator.yaml
kubectl wait --for=condition=Established \
crd/installations.operator.tigera.io --timeout=120s
cat > calico-installation.yaml <<'EOF'
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
name: default
spec:
calicoNetwork:
bgp: Disabled
linuxDataplane: Iptables
nodeAddressAutodetectionV4:
cidrs:
- 192.168.80.0/24
ipPools:
- name: default-ipv4-ippool
blockSize: 26
cidr: 10.244.0.0/16
encapsulation: VXLAN
natOutgoing: Enabled
nodeSelector: all()
EOF
kubectl create -f calico-installation.yaml
kubectl get tigerastatus
kubectl get pods -n calico-system -o wide
기본 구성에서는 네트워킹용 Installation만 만듭니다. 작은 VM에서 자원을 아끼기 위해 Whisker UI와 Goldmane는 추가하지 않습니다. 부가 기능은 같은 버전의 문서를 따라 검토합니다. create는 첫 설치용이므로 AlreadyExists가 나오면 기존 리소스를 먼저 확인하고 일괄 삭제하지 않습니다.
VXLAN은 Pod 패킷을 노드 주소의 UDP 패킷으로 감싸 전달합니다. 하부 네트워크가 Pod 대역을 몰라도 노드 사이 UDP 4789 연결로 다른 노드의 Pod에 도달할 수 있어요. 실습에서는 같은 서브넷에서도 캡슐화하도록 맞췄습니다. 헤더 공간이 추가되므로 VPN 등으로 하부 MTU가 작아졌다면 MTU도 별도로 점검합니다.
5. 워커 join
# cp01에서 새 조인 명령 생성
sudo kubeadm token create --print-join-command
출력된 join 명령을 두 워커에서 각각 sudo로 실행합니다. --cri-socket unix:///run/containerd/containerd.sock을 덧붙이고 토큰과 CA 해시는 실제 출력값을 사용합니다. 이 값은 공개하지 않습니다. --discovery-token-unsafe-skip-ca-verification으로 검증을 우회하지 않으며 토큰이 만료됐다면 cp01에서 다시 발급합니다.
# cp01에서 실행
kubectl get nodes -o wide
kubectl wait --for=condition=Ready node --all --timeout=300s
kubectl -n kube-system rollout status deployment/coredns --timeout=180s
kubectl get tigerastatus
세 노드의 Ready, CoreDNS의 사용 가능 상태, tigerastatus 네트워크 항목의 Available을 확인합니다. 워커 ROLES의 <none> 표시는 그 자체로 실패가 아닙니다. 역할은 레이블로 표시되므로 join 성공 여부는 상태와 함께 판단합니다.
6. 노드 상태와 Pod DNS
kubectl run dns-check --image=busybox:1.37.0 --restart=Never \
--command -- sh -c 'nslookup kubernetes.default.svc.cluster.local'
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/dns-check --timeout=120s
kubectl logs dns-check
kubectl delete pod dns-check
Pod 로그에 Kubernetes Service 주소가 나오는지 확인합니다. NXDOMAIN이면 질의 이름부터, timeout이면 CoreDNS·Service·CNI 경로를 살펴봅니다. API와 Pod DNS까지 점검해 두면 뒤의 앱 배포 문제와 설치 문제를 구분하기 편합니다.
참고 자료
공식 문서와 배포 파일 확인 기준: 2026년 10월 18일. 출력은 완료 여부를 판단하기 위한 예시이며 환경별로 달라질 수 있습니다.




답글 남기기