hugo-teek is loading...

实战项目

最后更新于:

更新于:2024年1月24日

实战项目

目录

[toc]

前言

接下来我们将用一个完整的项目来进行 Istio 实战。这里我们选择比较经典的 Online Boutique 微服务项目来进行说明。

Online Boutique 是一个微服务演示应用程序,该应用程序是一个基于网络的电子商务应用程序,用户可以在其中浏览商品、将其添加到购物车并购买。

说明

本节实战因镜像未拉取下来,导致测试失败,仅记录文档;

项目架构

该项目由不同编程语言编写的 11 个微服务组成,微服务之间主要通过 gRPC 进行互相通信。

img

每个微服务的功能如下表所示:

服务名称语言描述
frontendGo暴露一个 HTTP 服务器以服务网站。不需要注册/登录并为所有用户自动生成会话 ID
cartserviceC#在 Redis 中存储用户购物车中的商品,并检索它
productcatalogserviceGo提供一个来自 JSON 文件的产品列表和搜索产品及获取单个产品的能力
currencyserviceNode.js将一种货币金额转换为另一种货币,它是最高 QPS 的服务。
paymentserviceNode.js向(模拟的)给定信用卡信息收费,并返回一个交易 ID
shippingserviceGo根据购物车提供运费估算,并将商品运送到(模拟的)指定地址
emailservicePython发送用户订单确认电子邮件(模拟)
checkoutserviceGo检索用户购物车,准备订单并协调支付、快递和邮件通知
recommendationservicePython根据购物车中的内容推荐其他产品
adserviceJava根据给定上下文词提供文本广告
loadgeneratorPython/Locust持续发送请求以模仿真实用户购物流量

了解项目的架构对我们理解应用的运行方式非常重要,我们可以看到该项目中有 11 个微服务,每个微服务都是一个独立的进程,它们之间通过 gRPC 进行通信,这样就保证了微服务之间的解耦。

项目部署

接下来我们需要将该项目部署到 Kubernetes 集群中,每个服务的部署资源清单文件位于项目的 kubernetes-manifests 目录下面:

img

但是需要注意的是该目录中提供的清单不能直接部署到集群中,它们需要与 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 每个服务都创建了一个独立的命名空间,并将该项目部署到该命名空间中,需要注意目前我们并没有注入 Istiosidecar,所以该项目中的微服务并没有使用 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 这个微服务,上面我们部署的资源清单文件中就包括一个 frontendLoadBalancer 类型的 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 就会自动注入 Istiosidecar
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,让他们重新注入 Istiosidecar
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

表示这些服务已经成功的注入了 Istiosidecar,这样我们就可以使用 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        - "*"

当然前提是你的集群中已经部署了 Istioingressgateway

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 来访问该服务了。

img

  • 我们是通过 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 文件中读取的,所以我们可以修改 productcatalogJSON 文件,然后将这个 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 文件,将 idOLJCESPC7Z 的商品的 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

现在我们就拥有了 v1v2 两个版本的子集了。接下来我们可以先通过 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 项目了,可以多刷新几次页面。

img

但结果却是并没有出现期望中的 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 版本的服务上面去了。

img

然后我们就可以不断调整 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 这样的错误:

img

从提示信息可以看出我们的请求被拒绝了。我们还可以查看日志来确定,首先我们将 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,通过模板指定 kidyoudianzhishi-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 网站上来解析:

img

先看一下 Headers 部分,包含了一些元数据:

  • alg: 所使用的签名算法,这里是 RSA256
  • kid: JWKkid

然后是 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。

image-20230107215114763

🍀 微信公众号

《云原生架构师实战》

image-20230107215126971

🍀 个人博客站点

https://onedayxyy.cn/

🍀 语雀

https://www.yuque.com/xyy-onlyone

🍀 csdn

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

image-20230107215149885

🍀 知乎

https://www.zhihu.com/people/foryouone

image-20230107215203185

最后

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

推荐使用微信支付
微信支付二维码
推荐使用支付宝
支付宝二维码
最新文章

文档导航