Kubernetes Kubernetes Helm Cilium

Gateway API 전환기 (1) - Cilium을 Kubespray에서 Helm으로

Kubespray로 설치한 Cilium을 Helm 관리로 전환하는 과정에서 겪은 트러블슈팅과 교훈을 공유합니다.

홈랩 Kubernetes를 Kubespray로 운영하고 있습니다. CNI는 Cilium, Ingress는 NGINX를 쓰고 있었는데, Ingress NGINX가 2026년 3월에 지원 종료된다는 소식을 들었습니다.

이미 CNI로 Cilium을 쓰고 있으니 대안으로 Cilium Gateway API를 도입하려고 합니다.


Gateway API를 어떻게 켜지?

Cilium에서 Gateway API를 활성화하려면 Helm values에서 gatewayAPI.enabled: true를 설정해야 합니다. 현재 Cilium은 Kubespray로 관리되고 있는데요.

Kubespray가 이 옵션을 지원하는지 확인하려면:

  1. Kubespray를 최신 버전으로 업그레이드
  2. 해당 버전이 지원하는 Cilium 옵션 확인
  3. 원하는 옵션이 없으면? Kubespray에 PR을 올리거나 기다리거나…

Kubespray 자체를 업그레이드하는 것도 부담입니다. CNI 설정 하나 바꾸려고 클러스터 관리 도구 전체를 건드려야 하니까요.

그래서 CNI를 Kubespray에서 떼어내서 Helm으로 직접 관리하기로 했습니다. 이러면:

  • Cilium 설정을 자유롭게 변경 가능
  • Kubespray 버전과 무관하게 Cilium 업그레이드 가능
  • helm upgrade 한 줄로 설정 변경, helm rollback으로 롤백

마이그레이션 계획

목표는:

Before: Kubespray가 Cilium 설치/관리
After: Helm이 Cilium 설치/관리 (Kubespray는 CNI를 건드리지 않음)

현재 구성 확인

먼저 Kubespray가 설치한 Cilium 설정을 확인했습니다.

# Kubespray 설정 (k8s-net-cilium.yml)
cilium_version: v1.15.9
cilium_enable_hubble: true
cilium_hubble_install: true
cilium_enable_hubble_metrics: true
cilium_hubble_metrics:
- dns
- drop
- tcp
- flow
- icmp
- http

L2Announce도 활성화되어 있어서 LoadBalancer IP를 내부 네트워크에 광고하고 있었습니다.

# CiliumL2AnnouncementPolicy
name: advertise-specific-cidr
spec:
interfaces:
- enp1s0
loadBalancerIPs: true
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist

이 설정들을 Helm values로 옮기면 됩니다.

다운타임

CNI를 교체하는 작업이라 다운타임이 불가피합니다. 기존 Cilium을 삭제하고 새로 설치하는 동안 Pod 네트워크가 끊기니까요.


실행

1. 백업

혹시 모르니 etcd 스냅샷과 Cilium 리소스를 백업했습니다.

Terminal window
# etcd 스냅샷 (node1에서)
sudo ETCDCTL_API=3 etcdctl snapshot save /var/lib/etcd/snapshot-$(date +%Y%m%d-%H%M%S).db \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/ssl/etcd/ssl/ca.pem \
--cert=/etc/ssl/etcd/ssl/node-node1.pem \
--key=/etc/ssl/etcd/ssl/node-node1-key.pem
# 결과: 74MB 스냅샷 생성
Terminal window
# Cilium 리소스 백업
mkdir -p ~/backup/cilium-$(date +%Y%m%d)
cd ~/backup/cilium-$(date +%Y%m%d)
kubectl get configmap -n kube-system cilium-config -o yaml > cilium-config.yaml
kubectl get ciliuml2announcementpolicy -A -o yaml > cilium-l2-policy.yaml
kubectl get ciliumloadbalancerippool -A -o yaml > cilium-lb-pool.yaml
kubectl get ciliumnetworkpolicy -A -o yaml > cilium-network-policies.yaml

2. Kubespray 설정 변경

Kubespray가 더 이상 CNI를 관리하지 않도록 설정을 변경합니다.

inventory/home/group_vars/k8s_cluster/k8s-cluster.yml
# Before
kube_network_plugin: cilium
kube_owner: kube
# After
kube_network_plugin: cni
kube_owner: root

kube_network_plugin: cni로 설정하면 Kubespray는 CNI 플러그인 설치를 건너뜁니다. 직접 관리하겠다는 의미입니다.

kube_ownerroot로 바꾼 건 나중에 설명하겠습니다.

3. 기존 Cilium 제거

이제 Kubespray가 설치한 Cilium을 수동으로 제거합니다.

Terminal window
# DaemonSet, Deployment 제거
kubectl delete daemonset cilium -n kube-system
kubectl delete deployment cilium-operator hubble-relay hubble-ui -n kube-system
# ConfigMap, Secret 제거
kubectl delete configmap cilium-config hubble-relay-config hubble-ui-nginx -n kube-system
kubectl delete secret hubble-relay-client-certs hubble-server-certs -n kube-system
# ServiceAccount, RBAC 제거
kubectl delete serviceaccount cilium cilium-operator hubble-relay hubble-ui -n kube-system
kubectl delete clusterrole cilium cilium-operator hubble-relay hubble-ui
kubectl delete clusterrolebinding cilium cilium-operator hubble-relay hubble-ui
# Service 제거
kubectl delete svc hubble-metrics hubble-peer hubble-relay hubble-ui -n kube-system

CRD는 삭제하지 않았습니다. Kubernetes에서 CRD를 삭제하면 해당 타입의 모든 CR(Custom Resource)이 cascade delete됩니다. L2AnnouncementPolicy, LoadBalancerIPPool 같은 설정을 다시 만들지 않기 위해서였습니다.

Helm도 같은 이유로 CRD를 삭제하지 않습니다. 공식 문서에 따르면:

“Deleting a CRD automatically deletes all of the CRD’s contents across all namespaces in the cluster. Consequently, Helm will not delete CRDs.”

그래서 helm uninstall을 해도 CRD는 남아있고, 우리처럼 기존 CRD가 있는 상태에서 helm install을 하면 Helm이 기존 CRD를 그대로 사용합니다.

4. Helm으로 Cilium 설치

미리 준비해둔 Helm values 파일로 설치합니다.

cilium/1.15.9/cilium.yaml
cluster:
name: cluster.local
ipam:
mode: kubernetes
operator:
clusterPoolIPv4PodCIDRList:
- "10.x.x.0/18"
kubeProxyReplacement: false # 기존 kube-proxy 유지
l2announcements:
enabled: true
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true
metrics:
enabled:
- dns
- drop
- tcp
- flow
- icmp
- http
tls:
auto:
enabled: true
method: helm
prometheus:
enabled: true
operator:
replicas: 1
Terminal window
helm repo add cilium https://helm.cilium.io/
helm repo update
helm upgrade --install cilium cilium/cilium \
--version 1.15.9 \
--namespace kube-system \
-f cilium/1.15.9/cilium.yaml

자 이제 트러블슈팅의 시간.


트러블슈팅: Permission Denied

증상

Cilium Pod이 시작되지 않았습니다. mount-cgroup init container에서 에러가 발생했습니다.

cp: cannot create regular file '/hostbin/cilium-mount': Permission denied

원인 분석

Cilium이 /opt/cni/bin에 바이너리를 복사하려는데 권한이 없다는 거였습니다.

Terminal window
# 각 노드에서 확인
ls -la /opt/cni/bin
# drwxr-xr-x kube root /opt/cni/bin

디렉토리 소유자가 kube였습니다. Kubespray가 kube_owner: kube로 설정되어 있을 때 생성한 디렉토리입니다.

그런데 왜 root로 실행되는 Cilium이 쓰기를 못 할까요?

Helm으로 설치한 Cilium은 보안 강화를 위해 컨테이너에 drop: ALL capabilities를 설정합니다. 이렇게 하면 DAC_OVERRIDE capability가 제거됩니다.

DAC_OVERRIDE란?
파일 읽기/쓰기/실행 권한 검사를 우회하는 capability입니다.
root(UID 0)가 아무 파일이나 접근할 수 있는 건 이 capability 덕분입니다.

DAC_OVERRIDE 없이는 root라도 일반 사용자처럼 파일 권한을 따라야 합니다. /opt/cni/binkube:root 소유에 755 권한이면, others는 쓰기 권한이 없으니 실패하는 거죠.

해결

각 노드에서 디렉토리 소유권을 root로 변경했습니다.

Terminal window
# node1, node2, node3-gpu 각각에서 실행
sudo chown -R root:root /opt/cni/bin

그리고 Kubespray 설정에서 kube_owner: root로 바꿔둔 이유도 이것입니다. 향후 Kubespray를 다시 실행해도 소유권이 kube로 돌아가지 않게 하기 위해서입니다.

소유권 변경 후 Cilium Pod이 정상적으로 시작되었습니다.

Terminal window
kubectl -n kube-system rollout status daemonset/cilium
# daemonset "cilium" successfully rolled out

검증

Cilium 상태 확인

Cilium CLI가 설치되어 있으면 한 줄로 확인할 수 있습니다.

Terminal window
cilium status
/¯¯\
/¯¯\__/¯¯\ Cilium: OK
\__/¯¯\__/ Operator: OK
/¯¯\__/¯¯\ Envoy DaemonSet: disabled (using embedded mode)
\__/¯¯\__/ Hubble Relay: OK
\__/ ClusterMesh: disabled
DaemonSet cilium Desired: 3, Ready: 3/3, Available: 3/3
Deployment cilium-operator Desired: 1, Ready: 1/1, Available: 1/1
Deployment hubble-relay Desired: 1, Ready: 1/1, Available: 1/1
Deployment hubble-ui Desired: 1, Ready: 1/1, Available: 1/1
Cluster Pods: 101/101 managed by Cilium
Helm chart version: 1.15.9

Pod 네트워크 검증

CNI가 제대로 동작하는지 확인하려면 실제로 Pod 간 통신이 되는지 테스트해야 합니다.

Terminal window
# 테스트 Pod에서 클러스터 내부 DNS와 Service 접근 확인
kubectl run nettest --image=busybox --rm -it --restart=Never -- sh -c \
"nslookup kubernetes.default && wget -qO- --timeout=3 http://kubernetes.default/healthz"
Server: 169.254.25.10
Address 1: 169.254.25.10
Name: kubernetes.default
Address 1: 10.233.0.1 kubernetes.default.svc.cluster.local

DNS 조회와 Service 접근이 정상이면 CNI가 제대로 동작하는 겁니다.

Ingress 경유 외부 접근 확인

LoadBalancer IP 할당 여부만으로는 부족합니다. IP가 할당되어도 실제 트래픽이 Pod까지 도달하지 않을 수 있거든요. Ingress를 통해 실제 서비스에 요청을 보내서 응답을 확인해야 합니다.

Terminal window
curl -sk https://grafana.heeho.net/login | head -3
<!DOCTYPE html>
<html lang="en-US">
<head></head>
</html>

HTML 페이지가 정상 반환되면 CNI → Ingress → Service → Pod 전체 경로가 동작하는 겁니다.


결과

항목결과
다운타임약 10-15분
Cilium 버전v1.15.9 (Helm 관리)
발생 에러2건 (잔여 리소스 충돌, Permission Denied)

마이그레이션 전후 비교

항목Before (Kubespray)After (Helm)
버전 업그레이드playbook 전체 실행helm upgrade
설정 변경제한적 (Kubespray 변수만)자유로움 (모든 Helm values)
롤백블랙박스helm rollback
의존성Kubespray 버전에 종속독립적

얻은 것

  1. CNI 독립 관리 - 클러스터 전체 재구성 없이 Cilium만 업그레이드 가능
  2. Gateway API 준비 - 버전 업그레이드 후 gatewayAPI.enabled: true 추가할 준비 완료
  3. 트러블슈팅 경험 - drop: ALL + 디렉토리 소유권 이슈는 다른 CNI 마이그레이션에서도 발생할 수 있음

이제 Cilium을 자유롭게 설정할 수 있으니, 다음은 Gateway API를 활성화하고 NGINX Ingress에서 전환할 예정입니다.


참고

관련 콘텐츠

댓글