Kubernetes v1.30.0 온라인 설치 가이드 (Rocky Linux 9.6)
인터넷이 가능한 환경에서 kubeadm 기반 Kubernetes v1.30.0 클러스터를 구성하는 절차를 안내합니다. containerd를 컨테이너 런타임으로, Calico를 CNI로 사용합니다.
오프라인(폐쇄망) 환경은
offline-install.md를 참고하세요.
전제 조건
- Rocky Linux 9.6 서버
- 단일 구성: 컨트롤 플레인 1대 + 워커 노드 1대 이상
- HA(3중화) 구성: 컨트롤 플레인 3대 + 워커 노드 1대 이상 + VIP 1개
- 모든 노드에서 인터넷 접근 가능
- swap 비활성화 완료 (
swapoff -a및/etc/fstab주석 처리)
Phase 1: 패키지 설치 (전체 노드)
# 1. containerd 설치 (Docker 공식 리포지토리)
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y containerd.io
# 2. Kubernetes 리포지토리 등록 (v1.30)
cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
# 3. kubeadm, kubelet, kubectl 설치
sudo dnf install -y --disableexcludes=kubernetes kubelet kubeadm kubectl
# 4. kubelet 활성화 (kubeadm init 전에는 시작하지 않아도 됨)
sudo systemctl enable kubelet
Kubernetes 리포지토리는 v1.24부터
pkgs.k8s.io로 이전되었습니다. 버전별 리포 경로(/v1.30/)가 다르므로 다른 버전 설치 시 URL을 맞게 수정하세요.
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>
10.10.10.73 worker1
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(또는 사설 레지스트리) insecure registry 사용 시 config_path 설정
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 재시작 후에도
SystemdCgroup = true가 적용되지 않으면 아래 명령으로 확인하세요.
(선택) Harbor insecure registry 등록
Harbor를 HTTP(insecure)로 운영하는 경우 각 노드에 아래 설정을 추가합니다.
# Harbor 레지스트리 주소 (예: 192.168.1.10:30002)
HARBOR_HOST="<NODE_IP>:30002"
sudo mkdir -p /etc/containerd/certs.d/${HARBOR_HOST}
sudo tee /etc/containerd/certs.d/${HARBOR_HOST}/hosts.toml <<EOF
server = "http://${HARBOR_HOST}"
[host."http://${HARBOR_HOST}"]
capabilities = ["pull", "resolve", "push"]
skip_verify = true
EOF
sudo systemctl restart containerd
containerd v1.x vs v2.x 플러그인 키 차이
containerd config default로 생성한config.toml에서config_path가 없을 경우 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.toml의root값을 직접 변경하는 방법도 있지만, 소프트링크 방식은 기존 경로를 그대로 유지하므로 다른 툴과의 호환성을 더 쉽게 확보할 수 있습니다.
Phase 4: 이미지 사전 Pull (선택, Master-1)
온라인 환경에서는 kubeadm init 시 이미지를 자동으로 pull하므로 이 단계는 생략 가능합니다.
네트워크가 느리거나 init 전에 이미지 준비 여부를 확인하고 싶을 때 실행합니다.
sudo kubeadm config images pull --kubernetes-version v1.30.0
# 확인
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 변경 시 인증서 재발급 필수
옵션 A: 물리 로드밸런서 (Physical LB) 방식 (권장)
기업용 L4/L7 스위치나 클라우드 제공업체의 로드밸런서를 사용하는 경우입니다.
5-B-1. 물리 LB 동작 모드 확인 (관리자 확인 필수)
물리 LB가 트래픽을 백엔드 노드로 전달할 때의 방식을 먼저 확인해야 합니다.
- DNAT (NAT) 방식: LB가 패킷의 목적지 IP를 VIP에서 노드 IP로 변환하여 전달합니다. 별도의 노드 설정이 필요 없습니다.
- DSR (Direct Server Return) 또는 Transparent 방식: LB가 목적지 IP를 VIP 그대로 둔 채 MAC 주소만 바꿔서 전달합니다. 이 경우 5-A-3 단계의 루프백 설정이 필수입니다.
5-B-2. FQDN 등록 및 Hairpin NAT 방지 (전체 노드)
마스터 노드들이 자기 자신을 호출할 때 외부 LB를 거쳐 나갔다 들어오는 현상(Hairpin)을 방지하기 위해 노드별로 /etc/hosts를 다르게 설정합니다.
-
마스터 노드 (1, 2, 3):
- 워커 노드 및 외부 클라이언트:k8s-api.internal을 자기 자신의 실제 IP로 매핑합니다.k8s-api.internal을 물리 LB VIP로 매핑합니다.
5-B-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)
Master 3대와 가상 IP(VIP) 환경을 가정합니다. VIP를 K8s API Server(6443) 앞단에 두어 마스터 노드 장애 시에도 API 통신이 끊기지 않게 합니다.
5-B-1. (FQDN 방식 선택 시) FQDN 등록 (전체 노드)
VIP IP를 직접 사용하는 대신 내부 FQDN(k8s-api.internal)으로 추상화합니다.
나중에 VIP가 변경되어도 인증서 재발급 없이 DNS 서버 혹은 /etc/hosts만 수정하면 됩니다.
내부 DNS 서버가 있다면 관리자에게 요청하여 아래 내용을 추가합니다.
- 레코드 이름: k8s-api.internal
- IP 주소: VIP
만약 DNS 서버가 없다면 hosts 파일에 등록합니다.
전체 노드(마스터 + 워커)에서 실행합니다.
HAProxy의
bind는 안정성을 위해 VIP IP(<VIP>:6443)를 그대로 사용합니다. FQDN은 kubeconfig의 server 주소와 인증서 SAN에만 적용됩니다.
5-B-2. HAProxy, Keepalived 패키지 설치 (전체 마스터 노드)
5-B-3. 커널 파라미터 설정 (전체 마스터 노드)
VIP가 자신의 인터페이스에 없어도 바인딩할 수 있도록 설정합니다.
cat <<EOF | sudo tee /etc/sysctl.d/haproxy.conf
net.ipv4.ip_nonlocal_bind = 1
EOF
sudo sysctl --system
5-B-4. 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 # VIP로 바인딩 (API 서버와 포트 충돌 방지)
mode tcp
option tcplog
default_backend k8s-masters
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-B-5. 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 # Master-2, 3은 BACKUP
interface eth0 # 본인 네트워크 인터페이스명으로 변경 필수
virtual_router_id 51
priority 101 # M1: 101, M2: 100, M3: 99
advert_int 1
authentication {
auth_type PASS
auth_pass 42 # 모든 노드 동일하게 설정
}
virtual_ipaddress {
<VIP> # VIP 주소
}
track_script {
check_haproxy
}
}
EOF
5-B-6. 서비스 시작 및 VIP 확인
sudo systemctl enable --now haproxy
sudo systemctl enable --now keepalived
# VIP 활성화 확인 (Master-1에서 VIP가 보여야 함)
ip addr show eth0 | grep <VIP>
옵션 C: Localhost LB 방식 (VIP 사용 불가 환경)
VIP를 사용할 수 없는 환경에서 각 노드에 HAProxy를 띄워 Loopback(127.0.0.1:8443)으로 통신합니다.
전체 마스터 및 워커 노드에 동일하게 설정합니다.
sudo dnf install -y haproxy
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중화) — 물리 LB 방식 (Phase 5 옵션 A 에서 진행한 경우)
물리 LB가 외부에서 6443 포트를 중계하고 있으므로, 로컬 HAProxy 중지/시작이나 bind-address 수정 단계가 전혀 필요 없습니다.
# kubeadm init — FQDN 사용 시 (권장)
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.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
옵션 B: HA(3중화) — 소프트웨어 VIP 방식 (Phase 5 옵션 B 에서 진행한 경우)
--apiserver-cert-extra-sans에 VIP와 전체 마스터 IP를 포함해야 RHEL/Rocky 9계열의 엄격한 SAN 검증을 통과할 수 있습니다.
pod-network-cidr 와 service-cidr 는 현재 기본값으로 되어있습니다. 환경에 따라 해당 네트워크 대역 사용이 불가하다면 변경해야 합니다.
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. 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
옵션 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.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
# HAProxy 백엔드 헬스체크 확인 — Master-1 만 UP 으로 보여야 정상
ss -tlnp | grep 8443
옵션 D: 단일 구성
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 조인 명령을 복사하여 실행합니다.
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. 컨트롤 플레인 조인 (endpoint = VIP 또는 FQDN)
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: Calico CNI 설치 (Master-1)
Calico v3.28은 Kubernetes v1.30과 호환됩니다. 환경에 따라 엔터프라이즈용(Operator) 또는 경량용(Manifest) 방식 중 하나를 선택하여 설치합니다.
방식 1: Tigera Operator 방식 (엔터프라이즈 권장)
# 1. operator 먼저 설치
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/tigera-operator.yaml
# 2. Operator 준비 대기
kubectl wait --for=condition=Available deployment/tigera-operator -n tigera-operator --timeout=60s
# 3. custom-resources.yaml 적용
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/custom-resources.yaml
방식 2: Manifest 방식 (경량/학습용 권장)
# 단일 파일로 즉시 설치 (기본 CIDR 192.168.0.0/16 사용 시)
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml
Pod CIDR을 변경한 경우 (방식 2): 아래와 같이
calico.yaml을 다운로드하여CALICO_IPV4POOL_CIDR항목을 수정한 뒤 적용합니다.
Phase 8: Helm 설치 (컨트롤 플레인 노드)
특정 버전을 고정하고 싶다면 스크립트 실행 전 환경변수를 설정합니다.
Phase 8-1: nerdctl 설치 (선택, 전체 노드)
컨테이너 이미지 조회·조작이 필요한 노드에 설치합니다. containerd와 직접 통신하며 docker CLI와 유사한 UX를 제공합니다. Rootless 모드 사용을 위해 모든 의존성(rootlesskit, slirp4netns 등)이 포함된 Full 패키지 사용을 권장합니다.
# GitHub Releases에서 Full 패키지 다운로드
NERDCTL_VERSION="2.2.2"
curl -LO "https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-full-${NERDCTL_VERSION}-linux-amd64.tar.gz"
# 압축 해제 후 전체 바이너리 및 스크립트 배포
tar -xzvf nerdctl-full-${NERDCTL_VERSION}-linux-amd64.tar.gz
sudo mv bin/* /usr/local/bin/
sudo chmod +x /usr/local/bin/nerdctl /usr/local/bin/rootlesskit /usr/local/bin/slirp4netns /usr/local/bin/containerd-rootless*
# 설치 확인
nerdctl --version
Rocky/RHEL
sudoPATH 주의:nerdctl은/usr/local/bin/nerdctl에 설치되며, Rocky/RHEL 기본sudo secure_path에는/usr/local/bin이 없어sudo nerdctl호출 시 "command not found" 가 발생합니다. 셋 중 하나로 해결:
- 전체 경로:
sudo /usr/local/bin/nerdctl ...sudo visudo의Defaults secure_path끝에:/usr/local/bin추가- 심볼릭 링크:
sudo ln -sf /usr/local/bin/nerdctl /usr/bin/nerdctl
Rootless 모드 활성화 절차 (일반 사용자 계정)
바이너리 설치 후, sudo 없이 nerdctl을 사용하려면 아래 절차를 일반 사용자 계정으로 진행해야 합니다.
- 필수 패키지 확인:
shadow-utils패키지가 설치되어 있어야 합니다.
- 사용자 환경 설정 도구 실행: (root가 아닌 일반 계정으로 실행)
성공 시 ~/.config/systemd/user/containerd.service가 등록됩니다.
- 환경 변수 등록:
~/.bashrc또는~/.zshrc하단에 아래 내용을 추가하고 적용(source)합니다.
- Linger 설정 (권장): 로그아웃 후에도 서비스가 유지되도록 설정합니다.
주요 사용 예시:
# containerd k8s.io 네임스페이스 이미지 목록 확인
sudo nerdctl -n k8s.io images
# Harbor에서 이미지 pull (insecure registry)
sudo nerdctl -n k8s.io pull --insecure-registry <NODE_IP>:30002/library/myapp:1.0.0
Phase 8-2: skopeo 설치 (선택, 전체 노드)
레지스트리 간 이미지 복사, 이미지 메타데이터 검사 등에 사용합니다. 데몬 없이 동작하며 Harbor push/pull 검증에 유용합니다.
주요 사용 예시:
# Harbor 이미지 목록 조회 (insecure registry)
skopeo list-tags --tls-verify=false docker://<NODE_IP>:30002/library/myapp
# 레지스트리 간 이미지 복사
skopeo copy --src-tls-verify=false --dest-tls-verify=false \
docker://<SRC_REGISTRY>/myapp:1.0.0 \
docker://<NODE_IP>:30002/library/myapp:1.0.0
# 이미지 메타데이터 확인
skopeo inspect --tls-verify=false docker://<NODE_IP>:30002/library/myapp:1.0.0
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 |
추가 작업 불필요 |
Phase 10: 설치 확인
모든 노드가 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 등록 (마스터 + 워커)
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단계: 확인
Kubernetes control plane 주소가 https://k8s-api.internal:6443으로 표시되면 완료입니다.
케이스 A: FQDN 방식으로 초기 구성한 경우 (권장 구성)
FQDN(k8s-api.internal)이 인증서 SAN에 포함되어 있으므로, 인증서 재발급 없이 아래 순서만 따르면 됩니다.
1단계: 모든 노드의 /etc/hosts 업데이트 (마스터 + 워커)
# <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-masters의server항목(마스터 노드 IP)은 변경하지 않습니다.
4단계: API 서버 재시작 확인
케이스 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단계: 정상 동작 확인
모든 노드가 Ready 상태이고 kube-system Pod가 Running이면 완료입니다.