大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说[K8S] Isitio 入门总结[亲测有效],希望您对编程的造诣更进一步.
1. 了解 Service Mesh
-
Service Mesh是用于处理服务间通信的基础设施层,用于在云原生应用复杂的服务拓扑中实现可靠的请求传递。在实践中,Service Mesh通常是一组与应用一起部署,但对应用透明的轻量级网络代理。Service Mesh与传统基础设施层不同之处在于,它形成了一个分布式的互连代理网络,以sidecar形式部署在服务两侧,服务对于代理无感知,且服务间所有通信都由代理进行路由, 所以就算是异构程序借助Service Mesh也能统一调度和管理。
-
Istio 就是基于上述思想实现的一个服务治理架构, 其核心特性包含了 流量管理、安全 和 可观测性.
- 流量管理: Istio 简化了服务级属性(如熔断器、超时和重试)的配置,并且让它轻而易举的执行重要的任务(如 A/B 测试、金丝雀发布和按流量百分比划分的分阶段发布);
- 安全机制: Istio 提供了底层的安全通信通道,并为大规模的服务通信管理认证、授权和加密;
- 可观测性: Istio 健壮的追踪、监控和日志特性让您能够深入的了解服务网格部;
1.1 Istio 架构
官方文档: istio.io/latest/docs…
1.2 核心组件
由于 Istio 是一个快速迭代的产品, 所以一些组件的变化也在所难免 [当前版本 1.15]
- istio 服务网格其实核心还是做一个做流量调度, 其他的功能都是在此基础上进行发展的. 好比我们的熟悉的网络架构, 现有路由寻址再有网络安全, 网络监控等; 所以 istio 其实也类似网络一样分为 控制平面 和 数据平面.
- 控制平面: 在 istio 中主要是管就是配置流量路由和一些安全配置等, 而 istio 中主要关注的是 HTTP 及TCP 四层之上的流量调度;。
- 数据平面 (转发平面): 在 istio 中由一组智能代理(Envoy)组成,被部署为 Sidecar。这些代理负责协调和控制微服务之间的所有网络通信。它们还收集和报告所有网格流量的遥测数据。
1.2.1 控制平面
- 根据上面的架构图可以看到 istiod 控制平面分别由 Pilot, Citadel 和 Galley 组成.
Pilot
- Pilot 为 Envoy sidecar 提供服务发现、用于智能路由的流量管理功能(例如,A/B 测试、金丝雀发布等)以及弹性功能(超时、重试、熔断器等)。
Citadel
- Citadel 通过内置的身份和证书管理,可以支持强大的服务间以及最终用户的身份验证。您可以使用 Citadel 来升级服务网格中的未加密流量。使用 Citadel,operator 可以执行基于服务身份的策略,而不是相对不稳定的 3 层或 4 层网络标识。
Galley
- Galley 是 Istio 的配置验证、提取、处理和分发组件。它负责将其余的 Istio 组件与从底层平台(例如 Kubernetes)获取用户配置的细节隔离开来。
1.2.2 数据平面
Envoy
-
Istio 使用 Envoy 代理的扩展版本,该代理是以 C++ 开发的高性能代理,用于调解服务网格中所有服务的所有入站和出站流量。
-
Envoy 代理被部署为服务的 Sidecar,在逻辑上为服务增加了许多内置特性,例如:
- 动态服务发现
- 负载均衡
- TLS 终端
- HTTP/2 与 gRPC 代理
- 熔断器
- 健康检查
- 基于百分比流量分割的分阶段发布
- 故障注入
- 丰富的指标
2. 安装测试环境
前置条件: 需要一个可用的k8s集群, 云原生嘛懂的都懂.
2.1 安装 Istio
- 安装一个官方的样例 istio
$ cd /opt
$ curl -L https://istio.io/downloadIstio | sh -
$ cd istio-1.15.2
$ export PATH=$PWD/bin:$PATH
$ istioctl install --set profile=demo -y
✔ Istio core installed
✔ Istiod installed
✔ Egress gateways installed
✔ Ingress gateways installed
✔ Installation complete
- istioctl 是一个istio自带的网格应用配置管理工具, 上面安装的配置对应的配置位置 /opt/istio/istio-1.15.2/manifests/profiles/demo.yaml.
PS: 安装完成后需要将 istio-system/istio-ingressgateway 的 service type改成 NodePort.
2.2 部署测试项目
2.2.1 下载测试项目
-
结合测试项目来做技术显形;
$ git clone https://github.com/Shadow-linux/istio-practise
2.2.2 部署项目
- 该项目分为 prodapi 和 reviewapi, prodapi 会调用 reviewapi 获取评论信息模拟服务调用. 为了方便调试我并没有对镜像打包只是编译完成后放到服务器上,所以启动时请指定服务器;
$ kubectl apply -f yamls/api.yaml #在 myistio namespace 下创建
apiVersion: apps/v1 kind: Deployment metadata: name: prodapi namespace: myistio spec: selector: matchLabels: app: prod version: v1 replicas: 1 template: metadata: labels: app: prod version: v1 spec: # 这里指定部署节点, 需要调整 nodeName: k8s-02 containers: - name: prod image: alpine:3.12 imagePullPolicy: IfNotPresent command: ["/app/prods"] env: - name: "release" value: "1" volumeMounts: - name: appdata mountPath: /app ports: - containerPort: 8080 volumes: - name: appdata # 需要调整你的挂载点 hostPath: path: /opt/istio/study/code_app --- apiVersion: v1 kind: Service metadata: name: prodsvc namespace: myistio labels: app: prod spec: type: ClusterIP ports: - port: 80 targetPort: 8080 selector: app: prod --- apiVersion: apps/v1 kind: Deployment metadata: name: reviewapi namespace: myistio spec: selector: matchLabels: app: reviews replicas: 1 template: metadata: labels: app: reviews spec: # 这里指定部署节点, 需要调整 nodeName: k8s-02 containers: - name: reviews image: alpine:3.12 imagePullPolicy: IfNotPresent command: ["/app/reviews"] env: - name: "release" value: "1" volumeMounts: - name: appdata mountPath: /app ports: - containerPort: 8080 volumes: - name: appdata hostPath: path: /opt/istio/study/code_app --- apiVersion: v1 kind: Service metadata: name: reviewsvc namespace: myistio labels: app: reviews spec: type: ClusterIP ports: - port: 80 targetPort: 8080 selector: app: reviews
- 测试基本调用访问
$ kubectl get svc -n myistio NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE prodsvc ClusterIP 10.105.57.78 <none> 80/TCP 53m reviewsvc ClusterIP 10.107.19.202 <none> 80/TCP 53m # 测试访问没问题, prods 可能调用 reviews $ curl 0.105.57.78/prods/1 {"Id":1,"Name":"测试商品","Reviews":[{"Id":2000,"Title":"测试评论1"},{"Id":2002,"Title":"测试评论2"}]}
2.2.3 Sidecar 注入
-
sidecar 的注入方式分为手动注入和自动注入, 一般线上都会使用自动注入;
$ kubectl label namespace myistio istio-injection=enabled namespace/default myistio # 去掉自动注入 $ $ kubectl label namespace myistio istio-injection-
-
手动注入的方式
$ istioctl kube-inject -f yamls/app.yaml | kubectl apply -f -
-
Sidecar 的注入主要改写了部分配置文件新加入了两个主要的配置:
- istio-init :初始化任务,如iptables规则, 用于接管pod的出入口流量
- istio-proxy:代理容器(官方扩展过后的Envoy)
- Pilot-agent进程: 用来生成envoy 的配置
- Envoy进程: 具体运行
2.2.4 测试请求
- 查看是否成功注入 istio-proxy, 可以看到每个POD现在的容器数都变成了 2/2.
$ kubectl get pods -n myistio
NAME READY STATUS RESTARTS AGE
prodapi-689cfc5846-lwzvk 2/2 Running 0 3d10h
reviewapi-58f5887cb9-grmwm 2/2 Running 0 3d10h
- 请求 prodapi 是否正常调用 reviewapi, 调用也是正常
$ curl curl 10.105.57.78/prods/123
{"header":{"Accept":["*/*"],"User-Agent":["curl/7.29.0"],"X-B3-Sampled":["1"],"X-B3-Spanid":["c90556ffe4051eda"],"X-B3-Traceid":["860d2e80a255f5f6c90556ffe4051eda"],"X-Forwarded-Proto":["http"],"X-Request-Id":["ba17cba8-dc09-9416-bfb6-2ca0c6df4739"]},"result":{"Id":123,"Name":"测试商品","Reviews":[{"Id":2000,"Title":"测试评论1"},{"Id":2002,"Title":"测试评论2"}]},"version":"v1"}
- 到此测试项目环境成功部署.
3. Istio 核心功能演示
istio 中的资源大部分为抽象资源, 通过配置的整合转换成envoy的配置后通过 XDS 同步到不同的 envoy .
3.1 流量管理
- 由于很多功能的掩饰都是都是联动几个资源一起实现的, 下面就按功能来描述, 当遇到新的资源才进行解析;
3.1.1 路由请求
VirtualService
官方文档: istio.io/latest/docs…
-
VirtualService 虚拟服务(简称 VS), 它类似于k8s中的service 作为pod一个代理前端. 主要功能是做路由控制、超时控制、流量分发和网络的安全认证;
-
下面创建一个仅网格内的 VirtualService 作用于 prodsvc , 当匹配到
/p
重写成/prods
并路由到 prodsvc 的 80 端口apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: prodvs namespace: myistio spec: hosts: - prodsvc http: # 测试 curl prodsvc/p/123 - match: - uri: prefix: "/p" rewrite: uri: "/prods" route: - destination: # 真实的k8s服务 host: prodsvc port: number: 80
-
在网格内建立一个测试的容器
mytest
, 后续的网格测试功能都会使用该容器.apiVersion: apps/v1 kind: Deployment metadata: name: mytest namespace: myistio spec: selector: matchLabels: app: mytest replicas: 1 template: metadata: labels: app: mytest spec: containers: - name: mytest image: alpine:3.12 imagePullPolicy: IfNotPresent command: ["sh","-c","echo this is alpine && sleep 864000"]
-
测试请求是否被重写了;
$ kubectl get pods -n myistio NAME READY STATUS RESTARTS AGE mytest-7cdd69858f-gjrv5 2/2 Running 0 7s prodapi-689cfc5846-lwzvk 2/2 Running 0 3d13h reviewapi-58f5887cb9-grmwm 2/2 Running 0 3d13h # 为 mytest 安装下 curl $ kubectl exec mytest-7cdd69858f-gjrv5 -n myistio -- apk add curl # 测试 http://prodsvc/p/123 生效了 $ kubectl exec mytest-7cdd69858f-gjrv5 -n myistio -- curl http://prodsvc/p/123 2> /dev/null {"header":{"Accept":["*/*"],"User-Agent":["curl/7.79.1"],"X-B3-Parentspanid":["b44ab978924544cf"],"X-B3-Sampled":["1"],"X-B3-Spanid":["2600028a4e749d49"],"X-B3-Traceid":["39d6663ed42e8c83b44ab978924544cf"],"X-Envoy-Attempt-Count":["1"],"X-Envoy-Original-Path":["/p/123"],"X-Forwarded-Client-Cert":["By=spiffe://cluster.local/ns/myistio/sa/default;Hash=758808084a6903026f292c5c11ce4f16084dd814263526e634952a9da9fb1c11;Subject=\"\";URI=spiffe://cluster.local/ns/myistio/sa/default"],"X-Forwarded-Proto":["http"],"X-Request-Id":["17d61db9-6d07-95c7-93b2-d6acf5b27b5c"]},"result":{"Id":123,"Name":"测试商品","Reviews":[{"Id":2000,"Title":"测试评论1"},{"Id":2002,"Title":"测试评论2"}]},"version":"v1"}
3.1.2 请求超时设置
路由参数: istio.io/latest/docs…
-
请求超时 timeout, 默认是不超时
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: prodvs namespace: myistio spec: hosts: - prodsvc http: # 测试 curl prodsvc/p/123 - match: - uri: prefix: "/p" rewrite: uri: "/prods" # 设置请求超时 timeout: 3s route: - destination: # 真实的k8s服务 host: prodsvc port: number: 80
-
测试 curl http://prodsvc/p/123?timeout=yes, 当加上timeout 这个超时key的时候就 prodapi 会sleep 5 秒, 由于我们设置超时3秒所以最终返回了上游请求超时, upstream 这是个 envoy 中的概念后续有机会再聊.
$ kubectl exec mytest-7cdd69858f-gjrv5 -n myistio -- curl http://prodsvc/p/123?timeout=yes 2> /dev/null upstream request timeout
3.1.3 请求重试
重试参数: istio.io/latest/docs…
-
istio 中提供了多种条件下的HTTP重试策略, 这些策略都来自于 envoy 本身的支持 链接
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: prodvs namespace: myistio spec: hosts: - prodsvc http: # 测试 curl prodsvc/p/123 - match: - uri: prefix: "/p" rewrite: uri: "/prods" retries: # 重试次数 attempts: 1 # 重试超时 perTryTimeout: 6s # 重试策略 retryOn: connect-failure,refused-stream,503,reset,retriable-4xx route: - destination: # 真实的k8s服务 host: prodsvc port: number: 80
-
测试效果, 这次我们访问 curl http://prodsvc/p/123?err=1, 会停顿5秒后后返回 503 错误; 下面结果可以看到由于程序会停顿5秒, 于是我们重试超时设置6秒以避免主动断开, 所以最终我们能看到使用了10秒时间, 并且正确的返回503错误;
$ time kubectl exec mytest-7cdd69858f-gjrv5 -n myistio -- curl http://prodsvc/p/123?err=1 2> /dev/null {"result":"503 must error","version":"v2"} real 0m10.206s user 0m0.071s sys 0m0.028s
3.1.4 故障注入
故障注入参数: istio.io/latest/docs…
- 这次我们通过故障注入加入请求延时, 留意yaml中的注释;
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: prodvs namespace: myistio spec: hosts: - prodsvc http: # 测试 curl prodsvc/p/123 - match: - uri: prefix: "/p" rewrite: uri: "/prods" # 故障注入 fault: # 延迟设置 delay: # 注入占比 percent: 100 # 固定延迟 3 秒 fixedDelay: 3s route: - destination: # 真实的k8s服务 host: prodsvc port: number: 80
- 测试延迟效果, 最终可以看到延迟了 3秒
$ time kubectl exec mytest-7cdd69858f-gjrv5 -n myistio -- curl http://prodsvc/p/123 2> /dev/null {"header":{"Accept":["*/*"],"User-Agent":["curl/7.79.1"],"X-B3-Parentspanid":["c3dc4abd2a2fa71e"],"X-B3-Sampled":["1"],"X-B3-Spanid":["fab44183ed4a3862"],"X-B3-Traceid":["52b1b57dfed5bcd7c3dc4abd2a2fa71e"],"X-Envoy-Attempt-Count":["1"],"X-Envoy-Original-Path":["/p/123"],"X-Forwarded-Client-Cert":["By=spiffe://cluster.local/ns/myistio/sa/default;Hash=758808084a6903026f292c5c11ce4f16084dd814263526e634952a9da9fb1c11;Subject=\"\";URI=spiffe://cluster.local/ns/myistio/sa/default"],"X-Forwarded-Proto":["http"],"X-Request-Id":["6d4cd684-f4ee-9b83-b919-75143f913283"]},"result":{"Id":123,"Name":"测试商品","Reviews":[{"Id":2000,"Title":"测试评论1"},{"Id":2002,"Title":"测试评论2"}]},"version":"v2"} real 0m3.235s user 0m0.068s sys 0m0.028s
3.1.5 流量调度 (流量转移)
参考:
-
istio 提供几种匹配方式 HTTPRequestMatch, L4MatchAttributes, TLSMatchAttributes 且支持精确匹配, 前缀匹配和正则匹配;
HTTPRequestMatch
: 提供了可匹配的属性 uri, method, schema, headers, port, gateways, queryParams 等;L4MatchAttributes
: 提供了可匹配的属性 destinationSubnets(子网段/IP), port, gateways 等;TLSMatchAttributes
: 提供了可匹配的属性 destinationSubnets(子网段/IP), port, gateways, sni(DomainName) 等;
-
为了模拟流量调度的过程, 我们先需要多构建一个 prodapi 的v2版本来演示多版本下的流量调度;
apiVersion: apps/v1 kind: Deployment metadata: name: prodapiv2 namespace: myistio spec: selector: matchLabels: app: prod version: v2 replicas: 1 template: metadata: labels: app: prod version: v2 spec: nodeName: k8s-02 containers: - name: prodv2 image: alpine:3.12 imagePullPolicy: IfNotPresent command: ["/app/prodsv2"] env: - name: "release" value: "1" volumeMounts: - name: appdata mountPath: /app ports: - containerPort: 8080 volumes: - name: appdata hostPath: path: /opt/istio/study/code_app
3.1.5.1 简易多版本访问
DestinationRule (DR)
官方文档: istio.io/latest/docs…
-
要设置多版本就要引入一个新的虚拟资源 DestinationRule, 通过这个目标规则可以建立不同的版本, 所谓的版本其实也就是个名字更重要的是细化了目标策略, 让访问目标有更多的可能性.
- 关于 DR 的 Namespace 作用:
相同 Namespace
: VS (VirtualService) 和 DR (DestinationRule) 同一 Namespace 时, 来自其他 Namespace 到 本Namespace 的 VS流量都被会被DR作用;不同 Namespace
: VS 和 DR 不在同一 Namespace 下, DR 对 VS 的流量只作用在 DR 的Namespace下, 而其他没有设置 DR 的 Namespace 访问 VS 时不会受到DR的作用;
- 关于 DR 的 Namespace 作用:
-
先配置 (DR), 产生 v1 和 v2 版本, 留意下yaml的一些解析;
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: prod-rule namespace: myistio spec: host: prodsvc subsets: - name: v2 # 该label 匹配的是 k8s service 对应的 endpoints labels: version: v2 - name: v1 # 该label 匹配的是 k8s service 对应的 endpoints labels: version: v1
-
配置 VS 按权重访问不同的版本
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: prodvs namespace: myistio spec: hosts: - prodsvc http: # 测试 curl prodsvc/p/123 - match: - uri: prefix: "/p" rewrite: uri: "/prods" timeout: 3s route: - destination: host: prodsvc subset: v1 port: number: 80 # 按 50% 进行访问 weight: 50 - destination: host: prodsvc subset: v2 port: number: 80 weight: 50
-
测试效果, 看看是否是v1 和 v2 进行了 轮询问, 留意结果的 version 字段;
$ kubectl exec mytest-7cdd69858f-gjrv5 -n myistio -- curl http://prodsvc/p/123 2> /dev/null
3.1.5.2 匹配Header进行多版本访问
-
这次我们通过header 去构造出多版本控制的例子, 这种更适合我们生产上使用; 下面的配置当请求的 header 设置为 version: v1 时则访问 v1 版本的 prodapi; 如果什么header 都不设置则随机访问; DestinationRule 用回上一小节的设置即可;
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: prodvs namespace: myistio spec: hosts: - prodsvc http: # v1 - match: - headers: version: exact: v1 uri: prefix: "/p" rewrite: uri: "/prods" route: - destination: host: prodsvc subset: v1 port: number: 80 # v2 - match: - headers: version: exact: v2 uri: prefix: "/p" rewrite: uri: "/prods" route: - destination: host: prodsvc subset: v2 port: number: 80 # 默认路由 - match: - uri: prefix: "/p" rewrite: uri: "/prods" route: - destination: host: prodsvc port: number: 80
-
指定访问 version: v1
$ kubectl exec mytest-7cdd69858f-gjrv5 -n myistio -- curl -H "version: v1" http://prodsvc/p/123 2> /dev/null {"header":{"Accept":["*/*"],"User-Agent":["curl/7.79.1"],"Version":["v1"],"X-B3-Parentspanid":["c0c2abd89f886251"],"X-B3-Sampled":["1"],"X-B3-Spanid":["f71b52c823c86de4"],"X-B3-Traceid":["46d67770cd4ea275c0c2abd89f886251"],"X-Envoy-Attempt-Count":["1"],"X-Envoy-Original-Path":["/p/123"],"X-Forwarded-Client-Cert":["By=spiffe://cluster.local/ns/myistio/sa/default;Hash=758808084a6903026f292c5c11ce4f16084dd814263526e634952a9da9fb1c11;Subject=\"\";URI=spiffe://cluster.local/ns/myistio/sa/default"],"X-Forwarded-Proto":["http"],"X-Request-Id":["7503b4c0-dd8e-9838-8566-f9e52dee9978"]},"result":{"Id":123,"Name":"测试商品","Reviews":[{"Id":2000,"Title":"测试评论1"},{"Id":2002,"Title":"测试评论2"}]},"version":"v1"}
-
指定访问 version: v2
$ kubectl exec mytest-7cdd69858f-gjrv5 -n myistio -- curl -H "version: v2" http://prodsvc/p/123 2> /dev/null {"header":{"Accept":["*/*"],"User-Agent":["curl/7.79.1"],"Version":["v2"],"X-B3-Parentspanid":["1a46d3e384b3199b"],"X-B3-Sampled":["1"],"X-B3-Spanid":["0edcf942a00b82e2"],"X-B3-Traceid":["8fcd35fe6f10414f1a46d3e384b3199b"],"X-Envoy-Attempt-Count":["1"],"X-Envoy-Original-Path":["/p/123"],"X-Forwarded-Client-Cert":["By=spiffe://cluster.local/ns/myistio/sa/default;Hash=758808084a6903026f292c5c11ce4f16084dd814263526e634952a9da9fb1c11;Subject=\"\";URI=spiffe://cluster.local/ns/myistio/sa/default"],"X-Forwarded-Proto":["http"],"X-Request-Id":["f8baaa86-2e27-90f8-b061-7f14195a30c9"]},"result":{"Id":123,"Name":"测试商品","Reviews":[{"Id":2000,"Title":"测试评论1"},{"Id":2002,"Title":"测试评论2"}]},"version":"v2"}
3.1.6 跨域配置
官方文档: istio.io/latest/zh/d…
-
跨域本身是浏览器的一种安全设置, 如果需要测试跨域问题需要配合浏览器或Postman发起; 但像下面这种配置其实是不完整的配置, 因为一旦遇到服务报500错误的时候返回的头部中就会丢失了
access-control-allow-origin
从而导致了浏览器认为是跨域问题. 后面演示到 EnvoyFilter 资源时可以通过该操作进行头部补偿解决相应头部丢失的问题.;apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: prods.jtthink.com namespace: myistio spec: hosts: - prods.jtthink.com gateways: - prods-gateway http: - match: - uri: prefix: "/pd" rewrite: uri: "/prods" timeout: 3s route: - destination: # 真实的k8s服务 host: prodsvc port: number: 80 - route: - destination: host: prodsvc port: number: 80 corsPolicy: allowOrigins: - exact: "*" allowMethods: - GET - POST - PATCH - PUT - DELETE - OPTIONS allowCredentials: true allowHeaders: - authorization maxAge: "24h"
-
测试结果, 可以看到返回的头部中带了 access-control-allow-origin.
3.1.7 熔断
官方文档: istio.io/latest/zh/d…
-
这里的熔断和一些微服务框架提供的熔断意思还有点不一样, 微服务框架当熔断发生的时候可以提供一个服务降级的办法, 而 Istio 对代码是无侵入的所以当发生熔断时只会将不可用的服务进行离群操作.
-
Istio 的熔断机制, 当服务不可用时离群时间时按
基础时间 * 离群次数
, 且被离群后不能主动加入; 所以为了保障整体服务的可用性, 可以设置最大的离群百分比来限制; -
下面演示为了方便测试触发熔断机制, 我们可以给下游设置一个连接池 connectionPool;
-
创建 prodvs
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: prodvs namespace: myistio spec: hosts: - prodsvc http: - match: - uri: prefix: "/p" rewrite: uri: "/prods" timeout: 60s route: - destination: # 设置了一个 DR 规则 subset: circuit-break # 匹配该host host: prodsvc port: number: 80
-
创建 prodvs 的 DR 规则.
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: prod-rule namespace: myistio spec: host: prodsvc trafficPolicy: # 使用链接池 connectionPool: tcp: # 最大连接数 maxConnections: 1 # 熔断: 离群检测 outlierDetection: # 连续发生500 1次 consecutive5xxErrors: 1 # 检测间隔, 检查上次被隔离的POD interval: 10s # 熔断最大驱逐比, 因为我们现在有v1 和 v2 版本所以最糟糕的情况会驱逐所有Pod, 正常来说不应该配置成驱逐所有Pod maxEjectionPercent: 60 # 基本的驱逐时间,系统会因为故障次数而递增驱逐时间; baseEjectionTime: 10s subsets: - name: circuit-break labels: #version: v1 app: prod
-
通过 http://prodsvc/p/123?err=1 程序等待5秒后才返回500错误. 我们有两台服务由于都会返回错误, 且驱逐比超过 50%, 所以再次发起请求的时候会发现所有的服务都被驱逐了 (看到envoy错误: no helalthy upstream);
$ time kubectl exec mytest-7cdd69858f-gjrv5 -n myistio -- curl http://prodsvc/p/123?err=1 2> /dev/null no healthy upstream real 0m10.303s user 0m0.076s sys 0m0.040s
-
通过 istioctl 工具我们可以看到没有可用的endpoint, 过了隔离时间后又全部恢复了.
$ istioctl pc ep mytest-7cdd69858f-gjrv5.myistio |grep prodsvc 10.244.0.13:8080 HEALTHY FAILED outbound|80|circuit-break|prodsvc.myistio.svc.cluster.local 10.244.0.13:8080 HEALTHY OK outbound|80||prodsvc.myistio.svc.cluster.local 10.244.0.14:8080 HEALTHY FAILED outbound|80|circuit-break|prodsvc.myistio.svc.cluster.local 10.244.0.14:8080 HEALTHY OK outbound|80||prodsvc.myistio.svc.cluster.local
3.1.8 负载均衡
3.1.8.1 LoadBalancer
-
负载均衡有两种模式, 一种是标准的简单模式模式, 另一种是会话保持.
- 标准模式:
RANDOM
: 随机访问ROUND_ROBIN
: 轮询访问LEAST_REQUEST
: 端点最少请求
- 会话保持:
httpHeaderName
: 通过 HTTP Header 的标识key 对应的value值 获取hash;httpQueryParameterName
: 通过请求参数来分配请求节点useSourceIp
: 通过源 IP 获取hashminimumRingSize
: 用于哈希环的最小虚拟节点数。默认值为1024
- 标准模式:
-
由于配置不复杂这里就不一一测试, 只列出两种模式的示例, 需要配合 VirtualService 使用;
-
标准模式
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: prod-rule namespace: myistio spec: host: prodsvc # 负载均衡 trafficPolicy: loadBalancer: simple: ROUND_ROBIN subsets: - name: lb-simple labels: # 限制流量访问到 v2 版本的 pod # version: v2 app: prod
-
会话保持
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: prod-rule namespace: myistio spec: host: prodsvc # 负载均衡 trafficPolicy: loadBalancer: # 一致性hash, 通过http header key: myname consistentHash: httpHeaderName: myname subsets: - name: lb-consistent-hash labels: app: prod
-
3.1.8.2 LocalityLoadBalancer
官方文档: istio.io/latest/zh/d…
- 地域(地区)负载均衡是通过k8s的region标签和zone 标签来实现, 主要匹配地域标签
topology.kubernetes.io/region
(failure-domain.beta.kubernetes.io/region
) 和 地区标签topology.kubernetes.io/zone
(failure-domain.beta.kubernetes.io/zone
) 来做流量分发和地域间故障转移, 更多的标签使用请查看官方文档;
3.1.9 Ingress网关配置
官方文档: istio.io/latest/docs…
-
Gateway 通常用于南北向流量为主. Istio 中有两种 Gateway 分别是 Ingress 和 Egress, 大部分时候我们会更多的关注 Ingress Gateway 的路由匹配和安全等机制. Egress Gateway 用于管理出口的安全策略和DNS解析为主;
-
创建一个 myistio 命名空间的 Ingress Gateway (虚拟资源), 这里可以理解成 Nginx 的虚拟主机 (VirtualHost);
apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: prods-gateway namespace: myistio spec: selector: # 该 prod-gateway 资源作用在 默认的istio: ingressgateway; # 该标签在默认情况下是选择所有空间的deployment # 除非你用 PILOT_SCOPE_GATEWAY_TO_NAMESPACE 进行空间限制 istio: ingressgateway servers: - port: # port 的设置会影响envoy 的监听端口; number: 80 name: http protocol: HTTP hosts: # 需要配置本地host 才能进行访问 - prods.jtthink.com
-
查看 istio-ingressgateway service, 在istio 中当 k8s service 作用在 Gateway 的时候需要明确指出使用哪种传输协议 (可选的协议). 如果没有设定 istio 会自动检测是 http 还是 http2 协议, 若以上两种都不是则默认以TCP 协议来处理; 有以下两种指定的方法:
- k8s service 指定 port的name 字段: {protocol}-suffix, 如: http-web;
- k8s 1.18+ 版本之后指定 port 的 appProtocol 字段: http
$ kubectl get svc -n istio-system istio-ingressgateway -o yaml ... # 在 demo 的默认模版中并没有配置, 所以会自动选择使用的协议 - name: http2 nodePort: 30742 port: 80 protocol: TCP targetPort: 8080 ...
-
上面两段配置的描述: 当 客户端访问 prods.jtthink.com:30742 流量经由对外的 30742 端口进入 istio-ingressgateway service proxy 的 80 端口, 再次转发至 istio-ingressgateway pod 8080 端, 到此为止流量已经进入了 ingressgateway 但后端还没有设置接收流量的 VirtualService.
-
配置对应 prods.jtthink.com 的 VirtualService 作为接受请求的路由. 其中配置了 gateways 字段表面来自哪里的流量, 在 istio 中gateways 字段中还保留了一个名为 mesh 的关键字表达了来自服务网格内的流量.
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: prods.jtthink.com namespace: myistio spec: hosts: - prods.jtthink.com gateways: - prods-gateway http: - match: - uri: prefix: "/pd" rewrite: uri: "/prods" timeout: 3s route: - destination: # 真实的k8s服务 host: prodsvc port: number: 80 # 配置一条默认路由, 以免匹配不了返回404 - route: - destination: host: prodsvc port: number: 80
-
进行测试 curl prods.jtthink.com:30742/pd/123 或 curl prods.jtthink.com:30742/prods/123
$ curl http://prods.jtthink.com:30742/pd/123 {"header":{"Accept":["*/*"],"User-Agent":["curl/7.64.1"],"X-B3-Parentspanid":["adaaaf9a45e58393"],"X-B3-Sampled":["1"],"X-B3-Spanid":["7ee9a8fb5c8ecc1a"],"X-B3-Traceid":["292215b95efc1aa5adaaaf9a45e58393"],"X-Envoy-Attempt-Count":["1"],"X-Envoy-Expected-Rq-Timeout-Ms":["3000"],"X-Envoy-Internal":["true"],"X-Envoy-Original-Path":["/p/123"],"X-Forwarded-Client-Cert":["By=spiffe://cluster.local/ns/myistio/sa/default;Hash=caedf57ee251facc46f74b3b4142afaff5ba842c4087a3957ac4000dffddf451;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"],"X-Forwarded-For":["10.244.0.1"],"X-Forwarded-Proto":["http"],"X-Request-Id":["c9379704-ee9d-961d-89c3-987e79097d3c"]},"result":{"Id":123,"Name":"测试商品","Reviews":[{"Id":2000,"Title":"测试评论1"},{"Id":2002,"Title":"测试评论2"}]},"version":"v1"}
3.1.10 Egress网关配置
-
用于统一网格内的出口请求, 用于管理和控制访问外部服务的目标地址
-
首先我们类似 ingress-gateway 一样创建一个 egress-gateway. 样例中我们用访问 baidu.com 经过 egress 出.
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: istio-egressgateway namespace: myistio spec: selector: istio: egressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - www.baidu.com - baidu.com
-
配置
baidu.com
和www.baidu.com
VirtualService 第一条http路由匹配来自网格(mesh)内访问 baidu.com 的时候转向至 istio-egressgateway.istio-system.svc.cluster.local , 第二条http路由匹配来自 istio-egressgateway 网关转向至 www.baidu.com:80 .apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: test-egress-gateway namespace: myistio spec: hosts: - www.baidu.com - baidu.com gateways: - istio-egressgateway - mesh http: - match: - gateways: - mesh port: 80 route: - destination: host: istio-egressgateway.istio-system.svc.cluster.local port: number: 80 weight: 100 - match: - gateways: - istio-egressgateway port: 80 route: - destination: host: www.baidu.com port: number: 80 weight: 100
-
我们可以测试下是否配置成功了, 需要查看 egress-gateway 的日志看看是否有请求baidu.com的日志因为它是最终的出口.
- 终端 1
# 观察日志输出 $ kubectl logs -f istio-egressgateway-54bc6cbb4-xzml5 -n istio-system ... [2022-11-12T05:29:49.726Z] "GET / HTTP/2" 200 - via_upstream - "-" 0 2381 23 22 "10.244.0.10" "curl/7.79.1" "a0c3cb02-4361-9ed9-825d-1e11bcfc0dbf" "baidu.com" "110.242.68.4:80" outbound|80||www.baidu.com 10.244.0.4:46680 10.244.0.4:8080 10.244.0.10:41314 - -
- 终端 2
$ kubectl exec mytest-7cdd69858f-gjrv5 -n myistio -- curl http://baidu.com 2> /dev/null <!DOCTYPE html> <!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=alwa...href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a> <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号 <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
- 终端 1
3.2 安全相关
3.2.1 JWT 的鉴权与授权
- JWT 作为很多web服务的一种统一认证的手段很适合作用在网关上作为南北向流量的入口认证. 什么是 JWT ?
3.2.1.1 统一鉴权 (RequestAuthentication)
-
由于我这里用
RS256
算法生成jwk, 所以首先需要生成一个非对称密钥$ openssl genrsa -out mypri.pem 2048 $ openssl rsa -in mypri.pem -pubout -out mypub.pem
-
在本文提供的测试项目中找到
generate_jwk.go
和上面两个密钥文件同目录下执行这个生成对应的 jwk (secret key) 用于验证 jwt;- 一些 jwk 字段描述
- alg:具体的算法 HS256, RS256 等;
- kty: 秘钥类型
- use: 可选,sig或enc(签名还是加密)
- kid: key的唯一标识
- e: 秘钥的模
- n: 秘钥指数
- iss: 发行人
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- iat: jwt的签发时间
# 执行生成 jwk $ go run generate_jwk.go {"e":"AQAB","kty":"RSA","n":"vFxzRdVZ9YnxZDiMokYdUJjaAEUpHIGQ7sr2gnAfzg1y7VAe5AFwzktjJvI1AXXXHkQM2ZLPDI5vu-nc6BM7svQTE89ADBvY795XUrGzPmToaQL4UMLNEIJ8354cHMSDIsefs8sY-QR-I15Mr3-3rxvbbQTz56wEmeR2VljRAbzopcaOPSlMugd05PeJOaSe1diqVBw0PrlaStQhqs0sx9WmTW4afs_AM8kbW6Fd6cRXwLp2WBmzXbAnO2XU-LkYcHGaqq1cP4OSd0lA0f1b13zz4uUSbVftKhymWeAAm97DIJ8QELLmdmGsMXaCHhD6e07rs29kNlOkd1qsA5uvzw"}
- 一些 jwk 字段描述
-
我们可以这里采用静态配置 jwk 用以验证 jwt, 其他参数参考 jwt规则. 这里需要连同授权策略一起配置才可生效.
apiVersion: security.istio.io/v1beta1 kind: RequestAuthentication metadata: name: jwt-test namespace: istio-system spec: selector: matchLabels: # 作用在默认的入口网关 istio: ingressgateway jwtRules: # issuer 签发者 - issuer: "user@shadow.com" # 转发原始 token forwardOriginalToken: true # 把payload 提取到 userinfo 这个 header 中 outputPayloadToHeader: "Userinfo" jwks: | { "keys": [ { "e":"AQAB", "kty":"RSA", "n":"vFxzRdVZ9YnxZDiMokYdUJjaAEUpHIGQ7sr2gnAfzg1y7VAe5AFwzktjJvI1AXXXHkQM2ZLPDI5vu-nc6BM7svQTE89ADBvY795XUrGzPmToaQL4UMLNEIJ8354cHMSDIsefs8sY-QR-I15Mr3-3rxvbbQTz56wEmeR2VljRAbzopcaOPSlMugd05PeJOaSe1diqVBw0PrlaStQhqs0sx9WmTW4afs_AM8kbW6Fd6cRXwLp2WBmzXbAnO2XU-LkYcHGaqq1cP4OSd0lA0f1b13zz4uUSbVftKhymWeAAm97DIJ8QELLmdmGsMXaCHhD6e07rs29kNlOkd1qsA5uvzw" } ] } --- # 若不配置授权策略, 则表示允许所有流量都可以通过, 下面只允许带有jwt信息的流量通过; apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: prod-authpolicy namespace: istio-system spec: selector: matchLabels: istio: ingressgateway action: ALLOW rules: - from: - source: # JWT 的PayLoad信息 requestPrincipals: ["*"]
-
接下来需要生成jwt, jwt 的生成可以去 jwt.io 进行, 我们需要选择 RS256 算法并且需要复制
mypri.pem
和mypub.pem
文件, 还有要至少新增发行者字段"iss": "user@shadow.com"
. -
进行访问测试, 不带验证信息和带验证信息:
# 不带验证信息的时候认证不通过 $ curl http://prods.jtthink.com:30742/pd/123 RBAC: access denied # 带上 jwt header 后认证通过 $ curl --location --request GET 'http://prods.jtthink.com:30742/pd/123' --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJpc3MiOiJ1c2VyQHNoYWRvdy5jb20ifQ.jAzdJayaYRfHeNVLK-qqUDnDxufc4BzGKomM2ianPPxmP2WmY_36xCBGzX-XjUYCbw188xE8wl7ZIezPG7zmERXG_HliWUtOaVFB8G83kEZQ00TBNQ9qtS0xUTlzebMA0XIJCDhi5ipIwnP1-MzycWhOdZk2GW1dnTT56oiImuSyxTmDgs8-UfOJmGAx_fnTDPNCITxUu--st5m8_SmuHvQBKf6qM4uN2u8ptiF7MEsCerc8Nknq977JbELBDwKGGIuxZ9Y-B35k8kVKncV9V7gyAp4hTQWXl8nDM9PwBYK28rqDExgI3JHofwZZawmXVginLo7rt-sVJUl0xYy1gg' {"header":{"Accept":["*/*"],"Authorization":["Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJpc3MiOiJ1c2VyQHNoYWRvdy5jb20ifQ.jAzdJayaYRfHeNVLK-qqUDnDxufc4BzGKomM2ianPPxmP2WmY_36xCBGzX-XjUYCbw188xE8wl7ZIezPG7zmERXG_HliWUtOaVFB8G83kEZQ00TBNQ9qtS0xUTlzebMA0XIJCDhi5ipIwnP1-MzycWhOdZk2GW1dnTT56oiImuSyxTmDgs8-UfOJmGAx_fnTDPNCITxUu--st5m8_SmuHvQBKf6qM4uN2u8ptiF7MEsCerc8Nknq977JbELBDwKGGIuxZ9Y-B35k8kVKncV9V7gyAp4hTQWXl8nDM9PwBYK28rqDExgI3JHofwZZawmXVginLo7rt-sVJUl0xYy1gg"],"User-Agent":["curl/7.64.1"],"Userinfo":["eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJpc3MiOiJ1c2VyQHNoYWRvdy5jb20ifQ"],"X-B3-Parentspanid":["f7485bac2ee5fc75"],"X-B3-Sampled":["1"],"X-B3-Spanid":["20a5604799186d19"],"X-B3-Traceid":["685d10fe3b7efc43f7485bac2ee5fc75"],"X-Envoy-Attempt-Count":["1"],"X-Envoy-Expected-Rq-Timeout-Ms":["3000"],"X-Envoy-Internal":["true"],"X-Envoy-Original-Path":["/pd/123"],"X-Forwarded-Client-Cert":["By=spiffe://cluster.local/ns/myistio/sa/default;Hash=7f87620d1c8e5d000869a2a878ae51311fe6d62876f9c69c88beb3ca6e303c9c;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account"],"X-Forwarded-For":["10.244.0.1"],"X-Forwarded-Proto":["http"],"X-Request-Id":["ba601bf2-c695-9fc1-acc5-548f13ebf4ed"]},"result":{"Id":123,"Name":"测试商品","Reviews":[{"Id":2000,"Title":"测试评论1"},{"Id":2002,"Title":"测试评论2"}]},"version":"v2"}
-
若上面配置后未生效如何排错呢? 因为istio 只是控制面实际生效的配置是作用在envoy, 所以需要查看envoy 配置, 可以跟着下面的操作导出envoy配置, 然后搜索
jwks
3.2.1.2 授权策略 (AuthorizationProlicy)
-
Isitio 中的授权策略可以类比为网络安全策略中的 ACL (Access Control List). istio 的授权策略主从身份信息、http路径和http请求方法等维度进行授权, 以下是3个授权策略的关键字段;
from
: 指定请求的源。如果未设置,则允许任何源。to
: 指定请求的操作。如果未设置,则允许任何操作。when
: 指定请求的附加条件列表时。如果未设置,则允许任何条件。
-
示例 一, 允许所有的带JWT请求身份信息的请求通过, 下面省略了to的配置所以允许去往任何地方和任何操作. 只要被我们之前的jwt 验证通过.
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: prod-authpolicy namespace: istio-system spec: selector: matchLabels: # 策略作用在改label 对应的网关上 istio: ingressgateway action: ALLOW rules: - from: - source: # JWT 的PayLoad信息 requestPrincipals: ["*"]
-
示例 二, 允许所有的带JWT请求身份信息的请求通过, 但只能去往主机地址为 prods.jtthink.com:30742 并且请求方法只允许 GET 和 POST.
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: prod-authpolicy namespace: istio-system spec: selector: matchLabels: # 策略作用在改label 对应的网关上 istio: ingressgateway action: ALLOW rules: - from: - source: requestPrincipals: ["*"] to: - operation: hosts: ["*.jtthink.com:30742"] methods: ["GET","POST"]
-
示例 三, 在这个示例中我们加入 when 请求的附加条件进行 source 的判断, 并且把 Allow 和 Deny 分至两个规则内, 更符合安全规则管理的模式;
-
Allow 规则有两条:
- 允许所有的带JWT请求身份信息的请求进入, 且是 GET 方法去往 /prods/*
- 允许所有的带JWT请求身份信息的请求进入, 但只允许JWT身份有 role 为admin 或 super’admin 可以使用 GET 和 POST 方法去往 /admin.
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: prod-authpolicy namespace: istio-system spec: action: ALLOW selector: matchLabels: istio: ingressgateway rules: # 规则1:允许所有通过验证的请求身份的访问 /prods/* - from: - source: requestPrincipals: ["*"] to: - operation: methods: ["GET"] paths: ["/prods/*"] # 规则2: :允许所有通过验证身份的请求, 并在身份声明中带有 role in [admin, supueradmin] 的进行 GET 和 POST 请求 - from: - source: requestPrincipals: ["*"] to: - operation: methods: ["GET","POST"] paths: ["/admin"] # 条件限制 when: - key: request.auth.claims[role] values: ["admin","superadmin"]
-
Deny 规则, 拒绝JWT 请求信息 role 非 supueradmin 的进行 POST 请求到 /admin.
apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: prod-authpolicy-deny namespace: istio-system spec: action: DENY selector: matchLabels: istio: ingressgateway rules: # 规则1: :拒绝 role not in [supueradmin] 的进行 POST 请求 # 上面的deny 规则相当于和上面的 ALLOW 策略遥相呼应 - to: - operation: methods: ["POST"] paths: ["/admin"] # 条件限制 when: - key: request.auth.claims[role] notValues: ["superadmin"]
-
示例三的测试
- 测试 GET /prods/* 请求
$ curl --location --request GET 'http://prods.jtthink.com:30742/prods/123' --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0IiwibmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJpc3MiOiJ1c2VyQHNoYWRvdy5jb20ifQ.i5OUuiFpOlGnNTc3Te13k5IVin68izCerfbe3TREbjpzqi5nGs8jaBk7oc96PkihIOiJHdTnp7cq5djYdunhPbd3gwD5vZQykz9SQv0CPLHsFD7RirFB3V3TQXVao-Ps0ik8fzRaqF-Vq6ws7W5SvJe_d5jOorK9b_2rizKzK9TJ9EMy4rIyiDITqt0kmkUvs4FVMDdYhmu2-vGY8zdEFFOHp6JtcTYDfLDwfANz5Uogmvr-DrweuZdw8V0gvUQisCG75iqmFm7pGHCX6yPRkRwowckUDO1DIkXc9rpaAGTIuMhXK8q29kium4BBuwUkgGO10RXP2UVeStuS7KMbLQ' {"result":{"Id":123,"Name":"测试商品","Reviews":[{"Id":2000,"Title":"测试评论1"},{"Id":2002,"Title":"测试评论2"}]},"version":"v1"}
- 测试
admin
角色 GET /admin 请求, 该请求的 payload 加入了 role: admin, 所以可以使用 GET /admin, 如果POST请求则会被拒绝$ curl --location --request GET 'http://prods.jtthink.com:30742/admin' --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0IiwibmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJpc3MiOiJ1c2VyQHNoYWRvdy5jb20ifQ.i5OUuiFpOlGnNTc3Te13k5IVin68izCerfbe3TREbjpzqi5nGs8jaBk7oc96PkihIOiJHdTnp7cq5djYdunhPbd3gwD5vZQykz9SQv0CPLHsFD7RirFB3V3TQXVao-Ps0ik8fzRaqF-Vq6ws7W5SvJe_d5jOorK9b_2rizKzK9TJ9EMy4rIyiDITqt0kmkUvs4FVMDdYhmu2-vGY8zdEFFOHp6JtcTYDfLDwfANz5Uogmvr-DrweuZdw8V0gvUQisCG75iqmFm7pGHCX6yPRkRwowckUDO1DIkXc9rpaAGTIuMhXK8q29kium4BBuwUkgGO10RXP2UVeStuS7KMbLQ' {"message":"这是管理员才能看的数据"}
- 测试
superadmin
角色 POST /admin 请求, 该请求的 payload 加入了 role: superadmin$ curl --location --request POST 'http://prods.jtthink.com:30742/admin' --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0IiwibmFtZSI6ImFkbWluIiwicm9sZSI6InN1cGVyYWRtaW4iLCJpYXQiOjE1MTYyMzkwMjIsImlzcyI6InVzZXJAc2hhZG93LmNvbSJ9.ZnhLkXtlQzyB1foqDEhf1Il03Fl1OocbSKlt8heMs2prYsaFJ7w1xgSYmDFwbRSiZo_V_OHU_bJsqKhtSCzsfTjq0K1ytO3Ro9QDceaz0AoFhzuyH9sH5SPbttgAkm1Zahw59fGvR2GMwnREDHjU1p-3avywDXdASBEF1RAhnTFPbR5jtyeO0xjkwVc2fIkFcYWFs5vkJzRtli2tWtuwXoJC6PQm6eng-JKFtk4TkJvA4qkt052ms-lMm_YOWxv9cywMUHlnMiUGktptcfHDL0Oqj1MsYwvgwzljHl_bZiNhJBVbWugj5MwaO02ew5S69hIoAbv3jglCF6Swsmxawg' {"message":"这是管理员才能看的数据"}
- 测试 GET /prods/* 请求
3.2.2 GRPC 的单向与双向认证
-
网上GRPC 的样例比较少, 这里直接用GRPC 作为例子;首先需要先部署测试的服务, 在这之前我们先装一个独立 grpc-ingressgateway 用作grpc请求与之前默认安装的istio-ingressgateway 区分开来;
-
部署独立的 grpc-ingressgateway.yaml
apiVersion: install.istio.io/v1alpha1 kind: IstioOperator spec: profile: empty components: ingressGateways: - enabled: true label: app: grpc-ingressgateway istio: grpc-ingressgateway k8s: resources: requests: cpu: 10m memory: 40Mi service: ports: - name: status-port port: 15021 targetPort: 15021 - name: http2 port: 80 targetPort: 8080 nodePort: 37043 - name: https port: 443 targetPort: 8443 nodePort: 37044 - name: tcp port: 31400 targetPort: 31400 - name: tls port: 15443 targetPort: 15443 name: grpc-ingressgateway
-
部署命令, 这里我没有直接install, 而是通过生成的k8s 资源 yaml 然后把service 的 type 改成 NodePort
$ istioctl manifest generate -f grpc-ingressgateway.yaml > innstall_gw.yaml # 修改kind service 完后安装gateway 实例. $ kubectl apply -f innstall_gw.yaml # 查看grpc-gateway $ kubectl get pods -n istio-system NAME READY STATUS RESTARTS AGE grpc-ingressgateway-575665d559-dwht8 1/1 Running 0 29h
-
部署测试 grpc 服务 gprodapi 和 gprodsvc
apiVersion: apps/v1 kind: Deployment metadata: name: gprodapi namespace: myistio spec: selector: matchLabels: app: gprod replicas: 1 template: metadata: labels: app: gprod version: v1 spec: # 调整部署节点 nodeName: k8s-02 containers: - name: gprodapi image: alpine:3.12 imagePullPolicy: IfNotPresent command: ["/app/gprods"] volumeMounts: - name: appdata mountPath: /app ports: - containerPort: 8080 volumes: - name: appdata # 调整挂载点 hostPath: path: /opt/istio/study/code_app --- apiVersion: v1 kind: Service metadata: name: gprodsvc namespace: myistio labels: app: gprod spec: type: ClusterIP ports: - port: 80 targetPort: 8080 protocol: TCP name: grpc selector: app: gprod
-
接着部署 gateway 和 virtualservice 让本地也能正常访问测试 grpc 服务
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: grpcvs namespace: myistio spec: hosts: - grpc.shadow.com gateways: - grpc-gateway http: - route: - destination: host: gprodsvc port: number: 80 --- apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: grpc-gateway namespace: myistio spec: selector: istio: grpc-ingressgateway # use Istio default gateway implementation servers: - port: number: 80 name: grpc protocol: GRPC hosts: - "grpc.shadow.com"
-
查看grpc-ingressgateway service 查询 NodePort, NodePort 为30743
$ kubectl-02 get svc -n istio-system |grep grpc grpc-ingressgateway NodePort 10.102.151.245 <none> 15021:32188/TCP,80:30743/TCP,443:30744/TCP,31400:32478/TCP,15443:31238/TCP 29h
-
测试grpc 是否正常访问, 通过app目录下的 testGrpc.go 进行连接访问, 运行前需要改一下
target
变量的访问地址, 改为gateway 对应的地址, 然后运行.$ go run testGrpc.go id:123 name:"测试商品"
3.2.2.1 单向认证
为了实验方便证书我也一并提供了在 certs 目录下
-
单向认证是什么这里就不解析了, 线上正常会使用购买的HTTPS 证书, 我们做实验就自己生成比较方便; 下面生成证书会使用
certstrap
工具来辅助我们生成, 当然如果玩openssl
很熟的同学也可以使用openssl
进行证书生成.# 下载工具 $ wget https://github.com/square/certstrap/releases/download/v1.2.0/certstrap-1.2.0-linux-amd64 # 生成根证书 $ ./certstrap-1.2.0-linux-amd64 init --common-name "ShadowCA" --expires "20 years" # 生成服务端key 及 csr 证书请求 $ ./certstrap-1.2.0-linux-amd64 request-cert -cn grpc.shadow.com -domain "*.shadow.com" # 对csr进行证书签发 $ ./certstrap-1.2.0-linux-amd64 sign grpc.shadow.com --CA "ShadowCA"
-
创建 k8s grpc-ingressgateway-certs
# 进入证书输出目录 $ cd out $ kubectl create -n istio-system secret tls grpc-ingressgateway-certs --key=grpc.shadow.com.key --cert=grpc.shadow.com.crt
-
创建单向认证的 IngressGateway
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: grpc-gateway namespace: myistio spec: selector: istio: grpc-ingressgateway servers: - port: number: 80 name: grpc # 这里需要改成https 协议 protocol: HTTPS tls: # 单向认证模式 mode: SIMPLE # 刚刚创建的证书及私钥 credentialName: grpc-ingressgateway-certs hosts: - "*.shadow.com"
-
我们需要通过 app 目录下的 testGrpc.go 进行验证, 在此之前需要打开单向认证的代码块, 然后注释无认证的代码块;
... //单向认证 creds, err := credentials.NewClientTLSFromFile(serverCert, serverName) if err != nil { log.Fatal(err) } client, err := grpc.DialContext(context.Background(), target, grpc.WithTransportCredentials(creds)) ...
$ go run testGrpc.go id:123 name:"测试商品"
3.2.2.2 双向认证
-
双向认证我们需要在可以单向认证的基础下多生成一个客户端证书, 双向认证原理就不解析网上有很多优秀的文章解析过这个问题了;
-
生成Client 证书 (不想生成的直接用我提供的即可)
$ ./certstrap request-cert -cn clientgrpc $ ./certstrap sign clientgrpc --CA "ShadowCA"
-
由于这次是双向认证, 所以需要添加授信的CA来做证书的验证;
# 先删除之前我们创建的 grpc-ingressgateway-certs $ kubectl delete secrets grpc-ingressgateway-certs -n istio-system # 创建 secrets $ cd out $ kubectl create -n istio-system secret generic grpc-ingressgateway-certs --from-file=cacert=ShadowCA.crt --from-file=key=grpc.shadow.com.key --from-file=cert=grpc.shadow.com.crt
-
创建 双向认证 的 Gateway
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: grpc-gateway namespace: myistio spec: selector: istio: grpc-ingressgateway # use Istio default gateway implementation servers: - port: number: 80 name: grpc protocol: HTTPS tls: # 开启双向认证 mode: MUTUAL credentialName: grpc-ingressgateway-certs hosts: - "*.shadow.com"
-
发起测试请求, 通过app目录下的 testGrpc.go 进行连接访问, 先开放双向认证代码块, 注释单向认证代码块;
... //双向认证 cert, err := tls.LoadX509KeyPair(clientCert, clientKey) if err != nil { log.Fatal(err) } certPool := x509.NewCertPool() ca, err := ioutil.ReadFile(caCert) if err != nil { log.Fatal(err) } certPool.AppendCertsFromPEM(ca) creds := credentials.NewTLS(&tls.Config{ Certificates: []tls.Certificate{cert}, ServerName: serverName, RootCAs: certPool, }) client, err := grpc.DialContext(context.Background(), target, grpc.WithTransportCredentials(creds)) ...
$ go run testGrpc.go id:123 name:"测试商品"
PS: HTTP 请求的单向认证和双向认证参考以上配置即可.
3.2.3 服务网格内的对等认证
-
Istio 中提供了基于TLS网格内的加密认证配置非常方便, 作用于命名空间下或某个工作负载. 可选的身份认证模式如下:
PERMISSIVE
: 明文传输和TLS双向认证;STRICT
: 连接是 mTLS 隧道(必须提供具有客户端证书的 TLS)。
-
myistio 空间下会被加密请求, 当网格外的服务想请求myistio 空间的服务就无法通过明文访问, 并且返回 Connection reset by peer 错误.
apiVersion: security.istio.io/v1beta1 kind: PeerAuthentication metadata: name: mtls-namespace-scope namespace: myistio spec: mtls: mode: STRICT
3.3 EnvoyFilter
-
Envoyfilter 的用处就是让用户可以通过该资源对
SIDECAR_INBOUND
、SIDECAR_OUTBOUND
和GATEWAY
的envoy 配置进行一些自定义的操作. 当然这些操作需要符合 Envoy 配置的规范, Enovy的配置基本分为BootstrapConfig
、ClustersConfig
、ListenersConfig
、RoutesConfig
和SecretsConfig
. -
Istio 通过 EnvoyFilter 的 ApplyTo 字段支持了不少的配置应用位置, 如: ListenersConfig 下的 HTTP_FILTER 和 RoutesConfig 下的 VIRTUAL_HOST 等; 笔者研究有限就暂时不在这展开了, 有兴趣的同学可以通过 Envoy官方文档 研究.
-
下面演示下如何通过 EnvoyFilter 资源进行配置的更改.
3.3.1 跨域配置修改
-
在 3.1.6 的小节中我们已经做了一次跨域为什么还要进行跨域配置, 在 Envoy 中其实有很多http 过滤器, 如 RBAC、JWT、故障注入 和 跨域 等过滤. 它们以一定的顺序依次被调用, 当某个过滤器返回错误时就会中断请求并且返回错误. 所以这就有可能因为没执行跨域过滤器在返回时丢失了主机头部;
-
配置比较繁琐请留意配置中注释解析
apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: insert-cors-before-jwt namespace: istio-system spec: # 指定在ingress gateway 生效 workloadSelector: labels: istio: ingressgateway configPatches: # 应用在 envoy 的 listener config 中filter.http_filters 字段 - applyTo: HTTP_FILTER # 匹配到jwt过滤器后,在其前面加入新的 跨域过滤器 match: # 作用范围是 GATEWAY 类型的envoy context: GATEWAY listener: filterChain: filter: # 在 http_connection_manager 下的子过滤器 name: "envoy.filters.network.http_connection_manager" subFilter: # 该过滤器是由于我们之前配置了 jwt 认证所以存在该过滤器 name: "envoy.filters.http.jwt_authn" patch: operation: INSERT_BEFORE value: name: "envoy.filters.http.cors" typed_config: "@type": "type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors"
-
测试, 我们可以先通过 postman 等api请求工具先进行一次请求, 看看如果遇到错误时是否返回头部还存在 access-control-allow-origin, 然后再应用 envoyfilter 配置.
-
带 JWT认证 请求, access-control-allow-origin 正常返回
-
不带 JWT认证 请求, access-control-allow-origin 未返回
-
我们先应用上面的 envoyfilter 配置然后进行请求, 可以看到同样不带 JWT认证 请求也能得到 access-control-allow-origin 的返回.
-
最后我们还可以看看 envoy 真实生效的配置是如何的. 在 ingress gateway 上下载envoy的配置并且找到
dynamic_listeners.listener.filter_chains.filters.http_filters
便能看到envoyfilter 中提及的 jwt_authn 和 cors.$ kubectl exec -it istio-ingressgateway-84f4959778-wnfgw -n istio-system -- curl http://127.0.0.1:15000/config_dump > envoy.cfg
3.3.2 添加HTTP响应头
所有HTTP的过滤器: 链接
HTTP LUA 过滤器及一些可以用的方法: 链接
-
这次我们通过 http lua 过滤器写入一小段 lua 代码获取HTTP响应头, 并加入自己定义的头部;
apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: resp-header-ef-1 namespace: istio-system spec: # 匹配的 label workloadSelector: labels: istio: ingressgateway configPatches: - applyTo: HTTP_FILTER match: context: GATEWAY proxy: proxyVersion: ^1\.15.* patch: # resp-header-ef-1.lua 把它插入到 http_filter 的最前面 operation: INSERT_BEFORE value: name: resp-header-ef-1.lua typed_config: # lua 过滤器 "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua" # 在 response header 中加入 my_name:ShadowYD inlineCode: | function envoy_on_response(response_handle) response_handle:headers():add("my_name", "ShadowYD") end
-
访问一下测试是否返回了响应头
my_name:ShadowYD
, 可以看到下图确实返回了对应的头; -
下面再次插入一个resp-header-ef-2.lua lua脚本在 resp-header-ef-1.lua 之前获取 resp-header-ef-1.lua 插入的响应头
my_name
后加入my_new_name:NEW_SHADOWYD
; 为什么明明获取 resp-header-ef-1.lua 的响应头, 却加在其前面这就有点类似于压栈的过程;# 请求进入 Request -> resp-header-ef-2.lua -> resp-header-ef-1.lua -> envoy.filters.http.router # 返回响应 envoy.filters.http.router -> resp-header-ef-1.lua -> resp-header-ef-2.lua -> Response
-
编写 resp-header-ef-2.lua 插入到 resp-header-ef-1.lua 前面
apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: resp-header-ef-1 namespace: istio-system spec: # 匹配的 label workloadSelector: labels: istio: ingressgateway configPatches: - applyTo: HTTP_FILTER # 匹配到 http_connection_manager 下的 resp-header-ef-1.lua, 然后插入到它的前面 match: # 工作上下文, 指生效的地方, 在 gateway 处生效 context: GATEWAY listener: filterChain: filter: name: "envoy.filters.network.http_connection_manager" subFilter: name: "resp-header-ef-1.lua" proxy: # 只有在 1.15.* 的版本 envoyfilter 才会允许 proxyVersion: ^1\.15.* patch: operation: INSERT_BEFORE # 获取到 my_name 的value, 在其前面加入 new_ 并转成大写赋值给 my_new_name value: name: resp-header-ef-2.lua typed_config: "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua" inlineCode: | function envoy_on_response(response_handle) local my_new_name = string.upper("new_"..response_handle:headers():get("my_name")) response_handle:headers():add("my_new_name", my_new_name) end
-
访问一下测试是否返回了响应头 my_new_name
3.3.2 添加HTTP请求头
- 添加请求头部的示例
apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: req-header-ef-1 namespace: istio-system spec: workloadSelector: labels: istio: ingressgateway configPatches: - applyTo: HTTP_FILTER match: context: GATEWAY listener: filterChain: filter: name: "envoy.filters.network.http_connection_manager" subFilter: name: "resp-header-ef-2.lua" proxy: proxyVersion: ^1\.15.* patch: operation: INSERT_BEFORE value: name: req-header-ef-1.lua typed_config: "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua" inlineCode: | function envoy_on_request(request) request:headers():add("user_id", "123") end
- 这个就不测试, app 的返回body中会把响应的头部打印出来.
3.3.3 拦截请求并提前返回
- 当在http请求头部加入 appid 则请求被拦截直接返回 400 错误和错误结果 error appid.
apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: ef-check-app-id namespace: istio-system spec: workloadSelector: labels: istio: ingressgateway configPatches: - applyTo: HTTP_FILTER match: context: GATEWAY patch: operation: INSERT_BEFORE value: name: ef-check-app-id.lua typed_config: "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua" inlineCode: | function envoy_on_request(request) local getid=request:headers():get("appid") if(getid~=nil) then request:respond( {[":status"] = "400"}, "error appid") end end
- 测试结果
3.4 外部服务注册
- ServiceEntry 服务入口机制, istio 允许把外部服务注册到网格内部进行访问以便进行访问控制.
3.4.1 动态
- 我们将baidu.com这个域名注册到网关内, 并且标注其为外部服务. 通常用于指示通过 API 使用的外部服务。
apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: baidusvc namespace: myistio spec: hosts: - baidu.com ports: - number: 80 name: http protocol: HTTP - number: 443 name: https protocol: HTTPS resolution: DNS # DNS 动态来解析 IP 地址 location: MESH_EXTERNAL # MESH_EXTERNAL表示在网格外部 MESH_INTERNAL (表示在网格内部)
- 一般我们直接访问 baidu.com 是不通的, 当我们显式的将 baidu.com 进行暴露到网格内, 网格内的服务就可以正常访问 baidu.com. 我们可以通过 istioctl 来查看网格内的代理配置 cluster 及 endpoints.
$ istioctl pc cluster mytest-7cdd69858f-gjrv5.myistio --fqdn baidu.com SERVICE FQDN PORT SUBSET DIRECTION TYPE DESTINATION RULE baidu.com 80 - outbound EDS baidu.com 443 - outbound EDS $ istioctl pc ep mytest-7cdd69858f-gjrv5.myistio --cluster 'outbound|80||baidu.com' ENDPOINT STATUS OUTLIER CHECK CLUSTER 39.156.66.10:80 HEALTHY OK outbound|80||baidu.com
- 测试访问结果, 能正常打开 www.baidu.com 首页
$ kubectl exec -it mytest-7cdd69858f-gjrv5 -n myistio -- curl baidu.com <!DOCTYPE html> <!--STATUS OK--><html> <head><meta http-equiv=content-type ... ...
3.4.2 静态
-
这次的示例和上面的动态解析改成了静态指定, 并且标注为内部服务;
apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: baidusvc-se namespace: myistio spec: hosts: - baidu.com ports: - number: 80 name: http protocol: HTTP - number: 443 name: https protocol: HTTPS endpoints: - address: 14.215.177.38 resolution: STATIC # STATIC 静态IP地址 DNS 通过查询DNS 来解析 IP 地址 location: MESH_EXTERNAL # MESH_EXTERNAL表示在网格外部 MESH_INTERNAL (表示在网格内部)
-
测试结果是与上面一致的, 但唯一不同的是代理配置显示的 endpoint 不一样, 上面的动态解析显示的是dns解析出来的代理地址, 而这次的endpoint 是我们指定
14.215.177.38
百度的IP.$ istioctl pc ep mytest-7cdd69858f-gjrv5.myistio --cluster 'outbound|80||baidu.com' ENDPOINT STATUS OUTLIER CHECK CLUSTER 14.215.177.38:80 HEALTHY OK outbound|80||baidu.com
4. 写在最后
-
同学都看到这了, 输出不易, 请留个👍 (赞)以示鼓励吧;
-
参考
jimmysong.io jimmysong.io/kubernetes-…
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13287.html