카테고리 보관물: IT

ceph dashboard OSD alarm troubleshoot

host 정보 없이 남은 osd.3과 OSD down 경고

Kubernetes 클러스터에서 Rook-Ceph를 운영하다 보면 대시보드에 OSD down 경고가 뜨고 Ceph OSD down 알람 처리 절차가 필요로 하는 경우가 있습니다. 이 글에서는 osd.3이 host 지정 없이 down 상태로 클러스터에 등록된 원인을 분석하고, 불필요한 OSD를 안전하게 제거하여 Ceph 클러스터를 정상 상태로 복구하는 과정을 단계별로 정리합니다.

문제 상황

Ceph 대시보드에서 OSD 1 down 알람이 발생하였습니다.

처리 전 Ceph 대시보드에 표시된 OSD 1 down 알람 경고 화면

ceph status를 확인하면 osd: 4 osds: 3 up으로 OSD 4개 중 3개만 정상 동작 중이며 HEALTH_WARN 상태입니다. 클러스터 용량(90 GiB) 대비 사용량(7.4 GiB)은 정상이며, 함께 표시된 mon b is low on available space는 Monitor 노드의 디스크 공간 문제로 OSD down 이슈와는 별개입니다.

bash-4.4$ ceph status
  cluster:
    id:     d874b4ea-8deb-4aa3-a3ac-e750180a6a5b
    health: HEALTH_WARN
            mon b is low on available space

  services:
    mon: 3 daemons, quorum a,b,c (age 4M)
    mgr: b(active, since 4M), standbys: a
    mds: 1/1 daemons up, 1 hot standby
    osd: 4 osds: 3 up (since 4M), 3 in (since 13M)

  data:
    volumes: 1/1 healthy
    pools: 5 pools, 113 pgs
    objects: 20.17k objects, 871 MiB
    usage: 7.4 GiB used, 83 GiB / 90 GiB avail
    pgs: 113 active+clean

bash-4.4$ ceph osd status
ID  HOST           USED  AVAIL  WR OPS  WR DATA  RD OPS  RD DATA  STATE
 0  <worker-node>  2515M  27.5G      0        0       1       90   exists,up
 1  <worker-node>  2511M  27.5G      0        0       0        0   exists,up
 2  <worker-node>  2544M  27.5G      0        0       1       16   exists,up
 3                     0      0      0        0       0        0   autoout,exists,new

원인 분석

ceph osd tree 결과를 보면 osd.3이 host 정보 없이 autoout, exists, new 상태로 등록되어 있습니다. 워커 노드 3대(<worker-node>/02/03)에 OSD가 각각 1개씩 정상 배치(osd.0~2)되어 있는 반면, osd.3은 노드에 연결되지 않은 채 Ceph 클러스터에만 등록된 고아(orphan) 상태입니다. 노드 수보다 OSD 수가 많아진 경우 이런 현상이 발생할 수 있으며, 수동으로 제거해야 합니다.

bash-4.4$ ceph osd tree
ID  CLASS  WEIGHT   TYPE NAME            STATUS  REWEIGHT  PRI-AFF
-1         0.08789  root default
-3         0.02930      host <worker-node>
 0   ssd   0.02930          osd.0            up   1.00000  1.00000
-5         0.02930      host <worker-node>
 1   ssd   0.02930          osd.1            up   1.00000  1.00000
-7         0.02930      host <worker-node>
 2   ssd   0.02930          osd.2            up   1.00000  1.00000
 3            0               osd.3          down       0  1.00000

처리 절차

불필요한 OSD를 제거할 때는 반드시 CRUSH map 제거 → CephX 인증 키 삭제 → OSD 항목 삭제 순서로 진행합니다. 순서를 지키지 않으면 Ceph 클러스터에 인증 키나 항목이 잔류하여 이후에도 알람이 발생할 수 있습니다.

# ① CRUSH map에서 제거 (이미 등록되지 않은 경우 아래 메시지가 출력되며 정상)
bash-4.4$ ceph osd crush remove osd.3
device 'osd.3' does not appear in the crush map

# ② CephX 인증 키 삭제
bash-4.4$ ceph auth del osd.3

# ③ OSD 항목 최종 삭제
bash-4.4$ ceph osd rm 3
removed osd.3
  • ceph osd crush remove: 데이터 배치 알고리즘인 CRUSH map에서 OSD를 제거합니다. 처음부터 CRUSH map에 등록되지 않았다면 “does not appear” 메시지가 출력 되며 이는 정상입니다.
  • ceph auth del: 해당 OSD에 부여된 CephX 인증 키를 삭제합니다.
  • ceph osd rm: 클러스터 OSD 목록에서 항목을 최종 삭제합니다.

osd.3 제거 후 다시 본 ceph osd tree

OSD 제거 후 ceph osd tree를 다시 확인하면 osd.3이 사라지고 워커 노드당 OSD 1개씩 총 3개가 정상 배치된 것을 확인할 수 있습니다. Ceph OSD down 알람 처리 절차는 이것으로 끝났고, Ceph 클러스터도 정상 상태로 복구되었습니다.

bash-4.4$ ceph osd tree
ID  CLASS  WEIGHT   TYPE NAME            STATUS  REWEIGHT  PRI-AFF
-1         0.08789  root default
-3         0.02930      host <worker-node>
 0   ssd   0.02930          osd.0            up   1.00000  1.00000
-5         0.02930      host <worker-node>
 1   ssd   0.02930          osd.1            up   1.00000  1.00000
-7         0.02930      host <worker-node>
 2   ssd   0.02930          osd.2            up   1.00000  1.00000

Session log 가 누락 되어 Code block으로 남기진 못했지만 “ceph status” 명령어로 Ceph 클러스터, 서비스, 데이터 상태를 마지막으로 재확인 합니다.

Ceph 관련 운영 내용은 아래 포스트도 참고하시기 바랍니다.

참고 문서: Rook Ceph OSD Management · Ceph OSD Operations (공식 문서)

ceph storage class 사용 wordpress, mysql pod 생성

PVC 요청만으로 Ceph RBD 볼륨을 받는 구성

Rook-Ceph 구축 이후 Ceph StorageClass PVC 구성을 활용하면 Kubernetes Pod가 PersistentVolumeClaim(PVC)만 요청해도 Ceph RBD(RADOS Block Device) 기반의 영구 볼륨을 동적으로 프로비저닝할 수 있습니다. Pod가 삭제되거나 재스케줄링되어도 데이터가 Ceph에 안전하게 보존됩니다.

이 글에서는 Ceph RBD StorageClass를 생성하고, PVC가 포함된 MySQL과 WordPress Deployment를 배포하는 전체 절차를 정리합니다. MetalLB LoadBalancer를 통해 WordPress에 외부 IP가 할당되어 실제 서비스 접근까지 확인합니다. 이 구성은 Rook-Ceph 구축이 완료된 환경을 전제합니다.

사전 준비

이 절차를 진행하기 전에 다음 구성 요소가 준비되어 있어야 합니다.

  • Rook-Ceph 클러스터가 정상 기동 중 (ceph status에서 HEALTH_OK)
  • MetalLB가 구성되어 있고 LoadBalancer IP 풀이 설정된 상태
  • Rook 저장소 클론이 로컬에 존재 (rook/deploy/examples/ 디렉토리 필요)

Rook 예제 디렉토리에는 mysql.yaml, wordpress.yaml, csi/rbd/storageclass.yaml이 포함되어 있습니다. 이 파일들을 기반으로 StorageClass를 생성하고 WordPress와 MySQL을 배포합니다.

Ceph StorageClass 생성

Rook 저장소의 csi/rbd/storageclass.yaml을 적용하면 rook-ceph-block StorageClass가 생성됩니다. 이후 Pod에서 storageClassName: rook-ceph-block으로 PVC를 요청하면 Ceph RBD 볼륨이 자동으로 생성·마운트됩니다.

StorageClass의 reclaimPolicy: Delete는 PVC 삭제 시 볼륨도 함께 삭제됨을 의미합니다. 데이터를 보존해야 하는 경우 Retain으로 변경해야 합니다. allowVolumeExpansion: true로 설정되어 있어 운영 중에도 PVC의 용량을 늘릴 수 있습니다.

kubectl apply -f csi/rbd/storageclass.yaml

kubectl get storageclass
NAME              PROVISIONER                  RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION
rook-ceph-block   rook-ceph.rbd.csi.ceph.com   Delete          Immediate           true
PVC 동적 프로비저닝에 쓰는 Ceph StorageClass 목록 확인 결과 (rook-ceph-block, rook-cephfs)

MySQL Deployment 배포 (PVC 포함)

Service, PersistentVolumeClaim, Deployment가 포함된 MySQL manifest를 배포합니다. PVC가 생성되면 Rook CSI 드라이버가 Ceph 클러스터에 RBD 이미지를 자동으로 생성하고, kubelet이 해당 볼륨을 Pod에 마운트합니다. PVC Status가 Bound이면 Ceph RBD 볼륨이 정상 마운트된 것입니다.

MySQL Deployment의 환경변수(MYSQL_ROOT_PASSWORD 등)는 실제 운영 환경에서 Secret 리소스로 분리하는 것을 권장합니다. 또한 MySQL 데이터 디렉토리(/var/lib/mysql)를 PVC에 마운트하므로, Pod를 재시작하거나 노드가 변경되어도 데이터가 보존됩니다.

kubectl create -f mysql.yaml
service/wordpress-mysql created
persistentvolumeclaim/mysql-pv-claim created
deployment.apps/wordpress-mysql created

kubectl get pvc
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
mysql-pv-claim   Bound    pvc-6d458ff1-54bf-4f34-b010-a0f4b2a0966e  20Gi       RWO            rook-ceph-block   94s

WordPress Deployment 배포 (PVC 포함)

MySQL과 동일하게 Service, PVC, Deployment가 포함된 WordPress manifest를 배포합니다. WordPress는 업로드 파일 등을 /var/www/html에 저장하는데, 이 디렉토리가 PVC에 마운트되어 Pod 재시작 후에도 데이터가 유지됩니다. WordPress 서비스 타입은 LoadBalancer로 설정되어 MetalLB로부터 외부 IP를 할당받습니다.

kubectl create -f wordpress.yaml
service/wordpress created
persistentvolumeclaim/wp-pv-claim created
deployment.apps/wordpress created

kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
wordpress-7cf5c5c8b-5cgqk          1/1     Running   0          42s
wordpress-mysql-6f99c59595-9vs7z   1/1     Running   0          4m8s

MetalLB로 외부 접근 확인

MetalLB가 구성된 환경에서는 WordPress Service에 외부 IP가 자동 할당됩니다. 해당 IP로 브라우저에서 접속하면 WordPress 설치 화면이 표시되며, Ceph 기반 영구 스토리지 구성이 완료된 것입니다.

이 구성에서 WordPress와 MySQL은 서로 다른 Ceph RBD PVC를 사용합니다. 스토리지 레이어가 Ceph로 추상화되어 있기 때문에, 향후 WordPress나 MySQL Pod를 다른 Worker 노드로 재스케줄링해도 동일한 데이터에 접근할 수 있습니다. PVC의 accessMode: ReadWriteOnce(RWO) 특성상 한 번에 한 Pod만 볼륨에 마운트할 수 있으며, 수평 확장이 필요한 경우에는 CephFS(RWX)를 사용해야 합니다.

kubectl get svc wordpress
NAME        TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
wordpress   LoadBalancer   x.x.x.x         x.x.x.x       80:31494/TCP   27s

배포 결과 및 볼륨 확인

모든 리소스 배포 후 아래 명령어로 최종 상태를 확인합니다. PVC가 Bound, Pod가 Running, Service에 외부 IP가 할당된 것이 정상 상태입니다.

# PVC 상태 확인 (Bound이면 정상)
kubectl get pvc

# Pod 상태 확인
kubectl get pods

# WordPress Service 외부 IP 확인
kubectl get svc wordpress

# Ceph에서 실제 RBD 이미지 확인 (Toolbox Pod 내에서)
kubectl -n rook-ceph exec -it deploy/rook-ceph-tools -- bash
rbd ls replicapool

PVC 상태가 Pending에서 멈추는 경우 주요 원인으로는 Ceph 클러스터 상태 이상(HEALTH_WARN 이상), StorageClass 미적용, 또는 Rook CSI 드라이버 Pod 이상이 있습니다. kubectl describe pvc <pvc-name>으로 이벤트를 확인하고 조치합니다.

참고

관련 포스트:

참고 문서: Rook Ceph Block Storage 공식 문서 · Kubernetes Persistent Volumes 공식 문서

ceph 구축

Rook이 Ceph 배포를 자동화하는 방식

Rook은 Kubernetes용 스토리지 오케스트레이터로, Ceph의 배포와 운영을 Kubernetes 네이티브 방식으로 자동화합니다. Rook-Ceph 구축 과정에서는 Worker 노드에 연결된 Raw Disk를 기반으로 분산 오브젝트 스토리지를 구성하며, 이후 Kubernetes PersistentVolume(PV)을 동적으로 프로비저닝할 수 있습니다. Pod가 PersistentVolumeClaim(PVC)을 요청하면 Ceph RBD(Block Device) 또는 CephFS를 통해 영구 저장소가 자동으로 할당됩니다.

이 글에서는 Rook Operator를 배포하고 Ceph Cluster를 구성하는 전체 절차를 정리합니다. 최소 Worker 노드 3개와 각 노드에 파티션 또는 포맷이 되지 않은 Raw Disk가 필요합니다.

사전 준비 — Worker 노드 Raw Disk 확인

Ceph 스토리지는 각 Worker 노드에 할당된 초기화된(파티션/포맷 없는) Raw Disk를 OSD(Object Storage Daemon)로 사용합니다. 아래는 Worker 노드에 할당된 Raw Device 목록입니다.

OSD 외에도 Ceph는 여러 핵심 컴포넌트로 구성됩니다. MON(Monitor)은 클러스터 상태 맵을 관리하며 Quorum을 형성하기 위해 홀수(최소 3개)로 배포합니다. MGR(Manager)은 Ceph 대시보드와 메트릭 수집을 담당합니다. Rook Operator는 이 모든 컴포넌트를 Kubernetes CRD로 선언적으로 관리합니다.

Worker 노드에 할당된 raw device 목록

Rook Operator 배포

Rook 공식 저장소에서 특정 버전을 클론한 뒤, CRD(Custom Resource Definition), 공통 리소스, Operator를 순서대로 배포합니다. Operator Pod가 정상 기동된 것을 확인한 뒤 Ceph Cluster를 배포합니다.

crds.yaml은 CephCluster, CephBlockPool 등 Rook이 사용하는 커스텀 리소스 타입을 등록합니다. common.yaml은 RBAC, ServiceAccount 등 공통 리소스를 생성하며, operator.yaml은 Rook Operator Deployment를 배포합니다. Operator는 rook-ceph 네임스페이스에서 실행되며, 이후 적용되는 CephCluster CR을 감지하여 Ceph 컴포넌트를 자동으로 생성합니다.

git clone --single-branch --branch v1.10.10 https://github.com/rook/rook.git
cd rook/deploy/examples

kubectl create -f crds.yaml -f common.yaml -f operator.yaml

# Operator Pod 기동 확인
kubectl -n rook-ceph logs -l app=rook-ceph-operator -f

Ceph Cluster 배포

cluster.yaml을 적용하면 Rook Operator가 OSD, MON, MGR 등 Ceph 컴포넌트를 자동으로 생성합니다. 초기 배포 시 Raw Disk를 OSD로 포맷하는 작업이 포함되어 있어 5~10분 정도 소요될 수 있습니다.

기본 cluster.yaml은 클러스터 내 모든 Raw Disk를 자동으로 검색하여 OSD로 사용합니다. 특정 디바이스만 사용하려면 spec.storage.nodes에서 노드별로 사용할 디바이스를 명시해야 합니다. spec.mon.count: 3으로 MON을 3개 배포하여 Quorum을 유지하고, Worker 노드가 3개 미만이면 allowMultiplePerNode: true를 설정해야 합니다.

kubectl create -f cluster.yaml

# 배포 진행 상태 모니터링
kubectl -n rook-ceph get pods -w

Pod 상태 확인

Ceph 구축이 완료되면 rook-ceph 네임스페이스에 OSD, MON, MGR Pod가 Running 상태로 기동됩니다. OSD Pod 수는 사용 가능한 Raw Disk 수와 일치하며, MON은 3개(또는 설정값), MGR은 1개(액티브) + 1개(스탠바이)가 기동됩니다.

Rook-Ceph 구축 완료 후 rook-ceph 네임스페이스의 Pod 상태

Ceph 클러스터 상태 확인

Rook Toolbox Pod를 배포하면 ceph status, ceph osd tree 등 Ceph 관리 명령어를 직접 실행할 수 있습니다. ceph status 출력에서 HEALTH_OK가 표시되면 Rook-Ceph 구축 과정이 정상적으로 끝난 것입니다.

HEALTH_WARN이 표시되는 경우 주요 원인으로는 OSD 개수가 PG(Placement Group) 비율에 맞지 않는 경우, CLOCK_SKEW(노드 간 시간 차이), 또는 OSD가 아직 초기화 중인 경우가 있습니다. ceph health detail 명령어로 구체적인 경고 내용을 확인하고 조치합니다.

Toolbox Pod에서 ceph status 확인 결과
# Toolbox 배포 (Rook 예제 디렉토리에서)
kubectl create -f toolbox.yaml

# Toolbox Pod에 접속
kubectl -n rook-ceph exec -it deploy/rook-ceph-tools -- bash

# Ceph 클러스터 상태 확인
ceph status
ceph osd tree

참고

관련 포스트:

참고 문서: Rook Quickstart 공식 문서 · Ceph 공식 소개 문서

Mikrotik router & Cisco L2 Switch Trunking

802.1Q 태그로 한 링크에 여러 VLAN 싣기

VLAN Trunking은 하나의 물리 링크에서 여러 VLAN 트래픽을 IEEE 802.1Q 태그로 구분해 전달하는 기술입니다. 이 글은 MikroTik RouterOS와 Cisco IOS 스위치를 함께 쓰는 환경에서 양쪽 trunk 포트를 맞춘 MikroTik Cisco VLAN Trunking 구성 기록입니다.

Scenario

Mikrotik router 와 Cisco L2 스위치 이기종간에 회선 이중화(bonding, etherchannel) 후 두 장비 모두 vlan 100(192.168.x.x/24), vlan 200(192.168.x.x/24)의 통신이 가능하도록 구성

MikroTik bonding1(ether2, ether3)과 Cisco Port-channel1(Gi0/1, Gi0/2)을 LACP로 묶어 VLAN 100, 200을 802.1Q 태그로 전달하는 MikroTik Cisco VLAN Trunking 구성도
MikroTik Cisco VLAN Trunking 구성 — bonding1과 Port-channel1을 LACP로 묶고 VLAN 100, 200을 802.1Q 태그로 전달합니다.
# Mikrotik router side
# bridge 생성
/interface bridge
add name=bridge-lan vlan-filtering=yes

# vlan 생성 및 bridge 할당
/interface vlan
add interface=bridge-lan name=vlan100 vlan-id=100
add interface=bridge-lan name=vlan200 vlan-id=200

# bonding 생성(ether2, ether3 포트 대상)
/interface bonding
add mode=802.3ad name=bonding1 slaves=ether2,ether3 transmit-hash-policy=\
    layer-2-and-3

# trunk 처리, bridge-lan에 포함된 port 중 trunk 대상 아닌 경우 untagged port 처리
/interface bridge port
add bridge=bridge-lan frame-types=admit-only-vlan-tagged interface=bonding1
add bridge=bridge-lan tagged=bonding1,bridge-lan untagged=\
    ether4,ether5,ether6 vlan-ids=100,200

# vlan IP 및 대역 할당
/ip address
add address=192.168.x.x/24 interface=vlan100 network=192.168.x.x
add address=192.168.x.x/24 interface=vlan200 network=192.168.x.x
# Cisco L2 switch side

configure terminal

# vlan IP 및 대역 할당
interface Vlan100
 ip address 192.168.x.x 255.255.255.0
interface Vlan200
 ip address 192.168.x.x 255.255.255.0

# etherchannel 구성(GigabitEthernet0/1, GigabitEthernet0/2 포트 대상)
interface range GigabitEthernet0/1-2
 channel-protocol lacp
 channel-group 1 mode active

# 생성된 etherchannel에 trunk 모드 및 trunking할 vlan 적용
interface Port-channel1
 switchport trunk allowed vlan 100,200
 switchport mode trunk

end
write

링크 이중화(LACP) 방식 비교

이 구성에서는 MikroTik 측의 Bonding(802.3ad LACP)과 Cisco 측의 EtherChannel(LACP)을 상호 연결합니다. LACP는 두 장비의 물리 링크를 하나의 논리 인터페이스로 묶어 대역폭 확장과 링크 이중화를 동시에 제공합니다.

MikroTik은 mode=802.3ad로 LACP를 활성화하고 transmit-hash-policy=layer-2-and-3으로 로드밸런싱 해시를 L2+L3 기반으로 설정합니다. Cisco는 channel-protocol lacp와 channel-group 1 mode active로 LACP 협상을 능동적으로 시작합니다. 양쪽 모두 LACP active 모드로 설정하면 협상이 정상적으로 진행됩니다.

VLAN Trunk 통신 오류 점검 순서

이기종 장비 간 VLAN Trunk 구성 시 통신 오류가 발생하면 아래 항목을 순서대로 확인합니다.

# MikroTik: LACP 협상 상태 및 bonding 멤버 확인
/interface bonding monitor bonding1

# MikroTik: bridge VLAN 설정 확인
/interface bridge vlan print

# MikroTik: bridge port frame-type 확인
/interface bridge port print

# Cisco: EtherChannel 상태 확인
show etherchannel summary
show interfaces port-channel 1 trunk

# Cisco: VLAN trunk 허용 목록 확인
show interfaces trunk

가장 흔한 오류는 MikroTik의 bridge VLAN filtering이 비활성화된 경우와 Cisco trunk의 allowed VLAN 목록이 일치하지 않는 경우입니다. LACP 협상이 실패해도 단일 링크만 활성화되거나 링크 자체가 down될 수 있으므로, 양쪽의 LACP 상태를 함께 확인해야 합니다.

VLAN 구성 검증 절차

MikroTik과 Cisco 이기종 VLAN Trunk 구성 완료 후에는 단계적으로 통신을 검증합니다. 먼저 각 VLAN 인터페이스에 IP를 할당하고 동일 VLAN 내에서 핑이 통하는지 확인합니다. 다음으로 VLAN 간 통신은 MikroTik의 라우팅 또는 Cisco L3 스위치의 인터 VLAN 라우팅을 통해 확인합니다.

VLAN 100과 VLAN 200 사이의 통신이 필요한 경우 MikroTik에서 두 VLAN 인터페이스 간 라우팅이 활성화되어 있어야 합니다. MikroTik은 기본적으로 인터페이스 간 포워딩이 활성화되어 있어 별도 라우팅 설정 없이 VLAN 간 통신이 가능하지만, 방화벽 규칙에 따라 차단될 수 있습니다. /ip firewall filter print로 방화벽 규칙을 확인하고 필요한 경우 forward 체인에 허용 규칙을 추가합니다.

Cisco 측에서는 show interfaces trunk로 trunk 포트에서 허용된 VLAN 목록을 확인하고, show mac address-table로 각 VLAN의 MAC 주소가 올바른 포트에 학습되었는지 검증합니다. VLAN 통신 오류나 링크 다운이 발생하면 양쪽 장비의 로그를 동시에 확인하는 편이 원인 파악에 빠릅니다.

참고

참고 문서: MikroTik VLAN 공식 문서 · Cisco VLAN/VTP 설정 가이드

kubectl : certificate has expired or is not yet valid

kubeadm 인증서 1년 유효기간과 kubectl 접속 실패

Kubernetes 클러스터를 운영하다 보면 어느 날 갑자기 kubectl 명령이 동작하지 않고 kubectl certificate has expired or is not yet valid 오류가 발생하는 경우가 있습니다. 이는 kubeadm으로 구성된 클러스터의 API 서버 인증서가 기본 1년 유효기간을 초과했을 때 발생합니다. 이 글에서는 오류 원인을 확인하고 kubeadm certs renew all로 해결한 kubectl 인증서 만료 갱신 절차를 정리합니다.

CA는 유효하고 컴포넌트 인증서만 만료돼 kubectl 접속이 실패할 때 check-expiration부터 renew all, kubeconfig 교체, 재로드까지 마스터마다 반복하는 kubectl 인증서 만료 갱신 순서도
CA는 유효하고 1년짜리 컴포넌트 인증서만 만료된 상태 — 각 control-plane 노드에서 같은 갱신 순서를 반복합니다.

증상

kubectl 명령 실행 시 아래와 같이 x509 인증서 만료 오류가 반복 출력되며 클러스터에 접근하지 못합니다.

E1218 05:21:48.113070 1685746 memcache.go:265] couldn't get current server API group list: Get "https://x.x.x.x:6443/api?timeout=32s": tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current time 2024-12-18T05:21:48+09:00 is after 2024-12-05T15:09:04Z
Unable to connect to the server: tls: failed to verify certificate: x509: certificate has expired or is not yet valid

원인 확인 — 인증서 만료 현황 조회

kubeadm certs check-expiration으로 전체 인증서 만료 상태를 확인합니다. API 서버, etcd, controller-manager, scheduler 등 kubeadm이 관리하는 모든 컴포넌트 인증서의 만료일이 동시에 도래하는 경우가 많습니다.

<admin-user>@<master-node>:~$ sudo kubeadm certs check-expiration
[check-expiration] Reading configuration from the cluster...
[check-expiration] Error reading configuration from the Cluster. Falling back to default configuration

CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGED
admin.conf                 Dec 05, 2024 15:09 UTC   <invalid>       ca                      no
apiserver                  Dec 05, 2024 15:09 UTC   <invalid>       ca                      no
apiserver-etcd-client      Dec 05, 2024 15:09 UTC   <invalid>       etcd-ca                 no
apiserver-kubelet-client   Dec 05, 2024 15:09 UTC   <invalid>       ca                      no
controller-manager.conf    Dec 05, 2024 15:09 UTC   <invalid>       ca                      no
etcd-healthcheck-client    Dec 05, 2024 15:09 UTC   <invalid>       etcd-ca                 no
etcd-peer                  Dec 05, 2024 15:09 UTC   <invalid>       etcd-ca                 no
etcd-server                Dec 05, 2024 15:09 UTC   <invalid>       etcd-ca                 no
front-proxy-client         Dec 05, 2024 15:09 UTC   <invalid>       front-proxy-ca          no
scheduler.conf             Dec 05, 2024 15:09 UTC   <invalid>       ca                      no

CERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGED
ca                      Dec 03, 2033 15:09 UTC   8y              no
etcd-ca                 Dec 03, 2033 15:09 UTC   8y              no
front-proxy-ca          Dec 03, 2033 15:09 UTC   8y              no

해결 방법 — 인증서 갱신

갱신 전 기존 설정 파일을 백업합니다. 이후 kubeadm certs renew all로 모든 인증서를 일괄 갱신합니다.

# 기존 인증서 백업
sudo cp -pr /etc/kubernetes/ /etc/kubernetes_backup

# 인증서 전체 갱신
<admin-user>@<master-node>:~$ sudo kubeadm certs renew all
[renew] Reading configuration from the cluster...
[renew] Error reading configuration from the Cluster. Falling back to default configuration

certificate embedded in the kubeconfig file for the admin to use and for kubeadm itself renewed
certificate for serving the Kubernetes API renewed
certificate the apiserver uses to access etcd renewed
certificate for the API server to connect to kubelet renewed
certificate embedded in the kubeconfig file for the controller manager to use renewed
certificate for liveness probes to healthcheck etcd renewed
certificate for etcd nodes to communicate with each other renewed
certificate for serving etcd renewed

kubeconfig 갱신 및 컴포넌트 재시작

인증서 갱신 후 kubectl을 사용하는 계정의 홈 디렉토리 ~/.kube/config에도 새 인증서를 덮어써야 합니다. 이후 kube-apiserver, kube-controller-manager, kube-scheduler 프로세스에 SIGHUP을 전달해 재로드하고, kubelet을 재시작합니다.

# admin.conf을 kubectl 사용 계정의 kubeconfig로 복사
sudo cp /etc/kubernetes/admin.conf /home/<admin-user>/.kube/config
sudo chown <admin-user>:<admin-user> /home/<admin-user>/.kube/config

# 컨트롤 플레인 컴포넌트 SIGHUP (재시작 없이 인증서 재로드)
sudo kill -s SIGHUP $(pidof kube-apiserver)
sudo kill -s SIGHUP $(pidof kube-controller-manager)
sudo kill -s SIGHUP $(pidof kube-scheduler)

# kubelet 재시작
sudo systemctl restart kubelet
sudo systemctl daemon-reload

HA 멀티 마스터 클러스터에서의 인증서 갱신

3개 이상의 control-plane 노드로 구성된 HA 클러스터에서는 각 master 노드에서 개별적으로 인증서 갱신 절차를 진행해야 합니다. 하나의 마스터에서 kubeadm certs renew all을 실행해도 다른 마스터 노드의 인증서는 갱신되지 않습니다. 따라서 모든 control-plane 노드에 SSH 접속하여 같은 절차를 반복합니다.

갱신 완료 후 각 노드의 ~/.kube/config도 업데이트해야 합니다. HA 환경에서는 HAProxy 또는 keepalived가 마스터 VIP를 관리하므로, kubeconfig의 server: 주소가 VIP 주소인지 확인합니다. 만약 단일 마스터 주소가 고정되어 있다면 HA LB 주소로 교체합니다.

kubectl 복구 확인과 만료 재발 방지

kubectl 명령이 정상 동작하면 인증서 만료 갱신 절차가 끝난 것입니다. 모든 Pod가 Running 상태인지 확인합니다.

kubectl get pods -A

예방을 위해 인증서 갱신을 자동화하거나, 클러스터 업그레이드 시 kubeadm이 자동으로 인증서를 갱신하는 특성을 활용해 정기 업그레이드 주기를 유지하는 것을 권장합니다. 인증서 유효기간 만료 30일 전에 알림을 보내는 스크립트를 cron으로 등록하면 예고 없는 인증서 만료를 방지할 수 있습니다.

참고

관련 포스트:

참고 문서: kubeadm 인증서 관리 공식 문서 · kubeadm certs 커맨드 레퍼런스

Inspecting Direct Connect Traffic with AWS GWLB: Asymmetric AZ Drops and a Lambda Failover

레거시 IPS를 대신할 검사 지점이 필요했던 상황

IDC와 AWS가 DX(Direct Connect)로 연결된 하이브리드 환경에서 AWS GWLB 기반의 IPS 인라인 검사를 DX 구간에 넣은 기록입니다. GWLB 엔드포인트가 한 AZ에만 놓이는 특성 때문에 가용성과 비대칭 트래픽 문제를 차례로 겪었습니다. EventBridge와 Lambda로 라우팅 테이블의 다음 홉을 바꾸는 방식으로 마무리했습니다.

기존에 쓰던 레거시 IPS가 수명을 다하면서, 보안 요건상 AWS 쪽에 IPS를 새로 구성해야 했습니다. DX를 거쳐 오가는 트래픽을 빠짐없이 검사하는 것이 목표였습니다.

AWS에서 서드파티 보안 어플라이언스를 트래픽 경로에 끼워 넣는 관리형 수단이 GWLB(Gateway Load Balancer)입니다. GWLB는 원래 패킷을 Geneve(UDP 6081)로 감싸 어플라이언스에 넘기고, 검사를 마친 패킷을 돌려받아 원래 경로로 보냅니다. Cisco IPS를 새로 도입하는 만큼 Geneve 지원 여부부터 확인했고, 지원 사양임을 확인한 뒤 설계에 들어갔습니다.

기존 네트워크를 건드리지 않는 배치

기존 네트워크 구조를 크게 바꾸면 여러 계정의 VPC에 영향이 갈 수 있었습니다. TGW 같은 공용 구간은 형상 변경을 최소화하고, 변경은 DX 진입 구간에 모았습니다.

  • 진입 구간 VPC: IDC에서 DX를 거쳐 AWS로 들어오는 구간의 VPC에 GWLB 엔드포인트를 두었습니다.
  • Inspection VPC: 별도 VPC를 만들어 GWLB와 GWLB 엔드포인트 서비스를 두고, 그 뒤에 IPS를 연결했습니다.
  • 라우팅: 진입 구간 VPC의 라우팅 테이블에서 다음 홉을 GWLB 엔드포인트로 지정했습니다. GWLB 엔드포인트가 있는 서브넷의 라우팅 테이블은 외부로 나가는 기존 경로를 그대로 유지했습니다.

이렇게 하면 DX를 거치는 인바운드·아웃바운드 패킷이 모두 GWLB 엔드포인트로 넘어가고, GWLB를 거쳐 IPS에서 검사됩니다. IPS 검사를 마친 패킷은 엔드포인트가 있는 서브넷으로 되돌아와 원래대로 라우팅됩니다.

AWS GWLB 검사 구성도: IDC에서 DX로 들어온 트래픽이 진입 구간 VPC의 GWLB 엔드포인트를 거쳐 Inspection VPC의 Cisco IPS에서 검사된 뒤 기존 경로로 돌아가는 구조
DX 진입 구간 VPC에 GWLB 엔드포인트를 두고, 별도 Inspection VPC에서 IPS로 검사하는 AWS GWLB 배치입니다.

AWS GWLB 엔드포인트가 한 AZ에만 놓이는 문제

ALB나 NLB는 구성할 때 여러 AZ를 함께 선택해 가용성을 확보할 수 있습니다. GWLB 엔드포인트는 그와 달리 서브넷 하나, 즉 AZ 하나에만 만들어집니다.

라우팅 테이블의 한 경로에는 다음 홉을 하나만 지정할 수 있습니다. GWLB 엔드포인트를 다음 홉으로 지정하면 모든 트래픽이 그 엔드포인트의 AZ 하나로만 흐릅니다. AZ 하나에 문제가 생기면 DX 구간 전체가 영향을 받는 구조였습니다.

AZ별 라우팅 분리가 부른 비대칭 드롭

첫 번째 대응은 라우팅 테이블을 AZ별로 나누고, AZ마다 GWLB 엔드포인트를 만들어 각각 지정하는 방식이었습니다. AZ 하나가 빠져도 이론상으로는 나머지 AZ가 트래픽을 받습니다.

그런데 왕복 경로가 서로 다른 AZ를 지나는 경우가 생겼습니다. C존에서 출발한 연결은 C존 엔드포인트를 거쳐 DX로 나가고, IDC에서 SYN을 처리합니다. SYN/ACK가 A존으로 돌아오면 C존에서 SYN을 본 IPS와는 다른 경로로 검사됩니다. A존 경로의 IPS 세션 테이블에는 이 연결의 SYN 기록이 없으므로, 비대칭 트래픽으로 판단해 드롭했습니다.

상태 기반(stateful) 검사 장비는 한 연결의 양방향 패킷을 같은 장비가 봐야 합니다. AZ별로 경로를 나누면 이 전제가 깨지므로, AZ별 라우팅 분리는 이 구성에 맞지 않는 방법이었습니다.

AWS GWLB 엔드포인트를 AZ별로 나눴을 때 C존에서 나간 SYN의 응답이 A존으로 돌아와 IPS가 드롭하는 비대칭 흐름과 EventBridge·Lambda 다음 홉 전환 보완 개념도
AZ별 경로 분리가 상태 기반 IPS의 비대칭 드롭을 부른 흐름과, 엔드포인트 하나를 유지하며 장애 때만 다음 홉을 바꾸는 보완 방식입니다.

EventBridge와 Lambda로 만든 다음 홉 전환

그래서 가용성은 라우팅 분리 대신 자동화로 확보했습니다. AZ 단위의 트래픽 이상을 감시하는 EventBridge 규칙을 만들었습니다. EventBridge가 이상을 감지하면 트리거용 Lambda가 실행됩니다. Lambda는 다시 라우팅 테이블의 다음 홉을 다른 AZ의 GWLB 엔드포인트로 바꾸는 Lambda를 호출해 경로를 전환합니다.

평소에는 모든 트래픽이 엔드포인트 하나와 같은 IPS 경로를 지나므로 대칭이 유지됩니다. AZ에 이상이 생길 때만 경로가 통째로 옮겨 갑니다. AZ를 전환하는 순간 진행 중이던 연결은 새 경로의 IPS에 세션 기록이 없으므로 끊길 수 있다는 점도 감안해야 합니다.

지금 다시 설계한다면 먼저 볼 AWS 기능

이 구성은 당시 요건과 제약 안에서 고른 방법입니다. 같은 문제를 다시 다룬다면 아래 기능이 구조에 맞는지 먼저 확인해 볼 만합니다.

  • VGW 인그레스 라우팅의 GWLB 엔드포인트 지정: 2023년 8월부터 가상 프라이빗 게이트웨이(VGW) 라우팅 테이블에서 GWLB 엔드포인트를 다음 홉으로 지정할 수 있습니다. DX나 Site-to-Site VPN으로 들어오는 트래픽을 VGW 경계에서 바로 검사 경로로 보낼 수 있습니다.
  • TGW appliance mode: 트래픽이 TGW의 VPC 연결을 거쳐 검사 VPC로 간다면 이 기능을 먼저 봅니다. TGW가 한 연결의 왕복 트래픽을 같은 AZ의 연결로 보내 비대칭을 막아 줍니다.
  • GWLB 교차 영역 부하 분산: 기본값은 꺼져 있고, 켜면 다른 AZ의 어플라이언스로도 트래픽을 보낼 수 있습니다. AZ 하나에 엔드포인트가 놓이는 제약은 그래도 남습니다.

정리

GWLB는 패킷 검사 어플라이언스를 AWS 트래픽 경로에 넣으려면 사실상 필수인 구성 요소입니다. IPS 검사를 우회하거나 루프를 도는 트래픽이 라우팅 변경 하나로도 생길 수 있어, 흐름 전체를 신중하게 검토해야 했습니다.

이번 AWS GWLB 구성에서 얻은 가장 큰 교훈은 대칭성입니다. IPS 같은 상태 기반 장비를 쓰는 한, 가용성을 위해 경로를 나누는 순간 왕복 경로가 갈라질 수 있습니다. AZ 이중화 같은 가용성 설계는 대칭을 깨지 않는 범위 안에서 해야 했습니다.

참고

관련 포스트:

참고 문서: AWS Gateway Load Balancer 개요 · GWLB 속성(교차 영역 부하 분산) · VGW 인그레스 라우팅의 GWLB 지원 · GWLB와 TGW 중앙 검사 아키텍처(appliance mode) · Cisco Threat Defense의 GWLB·Geneve 구성