实战项目
更新于:2024年1月24日
实战项目

目录
[toc]
前言
接下来我们将用一个完整的项目来进行 Istio 实战。这里我们选择比较经典的 Online Boutique 微服务项目来进行说明。
Online Boutique 是一个微服务演示应用程序,该应用程序是一个基于网络的电子商务应用程序,用户可以在其中浏览商品、将其添加到购物车并购买。
说明
本节实战因镜像未拉取下来,导致测试失败,仅记录文档;
项目架构
该项目由不同编程语言编写的 11 个微服务组成,微服务之间主要通过 gRPC 进行互相通信。

每个微服务的功能如下表所示:
| 服务名称 | 语言 | 描述 |
|---|---|---|
| frontend | Go | 暴露一个 HTTP 服务器以服务网站。不需要注册/登录并为所有用户自动生成会话 ID |
| cartservice | C# | 在 Redis 中存储用户购物车中的商品,并检索它 |
| productcatalogservice | Go | 提供一个来自 JSON 文件的产品列表和搜索产品及获取单个产品的能力 |
| currencyservice | Node.js | 将一种货币金额转换为另一种货币,它是最高 QPS 的服务。 |
| paymentservice | Node.js | 向(模拟的)给定信用卡信息收费,并返回一个交易 ID |
| shippingservice | Go | 根据购物车提供运费估算,并将商品运送到(模拟的)指定地址 |
| emailservice | Python | 发送用户订单确认电子邮件(模拟) |
| checkoutservice | Go | 检索用户购物车,准备订单并协调支付、快递和邮件通知 |
| recommendationservice | Python | 根据购物车中的内容推荐其他产品 |
| adservice | Java | 根据给定上下文词提供文本广告 |
| loadgenerator | Python/Locust | 持续发送请求以模仿真实用户购物流量 |
了解项目的架构对我们理解应用的运行方式非常重要,我们可以看到该项目中有 11 个微服务,每个微服务都是一个独立的进程,它们之间通过 gRPC 进行通信,这样就保证了微服务之间的解耦。
项目部署
接下来我们需要将该项目部署到 Kubernetes 集群中,每个服务的部署资源清单文件位于项目的 kubernetes-manifests 目录下面:

但是需要注意的是该目录中提供的清单不能直接部署到集群中,它们需要与 Skaffold 命令一起使用以去替换对应的镜像地址。我们可以使用一个打包到一起的完整资源清单文件来部署该项目,该文件位于项目的 release 目录下面:https://github.com/GoogleCloudPlatform/microservices-demo/blob/v0.9.0/release/kubernetes-manifests.yaml。按照最佳做法,我们这里将每项服务都部署在具有唯一服务账号的单独命名空间中,这里我们将所有的资源清单文件托管在了 https://github.com/cnych/istio-demo 仓库中。
- 我们可以通过下面命令来部署该项目:
1$ git clone https://github.com/cnych/istio-demo && cd istio-demo
2$ kubectl apply -f kubernetes-manifests/namespaces/
正常会输出下面的信息:
1namespace/ad created
2namespace/cart created
3namespace/checkout created
4namespace/currency created
5namespace/email created
6namespace/frontend created
7namespace/loadgenerator created
8namespace/payment created
9namespace/redis created
10namespace/product-catalog created
11namespace/recommendation created
12namespace/shipping created
- 然后接下来可以部署 ServiceAccount 和 Deployment:
1kubectl apply -f kubernetes-manifests/deployments/
不过需要注意该清单文件中的镜像地址是 gcr.io/ 开头的,我们可以将其替换为 gcr.dockerproxy.com/ 开头的地址,否则会拉取不到镜像。
预期会输出如下结果:
1serviceaccount/ad created
2deployment.apps/adservice created
3serviceaccount/cart created
4deployment.apps/cartservice created
5serviceaccount/checkout created
6deployment.apps/checkoutservice created
7serviceaccount/currency created
8deployment.apps/currencyservice created
9serviceaccount/email created
10deployment.apps/emailservice created
11serviceaccount/frontend created
12deployment.apps/frontend created
13serviceaccount/loadgenerator created
14deployment.apps/loadgenerator created
15serviceaccount/payment created
16deployment.apps/paymentservice created
17serviceaccount/product-catalog created
18deployment.apps/productcatalogservice created
19serviceaccount/recommendation created
20deployment.apps/recommendationservice created
21serviceaccount/redis created
22deployment.apps/redis-cart created
23serviceaccount/shipping created
24deployment.apps/shippingservice created
- 然后创建 Service 服务:
1kubectl apply -f kubernetes-manifests/services/
预期会输出如下结果:
1service/adservice created
2service/cartservice created
3service/checkoutservice created
4service/currencyservice created
5service/emailservice created
6service/frontend created
7service/frontend-external created
8service/paymentservice created
9service/productcatalogservice created
10service/recommendationservice created
11service/redis-cart created
12service/shippingservice created
这里我们 m 每个服务都创建了一个独立的命名空间,并将该项目部署到该命名空间中,需要注意目前我们并没有注入 Istio 的 sidecar,所以该项目中的微服务并没有使用 Istio。
- 部署完成后我们可以查看该项目的所有
Pod:
1$ for ns in ad cart checkout currency email frontend loadgenerator \
2 payment product-catalog recommendation shipping redis; do
3 kubectl get pods -n $ns
4done;
5NAME READY STATUS RESTARTS AGE
6adservice-599557d587-k78zz 1/1 Running 0 8m14s
7NAME READY STATUS RESTARTS AGE
8cartservice-d965d797d-x6zm8 1/1 Running 0 7m34s
9NAME READY STATUS RESTARTS AGE
10checkoutservice-558cb79cd9-f8dv5 1/1 Running 0 2m19s
11NAME READY STATUS RESTARTS AGE
12currencyservice-7bccdbb75c-fhjjk 1/1 Running 0 8m13s
13NAME READY STATUS RESTARTS AGE
14emailservice-b77685c45-2l7p5 1/1 Running 0 8m13s
15NAME READY STATUS RESTARTS AGE
16frontend-8495d678c6-k2l45 1/1 Running 0 52s
17NAME READY STATUS RESTARTS AGE
18loadgenerator-7dcc798c94-l7lll 1/1 Running 0 8m13s
19NAME READY STATUS RESTARTS AGE
20paymentservice-5d488686d9-s7qz5 1/1 Running 0 2m19s
21NAME READY STATUS RESTARTS AGE
22productcatalogservice-784876db87-tpxwx 1/1 Running 0 2m19s
23NAME READY STATUS RESTARTS AGE
24recommendationservice-67ddb5f6dc-thgrd 1/1 Running 0 8m13s
25NAME READY STATUS RESTARTS AGE
26shippingservice-576794b87c-94fph 1/1 Running 0 8m12s
27NAME READY STATUS RESTARTS AGE
28redis-cart-69bcdbcc59-8vjc8 1/1 Running 0 8m13s
从前面的架构图中我们可以看到该项目对外暴露的服务是 frontend 这个微服务,上面我们部署的资源清单文件中就包括一个 frontend 的 LoadBalancer 类型的 Service 资源:
1apiVersion: v1
2kind: Service
3metadata:
4 name: frontend-external
5 namespace: frontend
6spec:
7 type: LoadBalancer
8 selector:
9 app: frontend
10 ports:
11 - name: http
12 port: 80
13 targetPort: 8080
如果你的集群支持 LoadBalancer 类型的 Service,那么该 Service 就会自动创建一个 LoadBalancer 类型的负载均衡器,并将其绑定到该 Service 上,这样我们就可以通过负载均衡器的地址来访问该服务了,可以通过下面命令查看该服务的地址:
1kubectl get service frontend-external -n frontend | awk '{print $4}'
由于我们这里的集群并不支持 LoadBalancer 类型的 Service,所以该 Service 并没有创建负载均衡器,但是我们还可以继续通过 NodePort 的方式来访问该服务,我们可以通过下面命令查看该服务的 NodePort:
1$ kubectl get svc frontend-external -n frontend
2NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
3frontend-external LoadBalancer 10.103.160.160 <pending> 80:30877/TCP 7m17s
这样我们就可以通过 http://<NodeIP>:30877 来访问该服务了。
- 哦,shift,我这里报错了
老师的没报错,我的又报错了。。。



就这样吧,这里不再折腾了,仅记录文档。
当然更好的方式是通过 Ingress 来访问该服务,我们可以通过下面命令创建一个 Ingress:
1$ kubectl -n demo apply -f - <<EOF
2apiVersion: networking.k8s.io/v1
3kind: Ingress
4metadata:
5 name: frontend
6 namespace: frontend
7spec:
8 ingressClassName: nginx
9 rules:
10 - host: ob.k8s.local
11 http:
12 paths:
13 - backend:
14 service:
15 name: frontend
16 port:
17 name: web
18 path: /
19 pathType: Prefix
20EOF
然后通过 http://ob.k8s.local 来访问该服务(记得解析)。
到这里我们只是将该项目部署到了集群中,但是并没有使用 Istio,接下来我们将使用 Istio 来管理该项目。
- 首先将上面的命名空间标记为
istio-injection=enabled,这样在该命名空间中的Pod就会自动注入Istio的sidecar:
1for ns in ad cart checkout currency email frontend loadgenerator \
2 payment product-catalog recommendation shipping redis; do
3 kubectl label namespace $ns istio-injection=enabled --overwrite
4done;
- 然后我们重启所有的 Pod,让他们重新注入
Istio的sidecar:
1for ns in ad cart checkout currency email frontend loadgenerator \
2 payment product-catalog recommendation shipping redis; do
3 kubectl rollout restart deployment -n ${ns}
4done;
- 再次查看这些 Pod 的时候,我们会发现每个 Pod 都变成了两个容器了,其中一个就是新增的
istio-proxy的容器:
1$ for ns in ad cart checkout currency email frontend loadgenerator \
2 payment product-catalog recommendation shipping redis; do
3 kubectl get pods -n $ns
4done;
5NAME READY STATUS RESTARTS AGE
6adservice-864dc4fc56-cvp24 2/2 Running 0 83s
7NAME READY STATUS RESTARTS AGE
8cartservice-b68fbffff-8trmd 2/2 Running 0 83s
9NAME READY STATUS RESTARTS AGE
10checkoutservice-67f54f5f76-j5cf6 2/2 Running 0 83s
11NAME READY STATUS RESTARTS AGE
12currencyservice-679fb5dd5d-pbkzz 2/2 Running 0 83s
13NAME READY STATUS RESTARTS AGE
14emailservice-68889c47d7-4dhg4 2/2 Running 0 82s
15NAME READY STATUS RESTARTS AGE
16frontend-6fc8b5c99c-6vjc8 2/2 Running 0 82s
17NAME READY STATUS RESTARTS AGE
18loadgenerator-84df465667-5pvx6 2/2 Running 0 83s
19NAME READY STATUS RESTARTS AGE
20paymentservice-8668445687-s7fvd 2/2 Running 0 83s
21NAME READY STATUS RESTARTS AGE
22productcatalogservice-7f8db6d6cc-xjrkh 2/2 Running 0 82s
23NAME READY STATUS RESTARTS AGE
24recommendationservice-84ddccb8f4-x9lbg 2/2 Running 0 82s
25NAME READY STATUS RESTARTS AGE
26shippingservice-df977cc6d-h866h 2/2 Running 0 82s
27NAME READY STATUS RESTARTS AGE
28redis-cart-cdd7d87dd-4w769 2/2 Running 0 82s
表示这些服务已经成功的注入了 Istio 的 sidecar,这样我们就可以使用 Istio 来管理这些服务了。
- 首先我们我们创建一个用来暴露
frontend服务的Gateway:
1# frontend-gateway.yaml
2apiVersion: networking.istio.io/v1beta1
3kind: Gateway
4metadata:
5 name: frontend-gateway
6 namespace: frontend
7spec:
8 selector:
9 istio: ingressgateway
10 servers:
11 - port:
12 number: 80
13 name: http
14 protocol: HTTP
15 hosts:
16 - "*"
当然前提是你的集群中已经部署了 Istio 的 ingressgateway:
1$ kubectl get pods -n istio-system -l app=istio-ingressgateway
2NAME READY STATUS RESTARTS AGE
3istio-ingressgateway-9c8b9b586-p2w67 1/1 Running 0 36d
4$ kubectl get svc -n istio-system -l app=istio-ingressgateway
5NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
6istio-ingressgateway LoadBalancer 10.103.227.57 <pending> 15021:32459/TCP,80:31896/TCP,443:30808/TCP,31400:32259/TCP,15443:31072/TCP 68d
- 然后我们创建一个
VirtualService来将frontend服务暴露出去:
1# frontend-virtualservice.yaml
2apiVersion: networking.istio.io/v1beta1
3kind: VirtualService
4metadata:
5 name: frontend-ingress
6 namespace: frontend
7spec:
8 hosts:
9 - "*"
10 gateways:
11 - frontend-gateway
12 http:
13 - route:
14 - destination:
15 host: frontend
16 port:
17 number: 80
- 这两个资源对象非常简单,直接应用到集群中即可:
1kubectl apply -f frontend-gateway.yaml
2kubectl apply -f frontend-virtualservice.yaml
- 部署完成后我们就可以通过 istio ingressgateway 的地址
http://<NODE-IP>:31896来访问该服务了。

- 我们是通过 Istio 的入口网关来访问该服务的,我们可以查看 Istio 的入口网关的日志:
1$ kubectl logs -f istio-ingressgateway-9c8b9b586-p2w67 -n istio-system
2[2024-01-09T06:35:38.249Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 10216 21 21 "10.244.0.0" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" "4f580734-78d6-49b1-91e4-d314e15e3f50" "192.168.0.100:31896" "10.244.1.44:8080" outbound|80||frontend.frontend.svc.cluster.local 10.244.2.176:57846 10.244.2.176:8080 10.244.0.0:17127 - -
3[2024-01-09T06:36:04.321Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 10216 37 37 "10.244.0.0" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" "cf6b1115-9c0b-4922-a371-c6f278b5a4b7" "192.168.0.100:31896" "10.244.1.44:8080" outbound|80||frontend.frontend.svc.cluster.local 10.244.2.176:57844 10.244.2.176:8080 10.244.0.0:35713 - -
4# ......
可以看到我们的请求都是通过 istio-ingressgateway 来访问的,同样的服务之间的调用也是通过 istio-proxy 来进行的,我们可以查看 frontend 服务的 sidecar 日志:
1$ kubectl logs -f frontend-6fc8b5c99c-6vjc8 -c istio-proxy -n frontend
2[2024-01-09T06:40:16.403Z] "POST /setCurrency HTTP/1.1" 302 - via_upstream - "-" 17 0 0 0 "-" "python-requests/2.31.0" "ba222e7f-7d4d-499f-ad93-8db210e24755" "frontend.frontend.svc.cluster.local" "10.244.1.44:8080" inbound|8080|| 127.0.0.6:56205 10.244.1.44:8080 10.244.2.220:52738 - default
3[2024-01-09T06:40:16.406Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 10336 42 41 "-" "python-requests/2.31.0" "542c288a-1dbd-45f4-a75d-0f3f74e4ea50" "frontend.frontend.svc.cluster.local" "10.244.1.44:8080" inbound|8080|| 127.0.0.6:56205 10.244.1.44:8080 10.244.2.220:52738 - default
4[2024-01-09T06:40:18.239Z] "GET /product/OLJCESPC7Z HTTP/1.1" 200 - via_upstream - "-" 0 7824 18 17 "-" "python-requests/2.31.0" "322b7f44-360a-4862-8ef7-b38456870cf8" "frontend.frontend.svc.cluster.local" "10.244.1.44:8080" inbound|8080|| 127.0.0.6:55699 10.244.1.44:8080 10.244.2.220:52730 - default
5# ......
可以看到有很多请求日志出现,这是因为请求被 istio-proxy 拦截了,然后再转发到对应的服务上面,这样我们就可以通过 Istio 来管理该项目了。
服务治理
接下来我们将使用 Istio 来管理该项目。
金丝雀发布
Canary 部署会将一小部分流量路由到微服务的新版本,然后我们可以逐步发布到整个用户群,同时逐步淘汰和弃用旧版本。如果在此过程中出现问题,可以将流量切换回旧版本。
这里我们亿 productcatalog-service 服务为例来说明如何进行金丝雀发布。首先为现有的 productcatalog 服务添加一个 version=v1 的标签,表示该服务的版本为 v1:
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: productcatalogservice
5 namespace: product-catalog
6spec:
7 selector:
8 matchLabels:
9 app: productcatalogservice
10 version: v1
11 template:
12 metadata:
13 labels:
14 app: productcatalogservice
15 version: v1
16# ......
然后我们可以使用 DestinationRule 来为该服务配置一个 v1 版本的子集:
1# destination-v1.yaml
2apiVersion: networking.istio.io/v1beta1
3kind: DestinationRule
4metadata:
5 name: productcatalogservice
6 namespace: product-catalog
7spec:
8 host: productcatalogservice.product-catalog.svc.cluster.local
9 subsets:
10 - labels:
11 version: v1
12 name: v1
然后通过 VirtualService 流量路由到该子集上面:
1# virtualservice-v1.yaml
2apiVersion: networking.istio.io/v1beta1
3kind: VirtualService
4metadata:
5 name: productcatalogservice
6 namespace: frontend
7spec:
8 hosts:
9 - productcatalogservice.product-catalog.svc.cluster.local
10 http:
11 - route:
12 - destination:
13 host: productcatalogservice.product-catalog.svc.cluster.local
14 subset: v1
需要注意的是这里的 VirtualService 对象需要在 frontend 命名空间下面,因为是该命名空间下面的服务来访问 productcatalog 的服务。直接应用上面的资源对象到集群中:
1kubectl apply -f destination-v1.yaml
2kubectl apply -f virtualservice-v1.yaml
现在我们再浏览器中去访问 Online Boutique 项目依然可以正常访问。
由于 productcatalog 的数据是从 JSON 文件中读取的,所以我们可以修改 productcatalog 的 JSON 文件,然后将这个 JSON 文件以 volumes 形式挂载到容器中去,重新部署该服务,将其作为 v2 版本的服务。
原始的 products.json 数据如下所示:
1// products.json
2{
3 "products": [
4 {
5 "id": "OLJCESPC7Z",
6 "name": "Sunglasses",
7 "description": "Add a modern touch to your outfits with these sleek aviator sunglasses.",
8 "picture": "/static/img/products/sunglasses.jpg",
9 "priceUsd": {
10 "currencyCode": "USD",
11 "units": 19,
12 "nanos": 990000000
13 },
14 "categories": ["accessories"]
15 },
16 {
17 "id": "66VCHSJNUP",
18 "name": "Tank Top",
19 "description": "Perfectly cropped cotton tank, with a scooped neckline.",
20 "picture": "/static/img/products/tank-top.jpg",
21 "priceUsd": {
22 "currencyCode": "USD",
23 "units": 18,
24 "nanos": 990000000
25 },
26 "categories": ["clothing", "tops"]
27 },
28 {
29 "id": "1YMWWN1N4O",
30 "name": "Watch",
31 "description": "This gold-tone stainless steel watch will work with most of your outfits.",
32 "picture": "/static/img/products/watch.jpg",
33 "priceUsd": {
34 "currencyCode": "USD",
35 "units": 109,
36 "nanos": 990000000
37 },
38 "categories": ["accessories"]
39 },
40 {
41 "id": "L9ECAV7KIM",
42 "name": "Loafers",
43 "description": "A neat addition to your summer wardrobe.",
44 "picture": "/static/img/products/loafers.jpg",
45 "priceUsd": {
46 "currencyCode": "USD",
47 "units": 89,
48 "nanos": 990000000
49 },
50 "categories": ["footwear"]
51 },
52 {
53 "id": "2ZYFJ3GM2N",
54 "name": "Hairdryer",
55 "description": "This lightweight hairdryer has 3 heat and speed settings. It's perfect for travel.",
56 "picture": "/static/img/products/hairdryer.jpg",
57 "priceUsd": {
58 "currencyCode": "USD",
59 "units": 24,
60 "nanos": 990000000
61 },
62 "categories": ["hair", "beauty"]
63 },
64 {
65 "id": "0PUK6V6EV0",
66 "name": "Candle Holder",
67 "description": "This small but intricate candle holder is an excellent gift.",
68 "picture": "/static/img/products/candle-holder.jpg",
69 "priceUsd": {
70 "currencyCode": "USD",
71 "units": 18,
72 "nanos": 990000000
73 },
74 "categories": ["decor", "home"]
75 },
76 {
77 "id": "LS4PSXUNUM",
78 "name": "Salt & Pepper Shakers",
79 "description": "Add some flavor to your kitchen.",
80 "picture": "/static/img/products/salt-and-pepper-shakers.jpg",
81 "priceUsd": {
82 "currencyCode": "USD",
83 "units": 18,
84 "nanos": 490000000
85 },
86 "categories": ["kitchen"]
87 },
88 {
89 "id": "9SIQT8TOJO",
90 "name": "Bamboo Glass Jar",
91 "description": "This bamboo glass jar can hold 57 oz (1.7 l) and is perfect for any kitchen.",
92 "picture": "/static/img/products/bamboo-glass-jar.jpg",
93 "priceUsd": {
94 "currencyCode": "USD",
95 "units": 5,
96 "nanos": 490000000
97 },
98 "categories": ["kitchen"]
99 },
100 {
101 "id": "6E92ZMYYFZ",
102 "name": "Mug",
103 "description": "A simple mug with a mustard interior.",
104 "picture": "/static/img/products/mug.jpg",
105 "priceUsd": {
106 "currencyCode": "USD",
107 "units": 8,
108 "nanos": 990000000
109 },
110 "categories": ["kitchen"]
111 }
112 ]
113}
我们修改 products.json 文件,将 id 为 OLJCESPC7Z 的商品的 name 修改为 Sunglasses V2,图片地址我们也可以替换下,然后将其作为 v2 版本的数据。使用下面的命令将其关联到 ConfigMap 中去:
1kubectl create configmap products-v2 --from-file=products.json -n product-catalog
然后将其挂载到容器中去,并为新版本的服务添加一个 version=v2 的标签:
1# productcatalog-v2.yaml
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5 name: productcatalogservice-v2
6 namespace: product-catalog
7spec:
8 selector:
9 matchLabels:
10 app: productcatalogservice
11 template:
12 metadata:
13 labels:
14 app: productcatalogservice
15 version: v2
16 spec:
17 volumes:
18 - name: products
19 configMap:
20 name: products-v2
21 containers:
22 - env:
23 - name: PORT
24 value: "3550"
25 - name: DISABLE_PROFILER
26 value: "1"
27 image: gcr.dockerproxy.com/google-samples/microservices-demo/productcatalogservice:v0.9.0
28 imagePullPolicy: IfNotPresent
29 livenessProbe:
30 grpc:
31 port: 3550
32 name: server
33 ports:
34 - containerPort: 3550
35 protocol: TCP
36 volumeMounts:
37 - name: products
38 subPath: products.json
39 mountPath: /src/products.json
40 readinessProbe:
41 grpc:
42 port: 3550
43 resources:
44 limits:
45 cpu: 200m
46 memory: 128Mi
47 requests:
48 cpu: 100m
49 memory: 64Mi
50 securityContext:
51 allowPrivilegeEscalation: false
52 capabilities:
53 drop:
54 - ALL
55 privileged: false
56 readOnlyRootFilesystem: true
57 securityContext:
58 fsGroup: 1000
59 runAsGroup: 1000
60 runAsNonRoot: true
61 runAsUser: 1000
62 serviceAccountName: product-catalog
63 terminationGracePeriodSeconds: 5
接下来我们将 v2 版本的服务部署到集群中去:
1kubectl apply -f productcatalog-v2.yaml -n product-catalog
现在我们就包含了两个版本的 productcatalog 服务了:
1$ kubectl get pods -n product-catalog -l app=productcatalogservice
2NAME READY STATUS RESTARTS AGE
3productcatalogservice-5b74bdd4d6-wblqn 2/2 Running 0 33m
4productcatalogservice-v2-8678ff9fcc-fzvtj 2/2 Running 0 3m5s
但是现在我们的服务还是只有一个 v1 版本的,因为我们只定义了 v1 的子集,要将流量路由到 v2 版本的服务上面去,首先我们还是得为 v2 版本的服务添加一个 v2 的子集,更新上面的 DestinationRule 对象,添加一个 v2 的子集:
1# destination-v2.yaml
2apiVersion: networking.istio.io/v1beta1
3kind: DestinationRule
4metadata:
5 name: productcatalogservice
6 namespace: product-catalog
7spec:
8 host: productcatalogservice.product-catalog.svc.cluster.local
9 subsets:
10 - labels:
11 version: v1
12 name: v1
13 - labels:
14 version: v2
15 name: v2
现在我们就拥有了 v1 和 v2 两个版本的子集了。接下来我们可以先通过 VirtualService 将一小部分流量定向到 v2 版去验证下。
1# vs-split.yaml
2apiVersion: networking.istio.io/v1beta1
3kind: VirtualService
4metadata:
5 name: productcatalogservice
6 namespace: frontend
7spec:
8 hosts:
9 - productcatalogservice.product-catalog.svc.cluster.local
10 http:
11 - route:
12 - destination:
13 host: productcatalogservice.product-catalog.svc.cluster.local
14 subset: v1
15 weight: 75
16 - destination:
17 host: productcatalogservice.product-catalog.svc.cluster.local
18 subset: v2
19 weight: 25
这里我们将 v2 版本的服务的流量权重设置为 25,然后将其应用到集群中去:
1kubectl apply -f vs-split.yaml -n product-catalog
创建后我们就可以去访问 Online Boutique 项目了,可以多刷新几次页面。

但结果却是并没有出现期望中的 V2 版本,那我们应该如何去排查问题呢?
还记得之前我们学习过的 istioctl proxy-config 命令吗?我们可以使用该命令来查看 envoy 配置中的一些规则是否符合预期。
1$ kubectl get pods -n frontend
2NAME READY STATUS RESTARTS AGE
3frontend-55df6b66cf-5tndm 2/2 Running 0 46s
首先使用 istioctl proxy-config routes 命令来查看 frontend 服务的路由规则:
1$ istioctl proxy-config routes frontend-55df6b66cf-5tndm -n frontend
2NAME VHOST NAME DOMAINS MATCH VIRTUAL SERVICE
380 frontend-external.frontend.svc.cluster.local:80 frontend-external, frontend-external.frontend + 1 more... /*
480 frontend.frontend.svc.cluster.local:80 frontend, frontend.frontend + 1 more... /*
580 productcatalogservice.product-catalog.svc.cluster.local:80 productcatalogservice.product-catalog.svc.cluster.local /* productcatalogservice.frontend
6inbound|8080|| inbound|http|80 * /*
7 backend * /stats/prometheus*
8InboundPassthroughClusterIpv4 inbound|http|0 * /*
9inbound|8080|| inbound|http|80 * /*
10InboundPassthroughClusterIpv4 inbound|http|0 * /*
11 backend * /healthz/ready*
从上面的输出可以看出包含一个 productcatalogservice.product-catalog.svc.cluster.local:80 的虚拟服务,看到这里我们就知道我们的 VirtualService 已经生效了。然后再次查看下 endpoint 下面是否包含我们定义的两个子集:
1$ istioctl proxy-config endpoint frontend-55df6b66cf-5tndm -n frontend
2ENDPOINT STATUS OUTLIER CHECK CLUSTER
310.100.10.116:5050 HEALTHY OK PassthroughCluster
410.103.97.248:7070 HEALTHY OK PassthroughCluster
510.104.11.53:7000 HEALTHY OK PassthroughCluster
610.108.221.130:8080 HEALTHY OK PassthroughCluster
710.109.51.243:3550 HEALTHY OK PassthroughCluster
810.110.175.145:9555 HEALTHY OK PassthroughCluster
910.111.7.61:50051 HEALTHY OK PassthroughCluster
1010.244.1.126:3550 HEALTHY OK outbound|3550|v1|productcatalogservice.product-catalog.svc.cluster.local
1110.244.1.126:3550 HEALTHY OK outbound|3550||productcatalogservice.product-catalog.svc.cluster.local
1210.244.1.128:8080 HEALTHY OK inbound|8080||
1310.244.1.128:8080 HEALTHY OK outbound|80||frontend-external.frontend.svc.cluster.local
1410.244.1.128:8080 HEALTHY OK outbound|80||frontend.frontend.svc.cluster.local
1510.244.2.62:3550 HEALTHY OK outbound|3550|v2|productcatalogservice.product-catalog.svc.cluster.local
1610.244.2.62:3550 HEALTHY OK outbound|3550||productcatalogservice.product-catalog.svc.cluster.local
1710.98.118.194:9411 HEALTHY OK zipkin
18127.0.0.1:15000 HEALTHY OK prometheus_stats
19127.0.0.1:15020 HEALTHY OK agent
20unix://./etc/istio/proxy/XDS HEALTHY OK xds-grpc
21unix://./var/run/secrets/workload-spiffe-uds/socket HEALTHY OK sds-grpc
从输出结果可以看到我们的 productcatalogservice 服务下面包含了两个子集,这说明我们的 DestinationRule 已经生效了。但是我们仔细观察就可以看到问题出在什么地方了,我们的 productcatalogservice 服务下面的两个子集的端口都是 3550,但是我们的 VirtualService 中的 productcatalogservice 服务的端口使用的是默认的 80 端口,没有匹配到,所以带权重的路由规则并没有生效。
我们可以去修改 productcatalogservice 的 Service 服务端口为 80:
1apiVersion: v1
2kind: Service
3metadata:
4 name: productcatalogservice
5 namespace: product-catalog
6spec:
7 type: ClusterIP
8 selector:
9 app: productcatalogservice
10 ports:
11 - name: grpc
12 port: 80
13 targetPort: 3550
这样就可以匹配到了,当然还要记得修改 frontend 中访问 productcatalogservice 服务的端口为 80:
1env:
2 - name: PORT
3 value: "8080"
4 - name: PRODUCT_CATALOG_SERVICE_ADDR
5 value: "productcatalogservice.product-catalog.svc.cluster.local:80"
理论上修改 VirtualService 中的端口也是可以的,但是我测试后没生效。
修改后我们再次访问 Online Boutique 项目,多刷新几次就有机会可以看到现在的商品列表中有一个商品的名称是 Sunglasses V2,这就表示我们的流量已经被路由到了 v2 版本的服务上面去了。

然后我们就可以不断调整 VirtualService 中的权重,最终将流量全部切换到 v2 版本的服务上面去。
授权
身份验证流程用于验证服务的身份,即服务是否具备自己声明的身份。授权流程则用于验证权限,即确定此服务是否有权执行某项操作。身份是这一概念的基础。我们可以通过 AuthorizationPolicies 控制网格内工作负载之间的通信,以提高安全性和访问控制。
微服务架构需跨网络边界进行调用,因此基于 IP 的传统防火墙规则通常不足以保护工作负载之间的访问。在 Istio 中我们可以使用 AuthorizationPolicy 对象来控制服务之间的访问。
接下来我们就来添加一个 AuthorizationPolicy 对象,用来拒绝发送到货币转换服务的所有传入流量。AuthorizationPolicies 的工作原理是将 AuthorizationPolicies 转换为 Envoy 可读的配置,并将配置应用于边车代理。这样就可以使 Envoy 代理能够授权或拒绝对服务的传入请求。
首先创建一个如下所示的 AuthorizationPolicy 对象:
1# currency-policy-deny.yaml
2apiVersion: security.istio.io/v1
3kind: AuthorizationPolicy
4metadata:
5 name: currency-policy
6 namespace: currency
7spec:
8 selector:
9 matchLabels:
10 app: currencyservice
11 action: DENY
12 rules:
13 - from:
14 - source:
15 notRequestPrincipals: ["*"] # 不存在任何请求身份(Principal)的 requests
需要注意上面对象的 selector 标签选择器,rules 里面我们这里没有指定任何身份,表示对任何请求都拒绝,这里我们将该对象应用到 currencyservice 服务上面去,然后我们将其应用到集群中去:
1kubectl apply -f currency-policy-deny.yaml
应用后当我们再次访问 Online Boutique 项目时,页面上就会出现类似 rpc error: code = PermissionDenied desc = RBAC: access denied could not retrieve currencies 这样的错误:

从提示信息可以看出我们的请求被拒绝了。我们还可以查看日志来确定,首先我们将 currencyservice 服务的 Envoy 代理日志级别设置为 trace:
1$ export CURRENCY_POD=$(kubectl get pod -n currency |grep currency|awk '{print $1}')
2$ kubectl exec -it $CURRENCY_POD -n currency -c istio-proxy -- curl -X POST "http://localhost:15000/logging?level=trace"
默认情况下,日志中不会记录被阻止的授权调用。然后我们可以使用 curl 命令将流量发送到应用,以生成日志:
1for i in {0..10}; do
2curl -s -I http://192.168.0.100:31896 ; done
然后我们可以查看 istio-proxy 中基于角色的访问权限控制 (RBAC) 相关的日志:
1kubectl logs -n currency $CURRENCY_POD -c istio-proxy | grep -m5 rbac
正常会输出如下所示内容:
12024-01-16T02:24:06.535211Z debug envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:114 checking request: requestedServerName: , sourceIP: 10.244.1.169:53386, directRemoteIP: 10.244.1.169:53386, remoteIP: 10.244.1.169:53386,localAddress: 10.244.2.97:7000, ssl: none, headers: ':method', 'POST'
22024-01-16T02:24:06.535248Z debug envoy rbac external/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:158 enforced denied, matched policy ns[currency]-policy[currency-policy]-rule[0] thread=23
32024-01-16T02:24:06.535256Z debug envoy http external/envoy/source/common/http/filter_manager.cc:946 [Tags: "ConnectionId":"652","StreamId":"10429042324993795331"] Preparing local reply with details rbac_access_denied_matched_policy[ns[currency]-policy[currency-policy]-rule[0]] thread=23
42024-01-16T02:24:06.535365Z trace envoy http external/envoy/source/common/http/filter_manager.cc:530 [Tags: "ConnectionId":"652","StreamId":"10429042324993795331"] decodeHeaders filter iteration aborted due to local reply: filter=envoy.filters.http.rbac thread=23
52024-01-16T02:24:06.535366Z trace envoy http external/envoy/source/common/http/filter_manager.cc:539 [Tags: "ConnectionId":"652","StreamId":"10429042324993795331"] decode headers called: filter=envoy.filters.http.rbac status=1 thread=23
我们可以在上面的日志中看到一条 enforced denied 的消息,表示 currencyservice 已设置为屏蔽入站请求了。
当然我们也可以设置允许访问部分工作负载,而不是设置 DENYALL 策略,在有的微服务架构中,我们可能会希望确保只有已获授权的服务才能相互通信。
这里我们可以配置访问 currency 这个 gRPC 服务的 /hipstershop.CurrencyService/Convert 和 /hipstershop.CurrencyService/GetSupportedCurrencies 两个端点的请求即可放行:
1# currency-policy-allow.yaml
2apiVersion: security.istio.io/v1beta1
3kind: AuthorizationPolicy
4metadata:
5 name: currency-policy
6 namespace: currency
7spec:
8 selector:
9 matchLabels:
10 app: currencyservice
11 action: ALLOW
12 rules:
13 - to:
14 - operation:
15 paths:
16 - /hipstershop.CurrencyService/Convert
17 - /hipstershop.CurrencyService/GetSupportedCurrencies
18 methods:
19 - POST
20 ports:
21 - "7000"
22 # rules:
23 # - from:
24 # - source:
25 # principals: ["cluster.local/ns/frontend/sa/frontend"]
26 # - from:
27 # - source:
28 # principals: ["cluster.local/ns/checkout/sa/checkout"]
正常配置 from.source.principals 属性即可,但是我测试后发现不生效。可以通过命令 istioctl proxy-config all <pod> -n currency -o yaml 来查看生成的 Envoy 配置。
然后我们将其应用到集群中去:
1kubectl apply -f currency-policy-allow.yaml
应用后我们再次访问 Online Boutique 项目,页面就可以正常显示了。
JWT 认证
Istio 还提供安全访问的身份验证机制,这里我们将使用 JWT Token 来启用身份验证。我们首先需要创建并应用一个强制 JWT 身份验证的策略。
在前面的课程中我们学习过可以使用 jwx 命令行工具来生成 JWK(Istio 使用 JWK 描述验证 JWT 签名所需要的信息)。
使用下面的命令来安装 jwx 命令行工具:
1$ export GOPROXY="https://goproxy.io"
2$ git clone https://github.com/lestrrat-go/jwx.git
3$ cd jwx
4$ make jwx
5go: downloading github.com/lestrrat-go/jwx/v2 v2.0.11
6go: downloading github.com/urfave/cli/v2 v2.24.4
7# ......
8go: downloading github.com/russross/blackfriday/v2 v2.1.0
9go: downloading golang.org/x/sys v0.8.0
10Installed jwx in /root/go/bin/jwx
下面我们使用 jwx 命令行工具生成一个 JWK,通过模板指定 kid 为 youdianzhishi-key:
1$ jwx jwk generate --keysize 4096 --type RSA --template '{"kid":"youdianzhishi-key"}' -o rsa.jwk
2$ cat rsa.jwk
3{
4 "d": "AxxxwBw6Jok",
5 "dp": "j3xxxuvQ",
6 "dq": "zzxxxqQ",
7 "e": "AQAB",
8 "kid": "youdianzhishi-key",
9 "kty": "RSA",
10 "n": "5sxxxwV8",
11 "p": "-yxxxQ",
12 "q": "6zkC_xxxxKw",
13 "qi": "LExxxTw"
14}
然后从 rsa.jwk 中提取 JWK 公钥:
1$ jwx jwk fmt --public-key -o rsa-public.jwk rsa.jwk
2$ cat rsa-public.jwk
3{
4 "e": "AQAB",
5 "kid": "youdianzhishi-key",
6 "kty": "RSA",
7 "n": "5sxxxV8"
8}
上面生成的 JWK 其实就是 RSA 公钥私钥换了一种存储格式而已,我们可以使用下面的命令将它们转换成 PEM 格式的公钥和私钥:
1$ jwx jwk fmt -I json -O pem rsa.jwk
2-----BEGIN PRIVATE KEY-----
3MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCym3O0Ik5QGZ8i
4......
5-----END PRIVATE KEY-----
6
7$ jwx jwk fmt -I json -O pem rsa-public.jwk
8-----BEGIN PUBLIC KEY-----
9MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsptztCJOUBmfIqSE8LR5
10......
11-----END PUBLIC KEY-----
接下来我们就可以使用 jwx 命令行签发 JWT Token 并验证其有效性了:
1jwx jws sign --key rsa.jwk --alg RS256 --header '{"typ":"JWT"}' -o token.txt - <<EOF
2{
3 "iss": "admin@youdianzhishi.com",
4 "sub": "cnych001",
5 "iat": 1700648397,
6 "exp": 1708066614,
7 "name": "Yang Ming"
8}
9EOF
然后查看生成的 Token 文件内容:
1$ cat token.txt
2eyJhbGciOiJSUzI1NiIsImtpZCI6InlvdWRpYW56aGlzaGkta2V5In0......
上面生成 JWT Token 实际上是由下面的算法生成的:
1base64url_encode(Header) + '.' + base64url_encode(Claims) + '.' + base64url_encode(Signature)
我们可以将该 Token 粘贴到 jwt.io 网站上来解析:

先看一下 Headers 部分,包含了一些元数据:
alg: 所使用的签名算法,这里是RSA256kid:JWK的kid
然后是 Payload(Claims) 部分,payload 包含了这个 token 的数据信息,JWT 标准规定了一些字段,另外还可以加入一些承载额外信息的字段。
iss: issuer,token 是谁签发的sub: token 的主体信息,一般设置为 token 代表用户身份的唯一 id 或唯一用户名exp: token 过期时间,Unix 时间戳格式iat: token 创建时间, Unix 时间戳格式
最后看一下签名 Signature 信息,签名是基于 JSON Web Signature (JWS) 标准来生成的,签名主要用于验证 token 是否有效,是否被篡改。签名支持很多种算法,这里使用的是 RSASHA256,具体的签名算法如下:
1RSASHA256(
2 base64UrlEncode(header) + "." +
3 base64UrlEncode(payload),
4 <rsa-public-key>,
5 <rsa-private-key>
最后可以使用 RSA Public Key 验证 JWT Token 的有效性:
1$ jwx jws verify --alg RS256 --key rsa-public.jwk token.txt
2{
3 "iss": "admin@youdianzhishi.com",
4 "sub": "cnych001",
5 "iat": 1700648397,
6 "exp": 1700656042,
7 "name": "Yang Ming"
8}
接下来我们就可以添加一个请求认证策略对象,该策略要求 Ingress 网关指定终端用户的 JWT。
1# jwt-policy.yaml
2apiVersion: "security.istio.io/v1"
3kind: RequestAuthentication
4metadata:
5 name: jwt-demo
6 namespace: istio-system
7spec:
8 selector:
9 matchLabels:
10 istio: ingressgateway
11 jwtRules:
12 - issuer: "admin@youdianzhishi.com" # 签发者,需要和 JWT payload 中的 iss 属性完全一致。
13 # forwardOriginalToken: true
14 jwks: | # jwk 公钥数据
15 {
16 "keys": [
17 {
18 "e": "AQAB",
19 "kid": "youdianzhishi-key",
20 "kty": "RSA",
21 "n": "nu3nRXyHjSX6lWI1oY8AGc7GpxXPrjHpWcAeZP8gFkA7gg8f81G8_RVJzCwcRBL71j13mc9Eftk4vJk4yjBgU_QhCyeiVcBXmkJyV0ciTLRttWIouHLw3vaLTaMBZ9r23PdA1r5WmtcjeYaVD8hk7vIDNpLMm1fv0PW6HrbDB4tJNa5C-CZax_qmlcL6XofctVijiPfDV6hnQqHVH0TuESSbTztgVocdC819IsTC7P080veqr2AWLMU-lLUlrfCOrBAs0AR7d8oLuplvdyCEhvTruqwChi6dT72F1vFH5FJl7P3bBpWnQHo1kMJirEwm5oy12NkXE1gSQ-YWw9hQ5A7QayMdbgPl-_DVeWoQXWqqejEITZB0SX1ORJOwBEjF178A3B15YedtHtbM43kHa04gv6LKV7sYXvvW7i6csj2JwUEwrM1AaxaOa94zWL2vyIv09aEmCrsn-E_7vp-mzfUSSkCmbwlXgTaTAyrRRDzVZx5asoY2bXEoRPrhfV1pt8MpAN9GCoWOVwuvEGYGQp3AIioHJsy37UQyN1yzb8byvlOgbadS_mRBe4RZF_Uj_GsOkpm0hf_TS0ZDeHLfqqiM0Cmm6dJl_lKWXVD1zXvIC6qA_NRU5PA8WycC6JbsXBS5aJ7OlI1Uu8B__ERUHljCiwv5XprFVH7SwSyhaws"
22 }
23 ]
24 }
直接应用上面的对象到集群中去:
1kubectl apply -f jwt-policy.yaml
现在我们去直接访问 Online Boutique 项目,可以看到现在依然可以正常访问,但是如果我们请求的时候带上一个无效的 JWT Token,则会返回 401 错误:
1$ curl --header "Authorization: Bearer abcd" "http://192.168.0.100:31896/" -s -o /dev/null -w "%{http_code}\n"
2401
要想正常访问,我们需要使用上面生成的 JWT Token 来进行访问:
1$ TOKEN=$(cat token.txt)
2$ curl --header "Authorization: Bearer $TOKEN" "http://192.168.0.100:31896/" -s -o /dev/null -w "%{http_code}\n"
3200
可以看到就可以正常访问了。
当然我们也可以在服务之间进行 JWT 认证,我们还可以在使用 AuthorizationPolicy 对象的时候来配置服务之间的 JWT 认证等。
关于我
我的博客主旨:
- 排版美观,语言精炼;
- 文档即手册,步骤明细,拒绝埋坑,提供源码;
- 本人实战文档都是亲测成功的,各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人帮您解决问题,让我们一起进步!
🍀 微信二维码
x2675263825 (舍得), qq:2675263825。

🍀 微信公众号
《云原生架构师实战》

🍀 个人博客站点


🍀 语雀
https://www.yuque.com/xyy-onlyone

🍀 csdn
https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421

🍀 知乎
https://www.zhihu.com/people/foryouone

最后
好了,关于本次就到这里了,感谢大家阅读,最后祝大家生活快乐,每天都过的有意义哦,我们下期见!

