Cilium BGP+ECMP 구성 (feat. Cilium 1.18.5 버그 발견)
Cilium Gateway API 활성화 과정에서 겪은 TPROXY 문제, v1.18.5 버그 발견, hostNetwork 모드 전환, BGP + ECMP 구성까지의 여정입니다.
관련 콘텐츠:
- Gateway API 전환기 (1) - Cilium을 Helm으로 마이그레이션
- MikroTik 라우터 도입 - BGP를 위해 라우터 교체 (스포일러: 진짜 원인은 다른 곳에)
NGINX Ingress EOL 대응으로 Cilium Gateway API를 활성화하려 했는데, 외부 접속이 안 됩니다. 이 글에서는 7번의 시도 끝에 문제를 해결하고 BGP+ECMP까지 구성한 과정을 다룹니다.

삽질 기록 - 왜 바로 안 됐나
Gateway API를 활성화하고 여러 모드를 시도했는데, 외부에서 접속이 안 됩니다.
Gateway API는 LoadBalancer Service 모드(기본)와 hostNetwork 모드 두 가지 노출 방식이 있으며, 둘 중 하나만 선택 가능합니다. hostNetwork 모드에서는 Gateway 자체에 VIP가 없지만, 별도의 LoadBalancer Service를 만들어 VIP를 할당받고 L2 또는 BGP로 광고할 수 있습니다.
MikroTik 도입 전 (L2 Announcement)
시도 1: L2 + LoadBalancer Service 모드 (kubeProxyReplacement: false)
- 증상: Gateway 생성됨, HTTPRoute Accepted, 하지만 Connection refused
- 원인: bare-metal 환경에서 TPROXY eBPF 리다이렉션 실패 (Envoy가 localhost:16637에서 listen하지만, 외부 트래픽이 eBPF hook을 우회)
시도 2: L2 + hostNetwork 모드 (v1.18.5)
- 증상: Gateway PROGRAMMED: False
- 결과: 실패
시도 3: L2 + LoadBalancer Service 모드 (kubeProxyReplacement: true)
- 증상: Envoy 동작 확인, L2 announcement 정상, 하지만 외부 접속 불가
- 원인: bare-metal 환경에서 TPROXY eBPF 리다이렉션 실패
How Cilium Ingress and Gateway API differ from other Ingress Controllers
시도 4: NodePort 직접 접근 검토
- 배경: VIP(External IP)로 접근하면 TPROXY 실패, hostNetwork는 실패 (이유 모름)
- 계획: LoadBalancer Service의 NodePort(30443)로 직접 접근 + 라우터에서 443→NodeIP:30443 포트포워딩
- 문제: 기존 KT 공유기는 단일 목적지로만 포트포워딩 가능 → SPOF
- 결론: SPOF 문제 해결 및 여러 노드로 분산하려면 ECMP 지원 라우터 필요 → MikroTik hEX S 주문
MikroTik 도입 후 (BGP)
시도 5: BGP + hostNetwork 모드 (v1.18.5)
- 증상: ConfigMap 반영되었지만 포트 listen 안 됨
- 원인: Cilium Issue #42786 - v1.18.3+ hostNetwork reconciler 회귀 버그
시도 6: 80/443 포트 바인딩 실패
- 증상:
cannot bind '0.0.0.0:80': Permission denied - 원인: Envoy가 privileged port (80, 443) 바인딩 권한 없음
- 해결:
envoy.securityContext.capabilities.keepCapNetBindService: true설정
Linux Capabilities - NET_BIND_SERVICE
시도 7: BGP + hostNetwork 모드 (v1.18.2) - 해결
v1.18.2로 다운그레이드 후 hostNetwork 모드 정상 동작!
| 버전 | hostNetwork Gateway |
|---|---|
| v1.18.2 | ✓ 동작 |
| v1.18.3+ | ✗ 버그 |
돌이켜보면 시도 2에서 v1.18.2로 다운그레이드했다면 hostNetwork가 동작했을 것입니다. 하지만 그랬더라도 KT 공유기의 ECMP 미지원으로 SPOF 문제는 남아있었기 때문에, MikroTik 도입은 고도화를 위해 필요한 부분이었습니다.
Helm Values 핵심 설정
Cilium Gateway API - Host Network Mode
values.yaml - BGP + hostNetwork 모드 설정
# kube-proxy 대체 (LoadBalancer Service 모드 전제조건, hostNetwork 모드에서는 선택사항)kubeProxyReplacement: true
# BGP Control Plane 활성화bgpControlPlane: enabled: true
# Gateway API hostNetwork 모드gatewayAPI: enabled: true hostNetwork: enabled: true
# Envoy privileged 포트 바인딩 허용envoy: securityContext: capabilities: envoy: - NET_ADMIN - SYS_ADMIN - NET_BIND_SERVICE keepCapNetBindService: true
# L2 announcements 비활성화 (hostNetwork 모드에서 불필요)l2announcements: enabled: false설정 포인트:
kubeProxyReplacement: LoadBalancer Service 모드에서는 필수, hostNetwork 모드에서는 선택 (kube-proxy 대체)bgpControlPlane.enabled: BGP 기반 VIP 광고 활성화gatewayAPI.hostNetwork.enabled: 모든 노드에서 80/443 리슨keepCapNetBindService: privileged 포트 바인딩 권한 유지l2announcements.enabled: false: hostNetwork 모드에서는 불필요
BGP 피어링 - HA와 로드밸런싱을 위한 추가 구성
hostNetwork 모드로 Gateway가 동작하게 되었습니다. 이 상태에서도 서비스는 정상 동작하지만, node1으로만 포트포워딩하면 SPOF가 됩니다.
이전 콘텐츠에서 도입한 MikroTik 라우터로 BGP + ECMP를 구성하여 고가용성을 확보하기로 했습니다.
flowchart LR subgraph MikroTik[" MikroTik AS 64512 "] RT[Router<br/>172.30.1.1] end
subgraph K8s[" K8s Cluster AS 64513 "] VIP[gateway-lb<br/>VIP 172.30.1.7] N1[node1<br/>Envoy :80/443] N2[node2<br/>Envoy :80/443] N3[node3<br/>Envoy :80/443] end
RT <-->|BGP peer| N1 RT <-->|BGP peer| N2 RT <-->|BGP peer| N3
N1 -.->|VIP 광고| RT N2 -.->|VIP 광고| RT N3 -.->|VIP 광고| RTAS 번호 설계:
- MikroTik: AS 64512
- Cilium (각 노드): AS 64513
CiliumBGPPeeringPolicy
CiliumBGPPeeringPolicy - BGP 피어링 정책
apiVersion: cilium.io/v2alpha1kind: CiliumBGPPeeringPolicymetadata: name: bgp-peering-policyspec: nodeSelector: matchLabels: kubernetes.io/os: linux virtualRouters: - localASN: 64513 exportPodCIDR: false neighbors: - peerAddress: "172.30.1.1/32" peerASN: 64512 serviceSelector: matchExpressions: - key: "io.cilium/bgp-announce" operator: In values: ["true"]설정 설명:
nodeSelector: 모든 Linux 노드에 적용localASN: 64513: Cilium 측 AS 번호peerAddress: MikroTik 라우터 IPserviceSelector:io.cilium/bgp-announce: "true"라벨이 있는 Service만 BGP로 광고
Gateway 리소스
Gateway - HTTPS 리스너 설정
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: name: cilium-gateway namespace: kube-systemspec: gatewayClassName: cilium infrastructure: labels: io.cilium/bgp-announce: "true" # BGP 광고 대상 listeners: - name: http protocol: HTTP port: 80 allowedRoutes: namespaces: from: All - name: https protocol: HTTPS port: 443 tls: mode: Terminate certificateRefs: - kind: Secret name: wildcard.heeho.net namespace: cilium-secrets allowedRoutes: namespaces: from: AllReferenceGrant (Cross-namespace Secret 참조)
Gateway와 TLS Secret이 다른 네임스페이스에 있으므로(Gateway: kube-system, Secret: cilium-secrets), ReferenceGrant로 cross-namespace 참조를 허용합니다.
Gateway API - Cross-Namespace References
ReferenceGrant - Secret 접근 권한
apiVersion: gateway.networking.k8s.io/v1beta1kind: ReferenceGrantmetadata: name: allow-gateway-to-secrets namespace: cilium-secretsspec: from: - group: gateway.networking.k8s.io kind: Gateway namespace: kube-system to: - group: "" kind: SecretLoadBalancer Service (VIP 할당용)
hostNetwork 모드에서는 Gateway 자체에 VIP가 없습니다. 별도의 LoadBalancer Service를 생성하여 VIP를 할당받습니다.
Cilium Gateway API - Host Network Mode
apiVersion: v1kind: Servicemetadata: name: gateway-lb namespace: kube-system labels: io.cilium/bgp-announce: "true" # BGP 광고 활성화spec: type: LoadBalancer selector: k8s-app: cilium-envoy # hostNetwork Envoy Pod ports: - name: http port: 80 - name: https port: 443Cilium LB IPAM이 VIP(172.30.1.7)를 할당하고, CiliumBGPPeeringPolicy로 모든 노드가 BGP로 광고합니다.


MikroTik Static ECMP
MikroTik 라우팅 테이블:
# Static ECMP (distance 10, 우선)VIP 172.30.1.7/32 → node1, node2, node3 (ECMP 분산)
# BGP (distance 20, 백업)VIP 172.30.1.7/32 → node1 → node2 → node3RouterOS 6.x는 BGP ECMP를 네이티브로 지원하지 않습니다 (BGP multipath는 RouterOS 7.22beta1에서 도입). Static ECMP route를 추가하고 (distance 10), BGP route (distance 20)보다 우선하도록 설정했습니다. BGP는 노드 상태 모니터링 역할을 합니다.

RouterOS BGP Multipath 지원 (7.22beta1~)
RouterOS 명령어 - Static ECMP route 추가
# Static ECMP route 추가 (distance 10, BGP보다 우선)/ip route add dst-address=172.30.1.7/32 \ gateway=172.30.1.20,172.30.1.70,172.30.1.82 \ distance=10 \ comment="ECMP to Gateway VIP"| 장점 | 적용 |
|---|---|
| 동적 라우팅 | ✓ VIP 자동 광고 |
| Failover | ✓ 노드 장애 시 자동 제외 |
| ECMP 로드밸런싱 | ✓ Static route로 3개 노드 분산 |
포트포워딩 구성의 단계적 개선
여기서 중요한 개념이 있습니다.
hostNetwork 모드에서 Gateway는 모든 노드에서 80/443을 리슨합니다. 즉, 어느 노드로 트래픽을 보내도 Gateway는 동작합니다.
Cilium Gateway API - Host Network Mode
그렇다면 문제는 뭘까요? 앞단 포트포워딩이 어디를 가리키느냐입니다.
1단계: node1 직접 포워딩 (초기)
MikroTik 도입 직후, 가장 단순한 구성으로 시작했습니다.
/ip firewall nat add chain=dstnat dst-address=121.170.122.127 dst-port=443 \ protocol=tcp action=dst-nat to-addresses=172.30.1.20 to-ports=443| 장점 | 단점 |
|---|---|
| 설정이 단순함 | node1 장애 시 서비스 중단 (SPOF) |
| 즉시 동작 | 트래픽이 한 노드에 집중 |
2단계: VIP + BGP (Failover)
SPOF 문제를 해결하기 위해 VIP를 도입했습니다. hostNetwork 모드에서는 Gateway 자체에 VIP가 없으므로, 별도의 LoadBalancer Service를 생성합니다.
apiVersion: v1kind: Servicemetadata: name: gateway-lb namespace: kube-system labels: io.cilium/bgp-announce: "true" # BGP 광고 활성화spec: type: LoadBalancer selector: k8s-app: cilium-envoy ports: - name: http port: 80 - name: https port: 443Cilium LB IPAM이 VIP(172.30.1.7)를 할당하고, CiliumBGPPeeringPolicy에 의해 모든 노드가 이 VIP를 BGP로 광고합니다.
# NAT 대상을 VIP로 변경/ip firewall nat set [find comment~"Gateway API"] to-addresses=172.30.1.7| 장점 | 단점 |
|---|---|
| 노드 장애 시 자동 failover | BGP가 특정 노드 하나만 선택 |
| VIP 기반 동적 라우팅 | 트래픽이 한 노드로 집중 |
3단계: VIP + BGP + ECMP (현재 구성)
RouterOS 6.x는 BGP ECMP를 네이티브로 지원하지 않습니다. Static ECMP route를 추가하여 트래픽을 분산합니다.
# Static ECMP route (distance 10, BGP보다 우선)/ip route add dst-address=172.30.1.7/32 \ gateway=172.30.1.20,172.30.1.70,172.30.1.82 \ distance=10 comment="ECMP to Gateway VIP"라우팅 테이블 (실제 상태):
# DST-ADDRESS GATEWAY DISTANCE 0 A S 172.30.1.7/32 172.30.1.20,172.30.1.70,172.30.1.82 10 ← ECMP (Active) 1 Db 172.30.1.7/32 172.30.1.20 20 ← BGP (백업) 2 Db 172.30.1.7/32 172.30.1.70 20 3 Db 172.30.1.7/32 172.30.1.82 20| 장점 | 단점 |
|---|---|
| 트래픽 3개 노드로 분산 | Static route 수동 관리 필요 |
| 노드 장애 시 자동 제외 | RouterOS 7.22+ 필요 시 업그레이드 |
| BGP가 노드 상태 모니터링 | - |
구성 비교 요약
| 단계 | 구성 | Failover | 로드밸런싱 | 비고 |
|---|---|---|---|---|
| 1단계 | node1 직접 | ✗ | ✗ | SPOF |
| 2단계 | VIP + BGP | ✓ | ✗ | 단일 노드 선택 |
| 3단계 | VIP + BGP + ECMP | ✓ | ✓ | 현재 구성 |
최종 MikroTik NAT 규칙
# 실제 NAT 규칙0 ;;; Gateway API HTTP (VIP) chain=dstnat action=dst-nat to-addresses=172.30.1.7 to-ports=80 protocol=tcp dst-address=121.170.122.127 dst-port=80
1 ;;; K8s API chain=dstnat action=dst-nat to-addresses=172.30.1.20 to-ports=6443 protocol=tcp dst-address=121.170.122.127 dst-port=6443
2 ;;; Gateway API HTTPS (VIP) chain=dstnat action=dst-nat to-addresses=172.30.1.7 to-ports=443 protocol=tcp dst-address=121.170.122.127 dst-port=443포인트:
- Gateway 트래픽(80/443)은 VIP(172.30.1.7) 로 → ECMP 분산
- K8s API(6443)는 node1 직접 → control plane은 분산 불필요
트래픽 흐름
외부 클라이언트 │ ▼ dst-nat (공인IP → VIP) MikroTik NAT │ ▼ Static ECMP (distance 10)┌──────┼──────┐│ │ │▼ ▼ ▼node1 node2 node3 ← 3개 노드로 분산 :443 :443 :443 │ │ │ └──────┼──────┘ ▼ cilium-envoy (hostNetwork) │ ▼ HTTPRoute → Backend Service장애 시나리오:
- node1 다운 → MikroTik이 ECMP에서 자동 제외 (ARP timeout)
- BGP session 끊김 → MikroTik 라우팅 테이블에서 해당 BGP route 제거
- 복구 시 → BGP re-establish, ECMP에 자동 복귀
HTTPRoute 마이그레이션
14개 서비스를 Ingress에서 HTTPRoute로 전환했습니다.

HTTPRoute 예시 (Grafana):
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: grafana namespace: monitoringspec: parentRefs: - name: cilium-gateway namespace: kube-system hostnames: - grafana.heeho.net rules: - backendRefs: - name: grafana port: 80모든 HTTPRoute 전환 후 NGINX Ingress를 제거했습니다.
helm uninstall ingress-nginx -n ingress-nginx최종 아키텍처
flowchart TB subgraph External[" 외부 "] Client[Client<br/>grafana.heeho.net] end
subgraph Router[" MikroTik "] NAT[dst-nat<br/>공인IP → VIP] ECMP[Static ECMP<br/>distance 10] end
subgraph Cluster[" K8s Cluster "] N1[node1:443] N2[node2:443] N3[node3:443] GW[Cilium Gateway<br/>hostNetwork] SVC[Backend Services] end
Client --> NAT NAT --> ECMP ECMP --> N1 & N2 & N3 N1 & N2 & N3 --> GW GW -->|HTTPRoute| SVC얻은 것:
- Gateway API 표준 도입 (NGINX Ingress 의존성 제거)
- BGP + ECMP 기반 고가용성 (자동 Failover + 로드밸런싱)
- Cilium v1.18.5 hostNetwork 버그 발견 및 리포트
배운 것:
- 공식 문서의 동작 원리(TPROXY)를 이해해야 문제 원인을 빠르게 파악할 수 있음
- 최신 버전 버그 가능성 → GitHub Issues 검색 + 다운그레이드 시도
- bare-metal 환경은 클라우드와 트래픽 경로가 다름 (eBPF hook 우회 가능)
관련 콘텐츠
Gateway API 전환기 (1) - Cilium을 Kubespray에서 Helm으로
Kubespray로 설치한 Cilium을 Helm 관리로 전환하는 과정에서 겪은 트러블슈팅과 교훈을 공유합니다.
KubernetesNGINX Ingress EOL 대응: OCI에서 Envoy Gateway로 마이그레이션
NGINX Ingress Controller 지원 종료에 대비해 OCI Always Free 클러스터에서 Envoy Gateway로 마이그레이션한 경험을 공유합니다.
DevOpsLinkedIn에서 발견한 Tencent WeKnora, GraphRAG PoC하고 PR까지 Merged
LinkedIn에서 발견한 Tencent WeKnora를 홈 Kubernetes 클러스터에서 PoC하고, Helm Chart PR까지 Merge한 여정