6W - 실리움 서비스 메시 - 인그레스

개요

이번 주차는 실리움의 서비스 메시에 대해 알아본다.
서비스 메시는 흔히 이스티오를 떠올리기 쉬운 개념인데, 실리움은 CNI이면서 해당 개념을 정의하고 관련한 기능도 제공하고 있다.

이번 주차의 내용은 실리움이 서비스 메시로서 제공하는 기능들을 실습해보는 것이다.
서비스 메시 자체에 대해 깊게 알아보는 것은 아니므로 실습 기반으로 내용을 정리한다.
그렇기에 활용되는 여러 리소스나 개념에 대한 설명은 이 글에서 가급적 작성하지 않는다.

서비스 메시

그래도 Istio 1기 - Istio Hands-on을 진행하며 공부했던 내용을 기반으로 먼저 서비스 메시의 개념을 알아본다.

서비스 메시는 다양한 서비스가 그물망처럼 다양하게 얽히고 연결되어 있는 구조, 아키텍처를 말한다.
큰 서비스의 관점에서, 서비스 메시는 네트워크만을 따로 담당하는 추상된 인터페이스 레이어를 가진다는 것이 핵심이다.
서비스 메시는 아키텍처 개념인 만큼 독립된 서비스들을 운용하며 이들을 효율적으로, 장애가 전파되지 않도록 운영하는 기술과 방식도 포함하여 일컫는다.

이 개념이 익숙치 않다면, 등장 배경을 먼저 아는 것이 좋다.

배경 - 마이크로서비스아키텍쳐의 문제

컨테이너 기술이 본격화되며 어플리케이션을 패키징하는 것이 용이해졌고, 이런 컨테이너를 여러 노드에서 통합적으로 관리할 수 있도록 오토스케일링과 자동화 기능을 하는 컨테이너 오케스트레이션(대표적으로 쿠버네티스]) 기술이 발달했다.
이로부터 유연한 확장성을 확보할 수 있는 MSA도 자연스럽게 정립됐다.
MSA의 가장 큰 특징 중 하나는 인프라 환경과 어플리케이션의 결합도를 낮춘다는 것으로, 물리적 자원의 제약에서 벗어나 자유롭게 기능을 확장하고 대처하는 것이 가능해졌다.
image.png
모놀리틱 아키텍처와 비교했을 때, 전체 아키텍쳐가 세분화되기에 구조는 복잡해지고 관리포인트가 늘어나게 된다.

해결 방안과 방향성

위 문제들에 대한 해결 전략을 여러 가지 생각해볼 수 있다.

근데.. 위 요소들을 누가 맡아서 작업을 수행해야 하는가?
서비스 메시가 등장하기 이전에는, 당연히 위 방법들은 어플리케이션을 만드는 개발팀이 해야만 했다.
메트릭을 노출하는 엔드포인트를 만들고, 모든 요청에 span을 넣어서 트레이싱이 가능하게 한다던지..
요청이 실패했을 때 알아서 재시작을 하게 한다던지..
이를 위해 Eureka, Zuul 등의 다양한 언어 라이브러리들이 등장하였고 지금도 쓰이고 있기는 하다.
그러나 MSA 환경에서 효율적인 전략이라고 하기에는 역시나 많은 문제가 있다.

운영팀으로서는 이 해결책들을 개발팀한테 "해 줘"해야 하는 입장이긴 하나, 이러한 방식은 개발팀의 업무에 지장을 주기도 하는 동시에 전체 운영 환경의 유연성을 해친다는 것이다.

서비스 운영팀에서 해결하기 - 네트워크 레이어 분리

위 문제들은 운영 상 발생하는 문제이기도 한데, 비즈니스 로직에 집중을 해야 하는 개발팀이 주체적으로 대응하는 방식은 매우 아쉬운 해결책이다.
또한 재시도, 트레이싱 등의 해결책들은 어플리케이션 기능과도 관련도 없고 언어와 프레임워크에 한정해 고려해야 하는 요소들도 아니다.
이로부터 자연스럽게 이것들을 서비스 관리를 책임지는 운영팀에서 처리할 수 있는 방안이 강구되기 시작했고, 그렇게 등장한 것이 바로 서비스 메시이다.
image.png
위 문제와 해결 방법들은 사실 전부 네트워크와 관련되는 문제라는 공통 분모를 가지고 있다.
이로부터 어플리케이션 간에 이뤄져야 하는 네트워크 관련 기능과 이슈들은 아예 하나의 레이어로 분리하여 관리하는 것.
서비스 메시 레이어가 있으면 어플리케이션의 세팅이 어떤지 상관 없이 모든 네트워크 기능들을 운영 단계에서 해결할 수 있게 된다.
이것이 바로 서비스 메시의 등장 배경이자, 주된 기능이라고 할 수 있다!

구조

image.png
image.png
image.png
클라우드넷 스터디의 자료가 굉장히 명쾌하게 이해된다고 생각해 가져왔다.
서비스 메시는 3번의 구조를 가지고 있는데, 왜 이런 구조를 가지고 있는지 단계적으로 이해하기 좋다.
이러한 구조로 발전한 이유는 간단하다.
위에서 말했듯 네트워크 영역을 분리는 해야겠으니, 네트워크에 대한 설정, 동작을 담당하는 별도의 에이전트를 두는 것이다.
근데 이걸 애플리케이션마다 일일히 관리하는 것이 어려우니 컨트롤 플레인을 두는 방식으로 해소한다.

기능

이제 서비스 메시라는 것은 하나의 추상화된 레이어를 일컫는 개념이자, 이를 활용하는 아키텍처라는 것을 명확히 할 수 있을 것이다.
그리고 서비스 메시는 인프라 레이어와 밀접한 연관을 가지는 어플리케이션의 네트워크 기능을 전적으로 담당하는 역할을 수행한다.
그럼 구체적으로 서비스 메시를 통해 어떤 것들을 할 수 있는지를 구체화시켜보자.

실리움의 서비스 메시

그렇다면 실리움의 서비스 메시는 무엇인가?
문서에서는 서비스 메시로서 다음의 기능을 언급한다.[1]

사실 보면 실리움이 말하는 서비스 메시도 위에서 말하는 서비스 메시와 다르지 않다.
제공하는 기능도 보면 얼추 이스티오에서 제공하는 기능들과 얼추 맞긴 하다.
image.png

사견

다만 뭐랄까.. 서비스 메시라고 떳떳하게 말하기에 조금은 부족한 느낌이 있다.
나는 서비스 메시라고 했을 때 가장 크게 보는 기능이 트래픽 관리, 신원 기반 트래픽 제어 및 보안, 관측 가능성이라 생각한다.
실리움도 이게 없다는 건 아닌데..
아래 그림은 이스티오의 문서에서 분류하는 기능들이다.
image.png
위 실리움 문서와 비교했을 때, 훨씬 명쾌하게 기능을 파악할 수 있다.
그렇다보니 조금은 부족하다는 느낌을 계속 받는 것 같다.

이스티오의 경우 Istio VirtualService, Istio DestinationRule 등 자체적인 커스텀 리소스를 통해 서비스 메시의 기능을 제공한다.
(앰비언트 모드부터는 Gateway API를 사용하긴 한다.)
그러나 실리움은 인그레스에 대한 지원을 첫번째 기능으로 제시하고 있다.

구조

실리움의 서비스 메시는 이스티오의 앰비언트 모드와 비슷한 구조를 가지고 있다.
image.png
기본적인 L3, L4 레벨의 서비스 메시 기능은 각 노드 별로 설치된 실리움 에이전트가 담당한다.
그리고 무거운 연산을 요구하는 L7 레벨의 작업은 엔보이로 트래픽을 전송하여 처리하게 만든다.
image.png
가만 보면 엔보이는 진짜 contour에서도 쓰고 안 쓰이는 곳이 없다
이러한 구조는 ztunnel, waypoint를 분리하여 사용하는 앰비언트와 매우 흡사하다.
그러나 실리움은 모든 노드에 하나씩 엔보이를 배치한다.

실습 환경 구성

실습은 공식 문서의 내용을 전반적으로 따라해보는 식으로 진행한다.
진행 간 자원 부족으로 차질이 발생할 수 있으니 가급적 넉넉하게 VM 스펙을 설정한다.

이번에는 저번 주차 스터디에서 소개된 적은 있으나 사용해보진 못했던 pwru도 설치한다.

echo "[TASK 7] Install pwru"
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
wget https://github.com/cilium/pwru/releases/download/v1.0.10/pwru-linux-${CLI_ARCH}.tar.gz >/dev/null 2>&1
tar -xvzf pwru-linux-${CLI_ARCH}.tar.gz >/dev/null 2>&1
mv pwru /usr/local/bin/pwru >/dev/null 2>&1

pwru는 실리움 커뮤니티에서 개발한 ebpf 기반 패킷 추적 도구이다.[2]
완전 로우레벨까지 나와서 세밀하게 조건을 지정해서 보는 게 좋다.

간단하게 iptables를 이용해 패킷을 차단하고 추적해보자.

vagrant ssh k8s-ctr

iptables -t filter -I OUTPUT 1 -m tcp --proto tcp --dst 1.1.1.1/32 -j DROP
	
curl 1.1.1.1 -v -m 1 --retry 5&

pwru 'dst host 1.1.1.1 and tcp and dst port 80'

iptables -t filter -D OUTPUT 1

image.png
http 요청이 커널에서 어떤 경로를 거치고 있는지 추적되는 것을 볼 수 있다.
소켓 구조체가 넷필터 드랍 규칙에 의해 드랍된 것을 이름을 통해 추적할 수 있다.

실리움 세팅은 다음의 설정이 조금 바뀌었다.

--set ingressController.enabled=true --set ingressController.loadbalancerMode=shared --set loadBalancer.l7.backend=envoy \
--set localRedirectPolicy=true --set l2announcements.enabled=true \

일단 L2 광고 형식으로 로드밸런서를 세팅한다.
그리고 인그레스 기능을 먼저 사용해보고자 인그레스 컨트롤러를 활용한다.

실리움에 설정은 어떻게 반영됐는지 확인해본다.

cilium config view | grep -E '^loadbalancer|l7'

image.png

인그레스

실리움에서는 cilium이란 인그레스 클래스를 제공한다.
이 클래스는 실리움의 인그레스 컨트롤러을 바탕으로 두고 있으며, 컨트롤러는 인그레스가 만들어질 때 로드밸런서 서비스를 만들어 기능을 제공한다.
실제로 트래픽이 들어온다면 해당 로드밸런서를 타고 들어오고, 커널 단에서 인그레스 정책을 적용할 엔보이로 연결된다.

사용 조건은 간단하다.

실리움의 인그레스 컨트롤러는 CNI와 매우 밀접하게 결합돼있다는 것이 특별점이다.
일반적인 인그레스 컨트롤러(대표적으로 nginx ingress controller)는 클러스터에 따로 로드밸런서 서비스 형태로 배포된다.
실리움도 이런 동작이 가능하지만, 특성 상 호스트 네트워크를 통해 트래픽을 처리하는 것도 가능하다.
어떻게 설정하든 실리움은 트래픽을 받았을 때 커널 단에서 트래픽을 엔보이로 보내버리기 때문이다.
이 동작은 구체적으로는 커널의 tproxy를 통해 이뤄지는데, 엔보이는 내부에 가상의 호스트를 세워 트래픽 처리가 가능하다 보니 연결이 매끄럽게 이어진다.

tproxy란

T Proxy는 iptables에서 사용할 수 있는 모듈 중 하나이다.[3]
기능은 매우 간단한데, 조건에 맞는 패킷을 어떠한 수정도 없이 로컬 소켓으로 보낸다.
가령 원래 80번 포트로 가는 요청이 있다면 이걸 5050포트로, 대신 포트번호는 그대로 보내는 게 가능하다.
그래서 투명한(transparent) 프록시라 하여 tproxy.
대신 이건 mangle 테이블의 prerouting이나 커스텀 체인에서만 사용할 수 있다.
자세한 설명은 참조[4]

tproxy로 어떤 룰들이 추가되는지 확인해본다.

vagrant ssh k8s-ctr -c "sudo iptables -t raw -L" 
vagrant ssh k8s-ctr -c "sudo iptables -t mangle -L" 
vagrant ssh k8s-ctr -c "sudo iptables -t filter -L" 

image.png
프리라우팅 체인에서 첫번째로 걸리는 raw 테이블에서는 컨트랙을 끊는 규칙들이 적혀있다.
image.png
이후 mangle 테이블에서는 특정 마크가 걸린 트래픽에 추가 마크를 거는 규칙이 들어있다.
이후 마크된 트래픽은 TPROXY 체인으로 걸리게 되는데, 로컬호스트 33111포트로 가도록 설정된다.

기본 확인

엔보이 설정을 확인해본다.

kubectl get pod -n kube-system -l k8s-app=cilium-envoy -owide
k describe pod -n kube-system -l k8s-app=cilium-envoy
vagrant ssh k8s-ctr -c "ls -al /var/run/cilium/envoy/sockets"

image.png
엔보이는 각 노드에 배치되어 L7 트래픽을 처리하는 역할을 수행한다.
image.png
image.png
아예 노드에 소켓과 파일을 두고 마운팅하여 사용하는 것을 확인할 수 있다.

엔보이의 초기 세팅은 다음의 파일을 통해 진행된다.

kubectl exec -it -n kube-system ds/cilium-envoy -- cat /var/run/cilium/envoy/bootstrap-config.json | fx
kubectl -n kube-system get configmap cilium-envoy-config -oyaml

image.png
설정을 주입받는 소켓 파일의 경로에 대한 설정이 보이는데, 실제 인그레스나 정책을 생성했을 때의 설정들은 동적으로 들어가기 때문에 위 정보로는 확인할 수 없다.

실리움 인그레스를 타고 들어오는 트래픽은 두 가지의 신원을 가진다.

먼저 외부에서 들어올 때 world라는 신원을 가지고, 엔보이로 들어가 실제 백엔드로 들어갈 때는 ingress 신원을 가진다.
이러한 동작은 인그레스로서 들어온 트래픽에 대한 네트워크 정책을 적용할 수 있는 포인트를 제공한다.
달리 말하자면 트래픽이 제대로 전달되기 위해서는 두 위치에서 정책에서 허용돼야 한다는 것이다.
참고로 클러스터 내부에서도 정책 설정, 모니터링 설정으로 엔보이를 타는 트래픽이 있을 수 있는데, 이들에 대해서도 ingress 신원이 부여되지는 않는다.

별도의 신원 정보는 이런 식으로 확인한다.

kubectl exec -it -n kube-system ds/cilium -- cilium ip list | grep ingress

image.png
파드 중에는 위 IP를 가지고 있는 것이 없다.
위 IP는 인그레스를 거치는 트래픽이 얻게 되는 새로운 신원을 위해 예약돼있는 값이다.

클러스터 리소스로서 조금 더 확인해보자면..

kubectl get svc,ep -n kube-system cilium-envoy
kubectl get svc,ep -n kube-system cilium-ingress
k get ep -n kube-system cilium-ingress -oyaml | yq

image.png
인그레스 자체에 대한 서비스로 설정된 녀석이 조금 특이한 게 보인다.
엔드포인트로 이상한 주소가 확인되고 있다.
image.png
보통의 엔드포인트는 kube-controller-manager에 들어있는 endpoint-controller가 서비스의 스펙을 보고 만들어준다.
그러나 해당 엔드포인트 리소스는 실리움이 자체적으로 만드는 것으로, 실제로는 사용되지 않는 가상의 주소이다.
어차피 커널을 지날 때 실리움이 트래픽의 경로를 조작할 것이므로 해당 IP는 그다지 중요하지 않다.

기본 http 테스트

이제 본격적으로 인그레스를 활용해보고 확인한다.
위의 로드밸런서는 아직 외부 IP 세팅이 되지 않았으므로 L2 광고를 활용한다.

apiVersion: "cilium.io/v2" 
kind: CiliumLoadBalancerIPPool
metadata:
  name: "cilium-lb-ippool"
spec:
  blocks:
  - start: "192.168.10.211"
    stop:  "192.168.10.215"
---
apiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
  loadBalancerIPs: true
  98766

제대로 IP로 통신이 가능한지 확인해본다.

LBIP=$(kubectl get svc -n kube-system cilium-ingress -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
vagrant ssh k8s-ctr -c "sudo arping -i eth1 $LBIP -c 2"

image.png

공식 문서의 예제를 따라가본다.[5]
문서에서는 이스티오(!)에서 사용하는 bookInfo 예제를 사용한다.

이 예제는 위와 같이 프로덕트 web 파드에서 책 정보들을 뒷단에 api를 날려 받아오는 간단한 구조를 가지고 있다.[6]

kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.26/samples/bookinfo/platform/kube/bookinfo.yaml
kubectl get pod,svc,ep
k describe ingressclasses.networking.k8s.io

image.png

기본적인 인그레스를 만들어본다.
인그레스 클래스를 지정하는 것을 캐치하자.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: basic-ingress
  namespace: default
spec:
  ingressClassName: 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

image.png
당연하지만 인그레스 리소스에 설정된 주소는 로드밸런서 서비스와 일치한다.
image.png

이제 실제로 트래픽이 가는지도 확인해본다.

hubble observe -f --identity ingress
# --- 다른 터미널에서 실행
LBIP=$(kubectl get svc -n kube-system cilium-ingress -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -sv -o /dev/null  http://$LBIP/
curl -so /dev/null -w "%{http_code}\n" http://$LBIP/
curl -so /dev/null -w "%{http_code}\n" http://$LBIP/details/1
curl -so /dev/null -w "%{http_code}\n" http://$LBIP/ratings
# 라우터에서도 호출 가능
vagrant ssh router -c "curl -s http://$LBIP/"

image.png
트래픽이 엔보이를 거치기 때문에 엔보이 헤더가 붙는 것을 확인할 수 있다.
image.png
웹으로도 확인할 수 있다.
image.png
보다시피 아예 인그레스라는 신원에서 내부로 트래픽이 들어가는 것으로 표시되고 있다.

파드로 들어가는 인터페이스에서 트래픽을 캡쳐하면 엔보이에서 워크로드로 전달될 당시의 트래픽도 확인할 수 있다.

# 해당 노드(k8s-w1)에서 veth 인터페이스 정보 확인
PROND=$(k get po -l app=productpage -ojson | jq '.items[].spec.nodeName' -r)
PROID=$(k get po -l app=productpage -ojson | jq '.items[].status.podIP' -r)
PROVETH=$(vagrant ssh $PROND -c "ip route |grep $PROID" | awk '{print $3}')

vagrant ssh $PROND -c "sudo ngrep -tW byline -d $PROVETH '' 'tcp port 9080'"
# ---
curl -so /dev/null -w "%{http_code}\n" http://$LBIP/

image.png
파드로 들어가기 전 헤더에는 XFF, XFP 등의 값이 확인된다.
이는 외부에서 들어온 트래픽을 엔보이가 받아서 워크로드로 전달했다는 것을 보여준다.

그럼 실제 iptables에서는 어떤 변화가 생기는가?
아래 명령을 실행하고 중간 중간 다시 예시 요청을 날려본다.

vagrant ssh $PROND -c "sudo iptables -t mangle -L -v"

image.png
먼저 보다시피 생성된 인그레스에 맞춰 mangle 테이블에는 tproxy 규칙이 새로 생긴 것이 보인다.
또한 맨 위 줄에서 마크된 패킷들이 바로 규칙의 적용을 받으며 카운티되는 것을 확인할 수 있다.

인그레스 로드밸런서 독점(dedicated)

실리움은 인그레스에 대해 두 가지 모드를 지원한다.

기본값은 shared로 한 로드밸런서 서비스를 모든 인그레스가 공유하는 구조이다.
모드는 동적으로 수정할 수 있는데, 대신 모드를 변경하면 로드밸런서 IP가 수정되기에 커넥션이 끊기는 것에 유의하자.

독점 인그레스를 만들어본다.
image.png
일단 현재까지의 실습에서는 ratings로 가는 인그레스를 열어두지 않았고, 그렇기 때문에 product 파드에 바로 url로 ratings을 요청할 시 404 에러가 출력된다.

항상 쓰던 기본 실습용 파드를 배포한 이후, 두 개의 인그레스를 만든다.
실습용 파드로 가는 인그레스는 독점, ratings로 가는 인그레스는 공유로 설정한다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: webpod-ingress
  namespace: default
  annotations:
    ingress.cilium.io/loadbalancer-mode: dedicated
spec:
  ingressClassName: cilium
  rules:
  - http:
      paths:
      - backend:
          service:
            name: webpod
            port:
              number: 80
        path: /
        pathType: Prefix
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ratings-ingress
  namespace: default
spec:
  ingressClassName: cilium
  rules:
    - http:
        paths:
          - backend:
              service:
                name: ratings
                port:
                  number: 9080
            path: /ratings
            pathType: Prefix

보다시피 dedicated를 만드는 건 간단하게 어노테이션을 붙이는 방식으로 충족된다.
image.png
webpod의 경우 완전히 다른 주소를 할당 받은 것이 보인다.
image.png
당연히 새로운 서비스가 따로 배포됐다.

통신이 잘 되는지도 확인해본다.
image.png
ui 상으로는 두 인그레스를 구분하거나 하지는 않는다.
image.png
인그레스는 결국 신원을 하나로 쓰기 때문에, dedicated를 써서 얻는 이득은 딱히 없다.

트래픽을 처리하는 영역이 결국 동일하기에 dedicated을 통해 자원 경합 회피나 성능 상의 이점을 누리기는 어려워보인다.
당연하지만 어떤 노드가 트래픽을 받아 처리할지에 대해 명확한 분리를 시키는 기능이 아니기 때문이다.
image.png

다만 같은 http 경로를 가진 두 워크로드가 있을 경우 dedicated를 꼭 써줘야 한다.
모든 인그레스를 shared로 수정해봤다.
image.png
그랬더니 겹치는 경로에 대해서는 그냥 로드밸런싱이 되는 동작을 보여주고 있다.
image.png
image.png

다시 실습용 파드로 가는 IP를 독점시키고, 트래픽을 몇 번 찍어봤다.
image.png
신기하게도 RemotAddr의 값에 변동이 발생한다.
엔보이에서 같은 노드의 파드로 트래픽을 보낼 때는 별도로 요청을 다시 날리는 동작을 하지 않지만, 다른 노드의 경우 추가적인 요청이 들어가는 것으로 보인다.
image.png
이때 엔보이는 인그레스의 예약된 주소를 활용한다.

네트워크 폴리시 적용

다음으로는 네트워크 정책을 적용해본다.[7]

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

이 정책은 외부에서 들어오는 요청을 전부 차단해버린다.
정책을 거는 순간 명시되지 않은 모든 트래픽은 차단되기 때문에, 클러스터에서 발생한 트래픽이 아니면 아예 통신이 불가능해진다.
image.png
근데 이제 보니 허블 ui도 차단 당해부렀다 ㅋㅋ
image.png

허블은 차단 당한 채로 두고, 실습 요청을 들어갈 수 있도록 정책을 바꿔본다.

apiVersion: "cilium.io/v2"
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "allow-cidr"
spec:
  description: "Allow all the traffic originating from a specific CIDR"
  endpointSelector:
    matchExpressions:
      - key: reserved:ingress
        operator: Exists
  ingress:
    - toPorts:
        - ports:
            - port: "80"

image.png
여기에서 주목할 점은 인그레스 엔드포인트를 대상으로 삼아 정책을 적용했다는 것이다.
앞선 정책에서 내부 통신은 전부 허용한 상태이기 때문에 인그레스에서 워크로드로 가는 트래픽 자체는 문제가 없었다.
그러므로 외부에서 인그레스로 들어가는 정책만 뚫어주면 통신이 가능해지는 것이다.

정리하다보니 드는 생각으로는 그냥 인그레스라는 신원은 그냥 엔보이의 신원이라고 생각해도 될 것 같다.
물론 엔보이는 다른 기능을 할 때 다른 신원도 가지므로 포함 관계 정도로 보는 게 좋다.

아래는 직접 하진 않았으나, 클러스터 내부에서 모든 트래픽을 기본 차단한 이후 인그레스 신원에서는 클러스터 내부 모든 곳으로 접근 가능하게 하는 예시이다.

apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: "default-deny"
spec:
  description: "Block all the traffic (except DNS) by default"
  egress:
  - toEndpoints:
    - matchLabels:
        io.kubernetes.pod.namespace: kube-system
        k8s-app: kube-dns
    toPorts:
    - ports:
      - port: '53'
        protocol: UDP
      rules:
        dns:
        - matchPattern: '*'
  endpointSelector:
    matchExpressions:
    - key: io.kubernetes.pod.namespace
      operator: NotIn
      values:
      - kube-system
---
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: allow-ingress-egress
spec:
  description: "Allow all the egress traffic from reserved ingress identity to any endpoints in the cluster"
  endpointSelector:
    matchExpressions:
    - key: reserved:ingress
      operator: Exists
  egress:
  - toEntities:
    - cluster

보통 내부 통신 제한을 한다면 인그레스 역시 특정 대상에 대해서만 트래픽이 허용되도록 하는 것이 바람직할 것이다.

인그레스 경로 유형

다음으로는 인그레스에서 경로 타입을 설정하는 실습을 해본다.

kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/main/examples/kubernetes/servicemesh/ingress-path-types.yaml
kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/main/examples/kubernetes/servicemesh/ingress-path-types-ingress.yaml

kubectl get -f https://raw.githubusercontent.com/cilium/cilium/main/examples/kubernetes/servicemesh/ingress-path-types.yaml

image.png
예시는 정말 경로 분기에 대한 실습용 워크로드 들이다.

각 규칙에 대해 설명하려다, 굳이 싶어서 넘어간다.
image.png
실리움은 ImplementationSpecific으로 REGEX을 지원한다.

제대로 라우팅됐는지 확인해본다.

export PATHTYPE_IP=`k get ing multiple-path-types -o json | jq -r '.status.loadBalancer.ingress[0].ip'`
curl -s -H "Host: pathtypes.example.com" http://$PATHTYPE_IP/ | jq '.pod | split("-")[0]'
curl -s -H "Host: pathtypes.example.com" http://$PATHTYPE_IP/exact | jq '.pod | split("-")[0]'
curl -s -H "Host: pathtypes.example.com" http://$PATHTYPE_IP/prefix | jq '.pod | split("-")[0]'
curl -s -H "Host: pathtypes.example.com" http://$PATHTYPE_IP/impl | jq '.pod | split("-")[0]'
curl -s -H "Host: pathtypes.example.com" http://$PATHTYPE_IP/implementation | jq '.pod | split("-")[0]'

image.png
겹치는 케이스가 있어도 더 자세한 조건이 우선된다는 것 정도만 기억하면 헷갈릴 게 없다.

kubectl delete -f https://raw.githubusercontent.com/cilium/cilium/main/examples/kubernetes/servicemesh/ingress-path-types.yaml
kubectl delete -f https://raw.githubusercontent.com/cilium/cilium/main/examples/kubernetes/servicemesh/ingress-path-types-ingress.yaml

GRPC 적용

이번에는 인그레스로 gRPC 프로토콜을 설정해본다.[8]

이번에는 GCP에서 제공하는 MSA 예제를 사용한다.[9]

kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/main/release/kubernetes-manifests.yaml
curl -o demo.proto https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/main/protos/demo.proto

grpc를 위해서는 프로토버프 파일을 공유해야 한다.
image.png
어떤 api를 날릴 수 있고 스펙이 어떤지 나와있다.

다음으로는 grpc를 받을 인그레스를 만든다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: grpc-ingress
  namespace: default
spec:
  ingressClassName: cilium
  rules:
  - http:
      paths:
      - backend:
          service:
            name: productcatalogservice
            port:
              number: 3550
        path: /hipstershop.ProductCatalogService
        pathType: Prefix
      - backend:
          service:
            name: currencyservice
            port:
              number: 7000
        path: /hipstershop.CurrencyService
        pathType: Prefix

grpc 요청을 날릴 때 사용할 터미널 도구를 사용한다.[10]

GRPC_INGRESS=$(kubectl get ingress grpc-ingress -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
grpcurl  -proto ./demo.proto $GRPC_INGRESS:80  list
grpcurl -plaintext -proto ./demo.proto $GRPC_INGRESS:80 hipstershop.CurrencyService/GetSupportedCurrencies
grpcurl -plaintext -proto ./demo.proto $GRPC_INGRESS:80 hipstershop.ProductCatalogService/ListProducts

해당 도구는 프로토 파일을 이용해 정의된 api를 날려주는 기능을 수행한다.
image.png
image.png
grpc는 http/2 통신만 가능하다면 사용할 수 있는데 실리움에서 이를 제대로 지원해주는 것을 확인할 수 있다.

TLS 터미네이션 실습

다음으로 TLS 터미네이션 기능을 실습해본다.
터미네이션은 인그레스 기본 스펙 상에서 지원하는 사항이라 어쩌면 당연할 수도 있는 기능이긴 하다.

먼저 인증서를 만들어야 하는데 mkcert란 간단한 툴을 사용할 것이다.[11]
mkcert는 자체 루트 인증서를 알아서 만든 후에, 해당 인증서로 서명 받은 여러 인증서를 쉽게 만들 수 있도록 도와준다.
로컬에서 편하게 사용할 수 있도록 루트 인증서를 신뢰 저장소에 넣어주는 명령어도 지원한다.

sudo apt install libnss3-tools -y
sudo apt install mkcert -y
mkcert -h
ls `mkcert -CAROOT`
mkcert '*.cilium.rocks'

먼저 cert-util이 들어간 패키지를 설치하는데, 이것은 이후 브라우저에서도 등록한 자체 인증서를 신뢰할 수 있도록 하는데 도움을 준다.
image.png
간단하게 사용하는데 있어서는 cfssl보다도 더 편리한 것 같다.
만들어진 키를 확인해보면..

openssl x509 -in _wildcard.cilium.rocks.pem -text -noout

image.png
정말 간단하게 발행자, 일반 이름 등의 정보를 채워준다.

image.png
TLS 용으로 초반에 지정한 이름으로 SAN까지 설정해준다.
이 값은 핸드셰이크 시 SNI 필드로 활용될 것이다.

그럼 바로 해당 인증서를 시크릿으로 등록하고 사용해본다.

kubectl create secret tls demo-cert --key=_wildcard.cilium.rocks-key.pem --cert=_wildcard.cilium.rocks.pem
kubectl get secret demo-cert -o json | jq

이제 해당 시크릿을 활용하는 인그레스를 만든다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tls-ingress
  namespace: default
  annotations:
    ingress.cilium.io/loadbalancer-mode: dedicated
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:
  - hosts:
    - webpod.cilium.rocks
    - bookinfo.cilium.rocks
    secretName: demo-cert

이전 인그레스들이 남아있다면 충돌이 날 수 있으니 dedicated로 설정했다.
image.png

다음으로는 mkcert의 루트 인증서를 신뢰 저장소에 등록한다.

mkcert -install

image.png
이제 해당 인증서에 대해서는 로컬에서 알아서 신뢰되는 루트 도메인을 가지고 있다고 인식하게 될 것이다.
image.png
참고로 위 명령은 간단하게 신뢰 저장소에 자체 루트 인증서를 추가하고, 정보를 업데이트하는 과정을 해준 것이다.
브라우저에서도 사용할 수 있도록 업데이트하는 작업까지 해주었다.

마지막으로 통신 확인을 해보자.

TLSIP=$(kubectl get ingress tls-ingress -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

curl -s --resolve bookinfo.cilium.rocks:443:${TLSIP} https://bookinfo.cilium.rocks/details/1 | jq
  
curl -s --resolve webpod.cilium.rocks:443:${TLSIP}   https://webpod.cilium.rocks/ -v

image.png
보다시피 만들었던 인증서를 기반으로 통신하고 있는 것이 확인된다.

참고로 아래와 같이 처음부터 인증서로 사용할 시크릿을 등록하면 인그레스에서 시크릿 이름을 명시하지 않아도 된다.[12]

helm upgrade cilium cilium/cilium --version 1.18.1 \
  --namespace kube-system \
  --reuse-values \
  --set ingressController.defaultSecretNamespace=kube-system \
  --set ingressController.defaultSecretName=default-cert

kubectl -n kube-system rollout restart deployment/cilium-operator
kubectl -n kube-system rollout restart ds/cilium

단일 인증서로만 사용할 거면 적당히 유용하겠으나.. 사실 인그레스 스펙에 쓰는 한 줄 줄이는 게 그렇게 의미가 있나 하는 생각이..

호스트 네트워크 모드

마지막으로 볼 것은 실리움만이 가능하다는, 호스트 네트워크 인그레스이다.
이 기능으로 로드밸런서를 사용할 수 없는 상황에서 그냥 호스트 네트워크를 사용하는 것이 가능하다.
이는 노드포트의 범위가 아닌 포트 번호를 사용할 수 있게 해주는데, 대신 1023 아래 포트를 사용하려면 엔보이가 NET_BIND_SERVICE 권한을 가지고 있어야 한다.

호스트 네트워크 모드는 실리움 자체에 대한 업데이트가 필요하다.

helm upgrade cilium cilium/cilium --version 1.18.1 \
  --namespace kube-system \
  --reuse-values \
  --set ingressController.hostNetwork.enabled=true \
  --set ingressController.hostNetwork.sharedListenerPort=8888

kubectl -n kube-system rollout restart deployment/cilium-operator
kubectl -n kube-system rollout restart ds/cilium

shared에 대해서는 호스트의 8080포트를 기본값으로 사용하나, 위처럼 바꿔줄 수 있다.

설정을 적용하자 shared 인그레스들은 전부 주소 정보가 사라졌다.
image.png
하지만 이 상태로 뭐가 되는 건 아니고, 에이전트와 오퍼레이터를 전부 재시작해줘야 제대로 동작한다.
image.png
재시작을 시켜준 이후에야 각 노드에 해당 포트들이 오픈됐다.
dedicated 인그레스들은 어노테이션으로 포트를 설정해야 하는데 아직 설정을 하지 않았기 때문에 위처럼 8080포트를 물고 있다.

실습 중간에 초반에 사용한 북인포 워크로드를 지웠는데, 다시 세팅하고 테스트하니 문제 없이 통신이 되는 것을 확인할 수 있었다.

NODEIP=`k get nodes k8s-w1 -ojson | jq '.status.addresses[] | select(.type=="InternalIP") | .address' -r`
curl $NODEIP:8888
curl $NODEIP:8080

image.png

dedicated 인그레스는 호스트의 포트를 바꿀 때 어노테이션을 다는 방식을 활용한다.

k annotate ing webpod-ingress ingress.cilium.io/host-listener-port=8889

image.png
변경이 바로 반영되는 것을 볼 수 있다.
호스트 IP 쓰는 주제라 dedicated이기 위해 포트를 커스텀할 수 있게 해준 거라 보면 되겠다.

참고로 호스트 네트워크를 사용할 노드 그룹을 한정하는 것도 가능하다.

ingressController:
  enabled: true
  hostNetwork:
    enabled: true
    nodes:
      matchLabels:
        role: infra
        component: ingress

그런데 TLS 통신을 할 수 있도록 설정하는 건 딱히 보이지 않았다.

결론

실리움이 서비스 메시로서 지원하는 기본적인 기능 중 하나는 인그레스에 기반한 경로 라우팅이다.
여태까지 본 바, 다음의 여러 특징을 가지고 있다.

그런데 사실 이 정도의 기능으로 서비스 메시의 기능이라 하기에는 한참 부족하다.
서비스 메시의 기능으로서 언급된 회복성, 세밀한 트래픽 제어 역할은 조금도 수행할 수 없다.
이건 어느 정도 인그레스라는 리소스 스펙 자체의 한계도 있는 관계로, 상위호환 리소스인 게이트웨이 api를 통해 또 다양한 기능들을 탐구해보도록 하겠다.

여기에 추가적으로, 실리움이란 서비스 메시가 가지는 아쉬운 점이 보인다.
이스티오 앰비언트와 비교했을 때 일단 설정이 들어가면 트래픽이 무조건 엔보이를 거친다.
그런데 엔보이는 노드마다 하나이기 때문에 엔보이는 노드에 파드가 많을 수록 큰 부하를 받게 될 것이다.
앰비언트의 경우 L7의 기능을 활용할 때만 웨이포인트(엔보이)가 사용되도록 만들었고, 심지어 웨이포인트는 자유롭게 스케일링이 가능하도록 만들었다.
사이드카 모드와 실리움을 비교한다고 해도 이스티오에서는 단일 엔보이가 큰 부하를 감당하게 될 것에 걱정을 할 필요는 없다.

아울러.. 앰비언트도 마찬가지이긴 하지만 굳이 인그레스라는 신원을 따로 두는 이유를 잘 모르겠다.
그냥 외부에서 들어온 트래픽이란 걸로 신원으로 충분하지 않은 걸까?
인그레스 신원을 쓴다고 해서 각 인그레스 별로 신원이 있는 것도 아닌데, 어떤 부분에서 유용할지 잘 그림이 그려지지 않는다.

이전 글, 다음 글

다른 글 보기

이름 index noteType created
1W - 실리움 기본 소개 1 published 2025-07-19
1W - 클러스터 세팅 및 cni 마이그레이션 2 published 2025-07-19
1W - 기본 실리움 탐색 및 통신 확인 3 published 2025-07-19
2W - 허블 기반 모니터링 4 published 2025-07-26
2W - 프로메테우스와 그라파나를 활용한 모니터링 5 published 2025-07-26
3W - 실리움 기본 - IPAM 6 published 2025-08-02
3W - 실리움 기본 - Routing, Masq, IP Frag 7 published 2025-08-02
4W - 실리움 라우팅 모드 실습 - native, vxlan, geneve 8 published 2025-08-09
4W - 실리움 로드밸런서 기능 - 서비스 IP, L2 9 published 2025-08-09
5W - BGP 실습 10 published 2025-08-16
5W - 클러스터 메시 11 published 2025-08-16
6W - 실리움 서비스 메시 - 인그레스 12 published 2025-08-23
7W - 실리움 성능 - 쿠버네티스 기본 13 published 2025-08-31
8W - 실리움 보안 14 published 2025-09-07

관련 문서

지식 문서, EXPLAIN

이름12is-folder생성 일자
E-이스티오 컨트롤 플레인 성능 최적화false2025-05-18 02:29
서비스 메시- 2024-05-21
Gateway APIfalse2025-01-06 17:51
Istio Securitytrue2025-05-04 19:58
사이드카 모드false2025-05-18 03:27
pilot-agentfalse2025-04-28 23:26
Istiotrue2025-04-07 14:26
Kialifalse2025-04-28 23:41
메시 배포 모델false2025-05-21 13:36
Amazon VPC Latticefalse2025-04-23 09:11
E-이스티오의 데이터 플레인 트래픽 세팅 원리false2025-05-27 21:55
앰비언트 모드false2025-06-02 14:51

기타 문서

Z0-연관 knowledge, Z1-트러블슈팅 Z2-디자인,설계, Z3-임시, Z5-프로젝트,아카이브, Z8,9-미분류,미완
이름4코드타입생성 일자
1W - 서비스 메시와 이스티오Z8published2025-04-10 20:04
8W - 엔보이와 iptables 뜯어먹기Z8published2025-06-01 12:14
4W - 번외 - 트레이싱용 심플 메시 서버 개발Z8published2025-05-03 22:48
6W - 이스티오 컨트롤 플레인 성능 최적화Z8published2025-05-18 02:29

참고


  1. https://docs.cilium.io/en/stable/network/servicemesh/ ↩︎

  2. https://github.com/cilium/pwru ↩︎

  3. https://www.kernel.org/doc/Documentation/networking/tproxy.txt ↩︎

  4. https://blog.cloudflare.com/ko-kr/how-we-built-spectrum/ ↩︎

  5. https://docs.cilium.io/en/stable/network/servicemesh/http/ ↩︎

  6. https://istio.io/latest/docs/examples/bookinfo/ ↩︎

  7. https://docs.cilium.io/en/stable/network/servicemesh/ingress-and-network-policy/ ↩︎

  8. https://docs.cilium.io/en/stable/network/servicemesh/grpc/ ↩︎

  9. https://github.com/GoogleCloudPlatform/microservices-demo ↩︎

  10. http://github.com/fullstorydev/grpcurl#binaries ↩︎

  11. https://github.com/FiloSottile/mkcert ↩︎

  12. https://docs.cilium.io/en/stable/network/servicemesh/tls-default-certificate/ ↩︎