콘텐츠로 이동

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 가 적용되지 않으면 아래 명령으로 확인하세요.

grep SystemdCgroup /etc/containerd/config.toml

(선택) 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 버전 확인
containerd --version
# containerd v1.x (io.containerd.grpc.v1.cri)
[plugins."io.containerd.grpc.v1.cri".registry]
  config_path = "/etc/containerd/certs.d"

# containerd v2.x (io.containerd.cri.v1.images)
[plugins."io.containerd.cri.v1.images".registry]
  config_path = "/etc/containerd/certs.d"

(선택) 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 값을 직접 변경하는 방법도 있지만, 소프트링크 방식은 기존 경로를 그대로 유지하므로 다른 툴과의 호환성을 더 쉽게 확보할 수 있습니다.

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가 트래픽을 백엔드 노드로 전달할 때의 방식을 먼저 확인해야 합니다.

  1. DNAT (NAT) 방식: LB가 패킷의 목적지 IP를 VIP에서 노드 IP로 변환하여 전달합니다. 별도의 노드 설정이 필요 없습니다.
  2. 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로 매핑합니다.

    # 예: 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-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 파일에 등록합니다.

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

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

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

5-B-2. HAProxy, Keepalived 패키지 설치 (전체 마스터 노드)

sudo dnf install -y 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-cidrservice-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 항목을 수정한 뒤 적용합니다.

curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml
# CALICO_IPV4POOL_CIDR 주석 해제 및 <YOUR_POD_CIDR> 수정 후 적용
kubectl apply -f calico.yaml

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

curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

helm version

특정 버전을 고정하고 싶다면 스크립트 실행 전 환경변수를 설정합니다.

DESIRED_VERSION=v3.14.0 bash <(curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3)

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 sudo PATH 주의: nerdctl/usr/local/bin/nerdctl 에 설치되며, Rocky/RHEL 기본 sudo secure_path 에는 /usr/local/bin 이 없어 sudo nerdctl 호출 시 "command not found" 가 발생합니다. 셋 중 하나로 해결:

  • 전체 경로: sudo /usr/local/bin/nerdctl ...
  • sudo visudoDefaults secure_path 끝에 :/usr/local/bin 추가
  • 심볼릭 링크: sudo ln -sf /usr/local/bin/nerdctl /usr/bin/nerdctl

Rootless 모드 활성화 절차 (일반 사용자 계정)

바이너리 설치 후, sudo 없이 nerdctl을 사용하려면 아래 절차를 일반 사용자 계정으로 진행해야 합니다.

  1. 필수 패키지 확인: shadow-utils 패키지가 설치되어 있어야 합니다.
sudo dnf install -y shadow-utils
  1. 사용자 환경 설정 도구 실행: (root가 아닌 일반 계정으로 실행)
containerd-rootless-setuptool.sh install

성공 시 ~/.config/systemd/user/containerd.service가 등록됩니다.

  1. 환경 변수 등록: ~/.bashrc 또는 ~/.zshrc 하단에 아래 내용을 추가하고 적용(source)합니다.
export CONTAINERD_ADDRESS="unix://$XDG_RUNTIME_DIR/containerd/containerd.sock"
  1. Linger 설정 (권장): 로그아웃 후에도 서비스가 유지되도록 설정합니다.
sudo loginctl enable-linger $(id -un)

주요 사용 예시:

# 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 검증에 유용합니다.

sudo dnf install -y skopeo

# 설치 확인
skopeo --version

주요 사용 예시:

# 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 추가 작업 불필요
sudo kubeadm join <ENDPOINT> --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 등록 (마스터 + 워커)

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 업데이트 (마스터 + 워커)

# <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이면 완료입니다.