후니의 IT인프라 사전
[KANS-3기] 여러기능 (Network Policy/Bandwidth Manager/L2 Announcements) 본문
[KANS-3기] 여러기능 (Network Policy/Bandwidth Manager/L2 Announcements)
james_janghun 2024. 10. 27. 06:43Network Policy
Cilium은 L3, L4 뿐 아니라 Envoy를 통해서 L7 까지 통제를 할 수가 있다. 아무래도 Envoy를 사용하기 때문에 다양한 기능이 가능한 것 같다.
Cilium은 3가지 기반의 보안 레벨을 제공한다.
ID(Identity-Based)기반 (L3보안)
아래 그림에서 role=frontend라고 라벨을 가지고 오는 통신은 role=backend라는 라벨이 붙은 리소스에 연결을 허용한다. 해당 라벨을 ID를 말한다고 볼 수 있다.
이에 해당하는 yaml은 다음과 같다.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l3-rule"
spec:
endpointSelector:
matchLabels:
role: backend
ingress:
- fromEndpoints:
- matchLabels:
role: frontend
포트기반 (L4보안)
inbound와 outbound 포트에 대해서 접근 가능한 포트를 제한할 수 있다. 공식문서에서 다양한 예시를 제공하고 있다.
예를 들어 role=frontend 라벨이 붙은 리소스에서 443(https) 포트의 outbound를 허용하고, role=backend에서 443 포트의 inbound를 허용할 수 있다. 아래 예시에서 해당 내용을 적용해 볼 수 있다.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "l4-rule"
spec:
endpointSelector:
matchLabels:
role: backend
ingress:
- fromEndpoints:
- matchLabels:
role: frontend
toPorts:
- ports:
- port: "443"
protocol: TCP
애플리케이션 기반 (L7보안)
일단 애플리케이션 기반 통신을 제어하기 위해서 Envoy를 사용하고 있다. 즉 HTTP 및 원격 프로시저 호출(RPC) 프로토콜을 애플리케이션 프로토콜 수준에서 세분화하여 관리할 수 있다. 예를 들어 role=frontend가 있는 엔드포인트는 REST API 호출 GET만 허용하도록 할 수 있다.
L7의 경우 L3와 L4와는 다르게 규칙 위반에 대해서 패킷 드롭으로 되지 않는다. 대신 애플리케이션 프로토콜별 액세스 거부 메시지를 작성해 반환한다. 예를 들면 정책 위반 HTTP 요청에 대해서 HTTP 403(액세스 거부) 메시지를 반환하거나 DNS 요청에 대해 DNS 거부 응답을 반환하는 방식으로 대응한다.
다음 예시는 env=prod 라벨을 달고 /public URL에서 오는 GET 요청에 대해서는 app=service 라벨의 리소스에 접근 가능하도록 허용한다. 그 외 모든 URL이나 다른 HTTP 요청에 대해서는 거부한다. 또한 80포트에 대해서만 허용한다.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "rule1"
spec:
description: "Allow HTTP GET /public from env=prod to app=service"
endpointSelector:
matchLabels:
app: service
ingress:
- fromEndpoints:
- matchLabels:
env: prod
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "GET"
path: "/public"
Network Policy 관련 eBPF Datapath
prefilter
맨 앞단에 위치해서 XDP 프로토콜을 사용해서 트래픽을 필터링하는데 사용한다.
Endpoint Policy
정책에 따라서 패킷을 차단/전달하거나 서비스로 전달할 수 있다.
L7 Policy
L7 정책은 직접적으로 cilium이 다루는 것은 아니고 Envoy를 통해서 통제하기 때문에 패킷을 Envoy로 전송해야 한다. 그래서 cilium은 먼저 해당 트래픽은 userspace proxy 인스턴스로 보내는데 이게 Envoy이다. 커널 hookpoint와 Userspace proxy를 사용하기 때문에 당연 성능은 조금 떨어질 수 있다.
StarWars 데모 실습
공식문서에서 제공하는 실습을 통해서 NetworkPolicy 실습을 진행합니다.
kubectl create -f https://raw.githubusercontent.com/cilium/cilium/1.16.3/examples/minikube/http-sw-app.yaml
kubectl get all
그림과 같이 라벨이 다 붙어있습니다.
# xwing 통신확인
kubectl exec xwing -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
# tiefighter 통신확인
kubectl exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
L3/L4 정책 적용
파드의 라벨을 통해서 보안 정책을 적용해 보자. 'org=empire' (tiefighter) 라벨이 부착된 파드만 통신을 허용한다.
cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "rule1"
spec:
description: "L3-L4 policy to restrict deathstar access to empire ships only"
endpointSelector:
matchLabels:
org: empire
class: deathstar
ingress:
- fromEndpoints:
- matchLabels:
org: empire
toPorts:
- ports:
- port: "80"
protocol: TCP
EOF
실제 이 내용을 적용하면 tiefighter의 접근만 허용된다. hubble UI에서도 dropped 로그와 빨간색으로 표시된 통신 장애를 확인할 수 있다.
kubectl exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
Ship landed
kubectl exec xwing -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
drop
이는 endpointlist를 통해서도 직관적으로 확인할 수 있다. 잘보면deathstar에 대해서 tiefighter에 대해서만 Enabled된 것을 1202에서 확인할 수 있다.
kubectl exec -it $CILIUMPOD1 -n kube-system -c cilium-agent -- cilium endpoint list
L7 통신 테스트
L7 필터링의 경우 PUT /v1/exhaust-port 요청을 차단하는 것으로 데모를 실행해본다.
tiefighter에서 deathstar 서비스에 PUT /v1/exhaust-port 요청을 먼저 날려본다.
kubectl exec tiefighter -- curl -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port
이제 POST /v1/request-landing API만 호출할 수 있도록 허용정책을 생성한다. 따라서 PUT /v1/exhaust-port 요청은 이제 거부되어야 한다.
cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "rule1"
spec:
description: "L7 policy to restrict access to specific HTTP call"
endpointSelector:
matchLabels:
org: empire
class: deathstar
ingress:
- fromEndpoints:
- matchLabels:
org: empire
toPorts:
- ports:
- port: "80"
protocol: TCP
rules:
http:
- method: "POST"
path: "/v1/request-landing"
EOF
바로 확인할 수 있었다.
POST /v1/request-landing API 요청에 대해서는 정상적으로 forward 되었지만 PUT /v1/exhaust-port 요청에 대해서는 dropped되었다. 위에서 언급했지만 L7요청의 경우 connection 자체가 drop되는 것이 아니라 응답을 주고 받지만 정해진 거부 메시지를 응답해주는 방식으로 거부하게 된다. 즉 403으로 거부를 한다.
따라서 이렇게 즉각적인 응답을 한다. 혼동하지 않기 바란다.
<- Request http from 3957 ([k8s:app.kubernetes.io/name=tiefighter k8s:class=tiefighter k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:org=empire]) to 3967 ([k8s:app.kubernetes.io/name=deathstar k8s:class=deathstar k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:org=empire]), identity 14763->22049, verdict Denied PUT http://deathstar.default.svc.cluster.local/v1/exhaust-port => 0
<- Response http to 3957 ([k8s:app.kubernetes.io/name=tiefighter k8s:class=tiefighter k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:org=empire]) from 3967 ([k8s:app.kubernetes.io/name=deathstar k8s:class=deathstar k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default k8s:io.cilium.k8s.policy.cluster=default k8s:io.cilium.k8s.policy.serviceaccount=default k8s:io.kubernetes.pod.namespace=default k8s:org=empire]), identity 22049->14763, verdict Forwarded PUT http://deathstar.default.svc.cluster.local/v1/exhaust-port => 403
Bandwidth Manager
네트워크 트래픽 통제장치로 QoS의 기능을 하고 있다. 즉 네트워크 대역폭을 제한할 수 있는 기능이다. 아직은 egress 트래픽만 통제가능하고 ingress 트래픽은 통제가 불가능하기 때문에 내부 서비스들의 부하를 조절하는데 어느정도 의미가 있어보인다.
TCP와 UDP의 워크로드를 통제하고 rate limit을 pod 개별적으로 담당할 수 있다.
BandWidth 기능을 사용하려면 리눅스 커널이 v5.1.x 버전 이상이어야 합니다. 또한 cilium의 direct routing mode, tunneling mode 모드에서 지원됩니다.
자세한 내용은 공식문서에서 확인할 수 있다.
TC Ingress는 L3단의 QueueDisk로 eBPF가 이 곳에서 동작해서 통제를 할 수 있다.
kubernetes.io/egress-bandwidth를 붙여 사용하면 된다.
tc 명령을 통해서 qdisc를 확인하면 다음과 같이 4개가 있따. ens5는 네트워크 인터페이스 이름으로 본인의 환경에 맞게 변경하기 바란다.
tc qdisc show dev ens5
현재는 limit이 10240p 임을 확인하고 해당 정보를 추후에 bandwidthManager를 켜고 다시 확인해보자.
helm 배포시 bandwidthManager를 켜고 다시 배포해보았다.
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values --set bandwidthManager.enabled=true
cilium에서 BandwidthManager로 동작하는 인터페이스 정보를 조회할 수 있다. ens5가 해당 동작을 하고 있다는 것을 확인해볼 수 있다.
kubectl exec -it $CILIUMPOD0 -n kube-system -c cilium-agent -- cilium status | grep BandwidthManager
다시 한 번 tc로 조회해보면 이번에는 limit도 변경되고 다양한 값들이 추가된 것을 확인할 수 있다.
tc qdisc show dev ens5
bandwidthManager를 pod에 적용할 때는 annotation 방식을 이용한다. 테스트를 위해서 netperf 서버와 클라이언트를 배포한다.
cat <<EOF | kubectl apply -f -
---
apiVersion: v1
kind: Pod
metadata:
annotations:
# Limits egress bandwidth to 10Mbit/s.
kubernetes.io/egress-bandwidth: "10M"
labels:
# This pod will act as server.
app.kubernetes.io/name: netperf-server
name: netperf-server
spec:
containers:
- name: netperf
image: cilium/netperf
ports:
- containerPort: 12865
---
apiVersion: v1
kind: Pod
metadata:
# This Pod will act as client.
name: netperf-client
spec:
affinity:
# Prevents the client from being scheduled to the
# same node as the server.
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- netperf-server
topologyKey: kubernetes.io/hostname
containers:
- name: netperf
args:
- sleep
- infinity
image: cilium/netperf
EOF
netperf-server에 annotation으로 10M을 적용해서 해당 설정이 된것을 확인할 수 있다.
(⎈|kubernetes-admin@kubernetes:N/A) root@k8s-s:~# kubectl describe pod netperf-server | grep Annotations:
Annotations: kubernetes.io/egress-bandwidth: 10M
Identity 146번이 해당 설정이 걸려있다.
kubectl exec -it $CILIUMPOD2 -n kube-system -c cilium-agent -- cilium bpf bandwidth list
kubectl exec -it $CILIUMPOD2 -n kube-system -c cilium-agent -- cilium endpoint list
트래픽 테스트
실제로 트래픽을 날려보면 10M 이하로 발생한다는 것을 확인할 수 있다.
NETPERF_SERVER_IP=$(kubectl get pod netperf-server -o jsonpath='{.status.podIP}')
kubectl exec netperf-client -- netperf -t TCP_MAERTS -H "${NETPERF_SERVER_IP}"
5M로 설정을 변경하면 4.91로 딱 설정값에 맞게 egress 트래픽이 발생하는 것을 확인할 수 있다.
kubectl get pod netperf-server -o json | sed -e 's|10M|5M|g' | kubectl apply -f -
테스트가 끝났다면 리소스를 삭제한다.
kubectl delete pod netperf-client netperf-server
L2 Announcements / L2 Aware LB
특히 L2 Aware LB는 MetalLB를 대신할 수 있다. 아직 베타이지만 해당 블로그글에서 확인할 수 있다.
L2 Announcements는 로컬 영역 네트워크에서 서비스를 표시하고 트래픽을 전송해주는 기능이다. 이 기능은 BGP 기반 라우팅이 없는 네트워크 내에서 온프레미스 배포를 위해 사용한다.
이 기능을 이용하면 ExternalIP 및 LoadBalancer IP에 대한 ARP 쿼리 응답이 가능해서 이러한 가상 IP에 대해 한 번에 한 노드가 ARP 쿼리에 응답하고 MAC 주소로 응답한다.
NodePort 보다 장점인 것은 각 서비스가 고유한 IP를 사용할 수 있어 여러 서비스가 동일한 포트 번호를 사용할 수 있다. NodePort를 이용할 때는 트래픽을 보낼 호스트를 결정하는 것은 클라이언트에게 달려 있어 노드가 다운되면 IP+Port를 사용할 수 밖에 없는데 이 기능을 사용하면 서비스 가상 IP가 다른 노드로 간단히 마이그레이션 되어 사용할 수 있다.
설정
간단히 기능을 켜서 배포하면 된다.
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set l2announcements.enabled=true --set externalIPs.enabled=true \
--set l2announcements.leaseDuration=3s --set l2announcements.leaseRenewDeadline=1s --set l2announcements.leaseRetryPeriod=200ms
L2 Announcement 정책을 설정한다.
cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumL2AnnouncementPolicy
metadata:
name: policy1
spec:
serviceSelector:
matchLabels:
color: blue
nodeSelector:
matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: DoesNotExist
interfaces:
- ^ens[0-9]+
externalIPs: true
loadBalancerIPs: true
EOF
LoadBalancer IP pool을 이용해 IP를 지정할 수 있도록 할당한다. MetalLB와 비슷하다.
cat <<EOF | kubectl apply -f -
apiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
name: "cilium-pool"
spec:
allowFirstLastIPs: "No"
blocks:
- cidr: "10.10.200.0/29"
EOF
이렇게 지정하면 자동으로 External IP를 부여하는 것을 확인할 수 있다.
실제로 해당 IP를 curl해보면 모두 호출 가능한 것을 확인할 수 있다.
MetalLB를 Cilium으로 마이그레이션
블로그 글에서 쉽게 Cilium으로 마이그레이션이 가능하다고 예시 yaml을 제공하고 있다.
실제로 보면 구성 자체가 매우 동일한 것을 확인할 수 있다.
동작도 IP pool을 만들고 policy만 지정하면 끝나기 때문에 매우 간단하고 node의 인터페이스 정보만 잘 맞춰주면 된다.
정말 엄청난 기능들을 담당하고 있어서 나중에는 cilium이 매우 중요하게 사용하는 곳이 많아질 것이라는 생각도든다.