콘텐츠로 이동

Kubernetes v1.30.0 오프라인 설치 가이드 (Rocky Linux 9.6)

폐쇄망 환경에서 kubeadm 기반 Kubernetes v1.30.0 클러스터를 구성하는 절차를 안내합니다. containerd v2.2.0을 컨테이너 런타임으로, Calico를 CNI로 사용합니다.

전제 조건

  • Rocky Linux 9.6 서버
  • 단일 구성: 컨트롤 플레인 1대 + 워커 노드 1대 이상
  • HA(3중화) 구성: 컨트롤 플레인 3대 + 워커 노드 1대 이상 + VIP 1개
  • 모든 노드에서 아래 설치 파일 접근 가능
  • swap 비활성화 완료 (swapoff -a/etc/fstab 주석 처리)

디렉토리 구조

경로 설명
common/rpms/ 공통 의존성 RPM (모든 노드)
k8s/rpms/ kubeadm, kubelet, kubectl, containerd RPM
k8s/binaries/ helm, cri-dockerd 등 바이너리
k8s/images/ kubeadm, Calico 등 컨테이너 이미지 .tar
k8s/charts/ Helm 차트
k8s/utils/ calico.yaml 등 매니페스트

Phase 0: 설치 파일 배포 (Bastion → 전체 노드)

# 배포 대상 노드 IP 목록 (환경에 맞게 수정)
NODES=("<MASTER1_IP>" "<MASTER2_IP>" "<MASTER3_IP>" "<WORKER1_IP>" "<WORKER2_IP>", "<WORKER3_IP>")

for IP in "${NODES[@]}"; do
    echo "Sending to $IP..."
    scp ~/k8s-1.30.tar.gz rocky@$IP:~/
done

# 모든 노드에서 압축 해제
tar -zxvf ~/k8s-1.30.tar.gz

Phase 1: 공통 RPM 설치 (전체 노드)

# 1. 공통 의존성 RPM 설치
sudo dnf localinstall -y --disablerepo='*' common/rpms/*.rpm

# 2. kubeadm, kubelet, kubectl, containerd RPM 설치
sudo dnf localinstall -y --disablerepo='*' k8s/rpms/*.rpm

# 3. kubelet 활성화 (kubeadm init 전에는 시작하지 않아도 됨)
sudo systemctl enable kubelet

Phase 2: OS 사전 설정 (전체 노드)

# 1. SELinux permissive 모드
sudo setenforce 0
sudo sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

# 2. 커널 모듈 로드
sudo modprobe overlay
sudo modprobe br_netfilter

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

# 3. sysctl 설정
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

# 4. hosts 파일 등록 (환경에 맞게 수정)
sudo tee -a /etc/hosts <<EOF
<MASTER1_IP> <MASTER1_HOSTNAME>
<MASTER2_IP> <MASTER2_HOSTNAME>
<MASTER3_IP> <MASTER3_HOSTNAME>
<WORKER1_IP> <WORKER1_HOSTNAME>
<WORKER2_IP> <WORKER2_HOSTNAME>
<WORKER3_IP> <WORKER3_HOSTNAME>
EOF

Phase 3: containerd 설정 (전체 노드)

# containerd 기본 설정 생성
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml

# cgroup driver를 systemd로 변경 (필수)
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# Harbor 인증서 경로 설정
sudo sed -i "s|config_path = '/etc/containerd/certs.d:/etc/docker/certs.d'|config_path = '/etc/containerd/certs.d'|g" /etc/containerd/config.toml

# containerd 시작 및 활성화
sudo systemctl enable --now containerd
sudo systemctl status containerd

(선택) containerd 데이터 경로 변경 — 소프트링크 방식

OS 루트 디스크 용량이 작고 별도 데이터 디스크(예: /app)가 마운트되어 있는 경우에 적용합니다. containerd 시작 전 또는 서비스를 중지한 상태에서 진행해야 합니다.

# 서비스 중지 (이미 실행 중인 경우)
sudo systemctl stop kubelet
sudo systemctl stop containerd

# 1. 실제 데이터를 저장할 디렉토리 생성 (경로는 환경에 맞게 수정)
sudo mkdir -p /app/containerd_data

# 2. 기존 데이터가 있으면 이동 (처음 설치라면 /var/lib/containerd 자체가 없으므로 자동 skip)
if [ -d "/var/lib/containerd" ]; then
    sudo mv /var/lib/containerd/* /app/containerd_data/
    sudo rmdir /var/lib/containerd
fi

# 3. 소프트링크 생성: /var/lib/containerd -> /app/containerd_data
sudo ln -s /app/containerd_data /var/lib/containerd

# 4. 링크 확인 (화살표가 표시되어야 함)
ls -ld /var/lib/containerd
# 결과 예시: lrwxrwxrwx ... /var/lib/containerd -> /app/containerd_data

# 5. 서비스 재시작
sudo systemctl start containerd
sudo systemctl start kubelet

config.tomlroot 값을 직접 변경하는 방법도 있지만, 소프트링크 방식은 기존 경로를 그대로 유지하므로 다른 툴과의 호환성을 더 쉽게 확보할 수 있습니다.

containerd 재시작 후에도 SystemdCgroup = true 가 적용되지 않으면 아래 명령으로 확인하세요.

grep SystemdCgroup /etc/containerd/config.toml

Phase 4: 이미지 로드 (전체 노드)

for tar_file in k8s/images/*.tar; do
    echo "Loading $tar_file..."
    sudo ctr -n k8s.io images import "$tar_file"
done

# 확인
sudo ctr -n k8s.io images list | grep kube-apiserver

Phase 5: 로드밸런서 구성 (HA 3중화 시에만 / 단일 구성이면 Phase 6으로)

HA 구성을 위해 로드밸런서가 필요합니다. 환경에 따라 아래 두 가지 방식 중 하나를 선택합니다.

[사전 결정] VIP 주소를 인증서에 직접 설정할지, FQDN으로 추상화할지 먼저 결정하세요.

이 선택은 이후 kubeadm init--control-plane-endpoint 및 인증서 SAN에 영향을 미치므로 설치를 시작하기 전에 결정해야 합니다.

방식 장점 단점
FQDN (k8s-api.internal) ← 권장 VIP 변경 시 /etc/hosts만 수정, 인증서 재발급 불필요 /etc/hosts 관리 필요
IP 직접 사용 설정 단순 VIP 변경 시 인증서 재발급 필수

FQDN 방식을 선택하면 5-A-1에서 바로 /etc/hosts 등록을 먼저 수행합니다. IP 직접 사용 방식이면 5-A-1을 건너뛰고 5-A-2부터 시작합니다.

옵션 A: VIP 방식 (표준, 권장)

Master 3대와 가상 IP(VIP) 환경을 가정합니다. VIP를 K8s API Server(6443) 앞단에 두어 마스터 노드 장애 시에도 API 통신이 끊기지 않게 합니다.

5-A-1. (FQDN 방식 선택 시) FQDN 등록 (전체 노드)

VIP IP를 직접 사용하는 대신 내부 FQDN(k8s-api.internal)으로 추상화합니다. 나중에 VIP가 변경되어도 인증서 재발급 없이 DNS 서버 혹은 /etc/hosts만 수정하면 됩니다.

내부 DNS 서버가 있다면 관리자에게 요청하여 아래 내용을 추가합니다.

  • 레코드 이름: k8s-api.internal
  • IP 주소: VIP

만약 DNS 서버가 없다면 hosts 파일에 등록합니다.

전체 노드(마스터 + 워커)에서 실행합니다.

echo "<VIP>  k8s-api.internal" | sudo tee -a /etc/hosts

HAProxy의 bind는 안정성을 위해 VIP IP(<VIP>:6443)를 그대로 사용합니다. FQDN은 kubeconfig의 server 주소와 인증서 SAN에만 적용됩니다.

5-A-2. 커널 파라미터 설정 (전체 마스터 노드)

VIP가 자신의 인터페이스에 없어도 바인딩할 수 있도록 설정합니다.

cat <<EOF | sudo tee /etc/sysctl.d/haproxy.conf
net.ipv4.ip_nonlocal_bind = 1
EOF

sudo sysctl --system

5-A-3. HAProxy 설정 (전체 마스터 노드)

sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.bak

cat <<EOF | sudo tee /etc/haproxy/haproxy.cfg
global
    log         127.0.0.1 local2
    maxconn     4000
    daemon

defaults
    mode                    tcp
    log                     global
    option                  tcplog
    timeout connect         10s
    timeout client          1m
    timeout server          1m

# Kubernetes API Server LB
frontend k8s-api
    bind <VIP>:6443      # TODO 실제 VIP로 변경 필요
    mode tcp
    option tcplog
    default_backend k8s-masters

# TODO 실제 MASTER_IP, HOSTNAME 변경 필요
backend k8s-masters
    mode tcp
    balance roundrobin
    option tcp-check
    server <MASTER1_HOSTNAME> <MASTER1_IP>:6443 check fall 3 rise 2
    server <MASTER2_HOSTNAME> <MASTER2_IP>:6443 check fall 3 rise 2
    server <MASTER3_HOSTNAME> <MASTER3_IP>:6443 check fall 3 rise 2
EOF

5-A-4. Keepalived 설정 (전체 마스터 노드)

각 마스터 노드별로 state, priority, interface 값을 다르게 설정합니다.

노드 state priority
Master-1 MASTER 101
Master-2 BACKUP 100
Master-3 BACKUP 99
# Master-1 기준 예시 (Master-2, 3은 state/priority 값 수정)
cat <<EOF | sudo tee /etc/keepalived/keepalived.conf
global_defs {
    router_id LVS_DEVEL
}

vrrp_script check_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 3
    weight -2
    fall 10
    rise 2
}

vrrp_instance VI_1 {
    state MASTER              # TODO Master-2, 3은 BACKUP
    interface eth0            # TODO 본인 네트워크 인터페이스명으로 변경 필수
    virtual_router_id 51
    priority 101              # TODO M1: 101, M2: 100, M3: 99
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass 42          # 모든 노드 동일하게 설정
    }

    virtual_ipaddress {
        <VIP>          # TODO VIP 주소
    }

    track_script {
        check_haproxy
    }
}
EOF

5-A-5. 서비스 시작 및 VIP 확인

sudo systemctl enable --now haproxy
sudo systemctl enable --now keepalived

# VIP 활성화 확인 (Master-1에서 VIP가 보여야 함)
ip addr show eth0 | grep VIP

옵션 B: Localhost LB 방식 (VIP 사용 불가 환경)

VIP를 사용할 수 없는 환경에서 각 노드에 HAProxy를 띄워 Loopback(127.0.0.1:8443)으로 통신합니다. 전체 마스터 및 워커 노드에 동일하게 설정합니다.

sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.bak

cat <<EOF | sudo tee /etc/haproxy/haproxy.cfg
global
    maxconn     4000
    daemon

defaults
    mode                    tcp
    timeout connect         10s
    timeout client          1m
    timeout server          1m

frontend k8s-api-proxy
    bind 127.0.0.1:8443
    default_backend k8s-masters

backend k8s-masters
    balance roundrobin
    option tcp-check
    server <MASTER1_HOSTNAME> <MASTER1_IP>:6443 check
    server <MASTER2_HOSTNAME> <MASTER2_IP>:6443 check
    server <MASTER3_HOSTNAME> <MASTER3_IP>:6443 check
EOF

sudo systemctl enable --now haproxy

Phase 6: kubeadm init (Master-1)

옵션 A: HA(3중화) 구성 (VIP 사용)

--apiserver-cert-extra-sans에 VIP와 전체 마스터 IP를 포함해야 RHEL/Rocky 9계열의 엄격한 SAN 검증을 통과할 수 있습니다.

pod-network-cidrservice-cidr 는 현재 기본값으로 되어있습니다. 환경에 따라 해당 네트워크 대역 사용이 불가하다면 변경해야 합니다.

FQDN을 사용하는 경우(5-A-1 적용 시) VIP 대신 k8s-api.internal로 대체합니다.

HAProxy 포트 충돌 주의 HAProxy가 VIP:6443을 점유하고 있으면 kubeadm init이 같은 포트를 열려다 실패합니다. kubeadm init 전에 HAProxy를 잠시 중지하고, API 서버 bind-address 설정 후 다시 시작합니다.

# 1. HAProxy 일시 중지
sudo systemctl stop haproxy

# 2. kubeadm init — VIP IP 직접 사용 시
sudo kubeadm init \
  --control-plane-endpoint "<VIP>:6443" \
  --upload-certs \
  --apiserver-cert-extra-sans="<VIP>,<MASTER1_IP>,<MASTER2_IP>,<MASTER3_IP>,127.0.0.1" \
  --pod-network-cidr=192.168.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --kubernetes-version v1.30.0

# 2. kubeadm init — FQDN 사용 시 (권장)
sudo kubeadm init \
  --control-plane-endpoint "k8s-api.internal:6443" \
  --upload-certs \
  --apiserver-cert-extra-sans="k8s-api.internal,<VIP>,<MASTER1_IP>,<MASTER2_IP>,<MASTER3_IP>,127.0.0.1" \
  --pod-network-cidr=192.168.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --kubernetes-version v1.30.0

# 3. API 서버 bind-address를 Master-1의 실제 IP로 수정
#    (설정하지 않으면 API 서버가 0.0.0.0으로 바인딩되어 HAProxy와 다시 충돌)
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
# spec.containers[].command 섹션에 추가:
# - --bind-address=<MASTER1_IP>

# 4. API 서버가 노드 IP로 재기동된 것을 확인 후 HAProxy 시작
sudo crictl pods --namespace kube-system | grep apiserver   # Running 확인
sudo systemctl start haproxy

# 5. kubeconfig 설정
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

옵션 B: 단일 구성

sudo kubeadm init \
  --control-plane-endpoint "<MASTER_IP>:6443" \
  --pod-network-cidr=192.168.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --kubernetes-version v1.30.0

# kubeconfig 설정
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Phase 6-1: 추가 마스터 노드 조인 (Master-2, 3 — HA 구성 시에만)

Master-1 초기화 출력에서 --control-plane 조인 명령을 복사하여 실행합니다. Master-2, 3에도 HAProxy가 VIP:6443을 점유하고 있으므로 조인 전에 중지합니다.

# 1. HAProxy 일시 중지
sudo systemctl stop haproxy

# 2. 컨트롤 플레인 조인
sudo kubeadm join <VIP>:6443 --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH> \
    --control-plane --certificate-key <CERT_KEY>

# 3. bind-address를 해당 노드 실제 IP로 수정
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
# Master-2: - --bind-address=<MASTER2_IP>
# Master-3: - --bind-address=<MASTER3_IP>

# 4. API 서버 재기동 확인 후 HAProxy 시작
sudo crictl pods --namespace kube-system | grep apiserver   # Running 확인
sudo systemctl start haproxy

kubeconfig도 각 노드에 설정합니다.

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

Phase 7: Calico CNI 설치 (Master-1)

--pod-network-cidr을 기본값(192.168.0.0/16)에서 변경한 경우, calico.yaml에서 CALICO_IPV4POOL_CIDR 항목을 찾아 주석을 해제하고 값을 수정한 뒤 적용합니다. --service-cidr는 Calico가 관리하지 않으므로 변경 불필요합니다.

# 라인 번호 확인
grep -n 'CALICO_IPV4POOL_CIDR' k8s/utils/calico.yaml

해당 라인으로 이동해 주석(#)을 제거하고 값을 수정합니다.

# 변경 전 (주석 처리된 상태)
# - name: CALICO_IPV4POOL_CIDR
#   value: "192.168.0.0/16"

# 변경 후 (주석 해제 + CIDR 수정)
- name: CALICO_IPV4POOL_CIDR
  value: "<YOUR_POD_CIDR>"
kubectl apply -f k8s/utils/calico.yaml

# Calico Pod가 Running이 될 때까지 대기
kubectl get pods -n kube-system -w

Phase 8: Helm 설치 (컨트롤 플레인 노드)

cd k8s/binaries
tar -xzvf helm-v3.14.0-linux-amd64.tar.gz
sudo mv linux-amd64/helm /usr/local/bin/helm
helm version

Phase 9: 워커 노드 조인

Master-1의 kubeadm init 출력에서 워커 조인 명령을 복사하여 실행합니다.

sudo kubeadm join <CONTROL_PLANE_ENDPOINT>:6443 \
  --token <TOKEN> \
  --discovery-token-ca-cert-hash sha256:<HASH>

Phase 10: 설치 확인

kubectl get nodes
kubectl get pods -n kube-system

모든 노드가 Ready 상태이고 kube-system Pod들이 Running 이면 설치 완료입니다.

# HA 구성 시 CIDR 설정 확인
kubectl get pod -n kube-system -l component=kube-controller-manager -o yaml | grep cluster

# Calico IP Pool 확인
kubectl get ippools -o yaml

재설치 시 초기화

오류 발생 등으로 재설치가 필요한 경우 아래 순서로 초기화합니다.

# 1. kubeadm reset
sudo kubeadm reset -f

# 2. CNI 및 kube 설정 파일 삭제
sudo rm -rf /etc/cni/net.d
rm -rf $HOME/.kube
sudo rm -rf /root/.kube

# 3. etcd, kubelet 데이터 삭제
sudo rm -rf /var/lib/etcd
sudo rm -rf /var/lib/kubelet

# 4. iptables 규칙 초기화
sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F && sudo iptables -X

# 5. containerd 재시작
sudo systemctl restart containerd

VIP 변경 시 조치

운영 중 VIP 대역이 변경되거나 새로운 IP를 할당받아야 하는 경우의 절차입니다.

케이스 0: 운영 중인 클러스터를 IP → FQDN으로 전환

이미 VIP IP로 초기 구성한 클러스터에 FQDN을 사후 적용하는 절차입니다. 이후 VIP가 변경되면 케이스 A 절차만으로 처리할 수 있게 됩니다.

1단계: 모든 노드에 FQDN 등록 (마스터 + 워커)

DNS 서버가 운영 중이라면 DNS 서버에 등록합니다.

echo "<OLD_VIP>  k8s-api.internal" | sudo tee -a /etc/hosts

2단계: API 서버 인증서에 FQDN SAN 추가 (전체 마스터 노드)

# 기존 인증서 백업
sudo cp /etc/kubernetes/pki/apiserver.crt ~/apiserver.crt.bak
sudo cp /etc/kubernetes/pki/apiserver.key ~/apiserver.key.bak

# 삭제 후 FQDN 포함하여 재발급
sudo rm /etc/kubernetes/pki/apiserver.crt /etc/kubernetes/pki/apiserver.key
sudo kubeadm init phase certs apiserver \
  --control-plane-endpoint "k8s-api.internal:6443" \
  --apiserver-cert-extra-sans="k8s-api.internal,<OLD_VIP>,<MASTER1_IP>,<MASTER2_IP>,<MASTER3_IP>,127.0.0.1"

# FQDN이 SAN에 포함되었는지 확인
openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -text | grep -A1 "Subject Alternative"

3단계: kube-apiserver 재시작 (전체 마스터 노드)

sudo mv /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/
sleep 10
sudo mv /tmp/kube-apiserver.yaml /etc/kubernetes/manifests/

watch sudo crictl pods --namespace kube-system

4단계: kubeconfig 및 kubelet.conf 업데이트 (전체 마스터 노드)

for conf in /etc/kubernetes/admin.conf \
            /etc/kubernetes/controller-manager.conf \
            /etc/kubernetes/scheduler.conf \
            /etc/kubernetes/kubelet.conf; do
    sudo sed -i "s|https://<OLD_VIP>:6443|https://k8s-api.internal:6443|g" "$conf"
done
sudo systemctl restart kubelet

# 현재 사용자 kubeconfig 갱신
cp /etc/kubernetes/admin.conf ~/.kube/config

5단계: 워커 노드 kubelet.conf 업데이트 (전체 워커 노드)

sudo sed -i 's|https://<OLD_VIP>:6443|https://k8s-api.internal:6443|g' /etc/kubernetes/kubelet.conf
sudo systemctl restart kubelet

6단계: 클러스터 내부 ConfigMap 갱신 (Master-1에서 1회 실행)

로컬 파일 수정과 별개로, 클러스터 내부 etcd에 저장된 엔드포인트도 갱신해야 합니다. 이를 누락하면 kube-proxy 파드가 재시작될 때 구 VIP로 접속을 시도하여 CrashLoopBackOff가 발생할 수 있습니다.

# kube-proxy ConfigMap 갱신
kubectl get configmap kube-proxy -n kube-system -o yaml | \
  sed 's|<OLD_VIP>:6443|k8s-api.internal:6443|g' | \
  kubectl apply -f -

# 변경 사항 적용을 위해 kube-proxy 파드 롤아웃
kubectl rollout restart daemonset kube-proxy -n kube-system

# (권장) kubeadm-config ConfigMap 갱신 — 추후 kubeadm upgrade 시 필요
kubectl get configmap kubeadm-config -n kube-system -o yaml | \
  sed 's|<OLD_VIP>:6443|k8s-api.internal:6443|g' | \
  kubectl apply -f -

7단계: 확인

kubectl get nodes
kubectl cluster-info

Kubernetes control plane 주소가 https://k8s-api.internal:6443으로 표시되면 완료입니다.


케이스 A: FQDN 방식으로 초기 구성한 경우 (권장 구성)

FQDN(k8s-api.internal)이 인증서 SAN에 포함되어 있으므로, 인증서 재발급 없이 아래 순서만 따르면 됩니다.

1단계: 모든 노드의 /etc/hosts 업데이트 (마스터 + 워커)

DNS 서버가 운영 중이라면 DNS 서버에 등록합니다.

# <OLD_VIP> → <NEW_VIP> 로 변경
sudo sed -i 's/<OLD_VIP>  k8s-api.internal/<NEW_VIP>  k8s-api.internal/' /etc/hosts

# 확인
grep k8s-api.internal /etc/hosts

2단계: Keepalived VIP 변경 (전체 마스터 노드)

sudo sed -i 's/<OLD_VIP>/<NEW_VIP>/' /etc/keepalived/keepalived.conf
sudo systemctl restart keepalived

# 새 VIP 활성화 확인 (Master-1)
ip addr show eth0 | grep <NEW_VIP>

3단계: HAProxy bind IP 변경 (전체 마스터 노드)

sudo sed -i 's/<OLD_VIP>:6443/<NEW_VIP>:6443/' /etc/haproxy/haproxy.cfg
sudo systemctl restart haproxy

backend k8s-mastersserver 항목(마스터 노드 IP)은 변경하지 않습니다.

4단계: API 서버 재시작 확인

# kubeconfig의 server 주소는 FQDN이므로 변경 불필요
kubectl get nodes

케이스 B: VIP IP를 직접 사용하여 초기 구성한 경우

인증서 SAN에 기존 VIP IP가 고정되어 있으므로, 인증서 재발급이 필수입니다. 전체 마스터 노드에서 순서대로 진행합니다.

1단계: Keepalived / HAProxy VIP 변경 (전체 마스터 노드)

sudo sed -i 's/<OLD_VIP>/<NEW_VIP>/' /etc/keepalived/keepalived.conf
sudo systemctl restart keepalived

sudo sed -i 's/<OLD_VIP>:6443/<NEW_VIP>:6443/' /etc/haproxy/haproxy.cfg
sudo systemctl restart haproxy

2단계: API 서버 인증서 재발급 (전체 마스터 노드)

# 기존 인증서 백업
sudo cp /etc/kubernetes/pki/apiserver.crt ~/apiserver.crt.bak
sudo cp /etc/kubernetes/pki/apiserver.key ~/apiserver.key.bak

# 삭제 후 재발급 (새 VIP 포함)
sudo rm /etc/kubernetes/pki/apiserver.crt /etc/kubernetes/pki/apiserver.key
sudo kubeadm init phase certs apiserver \
  --control-plane-endpoint "<NEW_VIP>:6443" \
  --apiserver-cert-extra-sans="<NEW_VIP>,<MASTER1_IP>,<MASTER2_IP>,<MASTER3_IP>,127.0.0.1"

3단계: kube-apiserver 재시작 (전체 마스터 노드)

static pod는 manifest를 잠시 제거했다가 복원하면 자동 재시작됩니다.

sudo mv /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/
sleep 10
sudo mv /tmp/kube-apiserver.yaml /etc/kubernetes/manifests/

# Pod가 다시 Running 상태가 될 때까지 대기
watch sudo crictl pods --namespace kube-system

4단계: kubeconfig 및 kubelet.conf 업데이트 (전체 마스터 노드)

# 마스터 노드 kubeconfig + kubelet.conf 업데이트
for conf in /etc/kubernetes/admin.conf \
            /etc/kubernetes/controller-manager.conf \
            /etc/kubernetes/scheduler.conf \
            /etc/kubernetes/kubelet.conf; do
    sudo sed -i "s|https://<OLD_VIP>:6443|https://<NEW_VIP>:6443|g" "$conf"
done
sudo systemctl restart kubelet

# 현재 사용자 kubeconfig 갱신
cp /etc/kubernetes/admin.conf ~/.kube/config

5단계: 워커 노드 kubelet.conf 업데이트 (전체 워커 노드)

sudo sed -i 's|https://<OLD_VIP>:6443|https://<NEW_VIP>:6443|g' /etc/kubernetes/kubelet.conf
sudo systemctl restart kubelet

6단계: 클러스터 내부 ConfigMap 갱신 (Master-1에서 1회 실행)

로컬 파일 수정과 별개로, 클러스터 내부 etcd에 저장된 엔드포인트도 갱신해야 합니다. 이를 누락하면 kube-proxy 파드가 재시작될 때 구 VIP로 접속을 시도하여 CrashLoopBackOff가 발생할 수 있습니다.

# kube-proxy ConfigMap 갱신
kubectl get configmap kube-proxy -n kube-system -o yaml | \
  sed 's|<OLD_VIP>:6443|<NEW_VIP>:6443|g' | \
  kubectl apply -f -

# 변경 사항 적용을 위해 kube-proxy 파드 롤아웃
kubectl rollout restart daemonset kube-proxy -n kube-system

# (권장) kubeadm-config ConfigMap 갱신 — 추후 kubeadm upgrade 시 필요
kubectl get configmap kubeadm-config -n kube-system -o yaml | \
  sed 's|<OLD_VIP>:6443|<NEW_VIP>:6443|g' | \
  kubectl apply -f -

7단계: 정상 동작 확인

kubectl get nodes
kubectl get pods -n kube-system

모든 노드가 Ready 상태이고 kube-system Pod가 Running이면 완료입니다.