Kubernetes Kubernetes Cilium GatewayAPI BGP

Cilium BGP+ECMP 구성 (feat. Cilium 1.18.5 버그 발견)

Cilium Gateway API 활성화 과정에서 겪은 TPROXY 문제, v1.18.5 버그 발견, hostNetwork 모드 전환, BGP + ECMP 구성까지의 여정입니다.

관련 콘텐츠:

NGINX Ingress EOL 대응으로 Cilium Gateway API를 활성화하려 했는데, 외부 접속이 안 됩니다. 이 글에서는 7번의 시도 끝에 문제를 해결하고 BGP+ECMP까지 구성한 과정을 다룹니다.

MikroTik 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

Cilium Envoy Security Context

시도 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 Helm Reference

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 광고| RT

AS 번호 설계:

  • MikroTik: AS 64512
  • Cilium (각 노드): AS 64513

CiliumBGPPeeringPolicy

Cilium BGP Control Plane

CiliumBGPPeeringPolicy - BGP 피어링 정책
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPPeeringPolicy
metadata:
name: bgp-peering-policy
spec:
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 라우터 IP
  • serviceSelector: io.cilium/bgp-announce: "true" 라벨이 있는 Service만 BGP로 광고

Gateway 리소스

Gateway API - Gateway

Cilium Gateway API

Gateway - HTTPS 리스너 설정
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: cilium-gateway
namespace: kube-system
spec:
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: All

ReferenceGrant (Cross-namespace Secret 참조)

Gateway와 TLS Secret이 다른 네임스페이스에 있으므로(Gateway: kube-system, Secret: cilium-secrets), ReferenceGrant로 cross-namespace 참조를 허용합니다.

Gateway API - ReferenceGrant

Gateway API - Cross-Namespace References

ReferenceGrant - Secret 접근 권한
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-gateway-to-secrets
namespace: cilium-secrets
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: kube-system
to:
- group: ""
kind: Secret

LoadBalancer Service (VIP 할당용)

hostNetwork 모드에서는 Gateway 자체에 VIP가 없습니다. 별도의 LoadBalancer Service를 생성하여 VIP를 할당받습니다.

Cilium Gateway API - Host Network Mode

apiVersion: v1
kind: Service
metadata:
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: 443

Cilium LB IPAM이 VIP(172.30.1.7)를 할당하고, CiliumBGPPeeringPolicy로 모든 노드가 BGP로 광고합니다.

cilium bgp peers

MikroTik BGP peer 상태

MikroTik Static ECMP

MikroTik IP Routing - 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
→ node3

RouterOS 6.x는 BGP ECMP를 네이티브로 지원하지 않습니다 (BGP multipath는 RouterOS 7.22beta1에서 도입). Static ECMP route를 추가하고 (distance 10), BGP route (distance 20)보다 우선하도록 설정했습니다. BGP는 노드 상태 모니터링 역할을 합니다.

RouterOS v7 BGP Multipath 지원 현황

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: v1
kind: Service
metadata:
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: 443

Cilium 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
장점단점
노드 장애 시 자동 failoverBGP가 특정 노드 하나만 선택
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

장애 시나리오:

  1. node1 다운 → MikroTik이 ECMP에서 자동 제외 (ARP timeout)
  2. BGP session 끊김 → MikroTik 라우팅 테이블에서 해당 BGP route 제거
  3. 복구 시 → BGP re-establish, ECMP에 자동 복귀

HTTPRoute 마이그레이션

14개 서비스를 Ingress에서 HTTPRoute로 전환했습니다.

HTTPRoute 목록

HTTPRoute 예시 (Grafana):

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: grafana
namespace: monitoring
spec:
parentRefs:
- name: cilium-gateway
namespace: kube-system
hostnames:
- grafana.heeho.net
rules:
- backendRefs:
- name: grafana
port: 80

모든 HTTPRoute 전환 후 NGINX Ingress를 제거했습니다.

Terminal window
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 우회 가능)

관련 콘텐츠

댓글