총 8주차 과정에서 이제 어느덧 6주차에 접어들었다. 하면 할수록 너무 내용이 많다고 생각한다.
지금 하고 있는건 겉햝기정도의 수준이고, 이걸 얼마나 습득해서 내재화하는것은 나의 노력에 달렸는데.. 어렵다. 어려워
특히 트래픽을 가로채기 때문에 tcpdump에서 안보이는 요소도 많다. 케이스에 따라서 거쳐가는 인터페이스도 서로 다르다.
그렇기 때문에 트러블슈팅도 어렵운것 같다. 어렵다 어려워
실습 환경 구성
이번 주차 실습 환경도 이전과 크게 다르지 않다.

cilium helm values는 아래와 같이 사용한다.
- 앞선 주차와 동일하게 cilium cluster-pool ipam을 사용하고, native + bpf masquerade를 활성화해서 사용한다.
- 그리고 service mesh를 하기 위해서는 kube proxy replacement도 반드시 활성화를 해야 한다.
그리고 ingress와 gateway api 등을 이용하기 때문에 ingress controller loadbalancer l7 backend를 활성화해야 한다.
ingress controller loadbalancermode는 현재 shared로 사용한다.
helm install cilium cilium/cilium --version $2 --namespace kube-system \
--set k8sServiceHost=192.168.10.100 --set k8sServicePort=6443 \
--set ipam.mode="cluster-pool" --set ipam.operator.clusterPoolIPv4PodCIDRList={"172.20.0.0/16"} --set ipv4NativeRoutingCIDR=172.20.0.0/16 \
--set routingMode=native --set autoDirectNodeRoutes=true --set endpointRoutes.enabled=true --set directRoutingSkipUnreachable=true \
--set kubeProxyReplacement=true --set bpf.masquerade=true --set installNoConntrackIptablesRules=true \
--set endpointHealthChecking.enabled=false --set healthChecking=false \
--set hubble.enabled=true --set hubble.relay.enabled=true --set hubble.ui.enabled=true \
--set hubble.ui.service.type=NodePort --set hubble.ui.service.nodePort=30003 \
--set prometheus.enabled=true --set operator.prometheus.enabled=true --set hubble.metrics.enableOpenMetrics=true \
--set hubble.metrics.enabled="{dns,drop,tcp,flow,port-distribution,icmp,httpV2:exemplars=true;labelsContext=source_ip\,source_namespace\,source_workload\,destination_ip\,destination_namespace\,destination_workload\,traffic_direction}" \
--set ingressController.enabled=true --set ingressController.loadbalancerMode=shared --set loadBalancer.l7.backend=envoy \
--set localRedirectPolicy=true --set l2announcements.enabled=true \
--set operator.replicas=1 --set debug.enabled=true >/dev/null 2>&1
그리고 통신 테스트를 하기 위해서 webpod, curlpod를 생성해서 간단한 통신테스트를 진행한다.
Cilium Service Mesh
service mesh라고 하면, micro service 환경에서 서비스 트래픽 모니터링, 제어하고 mTLS 같은 보안 기능을 제공하는 것을 말한다.
최종적으로는 인프라에서 이러한 기능을 제공해줌으로써 애플리케이션에서는 비즈니스 로직에만 집중하면 된다.
위 설명만 보면, istio를 설명할 때 하는 말과 크게 다르지 않다. 그리고 cilium service mesh도 istio와 유사한 기능을 제공한다.
cilium은 본래 k8s cni로 시작을 했는데, 네트워크 흐름을 제어할 수 있으니 mesh의 역할도 할 수 있지 않을까?해서 확장했다고 한다.
아래 그림을 보면 점차적으로 각 분야에서 기능을 제공하는것을 볼 수 있다.

istio와 다른점은 pod sidecar가 없다는 것이 가장 핵심이다. cilium이기 때문에 eBPF로 커널에서 트래픽을 제어할 수 있다.
- 물론 istio도 최근에는 ambient mode가 나와서 sidecar 없이 구현이 가능하지만
- L3, L4 수준에 대해서는 eBPF로 처리를 하고, L7에 대해서는 cilium-envoy로 트래픽을 전달한 다음에 해당 시스템에서 처리한다.

istio에서 제공하고 있는 L7 트래픽 관리, mTLS, Ingress/gateway api 통합 기능을 cilium에서는 cni와 더불어 사용이 가능하다.
k8s를 운영하는 입장에서는 운영을 단순화하기 위해서 많이 노력을 하게 되고, 복잡성을 줄이기 위해서 노력하는데
cni 와 service mesh를 단일 컴포넌트로 통합해서 운영을 단순화하는것도 좋은 방법이 될것 같다.
AWS EKS 기술블로그에서도 보면 cilium을 이용한 service mesh 내용도 잘 소개되어 있다.
Getting Started with Cilium Service Mesh on Amazon EKS | Amazon Web Services
Cilium is an open source solution for providing, securing, and observing network connectivity between workloads, powered by the revolutionary kernel technology called extended Berkeley Packet Filter (eBPF). eBPF enables the dynamic insertion of security, v
aws.amazon.com
cilium에서는 service mesh 구현을 위해서 k8s ingress 또는 gatewayapi 연동으로 구현이 가능하다.
Cilium ingress support 사전 작업
이름에서도 알 수 있듯이 k8s ingress 객체를 그대로 이용하면서 cilium service mesh를 구현하는 방법이다.
실습 환경을 구성할 때, 아래와 같이 배포를 했기 때문에 기본적으로 ingressclass를 보면 cilium이 존재한다.
helm install cilium cilium/cilium --version $2 --namespace kube-system \
'''
--set ingressController.enabled=true --set ingressController.loadbalancerMode=shared --set loadBalancer.l7.backend=envoy \
'''
--- cilium ingressclass ---
(⎈|HomeLab:test) root@k8s-ctr:~# k get ingressclass
NAME CONTROLLER PARAMETERS AGE
cilium cilium.io/ingress-controller <none> 57m
기본적으로 cilium ingress class를 사용해서 ingress 객체를 만들면 자동으로 load balancer 타입의 service가 생성된다.
그리고 loadbalancer mode를 shared로 했기 때문에, 하나의 load balancer가 모든 cilium ingress를 처리하게 된다.
- 개별 load balancer를 사용하기 위해서는 dedicated 모드가 있다.

1. 최종적으로 트래픽 흐름을 보면 클라이언트에서 cilium ingress load balancer service로 트래픽이 들어온다.
2. 그 다음에 cilium ebpf에서 트래픽을 감지하고 reserved:ingress ip(가상의 IP)로 매핑을 한다.
3. reserved:ingeess ip는 다시 envoy pod으로 리다이렉션 되어(TPROXY 기능을 이용한다고 한다.)
4. envoy pod에서 ingress rule을 검사한 다음에 다시 최종적으로 backend pod으로 전달이 된다.
그래서 cilium-ingress를 get으로 보면 각 필요한 객체가 배포되어 있는것을 볼 수 있다.
--- cilium ingress service ---
(⎈|HomeLab:test) root@k8s-ctr:~# kubectl get svc,ep -n kube-system cilium-ingress
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/cilium-ingress LoadBalancer 10.96.118.1 <pending> 80:31084/TCP,443:32631/TCP 90m
NAME ENDPOINTS AGE
endpoints/cilium-ingress 192.192.192.192:9999 90m
--- cilium reserved ip ---
(⎈|HomeLab:test) root@k8s-ctr:~# kubectl exec -it -n kube-system ds/cilium -- cilium ip list | grep ingress
172.20.0.173/32 reserved:ingress
172.20.1.110/32 reserved:ingress
--- cilium envoy ---
kubectl get svc,ep -n kube-system cilium-envoy
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/cilium-envoy ClusterIP None <none> 9964/TCP 90m
NAME ENDPOINTS AGE
endpoints/cilium-envoy 192.168.10.100:9964,192.168.10.101:9964 90m
cilium-ingress은 external ip를 할당 받아서, arp 광고를 해야하기 때문에 L2Announcement policy도 필요하다.
--- cilium ingress load balancer ip 할당
cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumLoadBalancerIPPool
metadata:
name: "cilium-lb-ippool"
spec:
blocks:
- start: "192.168.10.211"
stop: "192.168.10.215"
EOF
--- cilium ingress arp 광고 ---
cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
name: policy1
spec:
interfaces:
- eth1
externalIPs: true
loadBalancerIPs: true
EOF
Cilium Ingress Support 테스트
istio에서 제공해주는 테스트 앱을 배포한 다음에, 이를 backend로 해서 ingress 객체를 배포한다.
그리고 한가지 중요한것은 ingress에서 Ingress class를 cilium으로 지정하는것이다.
--- 테스트 앱 배포
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.26/samples/bookinfo/platform/kube/bookinfo.yaml
--- cilium ingress 사용해서 ingress 배포
cat << EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: basic-ingress
namespace: default
spec:
ingressClassName: cilium <<< ingresscalss에서 생성된 cilium을 사용한다.
rules:
- http:
paths:
- backend:
service:
name: details
port:
number: 9080
path: /details
pathType: Prefix
- backend:
service:
name: productpage
port:
number: 9080
path: /
pathType: Prefix
EOF
ingress를 배포하면, ingress address ip가 할당되고 cilium-ingress load balancer ip와 동일한것을 확인할 수 있다

k8s에 노드가 아닌 router 노드에서 ingress ip로 호출을 하면 호출이 잘 된다.
근데 ingress가 트래픽을 처리하는 과정을 보고 싶어서, tcpdump & pwru도 걸어놨지만 논리적으로 처리하는 과정은 보이지 않고
client router(real ip)-> server dst pod에 대한 트래픽 흐름만 보인다. 왜 tcpdump에서 안보이는걸까?

GPT 선생님 왈
tcpdump는 네트워크 인터페이스로 올라온 패킷에 대해서만 캡처를 한다.
하지만 cilium ingress는 논리적으로 처리를 하고, 네트워크 스택 이전에 eBPF hook으로 처리하고
이를 바로 TPROXY 리다이렉트하기 떄문에 네트워크 레벨의 패킷이 존재하지 않는다.
pwru는 특정 tracepoint에 대한 패킷을 캡처한다.
하지만 ingress reserved ip로 논리적으로 처리하는 과정은 cilium eBPF map lookup을
리다이렉트하는 과정하는 과정이고, reserved ip는 논리적 처리를 위한 가상의 IP 이므로 존재하지 않는다.
```
kubectl exec -it -n kube-system ds/cilium -- cilium ip list | grep ingress
172.20.0.173/32 reserved:ingress
172.20.1.110/32 reserved:ingress
```
만약 envoy-ingress leader 노드 = dst pod 노드일때
dst pod이 가지고 있는 lxc interface로 ngrep을 해보면 client ip는 그대로 유지가 된다.

client ip가 보존되는게 hubble에서도 확인할 수 있다.
그리고 hubble을 보면 ingress를 통해서 들어오기 때문에 pod에서 보면 ingress에서 인입되는것처럼 표현이 된다.

만약 envoy-ingress leader 노드 != dst pod 노드 일때
svc 객체를 만들 때 external traffic policy = local로 하면, 같은 노드의 backend로 리다이렉트 되기 때문에 NAT가 없다.
그래서 client ip를 바로 확인할 수 있다.
external traffic policy = cluster로 하면 트래픽을 받는 노드와 실제 backend가 다를 수 있기 때문에 이때 NAT 된다.
하지만 cilium은 external traffic policy 설정과 무관하게 backend에 있는 노드에 스케줄링되어 있는 envoy-ingress를 사용한다.
그래서 같은 노드에 있는 dst pod으로 최종적으로 리다이렉트되기 때문에 client real ip를 그대로 보존할 수 있다.
tcpdump로 좀 더 자세하게 보면,
- client -> ingress ip로 호출을 하면 envoy-ingress leader에서 먼저 트래픽을 받는다.
- 그리고 dst pod과 동일한 노드에 스케줄링되어 있는 envoy-ingress로 트래픽을 전달하고
- 최종적으로 dst pod에서 트래픽을 처리하고, 응답은 반대 과정대로 진행이 된다.


Cilium Load Balancer shared vs dedicated
최초에 cilium을 배포할 때 load balancer mode를 shared로 배포했다.
이것은 여러 ingress 객체를 단일 load balanacer svc에서 처리하겠다는 의미이다.
helm install cilium cilium/cilium --version $2 --namespace kube-system \
'''
--set ingressController.enabled=true --set ingressController.loadbalancerMode=shared --set loadBalancer.l7.backend=envoy \
'''
그래서 ingress를 몇개를 만들던지, 모두 같은 cilium ingress external ip를 할당받아서 사용한다.

하지만 ingress 라우팅 경로가 동일하거나 ip prefix 등으로 ingress 충돌 등이 발생하면 분리된 svc load balancer도 가능하다.
cat << EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: webpod-ingress
namespace: default
annotations:
ingress.cilium.io/loadbalancer-mode: dedicated <<< 추가하면 별도 svc load balancer가 생성된다.
spec:
ingressClassName: cilium
rules:
- http:
paths:
- backend:
service:
name: webpod
port:
number: 80
path: /
pathType: Prefix
EOF

Cilium ingress-nginx 테스트
실무에서 ingress nginx를 많이 사용하는데, 이것 또한 cilium ingress와 공존(?) 해서 사용할 수 있다.
--- ingress nginx 배포 ---
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx --create-namespace -n ingress-nginx
--- nginx ingress 배포 ---
cat << EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: webpod-ingress-nginx
namespace: default
spec:
ingressClassName: nginx
rules:
- host: nginx.webpod.local
http:
paths:
- backend:
service:
name: webpod
port:
number: 80
path: /
pathType: Prefix
EOF
ingress nginx 테스트 앱을 배포하면 마찬가지로 Ingress class가 생성되고, load balancer external ip가 할당되면서
reserved:ingress로 ingress-nginx로 할당되어 있는것을 볼 수 있다.

Cilium Cluster Wide Network Policy
이전에 cilium networkpolicy crd가 namespace 단위로 적용됬다면 cluster wide는 전체 namespace에 적용되는 정책이다.
endpointSelector가 비어있으면 모든 pod에 적용되는 것으로 ingress from = cluster이면 클러스터 내부 pod끼리만 통신이 된다.
cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
name: "external-lockdown"
spec:
description: "Block all the traffic originating from outside of the cluster"
endpointSelector: {}
ingress:
- fromEntities:
- cluster
EOF
그래서 외부 노드인 router에서 서비스 호출을 하면 DROP으로 403 응답은 반환한다.

하지만 실제로 위와 같이 클러스터 내부 pod끼리 통신하는 정책을 적용하는 케이스는 없을것 같다.
최소한 외부와의 접점을 만들어야 하고, 그러기 위해서 envoy ingress가 있는것이다. 그리고 ingress에 IP 제한을 해서 사용하는것이다.
cat << EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
name: "allow-cidr"
spec:
description: "Allow all the traffic originating from a specific CIDR"
endpointSelector:
matchExpressions: # identiry가 reserved:ingress은 서비스에 대해서
- key: reserved:ingress
operator: Exists
ingress:
- fromCIDRSet: # 특정 IP 대역에서 오는것만 허용하겠다.
- cidr: 192.168.10.200/32
- cidr: 127.0.0.1/32
EOF

Cilium Ingress Path Type
k8s에서 ingress 또는 istio virtualservice를 사용해봤다면, api path별로 서로 다른 backend를 사용하는것을 익숙하다.
path type은 총 3가지 type이 있다.
- Exact : API 경로가 반드시 일치해야 한다.
- Prefix : Prefix 하위 경로에 대해서는 다 매핑이 된다.
- ImplementationSpecific: Ingress 구현에 따라서 다르게 동작한다. 즉, 내가 판단하지 않고 시스템에서 판단하도록 하는것이다.
- cilium의 경우에는 cilium-envoy에서 이를 처리하고, 사실상 envoy 라우팅 규칙에 따라서 처리가 된다.
- 아래 예시에 있는것처럼 정규표현식이 가능하기 때문에 조금 더 복잡한 라우팅 경로에 대해서 처리가 가능하다는 장점이 있다.
spec:
ingressClassName: cilium
rules:
- host: pathtypes.example.com
http:
paths:
- backend:
service:
name: exactpath
port:
number: 80
path: /exact
pathType: Exact
- backend:
service:
name: prefixpath
port:
number: 80
path: /
pathType: Prefix
- backend:
service:
name: prefixpath2
port:
number: 80
path: /prefix
pathType: Prefix
- backend:
service:
name: implpath
port:
number: 80
path: /impl
pathType: ImplementationSpecific
- backend:
service:
name: implpath2
port:
number: 80
path: /impl.+
pathType: ImplementationSpecific
Ingress TLS Termination
TLS Termination은 외부에서 HTTPS 트래픽을 복호화해서 backend로 전달하는것을 의미한다.
- client -> ingress 구현체까지는 HTTPS로 암호화하고
- ingress 구현체 -> backend 구간은 HTTP 평문 통신을 한다.
보통은 LB에서 HTTP로 복호화해서 트래픽을 넘겨주는것이 많으나, End-To-End에 대한 트래픽 암호화가 필요한 경우 적용할 수 있다.
또는 LB에서 HTTP 복호화가 안되는 경우 사용할 수 있는 방법이기도 하다.
적용하는 방법은 Cert를 Secrets로 만들어서 Ingress 객체 만들때 적용하면 쉽게 반영이 가능하다.
그리고 mkcert 패키지를 이용하면 쉽게 Cert를 만들 수 있다.
Ingress TLS Termination 테스트 w.mkcert
mkcert는 Cert를 만들고, 적용을 쉽게 도와주는 도구이다.
mkcert를 하면 간단하게 Cert를 만들어 주고, 이를 Secrets으로 생성만 하면 된다.
--- mkcert를 이용해서 Cert 생성 ---
(⎈|HomeLab:default) root@k8s-ctr:~# mkcert '*.cilium.rocks'
Created a new local CA 💥
Note: the local CA is not installed in the system trust store.
Run "mkcert -install" for certificates to be trusted automatically ⚠️
Created a new certificate valid for the following names 📜
- "*.cilium.rocks"
Reminder: X.509 wildcards only go one level deep, so this won't match a.b.cilium.rocks ℹ️
The certificate is at "./_wildcard.cilium.rocks.pem" and the key at "./_wildcard.cilium.rocks-key.pem" ✅
It will expire on 23 November 2027 🗓
(⎈|HomeLab:default) root@k8s-ctr:~#
--- Cert 확인 ---
(⎈|HomeLab:default) root@k8s-ctr:~# ls -al *.pem
-rw------- 1 root root 1704 Aug 23 17:20 _wildcard.cilium.rocks-key.pem
-rw-r--r-- 1 root root 1452 Aug 23 17:20 _wildcard.cilium.rocks.pem
(⎈|HomeLab:default) root@k8s-ctr:~# openssl x509 -in _wildcard.cilium.rocks.pem --noout --text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
50:f6:54:f4:a3:be:3e:84:84:18:47:10:df:08:4d:c6
Signature Algorithm: sha256WithRSAEncryption
Issuer: O = mkcert development CA, OU = root@k8s-ctr, CN = mkcert root@k8s-ctr
--- Cert Secrets 생성 ---
(⎈|HomeLab:default) root@k8s-ctr:~# kubectl create secret tls demo-cert --key=_wildcard.cilium.rocks-key.pem --cert=_wildcard.cilium.rocks.pem
--- 신뢰 인증서로 추가 ---
(⎈|HomeLab:default) root@k8s-ctr:~# mkcert -install
The local CA is now installed in the system trust store! ⚡️
mkcert를 안하면 임의로 생성한 cert를 신뢰 인증서로 추가하지 않기 때문에, 신뢰하지 않은 사이트로 표시가 된다.
그리고 mkcert로 만든 Cert를 ingress tls 필드에 추가하면 된다.
cat << EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
namespace: default
spec:
ingressClassName: cilium
rules:
- host: webpod.cilium.rocks
http:
paths:
- backend:
service:
name: webpod
port:
number: 80
path: /
pathType: Prefix
- host: bookinfo.cilium.rocks
http:
paths:
- backend:
service:
name: details
port:
number: 9080
path: /details
pathType: Prefix
- backend:
service:
name: productpage
port:
number: 9080
path: /
pathType: Prefix
tls: # 여기에서 TLS 필드에 mkcert로 만든 Secrets을 넣으면 된다.
- hosts:
- webpod.cilium.rocks
- bookinfo.cilium.rocks
secretName: demo-cert
EOF

이번 주차는 논리적으로 동작하고, 가상의 인터페이스를 만들어서 동작하는것이 많았다. 그렇다보니깐 이해하기에 어려웠던것 같다.
최근에 cilium을 사용하는 클러스터에서 장애가 난적이 있다. 일부 트래픽이 유실이 되는것이다.
하지만 tcpdump를 했을때는 노드까지 인입되는것까지는 확인했고 이후 흐름이 확인되지 않았다.
그래서 왜? 확인이 안될까? 어디서 가는걸까? 트러블슈팅을 많이 했는데, 한 팀원이 cilium은 커널에서 동작하기 때문에 디버깅이 어렵다고 했었다. 이해가 안되는것은 아니다.
실제로 service mesh도 보면 상당 부분 논리적으로 동작하는것이 많아서 트러블슈팅 하기가 어려운것은 사실인것 같다.
하지만 따지고보면, 커널에서 동작해서 디버깅이 어렵다기 보다는 내가 cilium을 잘 모르기 때문에 디버깅이 어려웠던것 같다.
map list, endpoint/lb/nat list 등 cilium에서도 디버깅이 어렵다고 생각해서 여러가지 디버깅 도구를 제공하고 있다.
디버깅 도구를 얼마나 잘 사용할 수 있는지? 그리고 각 요소의 정보를 내가 얼마나 잘 알고있는지?가 관건일것 같다.
하지만 cilium이 어렵긴 어렵다.. 어려운건 사실이다.
'기술 토론장 > [K8s] Kubernetes' 카테고리의 다른 글
| [Cilium][6주차] L7 Aware Traffic Management (0) | 2025.08.24 |
|---|---|
| [Cilium][6주차] Service Mesh Gateway API Support (1) | 2025.08.23 |
| [Cilium][5주차] Cilium Clsuter Mesh (5) | 2025.08.17 |
| [Cilium][5주차] Cilium native + BGP 활용 (2) | 2025.08.16 |
| [Cilium][4주차] Node & Pod 네트워크 통신, Service External-IP 활용 (1) | 2025.08.10 |