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가 이 옵션을 지원하는지 확인하려면:
- Kubespray를 최신 버전으로 업그레이드
- 해당 버전이 지원하는 Cilium 옵션 확인
- 원하는 옵션이 없으면? 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.9cilium_enable_hubble: truecilium_hubble_install: truecilium_enable_hubble_metrics: truecilium_hubble_metrics: - dns - drop - tcp - flow - icmp - httpL2Announce도 활성화되어 있어서 LoadBalancer IP를 내부 네트워크에 광고하고 있었습니다.
# CiliumL2AnnouncementPolicyname: advertise-specific-cidrspec: interfaces: - enp1s0 loadBalancerIPs: true nodeSelector: matchExpressions: - key: node-role.kubernetes.io/control-plane operator: DoesNotExist이 설정들을 Helm values로 옮기면 됩니다.
다운타임
CNI를 교체하는 작업이라 다운타임이 불가피합니다. 기존 Cilium을 삭제하고 새로 설치하는 동안 Pod 네트워크가 끊기니까요.
실행
1. 백업
혹시 모르니 etcd 스냅샷과 Cilium 리소스를 백업했습니다.
# 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 스냅샷 생성# 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.yamlkubectl get ciliuml2announcementpolicy -A -o yaml > cilium-l2-policy.yamlkubectl get ciliumloadbalancerippool -A -o yaml > cilium-lb-pool.yamlkubectl get ciliumnetworkpolicy -A -o yaml > cilium-network-policies.yaml2. Kubespray 설정 변경
Kubespray가 더 이상 CNI를 관리하지 않도록 설정을 변경합니다.
# Beforekube_network_plugin: ciliumkube_owner: kube
# Afterkube_network_plugin: cnikube_owner: rootkube_network_plugin: cni로 설정하면 Kubespray는 CNI 플러그인 설치를 건너뜁니다. 직접 관리하겠다는 의미입니다.
kube_owner를 root로 바꾼 건 나중에 설명하겠습니다.
3. 기존 Cilium 제거
이제 Kubespray가 설치한 Cilium을 수동으로 제거합니다.
# DaemonSet, Deployment 제거kubectl delete daemonset cilium -n kube-systemkubectl 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-systemkubectl 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-systemkubectl delete clusterrole cilium cilium-operator hubble-relay hubble-uikubectl delete clusterrolebinding cilium cilium-operator hubble-relay hubble-ui
# Service 제거kubectl delete svc hubble-metrics hubble-peer hubble-relay hubble-ui -n kube-systemCRD는 삭제하지 않았습니다. 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 파일로 설치합니다.
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: 1helm 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에 바이너리를 복사하려는데 권한이 없다는 거였습니다.
# 각 노드에서 확인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/bin이 kube:root 소유에 755 권한이면, others는 쓰기 권한이 없으니 실패하는 거죠.
해결
각 노드에서 디렉토리 소유권을 root로 변경했습니다.
# node1, node2, node3-gpu 각각에서 실행sudo chown -R root:root /opt/cni/bin그리고 Kubespray 설정에서 kube_owner: root로 바꿔둔 이유도 이것입니다. 향후 Kubespray를 다시 실행해도 소유권이 kube로 돌아가지 않게 하기 위해서입니다.
소유권 변경 후 Cilium Pod이 정상적으로 시작되었습니다.
kubectl -n kube-system rollout status daemonset/cilium# daemonset "cilium" successfully rolled out검증
Cilium 상태 확인
Cilium CLI가 설치되어 있으면 한 줄로 확인할 수 있습니다.
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/3Deployment cilium-operator Desired: 1, Ready: 1/1, Available: 1/1Deployment hubble-relay Desired: 1, Ready: 1/1, Available: 1/1Deployment hubble-ui Desired: 1, Ready: 1/1, Available: 1/1Cluster Pods: 101/101 managed by CiliumHelm chart version: 1.15.9Pod 네트워크 검증
CNI가 제대로 동작하는지 확인하려면 실제로 Pod 간 통신이 되는지 테스트해야 합니다.
# 테스트 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.10Address 1: 169.254.25.10
Name: kubernetes.defaultAddress 1: 10.233.0.1 kubernetes.default.svc.cluster.localDNS 조회와 Service 접근이 정상이면 CNI가 제대로 동작하는 겁니다.
Ingress 경유 외부 접근 확인
LoadBalancer IP 할당 여부만으로는 부족합니다. IP가 할당되어도 실제 트래픽이 Pod까지 도달하지 않을 수 있거든요. Ingress를 통해 실제 서비스에 요청을 보내서 응답을 확인해야 합니다.
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 버전에 종속 | 독립적 |
얻은 것
- CNI 독립 관리 - 클러스터 전체 재구성 없이 Cilium만 업그레이드 가능
- Gateway API 준비 - 버전 업그레이드 후
gatewayAPI.enabled: true추가할 준비 완료 - 트러블슈팅 경험 -
drop: ALL+ 디렉토리 소유권 이슈는 다른 CNI 마이그레이션에서도 발생할 수 있음
이제 Cilium을 자유롭게 설정할 수 있으니, 다음은 Gateway API를 활성화하고 NGINX Ingress에서 전환할 예정입니다.
참고
관련 콘텐츠
Cilium BGP+ECMP 구성 (feat. Cilium 1.18.5 버그 발견)
Cilium Gateway API 활성화 과정에서 겪은 TPROXY 문제, v1.18.5 버그 발견, hostNetwork 모드 전환, BGP + ECMP 구성까지의 여정입니다.
DevOpsLinkedIn에서 발견한 Tencent WeKnora, GraphRAG PoC하고 PR까지 Merged
LinkedIn에서 발견한 Tencent WeKnora를 홈 Kubernetes 클러스터에서 PoC하고, Helm Chart PR까지 Merge한 여정
DevOpsEC2 해킹당하고, DevSecOps 파이프라인을 구축하다
사이드 프로젝트 개발 서버가 털린 경험을 계기로 DevSecOps 파이프라인을 구축한 이야기입니다.