콘텐츠로 이동

Kubernetes v1.33.11 오프라인 설치 가이드 (Ubuntu 24.04)

폐쇄망 환경에서 kubeadm 기반 Kubernetes v1.33.11 클러스터를 구성하는 수동 절차입니다. containerd v2.2.x를 컨테이너 런타임으로, CNI는 Calico(+ Envoy Gateway) 또는 Cilium 중 선택합니다.

스크립트를 이용한 빠른 설치는 아래 스크립트 사용 가이드 섹션을 먼저 참고하세요. 수동 절차(Phase 0~10)는 내부 동작 이해 및 트러블슈팅용입니다.

온라인 설치는 install-guide-online.md를 참고하세요.

설치 전 체크리스트

아래 항목은 단일 구성과 HA 구성 모두 설치 전에 확정합니다. 확정한 값은 install.conf, /etc/hosts, 로드밸런서 설정, kubeadm init 인자에 동일하게 반영해야 합니다.

1. 구성 및 작업 범위

  • [ ] 구성 유형 확정: WSL2 단일 노드, 단일 컨트롤 플레인, HA 3중화, 물리 서버, 가상 서버 여부
  • [ ] 노드별 역할, hostname, 관리 IP, Kubernetes 통신 NIC, gateway, DNS, NTP 서버 확정
  • [ ] 작업 가능 시간과 금지 작업 확인: reboot, 네트워크 서비스 재시작, 방화벽 reload, Docker/containerd 재시작, OS 전체 업데이트
  • [ ] 기존 서비스, 기존 컨테이너, 6443 포트 사용 서비스, NodePort 사용 서비스, HAProxy/keepalived 사용 여부 확인

2. 네트워크 대역 및 엔드포인트

  • [ ] Pod CIDR 확정: /20 권장, 최소 /22 수준으로 환경 규모와 CNI 선택에 맞게 조정
  • [ ] Service CIDR 확정: /24 가능, 여유가 필요하면 /22 권장
  • [ ] Pod CIDR과 Service CIDR이 서버 실제 IP, 사내망, VPN, DB망, 관리망, 백업망, 스토리지망과 겹치지 않음 확인
  • [ ] API endpoint 확정: 단일 노드 IP, VIP, FQDN 중 선택하고 인증서 SAN, DNS 또는 /etc/hosts 반영 방식 확정
  • [ ] VIP 사용 시 미사용 IP 여부, 로드밸런서 방식, keepalived VRRP protocol 112 허용 여부, HAProxy 6443 bind 충돌 여부 확인

3. 방화벽 및 포트

  • [ ] Control Plane 포트 허용: 6443/TCP, 2379-2380/TCP, 10250/TCP, 10257/TCP, 10259/TCP
  • [ ] Worker 포트 허용: 10250/TCP, 10256/TCP, NodePort 기본 범위 30000-32767/TCP,UDP
  • [ ] CNI, Ingress, 스토리지, Harbor, DNS, NTP 등 부가 구성에서 필요한 추가 포트 확인
  • [ ] 방화벽 flush, iptables/nftables 초기화, firewalld reload가 금지된 환경이면 허용 규칙 방식으로 사전 합의

4. 런타임, OS, 폐쇄망 자산

  • [ ] Docker/containerd 설치 상태, 기존 컨테이너 사용 여부, containerd 또는 Docker 재시작 가능 여부 확인
  • [ ] kubelet과 containerd의 cgroup driver를 systemd로 맞출 수 있는지 확인
  • [ ] OS 버전, 커널 버전, Kubernetes 버전, containerd 버전, CNI 버전의 호환성 확인
  • [ ] swap 비활성화 가능 여부, 시간 동기화 상태, 디스크 여유 공간, inode 여유, hostname/MAC address/product UUID 중복 없음 확인
  • [ ] 폐쇄망 반입 자산 확인: DEB, 바이너리, 이미지, 매니페스트, Helm chart, Harbor 인증서와 레지스트리 접속 정보

5. 설치 진행 판단

  • [ ] 미확정 항목이 없고, 운영 영향이 있는 조치의 승인 범위가 명확한지 확인
  • [ ] 장애 발생 시 되돌릴 범위와 담당자 연락 경로 확인
  • [ ] API Server 설정, etcd TLS, 인증서 취약점은 [Kubernetes 취약점 점검 및 보완 가이드](../../../security/005-vulnerability-check-remediation.md) 기준으로 점검

스크립트 사용 가이드

자동 / 수동 처리 범위

작업 단일 구성 HA 구성
DEB 설치 · OS 설정 · containerd · 이미지 로드 ✅ 자동 ✅ 자동
kubeadm init / join ✅ 자동 ✅ 자동
kubeconfig 설정 ✅ 자동 ✅ 자동
CNI 설치 (auto 선택 시) ✅ 자동 ✅ 자동
HAProxy 중지/재시작 (충돌 방지) ✅ 자동 (설치된 경우 감지)
HAProxy · Keepalived 설치 및 설정 🔧 수동 (Phase 5)
kube-apiserver --bind-address 설정 🔧 수동 (각 마스터 init/join 직후)
/etc/hosts FQDN 등록 🔧 수동 (FQDN 방식 선택 시)

스크립트 목록

스크립트 실행 위치 설명
scripts/download_assets_offline.sh 인터넷 호스트 (root) 오프라인 설치 파일 수집 → k8s/ 채움
scripts/wsl2_prep.sh WSL2 노드 (root) systemd 활성화 + iptables-legacy 전환
scripts/install.sh Master-1 (root) 컨트롤 플레인 설치 (CNI·Gateway 포함)
scripts/install.sh --join <token> <hash> <ep> Worker (root) 워커 노드 합류
scripts/install.sh --join <token> <hash> <ep> --control-plane <cert-key> Master-2, 3 (root) 추가 마스터 합류
scripts/uninstall.sh 모든 노드 (root) 클러스터 초기화

구성 유형별 실행 순서

단일 구성 (WSL2 / 단일 VM)

[WSL2만] scripts/wsl2_prep.sh  →  wsl --shutdown 재기동
[Master-1] scripts/install.sh
    - 환경 확인 (wsl2 / vm)
    - CNI 선택 + Pod CIDR
    - CNI 설치 모드 (auto / manual)
    - Service CIDR
    - 컨트롤 플레인 엔드포인트 (노드 IP, WSL2는 자동 감지)
완료 — 클러스터 Ready

HA 구성 (Master 3대 + Worker N대)

[전체 노드] 파일 배포 (scp + tar 해제)

[전체 마스터] 🔧 수동: Phase 5 — HAProxy + Keepalived 설치/설정 + VIP 확인

[전체 마스터, FQDN 방식] 🔧 수동: /etc/hosts 에 VIP → FQDN 등록

[Master-1] scripts/install.sh
    - 환경 확인 (vm)
    - CNI 선택 + Pod CIDR
    - CNI 설치 모드
    - Service CIDR
    - 컨트롤 플레인 엔드포인트: VIP 또는 FQDN 입력
    ※ HAProxy 자동 중지 → kubeadm init → HAProxy 자동 재시작
[Master-1] 🔧 수동: kube-apiserver bind-address 설정 (아래 참고)
[Master-2, 3] scripts/install.sh --join <token> <hash> <endpoint> --control-plane <cert-key>
    ※ HAProxy 자동 중지 → kubeadm join → HAProxy 자동 재시작
[Master-2, 3] 🔧 수동: kube-apiserver bind-address 설정 (아래 참고)
[Worker 1~N] scripts/install.sh --join <token> <hash> <endpoint>
완료 — 클러스터 Ready

install.sh 완료 시 워커/마스터 합류 명령(실제 토큰·hash 포함)이 자동 출력됩니다. certificate-key는 1시간 유효입니다. 만료 시 Master-1에서 재생성:

kubeadm init phase upload-certs --upload-certs

Step 1 — 파일 수집 (인터넷 호스트)

cd k8s-1.33.11-ubuntu24.04
sudo ./scripts/download_assets_offline.sh
# 완료 후 tar 묶음 생성 안내가 출력됨

cd ..
tar czf k8s-1.33.11-ubuntu24.04.tar.gz k8s-1.33.11-ubuntu24.04/

Step 2 — WSL2 전용: systemd 활성화 (최초 1회)

sudo ./scripts/wsl2_prep.sh
# "재기동 필요" 안내가 나오면 Windows 터미널에서:
#   wsl --shutdown
# 이후 WSL2 재진입 후 Step 3 진행

Step 3 — 컨트롤 플레인 설치 (Master-1)

⚠️ HA(3중화) 구성이라면 install.sh 실행 전에 반드시 Phase 5(로드밸런서 구성)를 먼저 완료하세요. HAProxy + Keepalived로 VIP를 확보한 뒤, 아래 스크립트 실행 시 엔드포인트에 VIP 또는 FQDN을 입력해야 합니다. 단일 구성(WSL2 / 단일 컨트롤 플레인)이라면 이 주의사항은 무시하세요.

sudo ./scripts/install.sh
# 대화형 메뉴:
#   1) 환경 확인 (wsl2 / vm)
#   2) CNI 선택 (calico / cilium) + Pod CIDR
#   3) CNI 설치 모드 (auto / manual)
#   4) Calico 설치 방식 (manifest / operator, calico 선택 시)
#   5) Envoy Gateway 모드 (calico+auto 선택 시)
#   6) Service CIDR
#   7) 컨트롤 플레인 엔드포인트
#      → 단일: 노드 IP (WSL2는 자동 감지)
#      → HA  : VIP 또는 FQDN

HA 구성이라면 완료 후 즉시 아래를 수동으로 수행합니다.

🔧 [HA 전용] kube-apiserver bind-address 설정

kube-apiserver가 기본 0.0.0.0으로 바인딩되면 HAProxy와 포트 충돌이 발생합니다. 이 노드의 실제 IP로 고정하세요.

# 이 노드의 실제 IP 확인
ip -4 -o addr show | grep -v '127\.' | awk '{print $4}' | cut -d/ -f1

# kube-apiserver 매니페스트 편집
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
# spec.containers[].command 섹션에 추가:
# - --bind-address=<이 노드의 실제 IP>   # 예: 192.168.10.11

저장 후 kubelet이 자동으로 apiserver를 재기동합니다. 약 30초 후 확인:

sudo crictl --runtime-endpoint unix:///run/containerd/containerd.sock pods \
    --namespace kube-system | grep apiserver
# Running 확인 후 HAProxy 정상 동작 확인
curl -k https://<VIP>:6443/livez

Step 4 — 추가 마스터 합류 (Master-2, 3 — HA 구성 시에만)

install.sh 완료 시 출력된 명령어를 그대로 사용합니다. certificate-key가 만료됐다면 Master-1에서 재생성 후 사용하세요.

# Master-2, Master-3 각각에서 실행
sudo ./scripts/install.sh --join <token> <hash> <endpoint> --control-plane <cert-key>

각 노드 완료 후 즉시 위 Step 3의 🔧 bind-address 설정을 동일하게 수행합니다. (각 노드의 실제 IP가 다르므로 노드별로 각각 실행)

Step 5 — 워커 노드 합류

# Worker 각각에서 실행
sudo ./scripts/install.sh --join <token> <hash> <endpoint>

Step 6 — 언인스톨

명령 동작
sudo ./scripts/uninstall.sh 대화형 확인 후 클러스터 상태 초기화
sudo ./scripts/uninstall.sh --yes 확인 생략 (install.sh 재설치 흐름에서 자동 호출)
sudo ./scripts/uninstall.sh --purge 클러스터 초기화 + DEB 패키지(kubeadm/kubelet/kubectl/containerd.io)까지 제거

초기화 후에도 kubectl / kubelet 바이너리는 남아있습니다. 폐쇄망 환경에서 재설치 시 DEB를 다시 가져올 수 없으므로 패키지는 기본적으로 유지합니다. 바이너리까지 완전히 제거하려면 --purge 옵션을 사용하세요.

sudo ./scripts/uninstall.sh          # 클러스터만 초기화 (바이너리 유지)
sudo ./scripts/uninstall.sh --yes    # 확인 생략
sudo ./scripts/uninstall.sh --purge  # 바이너리까지 완전 제거

설정 저장 (install.conf)

install.sh 실행 시 선택한 설정은 install.conf에 자동 저장됩니다. 재실행 시 저장된 값이 기본값으로 적용되어 동일 설정으로 빠르게 재설치 가능합니다.


전제 조건

  • Ubuntu 24.04 LTS (noble) 노드
  • WSL2 단일 노드: 로컬 개발/검증용
  • 폐쇄망 단일 구성: 컨트롤 플레인 1대 + 워커 1대 이상
  • 폐쇄망 HA 구성: 컨트롤 플레인 3대 + 워커 1대 이상 + VIP 1개
  • 모든 노드에서 아래 설치 파일 접근 가능
  • swap 비활성화 (swapoff -a + /etc/fstab 주석)
  • WSL2의 경우 /etc/wsl.conf[boot] systemd=true 적용 후 재기동 (scripts/wsl2_prep.sh 사용)

디렉토리 구조

경로 설명
k8s/debs/ kubeadm, kubelet, kubectl, cri-tools, containerd.io + 시스템 유틸 DEB
k8s/binaries/ helm, nerdctl tarball
k8s/images/ kubeadm 코어 + Calico 이미지 .tar
k8s/utils/ calico.yaml, tigera-operator.yaml, calico-custom-resources.yaml, local-path-storage.yaml 등 매니페스트
scripts/ download_assets_offline.sh, install.sh, uninstall.sh, wsl2_prep.sh

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

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

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

# 모든 노드에서 압축 해제
tar -zxvf ~/k8s-1.33.11-ubuntu24.04.tar.gz
cd k8s-1.33.11-ubuntu24.04

Phase 0.5: 시간 동기화 설정 (Chrony / systemd-timesyncd) — 전체 노드 필수

Kubernetes 클러스터는 노드 간 시간 동기화가 필수적입니다. 시간이 틀어지면 인증서 유효기간 오류, 클러스터 합류 실패 등이 발생하므로, 설치 전에 모든 노드의 시간을 동기화해야 합니다.

1. 시간 동기화 서비스 확인 및 설정

환경에 따라 Chrony 또는 Ubuntu 기본 데몬인 systemd-timesyncd를 사용할 수 있습니다. 에어갭(폐쇄망) 환경의 경우, 내부망에 구축된 NTP 서버 주소로 설정해야 합니다.

옵션 A: Chrony를 사용하는 경우

/etc/chrony/chrony.conf 파일을 열어 기존 public pool 설정을 주석 처리하고, 내부 NTP 서버 주소를 지정합니다.

sudo vi /etc/chrony/chrony.conf
# 기존 pool/server 설정을 주석 처리하고 내부 NTP 서버 지정
server <INTERNAL_NTP_SERVER_IP> iburst
설정 후 서비스를 재기동하고 활성화합니다.
sudo systemctl enable --now chrony
sudo systemctl restart chrony

옵션 B: systemd-timesyncd를 사용하는 경우

/etc/systemd/timesyncd.conf 파일을 편집하여 NTP 서버를 지정합니다.

sudo vi /etc/systemd/timesyncd.conf
[Time]
NTP=<INTERNAL_NTP_SERVER_IP>
설정 후 서비스를 재기동하고 활성화합니다.
sudo systemctl enable --now systemd-timesyncd
sudo systemctl restart systemd-timesyncd

2. 동기화 상태 확인

모든 노드에서 시스템 클럭 동기화 상태를 최종 검증합니다.

timedatectl status
출력 결과 중 System clock synchronized: yes 상태를 확인합니다. Chrony를 사용하는 경우 chronyc sources 또는 chronyc tracking을 수행하여 연동 상태를 정밀 진입 확인할 수 있습니다.


Phase 1: DEB 설치 (전체 노드)

# kubeadm/kubelet/kubectl/cri-tools/containerd.io + 필수 유틸(conntrack, socat, ebtables, ipset, jq, chrony)
sudo dpkg -i k8s/debs/*.deb

# 의존성 누락 시(인터넷 호스트에서 apt-rdepends로 full graph를 가져왔다면 없어야 함)
sudo apt-get install -f --no-download || true

# kubelet 활성화 (kubeadm init 전에는 시작하지 않음)
sudo systemctl enable kubelet

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

# 1. 커널 모듈
sudo modprobe overlay
sudo modprobe br_netfilter

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

# 2. 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

# 3. swap 비활성화 (영구 박멸)
sudo swapoff -a

# /etc/fstab 내 3번째 필드가 swap인 라인을 안전하게 주석 처리 (.bak 백업 생성)
if [ -f /etc/fstab ]; then
    sudo sed -i.bak -E '/^[[:space:]]*[^#[:space:]]+[[:space:]]+[^#[:space:]]+[[:space:]]+swap[[:space:]]+/ s/^/#/' /etc/fstab
fi

# systemd swap 유닛 목록 및 파일 검색 후 마스킹 (부팅 시 부활 방지)
# 1) systemctl list-units에 잡히는 swap 장치들 마스킹
for unit in $(sudo systemctl list-units --type=swap --all --no-legend --no-pager | grep -oE '\S+\.swap'); do
    if [ -n "$unit" ]; then
        sudo systemctl mask "$unit"
    fi
done

# 2) systemctl list-unit-files에 잡히는 swap 파일들 마스킹
for unit_file in $(sudo systemctl list-unit-files --type=swap --no-legend --no-pager | grep -oE '\S+\.swap'); do
    if [ -n "$unit_file" ]; then
        if [ "$(sudo systemctl is-enabled "$unit_file" 2>/dev/null)" != "masked" ]; then
            sudo systemctl mask "$unit_file"
        fi
    fi
done

# zram (Compressed swap) 비활성화 (사용 중일 경우)
if sudo systemctl is-active zram-generator >/dev/null 2>&1 || sudo systemctl list-unit-files | grep -q zram; then
    sudo systemctl disable --now zram-generator 2>/dev/null || true
    sudo systemctl disable --now zram-config 2>/dev/null || true
fi

sudo systemctl daemon-reload

# 4. 파일 디스크립터(FD) 및 시스템 Limits 상향 (정석 설정)
# K8s 노드의 안정성과 대규모 Pod 구동 시 'Too many open files' 방지를 위해 필수적으로 설정합니다.

# 1) sysctl Limits 설정
cat <<EOF | sudo tee /etc/sysctl.d/99-kubernetes-limits.conf
fs.file-max = 2097152
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 8192
EOF
sudo sysctl --system

# 2) security limits 설정
cat <<EOF | sudo tee /etc/security/limits.d/99-kubernetes-limits.conf
* soft nofile 1048576
* hard nofile 1048576
* soft nproc 1048576
* hard nproc 1048576
root soft nofile 1048576
root hard nofile 1048576
EOF

# 3) kubelet 서비스 Limits 설정 (systemd override)
sudo mkdir -p /etc/systemd/system/kubelet.service.d
cat <<EOF | sudo tee /etc/systemd/system/kubelet.service.d/limits.conf
[Service]
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
EOF
sudo systemctl daemon-reload

# 5. AppArmor 상태 확인 (Ubuntu 24.04 기본 활성)
sudo aa-status | head -5
# containerd 관련 이슈 시: sudo aa-complain /usr/bin/containerd

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

WSL2 추가 항목

# iptables 백엔드를 legacy로 변경 (Cilium/kube-proxy 호환)
sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy

Phase 3: 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

# pause 이미지 3.10으로 통일 (k8s 1.33 기본)
sudo sed -i 's|sandbox_image = ".*"|sandbox_image = "registry.k8s.io/pause:3.10"|' /etc/containerd/config.toml

# Harbor(또는 사설 레지스트리) 인증서 디렉토리 인식 경로 보장
cat <<'EOF' | sudo tee /tmp/ensure-containerd-registry-config-path.sh >/dev/null
#!/usr/bin/env bash
set -euo pipefail

config="/etc/containerd/config.toml"
plugin="io.containerd.grpc.v1.cri"

if containerd --version 2>/dev/null | grep -qE 'containerd .* 2\.'; then
  plugin="io.containerd.cri.v1.images"
fi

mkdir -p /etc/containerd/certs.d

if grep -qE '^[[:space:]]*config_path[[:space:]]*=' "$config"; then
  sed -i 's|^[[:space:]]*config_path[[:space:]]*=.*|  config_path = "/etc/containerd/certs.d"|' "$config"
elif grep -qF "[plugins.\"${plugin}\".registry]" "$config" || grep -qF "[plugins.'${plugin}'.registry]" "$config"; then
  awk -v plugin="$plugin" '
    BEGIN { sq = sprintf("%c", 39); dq = "\""; done = 0 }
    {
      print
      if (!done && ($0 == "[plugins." dq plugin dq ".registry]" || $0 == "[plugins." sq plugin sq ".registry]")) {
        print "  config_path = \"/etc/containerd/certs.d\""
        done = 1
      }
    }
  ' "$config" > "${config}.tmp" && mv "${config}.tmp" "$config"
else
  cat >> "$config" <<CONFIG

[plugins."${plugin}".registry]
  config_path = "/etc/containerd/certs.d"
CONFIG
fi
EOF
sudo bash /tmp/ensure-containerd-registry-config-path.sh
sudo rm -f /tmp/ensure-containerd-registry-config-path.sh

# 4. containerd 서비스 Limits 설정 (systemd override)
sudo mkdir -p /etc/systemd/system/containerd.service.d
cat <<EOF | sudo tee /etc/systemd/system/containerd.service.d/limits.conf
[Service]
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
EOF
sudo systemctl daemon-reload

sudo systemctl enable --now containerd
sudo systemctl status containerd --no-pager

/etc/security/limits.d는 주로 로그인 세션에 적용됩니다. kubeletcontainerd처럼 systemd가 직접 띄우는 서비스는 위 systemd override까지 적용해야 FD/프로세스 limits가 일관되게 반영됩니다.

(선택) Harbor TLS 인증서 등록

skip_verify는 사용하지 않습니다. Harbor 서버 인증서가 중간 CA를 포함한 체인으로 검증되어야 하므로, Harbor 접속 FQDN과 포트별 디렉토리에 전체 체인 인증서를 배치한 뒤 hosts.tomlca에 명시합니다.

# Harbor 레지스트리 주소와 체인 인증서 파일
HARBOR_HOST="harbor-product.strato.co.kr:8443"
HARBOR_SCHEME="https"
CHAIN_CERT="./strato.co.kr_chain.crt"

sudo mkdir -p "/etc/containerd/certs.d/${HARBOR_HOST}"
sudo cp "${CHAIN_CERT}" "/etc/containerd/certs.d/${HARBOR_HOST}/strato.co.kr_chain.crt"
sudo tee "/etc/containerd/certs.d/${HARBOR_HOST}/hosts.toml" <<EOF
server = "${HARBOR_SCHEME}://${HARBOR_HOST}"

[host."${HARBOR_SCHEME}://${HARBOR_HOST}"]
  capabilities = ["pull", "resolve", "push"]
  ca = ["strato.co.kr_chain.crt"]
EOF

sudo systemctl restart containerd

# TLS 검증을 유지한 상태로 이미지 pull 확인
sudo ctr -n k8s.io image pull \
  harbor-product.strato.co.kr:8443/lgcns/strato-landing-frontend:10.0.0

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으로)

WSL2 / 단일 구성은 이 단계를 건너뜁니다.

HA 구성을 위해 K8s API Server(6443) 앞단에 로드밸런서가 필요합니다. 환경에 따라 아래 세 가지 방식 중 하나를 선택합니다.

  • 옵션 A: 물리 로드밸런서 (Physical LB) — 기업용 L4/L7 스위치, 클라우드 LB
  • 옵션 B: 소프트웨어 VIP — keepalived + haproxy 기반
  • 옵션 C: Localhost LB — VIP를 사용할 수 없는 환경

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

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

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

옵션 A: 물리 로드밸런서 (Physical LB) 방식 (권장)

기업용 L4/L7 스위치나 클라우드 제공업체의 로드밸런서를 사용하는 경우입니다. 별도의 패키지 설치 없이 노드 OS 설정만으로 구성하므로 오프라인 환경에 가장 적합합니다.

5-A-1. 물리 LB 동작 모드 확인 (관리자 확인 필수)

물리 LB가 트래픽을 백엔드 노드로 전달할 때의 방식을 먼저 확인해야 합니다.

  1. DNAT (NAT) 방식: LB가 패킷의 목적지 IP를 VIP에서 노드 IP로 변환하여 전달합니다. 별도의 노드 설정이 필요 없습니다.
  2. DSR (Direct Server Return) 또는 Transparent 방식: LB가 목적지 IP를 VIP 그대로 둔 채 MAC 주소만 바꿔서 전달합니다. 이 경우 5-A-3 단계의 루프백 설정이 필수입니다.

5-A-2. FQDN 등록 및 Hairpin NAT 방지 (전체 노드)

마스터 노드들이 자기 자신을 호출할 때 외부 LB를 거쳐 나갔다 들어오는 현상(Hairpin)을 방지하기 위해 노드별로 /etc/hosts를 다르게 설정합니다.

  • 마스터 노드 (1, 2, 3): k8s-api.internal자기 자신의 실제 IP로 매핑합니다.

    # 예: Master-1 (39번 IP) 에서 실행 시
    echo "192.168.1.39  k8s-api.internal" | sudo tee -a /etc/hosts
    
  • 워커 노드 및 외부 클라이언트: k8s-api.internal물리 LB VIP로 매핑합니다.

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

5-A-3. (DSR/Transparent 모드인 경우만) VIP 루프백 설정

물리 LB가 목적지 IP를 VIP로 유지하여 패킷을 던질 때, 커널이 이를 "내 것"으로 인식하게 하기 위해 루프백(lo)에 VIP를 할당하고 ARP 응답을 끕니다.

# 전체 마스터 노드 실행
# 1. 루프백에 VIP 할당
sudo ip addr add <물리_LB_VIP>/32 dev lo

# 2. ARP 응답 방지 (물리 LB와 IP 충돌 방지)
cat <<EOF | sudo tee /etc/sysctl.d/k8s-dsr.conf
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.all.arp_announce = 2
net.ipv4.conf.lo.arp_ignore = 1
net.ipv4.conf.lo.arp_announce = 2
EOF
sudo sysctl --system

옵션 B: 소프트웨어 VIP 방식 (keepalived + haproxy)

별도의 물리 장비 없이 마스터 노드 3대에 keepalivedhaproxy를 설치하여 HA를 구현하는 방식입니다. Master 3대와 가상 IP(VIP) 환경을 가정합니다.

Ubuntu 24.04에서는 haproxy / keepalived DEB를 k8s/debs/에 포함시켜 두었어야 합니다. scripts/download_assets_offline.shapt-get download haproxy keepalived + 의존성을 함께 수집합니다.

5-B-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-B-2. 커널 파라미터 설정 (전체 마스터 노드)

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

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

sudo sysctl --system

5-B-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

Ubuntu 24.04 AppArmor 주의: /etc/apparmor.d/usr.sbin.haproxy 프로파일이 기본 활성화되어 있으며 위 설정은 기본 허용 범위 내입니다. 외부 소켓이나 비표준 경로를 쓰는 경우 sudo aa-complain /usr/sbin/haproxy 로 임시 우회.

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

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

노드 state priority
Master-1 MASTER 101
Master-2 BACKUP 100
Master-3 BACKUP 99

Ubuntu 24.04의 기본 네트워크 인터페이스명은 eth0, ens*, enp* 등 환경마다 다릅니다. ip -br link 로 확인한 후 아래 interface 값을 실제 인터페이스명으로 바꿉니다.

# 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 실제 네트워크 인터페이스명으로 변경 (ip -br link 로 확인)
    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

killall 미설치 환경(Ubuntu 24.04 minimal)이라면 sudo apt-get install psmisc 또는 DEB(psmisc_*.deb)를 k8s/debs/에 포함시킵니다. 대안으로 pgrep -x haproxyscript 값으로 사용할 수 있습니다.

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

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

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

옵션 C: 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)

구성 유형(단일 / HA)과 CNI 선택(Calico / Cilium)에 따라 옵션을 조합합니다.

  • 단일 구성 → 옵션 D 사용
  • HA(3중화) 구성 → 옵션 A, B, C 중 선택
  • CNI = Cilium 인 경우 모든 옵션에 --skip-phases=addon/kube-proxy--pod-network-cidr=10.0.0.0/16 를 적용합니다. (Calico는 192.168.0.0/16 기본값)

옵션 A: HA(3중화) — 물리 LB 방식 (Phase 5 옵션 A 에서 진행한 경우)

물리 LB가 외부에서 6443 포트를 중계하고 있으므로, 로컬 HAProxy 중지/시작이나 bind-address 수정 단계가 전혀 필요 없습니다.

# kubeadm init — FQDN 사용 + CNI=Calico (권장)
sudo kubeadm init \
  --control-plane-endpoint "k8s-api.internal:6443" \
  --upload-certs \
  --apiserver-cert-extra-sans="k8s-api.internal,<물리_LB_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.33.11

# kubeadm init — FQDN 사용 + CNI=Cilium
sudo kubeadm init \
  --skip-phases=addon/kube-proxy \
  --control-plane-endpoint "k8s-api.internal:6443" \
  --upload-certs \
  --apiserver-cert-extra-sans="k8s-api.internal,<물리_LB_VIP>,<MASTER1_IP>,<MASTER2_IP>,<MASTER3_IP>,127.0.0.1" \
  --pod-network-cidr=10.0.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --kubernetes-version v1.33.11

# 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: HA(3중화) 구성 (소프트웨어 VIP 방식 - Phase 5 옵션 B 에서 진행한 경우)

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

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

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

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

# 2-a. kubeadm init — VIP IP 직접 사용 + CNI=Calico
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.33.11

# 2-b. kubeadm init — FQDN 사용 + CNI=Calico (권장)
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.33.11

# 2-c. kubeadm init — FQDN 사용 + CNI=Cilium
sudo kubeadm init \
  --skip-phases=addon/kube-proxy \
  --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=10.0.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --kubernetes-version v1.33.11

# 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

옵션 C: HA(3중화) — Localhost LB 방식 (Phase 5 옵션 C 에서 진행한 경우)

각 노드의 HAProxy 가 127.0.0.1:8443 만 점유하고, 백엔드는 마스터들의 6443 으로 포워딩합니다. kube-apiserver 의 6443 과 포트가 겹치지 않으므로 HAProxy 중지·재시작 단계가 불필요하고, bind-address 수정도 필요 없습니다(기본 0.0.0.0 사용).

인증서 SAN 에 반드시 127.0.0.1 을 포함해야 모든 노드의 kubeconfig(https://127.0.0.1:8443)가 동일 인증서로 검증됩니다.

sudo kubeadm init \
  --control-plane-endpoint "127.0.0.1:8443" \
  --upload-certs \
  --apiserver-cert-extra-sans="127.0.0.1,<MASTER1_IP>,<MASTER2_IP>,<MASTER3_IP>" \
  --pod-network-cidr=192.168.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --kubernetes-version v1.33.11

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

# HAProxy 백엔드 헬스체크 확인 — Master-1 만 UP 으로 보여야 정상
ss -tlnp | grep 8443

옵션 D: 단일 구성

# Calico 선택 시
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.33.11

# Cilium 선택 시 (kube-proxy skip)
sudo kubeadm init \
  --skip-phases=addon/kube-proxy \
  --control-plane-endpoint "<MASTER_IP>:6443" \
  --pod-network-cidr=10.0.0.0/16 \
  --service-cidr=10.96.0.0/12 \
  --kubernetes-version v1.33.11

# 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 조인 명령을 복사하여 실행합니다. Phase 5 에서 선택한 LB 방식에 따라 절차가 달라집니다.

물리 LB 방식 (Phase 5 옵션 A)

물리 LB가 외부에서 트래픽을 중계하므로, HAProxy 중지나 bind-address 수정 단계가 전혀 필요 없습니다.

# 1. 컨트롤 플레인 조인 (endpoint = FQDN)
sudo kubeadm join k8s-api.internal:6443 --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH> \
    --control-plane --certificate-key <CERT_KEY>

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

소프트웨어 VIP 방식 (Phase 5 옵션 B)

# 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

# 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

Localhost LB 방식 (Phase 5 옵션 C)

각 마스터의 HAProxy 가 127.0.0.1:8443 만 점유하므로 HAProxy 중지 / bind-address 수정 단계 모두 불필요합니다. Master-1 의 kubeadm init 출력에 표시된 join 명령은 endpoint 가 127.0.0.1:8443 으로 이미 지정되어 있습니다.

# 1. 컨트롤 플레인 조인 (endpoint = 127.0.0.1:8443)
sudo kubeadm join 127.0.0.1:8443 --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH> \
    --control-plane --certificate-key <CERT_KEY>

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

# 3. (선택) HAProxy 백엔드 상태 — 모든 마스터가 합류하면 3대 모두 UP
sudo journalctl -u haproxy -n 20 --no-pager

Phase 7: CNI 설치

옵션 A: Calico + Envoy Gateway

Calico는 환경 규모와 운영 선호에 따라 두 방식 중 하나를 선택합니다.

방식 특징 사용 파일
manifest 단일 calico.yaml 적용. 더 단순하고 가벼운 기본 경로 k8s/utils/calico.yaml
operator Tigera Operator 기반. CRD와 operator를 통해 Calico 구성 관리 k8s/utils/tigera-operator.yaml, k8s/utils/calico-custom-resources.yaml

옵션 A-1: manifest 방식 (권장 기본값)

# 1. Calico 설치
kubectl apply -f k8s/utils/calico.yaml

# 2. Calico 파드가 전부 Running이 될 때까지 대기
kubectl get pods -n kube-system -w

# 3. Envoy Gateway 설치 (L7 라우팅)
cd ../envoy-1.37.2
./scripts/install.sh

Pod CIDR을 기본(192.168.0.0/16)에서 변경한 경우 k8s/utils/calico.yamlCALICO_IPV4POOL_CIDR 주석을 해제하고 값을 수정한 뒤 적용합니다.

옵션 A-2: Tigera Operator 방식

# 1. Tigera Operator 설치
kubectl create -f k8s/utils/tigera-operator.yaml

# 2. CRD 등록 확인 후 custom resources 적용
kubectl wait --for=condition=established crd/installations.operator.tigera.io --timeout=180s
kubectl create -f k8s/utils/calico-custom-resources.yaml

# 3. Calico 파드가 전부 Running이 될 때까지 대기
kubectl get pods -n calico-system -w

# 4. Envoy Gateway 설치 (L7 라우팅)
cd ../envoy-1.37.2
./scripts/install.sh

Pod CIDR을 기본(192.168.0.0/16)에서 변경한 경우 k8s/utils/calico-custom-resources.yamlcidr: 값을 수정한 뒤 적용합니다.

멀티 홈 IP 환경 대응 (BGP 피어링 오동작 방지)

노드에 네트워크 카드가 여러 개 장착되어 있거나 가상 인터페이스가 많아 IP가 여러 개 할당된 경우, Calico가 BGP 통신에 적합하지 않은 IP를 자동 감지하여 노드 간 Pod 통신이 단절될 수 있습니다. 이를 방지하기 위해 다음 조치가 적극 권장됩니다.

  • 옵션 A-1 (manifest) 적용 시: k8s/utils/calico.yamlcalico-node DaemonSet 환경 변수에 IP_AUTODETECTION_METHOD 설정을 추가합니다.

    - name: IP_AUTODETECTION_METHOD
      value: "cidr=10.10.10.0/24"  # 주 인터페이스 대역 지정
    

  • 옵션 A-2 (operator) 적용 시: k8s/utils/calico-custom-resources.yamlInstallation 리소스에 nodeAddressAutodetectionV4 설정을 지정합니다.

    spec:
      calicoNetwork:
        ipPools:
        - cidr: 192.168.0.0/16
          # ... 생략 ...
        nodeAddressAutodetectionV4:
          cidrs:
          - 10.10.10.0/24  # 주 인터페이스 대역 지정
    

옵션 B: Cilium

# Cilium은 별도 컴포넌트에서 설치 (이미지 pre-load + helm install 포함)
cd ../cilium-1.19.3
# install.conf에 K8S_SERVICE_HOST/PORT, kubeProxyReplacement=true 가 저장되어 있어야 함
./scripts/install.sh

Cilium은 Gateway API를 기본 지원하므로 Envoy Gateway를 별도로 설치하지 않습니다.

Phase 8: helm & nerdctl 설치 (컨트롤 플레인)

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

# nerdctl (full)
tar -xzvf nerdctl-full-2.2.2-linux-amd64.tar.gz -C /tmp/nerdctl-full
sudo cp /tmp/nerdctl-full/bin/* /usr/local/bin/
nerdctl --version

Phase 9: 워커 노드 조인

Master-1의 kubeadm init 출력에서 워커 조인 명령을 복사하여 실행합니다. Phase 5(또는 6)에서 선택한 구성 방식에 맞춰 아래 옵션을 선택하세요.

위 출력의 <ENDPOINT> 는 Phase 5 에서 선택한 LB 방식에 따라 달라집니다:

Phase 5 옵션 워커가 사용할 endpoint 사전 작업
A (HA — 물리 LB) k8s-api.internal:6443 워커 노드 /etc/hosts 에 FQDN 등록 필요 (Phase 5-A-2)
B (HA — 소프트웨어 VIP) k8s-api.internal:6443 워커 노드 /etc/hosts 에 FQDN 등록 필요 (Phase 5-B-1)
C (HA — Localhost LB) 127.0.0.1:8443 워커 노드에도 HAProxy 설치·설정 완료되어 있어야 함 (Phase 5 옵션 C)
D (단일 구성) <MASTER_IP>:6443 추가 작업 불필요
sudo kubeadm join <ENDPOINT> --token <TOKEN> \
  --discovery-token-ca-cert-hash sha256:<HASH>

Phase 10: 설치 확인

kubectl get nodes
kubectl get pods -A

모든 노드가 Ready, 전 네임스페이스 파드가 Running이면 완료입니다.

추가 확인:

# Cilium 선택 시
kubectl get ciliumendpoints -A
ip link show cni0  # 존재하지 않아야 함 (flannel 잔재 없음)
iptables -L -n | grep KUBE-SVC  # 비어있어야 함 (kubeProxyReplacement 동작)

# Calico 선택 시
kubectl get ippools -o yaml
kubectl get pods -n projectcalico-system

재설치 시 초기화

scripts/uninstall.sh를 사용하는 것을 권장하며, 수동으로 할 경우:

# 1. kubeadm reset
sudo kubeadm reset -f

# 2. CNI/kube 설정 삭제
sudo rm -rf /etc/cni/net.d /var/lib/cni
rm -rf $HOME/.kube
sudo rm -rf /root/.kube /var/lib/etcd /var/lib/kubelet

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

# 4. CNI 인터페이스 제거 (있을 시)
for iface in cni0 flannel.1 cilium_host cilium_net cilium_vxlan; do
    sudo ip link show "$iface" &>/dev/null && sudo ip link del "$iface"
done

# 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가 발생할 수 있습니다.

CNI = Cilium 구성의 경우 kube-proxy 가 없으므로 아래 kube-proxy ConfigMap 단계는 생략하고, kubeadm-config 갱신만 수행합니다. Cilium 파드는 K8S_SERVICE_HOST/K8S_SERVICE_PORT 환경 변수로 API를 찾으므로 kubectl -n kube-system rollout restart ds cilium 으로 재기동합니다.

# kube-proxy ConfigMap 갱신 (Calico 경로만)
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 | 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 업데이트 (전체 마스터 노드)

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회 실행)

CNI = Cilium 구성의 경우 kube-proxy 가 없으므로 아래 kube-proxy ConfigMap 단계는 생략하고, kubeadm-config 갱신만 수행합니다. Cilium 파드는 K8S_SERVICE_HOST/K8S_SERVICE_PORT 환경 변수로 API를 찾으므로 kubectl -n kube-system rollout restart ds cilium 으로 재기동합니다.

# kube-proxy ConfigMap 갱신 (Calico 경로만)
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이면 완료입니다.


Ubuntu 24.04 vs Rocky 9.6 주요 치환 포인트

이 가이드가 HA 절차를 직접 포함하지만, 보조 참조용으로 정리합니다.

Rocky 9.6 Ubuntu 24.04
dnf install haproxy keepalived psmisc apt-get install haproxy keepalived psmisc (DEB는 k8s/debs/)
/etc/selinux/config + setenforce 0 AppArmor (aa-status, 필요 시 aa-complain)
ip addr show eth0 \| grep VIP ip addr show \| grep <VIP> (인터페이스명 다를 수 있음)
firewalld 고려 ufw 고려 (폐쇄망에서는 대부분 sudo ufw disable)
iptables backend 고정 WSL2만 iptables-legacy 강제, VM은 기본값 충분

참고: 취약점 점검 및 보완

Kubernetes kubeadm API Server 설정, etcd TLS, 인증서 취약점 점검 및 보완 절차는 [Kubernetes 취약점 점검 및 보완 가이드](../../../security/005-vulnerability-check-remediation.md)를 참조하세요.