[K8S] Envoy 接管 Pod 的流量(2)
theme: cyanosis highlight: tomorrow-night-bright
0. 简介
在服务网格中,我们经常提到控制面和数据面。而我们熟知的 Istio 服务网格就是使用 Envoy 作为它的数据转发面。简单来说,它通过 XDS 协议获取由 istiod 下发的 Envoy 配置。一旦 Envoy 获取到配置,它就会对入站(Inbound)和出站(Outbound)规则进行应用。这个过程实际上是 Envoy 对我们的 Pod 进行 正向代理 和 反向代理。
通过这篇文章,您将了解Envoy是如何作为数据转发面进行数据的拦截与发送。下篇文章将讲述 如何使用Envoy的XDS协议 进行开发。阅读这两篇文章后,您将对如何构建自己的迷你服务网格有大致的了解.
1. 拦截入口流量
我们将模拟入口流量拦截,然后将原本访问 Pod 中的应用流量转向 Envoy,通过 Envoy 进行反向代理,最终到达应用程序这一过程。
通过上图我们可以了解到流量是如何转发到Pod 中的 APP, 下面将描述一下上图的每个步骤.
- 用户访问 10.1.1.1 访问 pod 的 80 端口;
- 在上图中,流量经过 iptables 的 PREROUTING 链上的 NAT 表进行处理。具体地,如果流量的目的地不是 127.0.0.1 并且目标端口为 80,那么它将被重定向到 Envoy 监听的端口 15006。这个步骤使用 iptables 的 PREROUTING 链来拦截入口流量并进行重定向。NAT 表用于修改流量的目的地 IP 和端口信息,将其重定向到 Envoy 实例监听的端口,即 15006。这样,原本要访问 Pod 中的应用程序的流量现在将被重定向到 Envoy 进行后续处理。
- 在接收到流量并重定向到 Envoy 监听的端口 15006 后,流量将经过 Envoy 的 filter_chain 进行处理, 并进行匹配项的检查。如果匹配成功,流量将被传递到指定的 Cluster(集群)。在这种情况下,流量将被发送到 Cluster(127.0.0.1:80),即本地主机上的端口 80。
现在我们大致知道了envoy是如何拦截流量, 接下来看看如何通过配置, 把这个实验过程搭建起来.
-
pod.yaml 留意注释的解析.
apiVersion: v1 kind: Pod metadata: name: envoy-traffic-mock labels: app: envoy-traffic-mock spec: restartPolicy: Never initContainers: - name: init # 使用当前带有 iptables 工具的镜像 image: bgiddings/iptables imagePullPolicy: IfNotPresent command: [ "sh","-c" ] # iptables 的命令意思就是上面第 2 点的描述 args: [ "iptables -t nat -A PREROUTING ! -d 127.0.0.1/32 -p tcp --dport 80 -j REDIRECT --to-ports 15006" ] # 安全上下文, 需要NET_ADMIN, 它是一个具有特殊权限的用户组, # 该用户组的成员可以执行网络管理操作 securityContext: capabilities: add: - NET_ADMIN containers: # 核心容器 - name: nginx image: nginx:1.18-alpine imagePullPolicy: IfNotPresent ports: # 监听 80 端口 - containerPort: 80 # envoy 反向代理 - name: envoyproxy image: envoyproxy/envoy-alpine:v1.21.0 imagePullPolicy: IfNotPresent # 挂载配置 volumeMounts: - name: envoyconfig mountPath: /etc/envoy/ volumes: # 使用 configmap 挂载配置 - name: envoyconfig configMap: defaultMode: 0655 name: envoyconfig --- apiVersion: v1 kind: Service metadata: name: envoy-traffic-mock spec: type: ClusterIP ports: # 代理 80 端口 - port: 80 targetPort: 80 selector: #service通过selector和pod建立关联 app: envoy-traffic-mock
-
envoy-configmyap.yaml 这里模仿了 istio 监听了 15006 作为代理端口. 然后匹配 * 表示所有虚拟主机, 将流量转发到 shadow_cluster_config 即 127.0.0.1:80
apiVersion: v1 kind: ConfigMap metadata: name: envoyconfig data: envoy.yaml: | admin: address: socket_address: { address: 0.0.0.0, port_value: 15000 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 15006 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO route_config: name: shadow_route virtual_hosts: - name: myhost domains: ["*"] routes: - match: {prefix: "/"} route: cluster: shadow_cluster_config http_filters: - name: envoy.filters.http.router clusters: - name: shadow_cluster_config connect_timeout: 1s type: Static dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: shadow_cluster_config endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 port_value: 80
2. 拦截出口流量
- 上一节, 我们进行流量入口的拦截实现了反向代理, 这次我们将模拟出口流量拦截实现正向代理. 为了更清晰的配置所以仅保留envoy出口方向的配置.
2.1 Block All 禁止所有流量
-
如图所示
- 我们登陆到 pod 中模拟程序在容器内发起 http 请求 www.baidu.com;
- 由于被 iptables 的 output 链拦截并通过 nat 表进行重定向到端口 15001 的 envoy 去;
- 我们流量进入到 envoy 中, 通过 filter_chain 中 的 tcp_proxy 转发到 block_all_cluster;
- 由于 block_all_cluster 没有写任何目标地址, 而且配置了类型为static. 所以最终tcp 连接被 reset 中断;
-
envoy.yaml 请自行替换 configmap, 留意下配置的注释
admin: address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: virtualOutbound address: socket_address: { address: 0.0.0.0, port_value: 15001 } filter_chains: - filters: - name: envoy.filters.network.tcp_proxy typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy # 接受到流量转入上游集群配置 BlockAllCluster cluster: BlockAllCluster statPrefix: BlockAllCluster name: virtualOutbound-tcp # 流量方向指定为出口方向, 默认 INBOUND traffic_direction: OUTBOUND use_original_dst: true clusters: - connect_timeout: 1s # BlockAllCluster 由于没有任务配置所以流量出不去 name: BlockAllCluster type: STATIC
-
pod.yaml 和之前的变化不大, 唯一需要改变的就是对出口流量的拦截 iptable 规则需要改变, 把流量重定向到15001. 这里是模拟了 istio 的agent 出口使用 15001.
apiVersion: v1 kind: Pod metadata: name: outpod labels: app: outpod spec: nodeName: dsjs initContainers: - name: init image: bgiddings/iptables imagePullPolicy: IfNotPresent command: [ "sh","-c" ] args: # 不拦截由 envoy 程序发起的请求 - iptables -t nat -A OUTPUT -m owner --uid-owner 1337 -j RETURN; # 拦截出去的流量到 envoy iptables -t nat -A OUTPUT ! -d 127.0.0.1/32 -p tcp -j REDIRECT --to-ports 15001; securityContext: privileged: true containers: - name: nginx image: nginx:1.18-alpine imagePullPolicy: IfNotPresent ports: - containerPort: 80 - name: envoyproxy image: envoyproxy/envoy-alpine:v1.21.0 imagePullPolicy: IfNotPresent securityContext: # 指定了运行用户 id, 这里非常的关键, 因为只允许 runAsUser: 1337 runAsGroup: 1337 volumeMounts: - name: envoyconfig mountPath: /etc/envoy/ volumes: # 使用 configmap 挂载配置 - name: envoyconfig configMap: defaultMode: 0655 name: envoyconfig ... ... ...和第一节的 pod.yaml 一致
-
登陆容器进行测试流量是否出不去了.
[root@k8s-01 mock-istio]# kubectl exec -it outpod -- sh Defaulted container "nginx" out of: nginx, envoyproxy, init (init) / # curl baidu.com curl: (56) Recv failure: Connection reset by peer
2.2 Allow All 允许所有流量出去
-
如图所示
- 同 2.1 章节一致;
- 同上;
- 我们流量进入到 envoy 中, 通过 filter_chain 中 的 tcp_proxy 转发到 allow_all_cluster;
- 由于 allow_all_cluster 配置中, 我们把类型设置成了orignal_dst, 所以envoy 会将我们的的目标地址设置为 wwww.baidu.com:80 对应的 ip:80;
- 当我们的流量是有 uid 为 1337 的程序发起时就不会往下匹配iptables 规则, 所以流量便可以正常通过 OUTPUT 链;
-
envoy.yaml 我们这次只需要替换 envoy 的配置, 替换完成后记住重启一下pod 让其重新应用新的配置.
admin: address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: virtualOutbound address: socket_address: { address: 0.0.0.0, port_value: 15001 } filter_chains: - filters: - name: envoy.filters.network.tcp_proxy typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy cluster: AllowAll statPrefix: AllowAll name: virtualOutbound-tcp trafficDirection: OUTBOUND useOriginalDst: true clusters: - connectTimeout: 1s name: AllowAll # 当type字段设置为ORIGINAL_DST时, # Envoy会使用客户端连接请求的原始目标地址( # 即通过使用iptables透明代理设置的目标地址)来确定集群成员的目标地址 type: ORIGINAL_DST lbPolicy: CLUSTER_PROVIDED - connectTimeout: 1s name: BlockAllCluster type: STATIC
-
测试一下是否能正常访问 baidu.com
[root@k8s-01 mock-istio]# kubectl exec -it outpod -- sh / # curl -I baidu.com HTTP/1.1 200 OK Date: Fri, 16 Jun 2023 17:25:43 GMT Server: Apache Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT ETag: "51-47cf7e6ee8400" Accept-Ranges: bytes Content-Length: 81 Cache-Control: max-age=86400 Expires: Sat, 17 Jun 2023 17:25:43 GMT Connection: Keep-Alive Content-Type: text/html
2.3 模拟 istio 访问 mesh 中的服务
-
如图所示
- 我们登陆到 pod 中模拟程序在容器内发起 http 请求一个 pod 的服务 10.107.42.16:80
- 由于被 iptables 的 output 链拦截并通过 nat 表进行重定向到端口 15001 的 envoy 去;
- 我们流量进入到 envoy 中, 通过 filter_chain 中 的 tcp_proxy 转发到 allow_all_cluster;
- 我们在 outbound | myngxsvc 上配置了 bind_to_port = false , 当 allow_all_cluster 在 original_dst 模式下会与 listeners 进行匹配, 而我们 listener 中存在 10.107.42.16:80所以匹配成功;
- 接着 virtualhost 的 domain 匹配的 * 所以我们也通过了匹配, 最终发起目标pod的访问;
- 当我们的流量是有 uid 为 1337 的程序发起时就不会往下匹配iptables 规则, 所以流量便可以正常通过 OUTPUT 链;
- 当 10.107.42.16:80 响应返回时, 通过 http_filters 的 response 拦截, 我们写了一段 lua 脚本进行头部增加, 并把结果返回到终端上;
-
我们可以通过配置 envoy 的出口方向配置正向代理的方式来实现路由匹配访问目标服务, 留意注释解析;
admin: address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: # 学习 istio 把 service 作为名称 - name: 10.107.42.16_80 address: socket_address: { address: 10.107.42.16, port_value: 80 } # bind_to_port = false 的时候不会真实绑定端口 # 一般这种配置是通过了 iptables 进行了转发, 这种使用需要是在 original_dst 模式下才能接受 # 在当前例子流量会从 virtualOutbound 进入, 通过 tcp proxy 代理流量, 转发到 cluster AllowALL, 而 AllowAll 下配置 type = ORIGINAL_DST bind_to_port: false filter_chains: filters: - name: envoy.filters.network.http_connection_manager typed_config: '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager # 用于 metrics 名字前缀 stat_prefix: outbound|10.107.42.16_80 route_config: name: outbound_myngxsvc_route virtual_hosts: - name: myngxsvc_host domains: [ "*" ] routes: - match: { prefix: "/" } route: cluster: outbound|myngxsvc http_filters: # 使用 lua 脚本加入响应头部, 表明我们的访问经过了 envoy 进行处理 - name: myheader.lua typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua inline_code: | function envoy_on_response(response_handle) response_handle:headers():add("myage", "18") end - name: envoy.filters.http.router typed_config: '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router traffic_direction: OUTBOUND - name: virtualOutbound address: socket_address: { address: 0.0.0.0, port_value: 15001 } filter_chains: - filters: - name: envoy.filters.network.tcp_proxy typedConfig: '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy cluster: AllowAll statPrefix: AllowAll name: virtualOutbound-tcp trafficDirection: OUTBOUND useOriginalDst: true clusters: - connectTimeout: 1s name: AllowAll type: ORIGINAL_DST lbPolicy: CLUSTER_PROVIDED - connectTimeout: 1s name: BlockAllCluster type: STATIC - name: outbound|myngxsvc connect_timeout: 1s type: Static dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: outbound|myngxsvc endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 10.107.42.16 port_value: 80
-
测试访问 10.107.42.16 是否能正常访问, 并且在响应的时候增加 myage 头部以表明是通过envoy 进行拦截处理
[root@k8s-01 mock-istio]# kubectl exec -it outpod -- sh Defaulted container "nginx" out of: nginx, envoyproxy, init (init) / # curl 10.107.42.16 -I HTTP/1.1 200 OK server: envoy date: Sun, 18 Jun 2023 13:38:03 GMT content-type: text/html content-length: 612 last-modified: Thu, 29 Oct 2020 15:23:06 GMT etag: "5f9ade5a-264" accept-ranges: bytes x-envoy-upstream-service-time: 2 myage: 18
3. 作为Gateway使用
- Gatway 我就不画图了, 我想大家都非常熟悉Gateway的功能, 首要就是作为流量入口进行多服务的反向代理、负载均衡、权重配置、限流、头部匹配和用户认证功能等. 接下来会列出一些配置提供各位进行一些参考与实验;
3.1 头部匹配
admin:
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: 0.0.0.0_8080
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: gateway_http
codec_type: AUTO
route_config:
name: default_route
virtual_hosts:
- name: myhost
domains: [ "app.shadow.com" ]
routes:
- match:
prefix: "/"
# 通过头部匹配定义版本本号,访问不同的app 版本
headers:
- name: "version"
exact_match: "v2"
route:
cluster: app_cluster_v2
- match:
prefix: "/"
route:
cluster: app_cluster_v1
http_filters:
- name: envoy.filters.http.router
trafficDirection: OUTBOUND
clusters:
- name: app_cluster_v2
connect_timeout: 1s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: app_cluster_v2
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: app.v2.svc.cluster.local
port_value: 80
- name: app_cluster_v1
connect_timeout: 1s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: app_cluster_v1
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: app.v1.svc.cluster.local
port_value: 80
3.2 限流
-
envoy 提供了两种级别的限流方式, 一种是 tcp 限流 另一种是 http 限流.
-
TCP 限流, 值得注意的是 TCP 是统计 ip:port , 所以同一个客户端是没法进行限流测试的;
admin: address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } listener_filters: - name: "envoy.filters.listener.http_inspector" filter_chains: - filters: # 在 filter_chains 下加入 envoy.filters.network.local_ratelimit - name: envoy.filters.network.local_ratelimit typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.local_ratelimit.v3.LocalRateLimit stat_prefix: local_rate_limiter # 配置 token bucket token_bucket: # 最大 token 数量 max_tokens: 2 # 每次加入桶中的数量 tokens_per_fill: 1 # token 加入桶的间隔时间 fill_interval: seconds: 1 - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO route_config: ... ...
-
HTTP 限流 , 颗粒度可以为每个主机的匹配项
admin: address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 8080 } listener_filters: - name: "envoy.filters.listener.http_inspector" filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO route_config: name: default_route response_headers_to_add: - header: key: myage value: "19" virtual_hosts: - name: myhost domains: ["*"] routes: - match: { prefix: "/aa" } route: cluster: shadow_cluster typed_per_filter_config: envoy.filters.http.local_ratelimit: "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit # 这是监控使用的前缀 stat_prefix: aa_local_rate_limiter token_bucket: max_tokens: 2 tokens_per_fill: 1 fill_interval: 1s # 启用部分限流, 但不一定会被限制 filter_enabled: runtime_key: local_rate_limit_enabled default_value: numerator: 100 denominator: HUNDRED # 启用部分强制限流 filter_enforced: runtime_key: local_rate_limit_enforced default_value: numerator: 100 denominator: HUNDRED response_headers_to_add: - append: false header: key: x-local-rate-limit value: 'true' - match: {prefix: "/bb"} route: cluster: shadow_cluster http_filters: - name: envoy.filters.http.local_ratelimit typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit stat_prefix: http_local_rate_limiter - name: envoy.filters.http.router ... ... ...
3.3 权重
admin:
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: 0.0.0.0_8080
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: gateway_http
codec_type: AUTO
route_config:
name: default_route
virtual_hosts:
- name: myhost
domains: [ "app.shadow.com" ]
routes:
- match:
prefix: "/"
route:
weighted_clusters:
# 权重总量设置为 10
total_weight: 10
# 权重配置,
clusters:
- name: app_v1
weight: 5
- name: app_v2
weight: 5
http_filters:
- name: envoy.filters.http.router
trafficDirection: OUTBOUND
clusters:
- name: app_v1
connect_timeout: 1s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: app_v1
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: app.v1.svc.cluster.local
port_value: 80
- name: app_v2
connect_timeout: 1s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: app_v2
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: app.v2.svc.cluster.local
port_value: 80
3.4 认证
- 认证这块可以通过 lua 脚本或者 wasm 对认证服务进行请求, 通过认证服务进行认证即可;
4. 写在最后
- 您都读到这了应该能对 envoy 本身有大致了解和认识. 内容制作不易, 如果您觉得文章对您有帮助请 点赞、关注➕、收藏. 下篇文章见
- 下篇将更新: 如何使用Envoy的XDS协议 (golang) , 需要的同学请催更;
5. 扩展
- 在 istio中 envoy 的完整流量路径解析: https://jimmysong.io/kubernetes-handbook/usecases/understand-sidecar-injection-and-traffic-hijack-in-istio-service-mesh.html