6W - 실리움 서비스 메시 - 인그레스
개요
이번 주차는 실리움의 서비스 메시에 대해 알아본다.
서비스 메시는 흔히 이스티오를 떠올리기 쉬운 개념인데, 실리움은 CNI이면서 해당 개념을 정의하고 관련한 기능도 제공하고 있다.
이번 주차의 내용은 실리움이 서비스 메시로서 제공하는 기능들을 실습해보는 것이다.
서비스 메시 자체에 대해 깊게 알아보는 것은 아니므로 실습 기반으로 내용을 정리한다.
그렇기에 활용되는 여러 리소스나 개념에 대한 설명은 이 글에서 가급적 작성하지 않는다.
서비스 메시
그래도 Istio 1기 - Istio Hands-on을 진행하며 공부했던 내용을 기반으로 먼저 서비스 메시의 개념을 알아본다.
서비스 메시는 다양한 서비스가 그물망처럼 다양하게 얽히고 연결되어 있는 구조, 아키텍처를 말한다.
큰 서비스의 관점에서, 서비스 메시는 네트워크만을 따로 담당하는 추상된 인터페이스 레이어를 가진다는 것이 핵심이다.
서비스 메시는 아키텍처 개념인 만큼 독립된 서비스들을 운용하며 이들을 효율적으로, 장애가 전파되지 않도록 운영하는 기술과 방식도 포함하여 일컫는다.
이 개념이 익숙치 않다면, 등장 배경을 먼저 아는 것이 좋다.
배경 - 마이크로서비스아키텍쳐의 문제
컨테이너 기술이 본격화되며 어플리케이션을 패키징하는 것이 용이해졌고, 이런 컨테이너를 여러 노드에서 통합적으로 관리할 수 있도록 오토스케일링과 자동화 기능을 하는 컨테이너 오케스트레이션(대표적으로 쿠버네티스]) 기술이 발달했다.
이로부터 유연한 확장성을 확보할 수 있는 MSA도 자연스럽게 정립됐다.
MSA의 가장 큰 특징 중 하나는 인프라 환경과 어플리케이션의 결합도를 낮춘다는 것으로, 물리적 자원의 제약에서 벗어나 자유롭게 기능을 확장하고 대처하는 것이 가능해졌다.
모놀리틱 아키텍처와 비교했을 때, 전체 아키텍쳐가 세분화되기에 구조는 복잡해지고 관리포인트가 늘어나게 된다.
- 트래픽 추적의 어려움
- 각기 다른 개발 환경을 가진 서비스로 인한 운영 복잡성
해결 방안과 방향성
위 문제들에 대한 해결 전략을 여러 가지 생각해볼 수 있다.
- 서비스 안정성을 위한 자동화, 복원 로직 구축
- 의도되지 않은 문제가 발생해도 서비스에 지장이 없도록 하는 복원력(resiliency) 확보를 위한 다양한 방법
- 클라이언트 사이드 로드밸런싱 - 여러 엔드포인트를 제공하고, 가능한 엔드포인트로 알아서 요청하도록 하기
- 서비스 디스커버리 - 논리적 서비스의 건강한 엔드포인트들을 주기적으로 리스트 업하기
- bulkheading - 클라마다 자원 사용량(커넥션 횟수, 스레드, 세션 등)을 제한
- 타임아웃 - 트래픽이나 요청의 최대 시간을 걸어서 에러낼 거면 빠르게 내기
- 재시도 - 실패한 요청을 알아서 재시도
- 데드라인 - 요청에 대한 응답의 수명 지정
- 관측 가능성 확보
- 자동화하여 안정성을 높여도, 궁극적으로는 어디에서 문제가 발생하는지 파악을 할 수 있도록 모니터링이 돼야 한다.
- 흔히 메트릭, 로그, 트레이싱이 필요할 텐데, MSA에서 특히 어려우면서 중요한 것은 트레이싱이라 할 수 있다.
- 트래픽이 이리저리 돌아다니면서 문제가 생기는 포인트를 명확하게 확인하기 위한 추적 수단이 필요하다!
근데.. 위 요소들을 누가 맡아서 작업을 수행해야 하는가?
서비스 메시가 등장하기 이전에는, 당연히 위 방법들은 어플리케이션을 만드는 개발팀이 해야만 했다.
메트릭을 노출하는 엔드포인트를 만들고, 모든 요청에 span을 넣어서 트레이싱이 가능하게 한다던지..
요청이 실패했을 때 알아서 재시작을 하게 한다던지..
이를 위해 Eureka, Zuul 등의 다양한 언어 라이브러리들이 등장하였고 지금도 쓰이고 있기는 하다.
그러나 MSA 환경에서 효율적인 전략이라고 하기에는 역시나 많은 문제가 있다.
- 라이브러리와 비즈니스 로직의 결합도
- 각 라이브러리가 효율적으로 동작하기 위해, 비즈니스 로직 중간에 코드가 삽입돼야 하는 경우가 생길 수 있다.
- 코드가 잘 정제됐다면 결합도를 느슨하게 하는 것이 가능하겠지만, 이조차도 높은 러닝 커브를 유발하게 된다.
- 서비스 간 결합도
- 언어 라이브러리는 저마다의 규약이 정해져있는 경우가 많고, 이로부터 한 언어 내에서도 사용할 수 있는 기술 스택이 한정되는 결과를 초래할 수 있다.
운영팀으로서는 이 해결책들을 개발팀한테 "해 줘"해야 하는 입장이긴 하나, 이러한 방식은 개발팀의 업무에 지장을 주기도 하는 동시에 전체 운영 환경의 유연성을 해친다는 것이다.
서비스 운영팀에서 해결하기 - 네트워크 레이어 분리
위 문제들은 운영 상 발생하는 문제이기도 한데, 비즈니스 로직에 집중을 해야 하는 개발팀이 주체적으로 대응하는 방식은 매우 아쉬운 해결책이다.
또한 재시도, 트레이싱 등의 해결책들은 어플리케이션 기능과도 관련도 없고 언어와 프레임워크에 한정해 고려해야 하는 요소들도 아니다.
이로부터 자연스럽게 이것들을 서비스 관리를 책임지는 운영팀에서 처리할 수 있는 방안이 강구되기 시작했고, 그렇게 등장한 것이 바로 서비스 메시이다.
위 문제와 해결 방법들은 사실 전부 네트워크와 관련되는 문제라는 공통 분모를 가지고 있다.
이로부터 어플리케이션 간에 이뤄져야 하는 네트워크 관련 기능과 이슈들은 아예 하나의 레이어로 분리하여 관리하는 것.
서비스 메시 레이어가 있으면 어플리케이션의 세팅이 어떤지 상관 없이 모든 네트워크 기능들을 운영 단계에서 해결할 수 있게 된다.
이것이 바로 서비스 메시의 등장 배경이자, 주된 기능이라고 할 수 있다!
구조
클라우드넷 스터디의 자료가 굉장히 명쾌하게 이해된다고 생각해 가져왔다.
서비스 메시는 3번의 구조를 가지고 있는데, 왜 이런 구조를 가지고 있는지 단계적으로 이해하기 좋다.
이러한 구조로 발전한 이유는 간단하다.
위에서 말했듯 네트워크 영역을 분리는 해야겠으니, 네트워크에 대한 설정, 동작을 담당하는 별도의 에이전트를 두는 것이다.
근데 이걸 애플리케이션마다 일일히 관리하는 것이 어려우니 컨트롤 플레인을 두는 방식으로 해소한다.
기능
이제 서비스 메시라는 것은 하나의 추상화된 레이어를 일컫는 개념이자, 이를 활용하는 아키텍처라는 것을 명확히 할 수 있을 것이다.
그리고 서비스 메시는 인프라 레이어와 밀접한 연관을 가지는 어플리케이션의 네트워크 기능을 전적으로 담당하는 역할을 수행한다.
그럼 구체적으로 서비스 메시를 통해 어떤 것들을 할 수 있는지를 구체화시켜보자.
- 서비스 회복성, 탄력성
- 서비스 디스커버리를 통해 건강한 엔드포인트만 트래픽이 흐르도록 설정한다.
- 네트워크가 계속 안정적일 수 있도록 실패 지점을 분리하는 것을 서킷 브레이커라고 부른다.
- 비정상적인 트래픽은 타임아웃을 걸고 요청에 대한 실패를 재시도하는 식으로 대응한다.
- 서비스 디스커버리를 통해 건강한 엔드포인트만 트래픽이 흐르도록 설정한다.
- 관측 가능성 설정
- 서비스 간 트래픽의 흐름을 파악할 수 있도록 트레이싱 설정을 한다.
- 트레이싱은 관측 가능성#Trace을 보자.
- 요청 처리 시간, 수행 결과 등을 메트릭과 로그를 통해 볼 수 있도록 설정한다.
- 서비스 간 트래픽의 흐름을 파악할 수 있도록 트레이싱 설정을 한다.
- 트래픽 컨트롤 기능 - 가장 당연한 기능이긴 한데, 조금 더 세밀한 컨트롤이 가능하도록 돕는다.
- 무중단 배포 중 어떤 헤더를 가진 트래픽만 새 버전으로 라우팅한다.
- 트래픽을 가중치 기반으로 일정 비율로 분산한다.
- 다른 서브 패스를 가진 요청 주소에 따라 다른 서비스로 라우팅한다.
- 클러스터 외부 트래픽을 받거나 내보내는 게이트웨이를 설정한다.
- 보안
- 통신 간 암호화(mTLS)를 설정한다.
- 트래픽에 신원을 부여하고 이를 기반으로 인증을 수행한다.
- 정책 적용
- 운영 상황에 적절한 각종 정책을 설정하고 이를 동적으로 적용한다.
실리움의 서비스 메시
그렇다면 실리움의 서비스 메시는 무엇인가?
문서에서는 서비스 메시로서 다음의 기능을 언급한다.[1]
- 복원력 있는 연결성 - 여러 환경을 넘나드는 서비스들의 안전한 통신
- L7 트래픽 관리 - 로드 밸런싱, 트래픽 제한, 프로토콜 지원 등
- 신원 기반 보안 - 트래픽에 고유한 신원을 추적하고 관리
- 관측 가능성
- 투명성 - 어플리케이션의 수정 없이 네트워크 설정 가능
사실 보면 실리움이 말하는 서비스 메시도 위에서 말하는 서비스 메시와 다르지 않다.
제공하는 기능도 보면 얼추 이스티오에서 제공하는 기능들과 얼추 맞긴 하다.
다만 뭐랄까.. 서비스 메시라고 떳떳하게 말하기에 조금은 부족한 느낌이 있다.
나는 서비스 메시라고 했을 때 가장 크게 보는 기능이 트래픽 관리, 신원 기반 트래픽 제어 및 보안, 관측 가능성이라 생각한다.
실리움도 이게 없다는 건 아닌데..
아래 그림은 이스티오의 문서에서 분류하는 기능들이다.
위 실리움 문서와 비교했을 때, 훨씬 명쾌하게 기능을 파악할 수 있다.
그렇다보니 조금은 부족하다는 느낌을 계속 받는 것 같다.
이스티오의 경우 Istio VirtualService, Istio DestinationRule 등 자체적인 커스텀 리소스를 통해 서비스 메시의 기능을 제공한다.
(앰비언트 모드부터는 Gateway API를 사용하긴 한다.)
그러나 실리움은 인그레스에 대한 지원을 첫번째 기능으로 제시하고 있다.
구조
실리움의 서비스 메시는 이스티오의 앰비언트 모드와 비슷한 구조를 가지고 있다.
기본적인 L3, L4 레벨의 서비스 메시 기능은 각 노드 별로 설치된 실리움 에이전트가 담당한다.
그리고 무거운 연산을 요구하는 L7 레벨의 작업은 엔보이로 트래픽을 전송하여 처리하게 만든다.
가만 보면 엔보이는 진짜 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
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'
인그레스
실리움에서는 cilium이란 인그레스 클래스를 제공한다.
이 클래스는 실리움의 인그레스 컨트롤러을 바탕으로 두고 있으며, 컨트롤러는 인그레스가 만들어질 때 로드밸런서 서비스를 만들어 기능을 제공한다.
실제로 트래픽이 들어온다면 해당 로드밸런서를 타고 들어오고, 커널 단에서 인그레스 정책을 적용할 엔보이로 연결된다.
사용 조건은 간단하다.
nodePort.enabled=true
or,kubeProxyReplacement=true
- 실리움의 인그레스를 위해 로드밸런서를 만드므로, 로드밸런서를 먼저 사용할 수 있는 클러스터여야 한다.
- 1.16버전 이후에는 대신 호스트 네트워크를 사용하는 게 가능하다.
l7Proxy=true
(기본 활성화)
실리움의 인그레스 컨트롤러는 CNI와 매우 밀접하게 결합돼있다는 것이 특별점이다.
일반적인 인그레스 컨트롤러(대표적으로 nginx ingress controller)는 클러스터에 따로 로드밸런서 서비스 형태로 배포된다.
실리움도 이런 동작이 가능하지만, 특성 상 호스트 네트워크를 통해 트래픽을 처리하는 것도 가능하다.
어떻게 설정하든 실리움은 트래픽을 받았을 때 커널 단에서 트래픽을 엔보이로 보내버리기 때문이다.
이 동작은 구체적으로는 커널의 tproxy를 통해 이뤄지는데, 엔보이는 내부에 가상의 호스트를 세워 트래픽 처리가 가능하다 보니 연결이 매끄럽게 이어진다.
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"
프리라우팅 체인에서 첫번째로 걸리는 raw 테이블에서는 컨트랙을 끊는 규칙들이 적혀있다.
이후 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"
엔보이는 각 노드에 배치되어 L7 트래픽을 처리하는 역할을 수행한다.
아예 노드에 소켓과 파일을 두고 마운팅하여 사용하는 것을 확인할 수 있다.
엔보이의 초기 세팅은 다음의 파일을 통해 진행된다.
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
설정을 주입받는 소켓 파일의 경로에 대한 설정이 보이는데, 실제 인그레스나 정책을 생성했을 때의 설정들은 동적으로 들어가기 때문에 위 정보로는 확인할 수 없다.
실리움 인그레스를 타고 들어오는 트래픽은 두 가지의 신원을 가진다.
먼저 외부에서 들어올 때 world
라는 신원을 가지고, 엔보이로 들어가 실제 백엔드로 들어갈 때는 ingress
신원을 가진다.
이러한 동작은 인그레스로서 들어온 트래픽에 대한 네트워크 정책을 적용할 수 있는 포인트를 제공한다.
달리 말하자면 트래픽이 제대로 전달되기 위해서는 두 위치에서 정책에서 허용돼야 한다는 것이다.
참고로 클러스터 내부에서도 정책 설정, 모니터링 설정으로 엔보이를 타는 트래픽이 있을 수 있는데, 이들에 대해서도 ingress
신원이 부여되지는 않는다.
별도의 신원 정보는 이런 식으로 확인한다.
kubectl exec -it -n kube-system ds/cilium -- cilium ip list | grep ingress
파드 중에는 위 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
인그레스 자체에 대한 서비스로 설정된 녀석이 조금 특이한 게 보인다.
엔드포인트로 이상한 주소가 확인되고 있다.
보통의 엔드포인트는 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"
공식 문서의 예제를 따라가본다.[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
기본적인 인그레스를 만들어본다.
인그레스 클래스를 지정하는 것을 캐치하자.
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
당연하지만 인그레스 리소스에 설정된 주소는 로드밸런서 서비스와 일치한다.
이제 실제로 트래픽이 가는지도 확인해본다.
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/"
트래픽이 엔보이를 거치기 때문에 엔보이 헤더가 붙는 것을 확인할 수 있다.
웹으로도 확인할 수 있다.
보다시피 아예 인그레스라는 신원에서 내부로 트래픽이 들어가는 것으로 표시되고 있다.
파드로 들어가는 인터페이스에서 트래픽을 캡쳐하면 엔보이에서 워크로드로 전달될 당시의 트래픽도 확인할 수 있다.
# 해당 노드(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/
파드로 들어가기 전 헤더에는 XFF, XFP 등의 값이 확인된다.
이는 외부에서 들어온 트래픽을 엔보이가 받아서 워크로드로 전달했다는 것을 보여준다.
그럼 실제 iptables에서는 어떤 변화가 생기는가?
아래 명령을 실행하고 중간 중간 다시 예시 요청을 날려본다.
vagrant ssh $PROND -c "sudo iptables -t mangle -L -v"
먼저 보다시피 생성된 인그레스에 맞춰 mangle 테이블에는 tproxy 규칙이 새로 생긴 것이 보인다.
또한 맨 위 줄에서 마크된 패킷들이 바로 규칙의 적용을 받으며 카운티되는 것을 확인할 수 있다.
인그레스 로드밸런서 독점(dedicated)
실리움은 인그레스에 대해 두 가지 모드를 지원한다.
- dedicated - 해당 모드를 사용하는 인그레스를 위해 별도의 로드밸런서 서비스를 만든다.
- shared - 여러 인그레스가 한 로드밸런서를 공유한다.
기본값은 shared로 한 로드밸런서 서비스를 모든 인그레스가 공유하는 구조이다.
모드는 동적으로 수정할 수 있는데, 대신 모드를 변경하면 로드밸런서 IP가 수정되기에 커넥션이 끊기는 것에 유의하자.
독점 인그레스를 만들어본다.
일단 현재까지의 실습에서는 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를 만드는 건 간단하게 어노테이션을 붙이는 방식으로 충족된다.
webpod의 경우 완전히 다른 주소를 할당 받은 것이 보인다.
당연히 새로운 서비스가 따로 배포됐다.
통신이 잘 되는지도 확인해본다.
ui 상으로는 두 인그레스를 구분하거나 하지는 않는다.
인그레스는 결국 신원을 하나로 쓰기 때문에, dedicated를 써서 얻는 이득은 딱히 없다.
트래픽을 처리하는 영역이 결국 동일하기에 dedicated을 통해 자원 경합 회피나 성능 상의 이점을 누리기는 어려워보인다.
당연하지만 어떤 노드가 트래픽을 받아 처리할지에 대해 명확한 분리를 시키는 기능이 아니기 때문이다.
다만 같은 http 경로를 가진 두 워크로드가 있을 경우 dedicated를 꼭 써줘야 한다.
모든 인그레스를 shared로 수정해봤다.
그랬더니 겹치는 경로에 대해서는 그냥 로드밸런싱이 되는 동작을 보여주고 있다.
다시 실습용 파드로 가는 IP를 독점시키고, 트래픽을 몇 번 찍어봤다.
신기하게도 RemotAddr의 값에 변동이 발생한다.
엔보이에서 같은 노드의 파드로 트래픽을 보낼 때는 별도로 요청을 다시 날리는 동작을 하지 않지만, 다른 노드의 경우 추가적인 요청이 들어가는 것으로 보인다.
이때 엔보이는 인그레스의 예약된 주소를 활용한다.
네트워크 폴리시 적용
다음으로는 네트워크 정책을 적용해본다.[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
이 정책은 외부에서 들어오는 요청을 전부 차단해버린다.
정책을 거는 순간 명시되지 않은 모든 트래픽은 차단되기 때문에, 클러스터에서 발생한 트래픽이 아니면 아예 통신이 불가능해진다.
근데 이제 보니 허블 ui도 차단 당해부렀다 ㅋㅋ
허블은 차단 당한 채로 두고, 실습 요청을 들어갈 수 있도록 정책을 바꿔본다.
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"
여기에서 주목할 점은 인그레스 엔드포인트를 대상으로 삼아 정책을 적용했다는 것이다.
앞선 정책에서 내부 통신은 전부 허용한 상태이기 때문에 인그레스에서 워크로드로 가는 트래픽 자체는 문제가 없었다.
그러므로 외부에서 인그레스로 들어가는 정책만 뚫어주면 통신이 가능해지는 것이다.
정리하다보니 드는 생각으로는 그냥 인그레스라는 신원은 그냥 엔보이의 신원이라고 생각해도 될 것 같다.
물론 엔보이는 다른 기능을 할 때 다른 신원도 가지므로 포함 관계 정도로 보는 게 좋다.
아래는 직접 하진 않았으나, 클러스터 내부에서 모든 트래픽을 기본 차단한 이후 인그레스 신원에서는 클러스터 내부 모든 곳으로 접근 가능하게 하는 예시이다.
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
예시는 정말 경로 분기에 대한 실습용 워크로드 들이다.
각 규칙에 대해 설명하려다, 굳이 싶어서 넘어간다.
실리움은 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]'
겹치는 케이스가 있어도 더 자세한 조건이 우선된다는 것 정도만 기억하면 헷갈릴 게 없다.
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를 위해서는 프로토버프 파일을 공유해야 한다.
어떤 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를 날려주는 기능을 수행한다.
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이 들어간 패키지를 설치하는데, 이것은 이후 브라우저에서도 등록한 자체 인증서를 신뢰할 수 있도록 하는데 도움을 준다.
간단하게 사용하는데 있어서는 cfssl보다도 더 편리한 것 같다.
만들어진 키를 확인해보면..
openssl x509 -in _wildcard.cilium.rocks.pem -text -noout
정말 간단하게 발행자, 일반 이름 등의 정보를 채워준다.
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로 설정했다.
다음으로는 mkcert의 루트 인증서를 신뢰 저장소에 등록한다.
mkcert -install
이제 해당 인증서에 대해서는 로컬에서 알아서 신뢰되는 루트 도메인을 가지고 있다고 인식하게 될 것이다.
참고로 위 명령은 간단하게 신뢰 저장소에 자체 루트 인증서를 추가하고, 정보를 업데이트하는 과정을 해준 것이다.
브라우저에서도 사용할 수 있도록 업데이트하는 작업까지 해주었다.
마지막으로 통신 확인을 해보자.
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
보다시피 만들었던 인증서를 기반으로 통신하고 있는 것이 확인된다.
참고로 아래와 같이 처음부터 인증서로 사용할 시크릿을 등록하면 인그레스에서 시크릿 이름을 명시하지 않아도 된다.[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 인그레스들은 전부 주소 정보가 사라졌다.
하지만 이 상태로 뭐가 되는 건 아니고, 에이전트와 오퍼레이터를 전부 재시작해줘야 제대로 동작한다.
재시작을 시켜준 이후에야 각 노드에 해당 포트들이 오픈됐다.
dedicated 인그레스들은 어노테이션으로 포트를 설정해야 하는데 아직 설정을 하지 않았기 때문에 위처럼 8080포트를 물고 있다.
실습 중간에 초반에 사용한 북인포 워크로드를 지웠는데, 다시 세팅하고 테스트하니 문제 없이 통신이 되는 것을 확인할 수 있었다.
NODEIP=`k get nodes k8s-w1 -ojson | jq '.status.addresses[] | select(.type=="InternalIP") | .address' -r`
curl $NODEIP:8888
curl $NODEIP:8080
dedicated 인그레스는 호스트의 포트를 바꿀 때 어노테이션을 다는 방식을 활용한다.
k annotate ing webpod-ingress ingress.cilium.io/host-listener-port=8889
변경이 바로 반영되는 것을 볼 수 있다.
호스트 IP 쓰는 주제라 dedicated이기 위해 포트를 커스텀할 수 있게 해준 거라 보면 되겠다.
참고로 호스트 네트워크를 사용할 노드 그룹을 한정하는 것도 가능하다.
ingressController:
enabled: true
hostNetwork:
enabled: true
nodes:
matchLabels:
role: infra
component: ingress
그런데 TLS 통신을 할 수 있도록 설정하는 건 딱히 보이지 않았다.
결론
실리움이 서비스 메시로서 지원하는 기본적인 기능 중 하나는 인그레스에 기반한 경로 라우팅이다.
여태까지 본 바, 다음의 여러 특징을 가지고 있다.
- 인그레스 자체 신원 -
reserved:ingress
라는 신원을 가지고 트래픽 정책의 제어를 받는다. - 원본 IP - 엔보이가 원본 IP를 XFF헤더에 넣어 백엔드로 전달
- 호스트 네트워크 모드
- 로드밸런서를 사용할 수 없는 상황에서 그냥 호스트 네트워크를 사용하는 것이 가능하다.
- tproxy 기반으로 그저 호스트로 들어온 모든 트래픽을 별도의 nat 없이 엔보이를 거치게 만들 수 있다.
- 대신 1023 아래 포트를 사용하려면 엔보이가
NET_BIND_SERVICE
권한을 가지고 있어야 한다.
- externalTrafficPolicy
- tproxy는 무조건 노드의 엔보이로만 패킷을 보낸다.
- 그래서 외부 트래픽 정책이 Cluster여도 전달되는 위치는 각 노드이다.
- XFF 덕에 언제나 원본 IP는 백엔드로 전달된다.
- IP 독점, 공유 모드 설정
- 인그레스 경로 유형
- ImplementationSpecific 타입이 있다.
- REGEX 기반 경로 매칭을 지원한다.
- TLS 패스스루
- TCP 연결은 전부 엔보이를 거치기 때문에, 엔보이가 패스스루를 수행할 때 백엔드는 엔보이의 IP를 보게 된다.
그런데 사실 이 정도의 기능으로 서비스 메시의 기능이라 하기에는 한참 부족하다.
서비스 메시의 기능으로서 언급된 회복성, 세밀한 트래픽 제어 역할은 조금도 수행할 수 없다.
이건 어느 정도 인그레스라는 리소스 스펙 자체의 한계도 있는 관계로, 상위호환 리소스인 게이트웨이 api를 통해 또 다양한 기능들을 탐구해보도록 하겠다.
여기에 추가적으로, 실리움이란 서비스 메시가 가지는 아쉬운 점이 보인다.
이스티오 앰비언트와 비교했을 때 일단 설정이 들어가면 트래픽이 무조건 엔보이를 거친다.
그런데 엔보이는 노드마다 하나이기 때문에 엔보이는 노드에 파드가 많을 수록 큰 부하를 받게 될 것이다.
앰비언트의 경우 L7의 기능을 활용할 때만 웨이포인트(엔보이)가 사용되도록 만들었고, 심지어 웨이포인트는 자유롭게 스케일링이 가능하도록 만들었다.
사이드카 모드와 실리움을 비교한다고 해도 이스티오에서는 단일 엔보이가 큰 부하를 감당하게 될 것에 걱정을 할 필요는 없다.
아울러.. 앰비언트도 마찬가지이긴 하지만 굳이 인그레스라는 신원을 따로 두는 이유를 잘 모르겠다.
그냥 외부에서 들어온 트래픽이란 걸로 신원으로 충분하지 않은 걸까?
인그레스 신원을 쓴다고 해서 각 인그레스 별로 신원이 있는 것도 아닌데, 어떤 부분에서 유용할지 잘 그림이 그려지지 않는다.
이전 글, 다음 글
- 5W - 클러스터 메시: 11
- 7W - 실리움 성능 - 쿠버네티스 기본: 13
다른 글 보기
이름 | 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
이름12 | is-folder | 생성 일자 |
---|---|---|
E-이스티오 컨트롤 플레인 성능 최적화 | false | 2025-05-18 02:29 |
서비스 메시 | - | 2024-05-21 |
Gateway API | false | 2025-01-06 17:51 |
Istio Security | true | 2025-05-04 19:58 |
사이드카 모드 | false | 2025-05-18 03:27 |
pilot-agent | false | 2025-04-28 23:26 |
Istio | true | 2025-04-07 14:26 |
Kiali | false | 2025-04-28 23:41 |
메시 배포 모델 | false | 2025-05-21 13:36 |
Amazon VPC Lattice | false | 2025-04-23 09:11 |
E-이스티오의 데이터 플레인 트래픽 세팅 원리 | false | 2025-05-27 21:55 |
앰비언트 모드 | false | 2025-06-02 14:51 |
기타 문서
Z0-연관 knowledge, Z1-트러블슈팅 Z2-디자인,설계, Z3-임시, Z5-프로젝트,아카이브, Z8,9-미분류,미완이름4 | 코드 | 타입 | 생성 일자 |
---|---|---|---|
1W - 서비스 메시와 이스티오 | Z8 | published | 2025-04-10 20:04 |
8W - 엔보이와 iptables 뜯어먹기 | Z8 | published | 2025-06-01 12:14 |
4W - 번외 - 트레이싱용 심플 메시 서버 개발 | Z8 | published | 2025-05-03 22:48 |
6W - 이스티오 컨트롤 플레인 성능 최적화 | Z8 | published | 2025-05-18 02:29 |
참고
https://www.kernel.org/doc/Documentation/networking/tproxy.txt ↩︎
https://docs.cilium.io/en/stable/network/servicemesh/http/ ↩︎
https://docs.cilium.io/en/stable/network/servicemesh/ingress-and-network-policy/ ↩︎
https://docs.cilium.io/en/stable/network/servicemesh/grpc/ ↩︎
https://github.com/GoogleCloudPlatform/microservices-demo ↩︎
https://docs.cilium.io/en/stable/network/servicemesh/tls-default-certificate/ ↩︎