Skip to content

实战项目

更新于: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:$kubectlapply-fkubernetes-manifests/namespaces/

正常会输出下面的信息:

bash
namespace/adcreatednamespace/cartcreatednamespace/checkoutcreatednamespace/currencycreatednamespace/emailcreatednamespace/frontendcreatednamespace/loadgeneratorcreatednamespace/paymentcreatednamespace/rediscreatednamespace/product-catalogcreatednamespace/recommendationcreatednamespace/shippingcreated
  • 然后接下来可以部署 ServiceAccount 和 Deployment:
bash
kubectlapply-fkubernetes-manifests/deployments/

不过需要注意该清单文件中的镜像地址是 gcr.io/开头的,我们可以将其替换为 gcr.dockerproxy.com/开头的地址,否则会拉取不到镜像。

预期会输出如下结果:

bash
serviceaccount/adcreateddeployment.apps/adservicecreatedserviceaccount/cartcreateddeployment.apps/cartservicecreatedserviceaccount/checkoutcreateddeployment.apps/checkoutservicecreatedserviceaccount/currencycreateddeployment.apps/currencyservicecreatedserviceaccount/emailcreateddeployment.apps/emailservicecreatedserviceaccount/frontendcreateddeployment.apps/frontendcreatedserviceaccount/loadgeneratorcreateddeployment.apps/loadgeneratorcreatedserviceaccount/paymentcreateddeployment.apps/paymentservicecreatedserviceaccount/product-catalogcreateddeployment.apps/productcatalogservicecreatedserviceaccount/recommendationcreateddeployment.apps/recommendationservicecreatedserviceaccount/rediscreateddeployment.apps/redis-cartcreatedserviceaccount/shippingcreateddeployment.apps/shippingservicecreated
  • 然后创建 Service 服务:
bash
kubectlapply-fkubernetes-manifests/services/

预期会输出如下结果:

bash
service/adservicecreatedservice/cartservicecreatedservice/checkoutservicecreatedservice/currencyservicecreatedservice/emailservicecreatedservice/frontendcreatedservice/frontend-externalcreatedservice/paymentservicecreatedservice/productcatalogservicecreatedservice/recommendationservicecreatedservice/redis-cartcreatedservice/shippingservicecreated

这里我们 m 每个服务都创建了一个独立的命名空间,并将该项目部署到该命名空间中,需要注意目前我们并没有注入 Istiosidecar,所以该项目中的微服务并没有使用 Istio

  • 部署完成后我们可以查看该项目的所有 Pod
bash
$fornsinadcartcheckoutcurrencyemailfrontendloadgenerator\paymentproduct-catalogrecommendationshippingredis;dokubectlgetpods-n$nsdone;NAMEREADYSTATUSRESTARTSAGEadservice-599557d587-k78zz1/1Running08m14sNAMEREADYSTATUSRESTARTSAGEcartservice-d965d797d-x6zm81/1Running07m34sNAMEREADYSTATUSRESTARTSAGEcheckoutservice-558cb79cd9-f8dv51/1Running02m19sNAMEREADYSTATUSRESTARTSAGEcurrencyservice-7bccdbb75c-fhjjk1/1Running08m13sNAMEREADYSTATUSRESTARTSAGEemailservice-b77685c45-2l7p51/1Running08m13sNAMEREADYSTATUSRESTARTSAGEfrontend-8495d678c6-k2l451/1Running052sNAMEREADYSTATUSRESTARTSAGEloadgenerator-7dcc798c94-l7lll1/1Running08m13sNAMEREADYSTATUSRESTARTSAGEpaymentservice-5d488686d9-s7qz51/1Running02m19sNAMEREADYSTATUSRESTARTSAGEproductcatalogservice-784876db87-tpxwx1/1Running02m19sNAMEREADYSTATUSRESTARTSAGErecommendationservice-67ddb5f6dc-thgrd1/1Running08m13sNAMEREADYSTATUSRESTARTSAGEshippingservice-576794b87c-94fph1/1Running08m12sNAMEREADYSTATUSRESTARTSAGEredis-cart-69bcdbcc59-8vjc81/1Running08m13s

从前面的架构图中我们可以看到该项目对外暴露的服务是 frontend这个微服务,上面我们部署的资源清单文件中就包括一个 frontendLoadBalancer类型的 Service资源:

yaml
apiVersion:v1kind:Servicemetadata:name:frontend-externalnamespace:frontendspec:type:LoadBalancerselector:app:frontendports:- name:httpport:80targetPort:8080

如果你的集群支持 LoadBalancer类型的 Service,那么该 Service就会自动创建一个 LoadBalancer类型的负载均衡器,并将其绑定到该 Service上,这样我们就可以通过负载均衡器的地址来访问该服务了,可以通过下面命令查看该服务的地址:

bash
kubectlgetservicefrontend-external-nfrontend|awk'{print $4}'

由于我们这里的集群并不支持 LoadBalancer类型的 Service,所以该 Service并没有创建负载均衡器,但是我们还可以继续通过 NodePort的方式来访问该服务,我们可以通过下面命令查看该服务的 NodePort

bash
$kubectlgetsvcfrontend-external-nfrontendNAMETYPECLUSTER-IPEXTERNAL-IPPORT(S) AGEfrontend-externalLoadBalancer10.103.160.160<pending>80:30877/TCP7m17s

这样我们就可以通过 http:apiVersion:networking.k8s.io/v1kind:Ingressmetadata:name:frontendnamespace:frontendspec:ingressClassName:nginxrules:- host:ob.k8s.localhttp:paths:- backend:service:name:frontendport:name:webpath:/pathType:PrefixEOF

然后通过 http:paymentproduct-catalogrecommendationshippingredis;dokubectllabelnamespace$ns istio-injection=enabled--overwritedone;

  • 然后我们重启所有的 Pod,让他们重新注入 Istiosidecar
bash
forns inadcartcheckoutcurrencyemailfrontendloadgenerator\paymentproduct-catalogrecommendationshippingredis;dokubectlrolloutrestartdeployment-n${ns}done;
  • 再次查看这些 Pod 的时候,我们会发现每个 Pod 都变成了两个容器了,其中一个就是新增的 istio-proxy的容器:
bash
$fornsinadcartcheckoutcurrencyemailfrontendloadgenerator\paymentproduct-catalogrecommendationshippingredis;dokubectlgetpods-n$nsdone;NAMEREADYSTATUSRESTARTSAGEadservice-864dc4fc56-cvp242/2Running083sNAMEREADYSTATUSRESTARTSAGEcartservice-b68fbffff-8trmd2/2Running083sNAMEREADYSTATUSRESTARTSAGEcheckoutservice-67f54f5f76-j5cf62/2Running083sNAMEREADYSTATUSRESTARTSAGEcurrencyservice-679fb5dd5d-pbkzz2/2Running083sNAMEREADYSTATUSRESTARTSAGEemailservice-68889c47d7-4dhg42/2Running082sNAMEREADYSTATUSRESTARTSAGEfrontend-6fc8b5c99c-6vjc82/2Running082sNAMEREADYSTATUSRESTARTSAGEloadgenerator-84df465667-5pvx62/2Running083sNAMEREADYSTATUSRESTARTSAGEpaymentservice-8668445687-s7fvd2/2Running083sNAMEREADYSTATUSRESTARTSAGEproductcatalogservice-7f8db6d6cc-xjrkh2/2Running082sNAMEREADYSTATUSRESTARTSAGErecommendationservice-84ddccb8f4-x9lbg2/2Running082sNAMEREADYSTATUSRESTARTSAGEshippingservice-df977cc6d-h866h2/2Running082sNAMEREADYSTATUSRESTARTSAGEredis-cart-cdd7d87dd-4w7692/2Running082s

表示这些服务已经成功的注入了 Istiosidecar,这样我们就可以使用 Istio来管理这些服务了。

  • 首先我们我们创建一个用来暴露 frontend服务的 Gateway
yaml
# frontend-gateway.yamlapiVersion:networking.istio.io/v1beta1kind:Gatewaymetadata:name:frontend-gatewaynamespace:frontendspec:selector:istio:ingressgatewayservers:- port:number:80name:httpprotocol:HTTPhosts:- "*"

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

bash
$kubectlgetpods-nistio-system-lapp=istio-ingressgatewayNAMEREADYSTATUSRESTARTSAGEistio-ingressgateway-9c8b9b586-p2w671/1Running036d$kubectlgetsvc-nistio-system-lapp=istio-ingressgatewayNAMETYPECLUSTER-IPEXTERNAL-IPPORT(S) AGEistio-ingressgatewayLoadBalancer10.103.227.57<pending>15021:32459/TCP,80:31896/TCP,443:30808/TCP,31400:32259/TCP,15443:31072/TCP68d
  • 然后我们创建一个 VirtualService来将 frontend服务暴露出去:
yaml
# frontend-virtualservice.yamlapiVersion:networking.istio.io/v1beta1kind:VirtualServicemetadata:name:frontend-ingressnamespace:frontendspec:hosts:- "*"gateways:- frontend-gatewayhttp:- route:- destination:host:frontendport:number:80
  • 这两个资源对象非常简单,直接应用到集群中即可:
bash
kubectlapply-ffrontend-gateway.yamlkubectlapply-ffrontend-virtualservice.yaml
  • 部署完成后我们就可以通过 istio ingressgateway 的地址 http:[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.local10.244.2.176:5784610.244.2.176:808010.244.0.0:17127--[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.local10.244.2.176:5784410.244.2.176:808010.244.0.0:35713--# ......

可以看到我们的请求都是通过 istio-ingressgateway来访问的,同样的服务之间的调用也是通过 istio-proxy来进行的,我们可以查看 frontend服务的 sidecar 日志:

bash
$kubectllogs-ffrontend-6fc8b5c99c-6vjc8-cistio-proxy-nfrontend[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:5620510.244.1.44:808010.244.2.220:52738-default[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:5620510.244.1.44:808010.244.2.220:52738-default[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:5569910.244.1.44:808010.244.2.220:52730-default# ......

可以看到有很多请求日志出现,这是因为请求被 istio-proxy 拦截了,然后再转发到对应的服务上面,这样我们就可以通过 Istio 来管理该项目了。

服务治理

接下来我们将使用 Istio 来管理该项目。

金丝雀发布

Canary 部署会将一小部分流量路由到微服务的新版本,然后我们可以逐步发布到整个用户群,同时逐步淘汰和弃用旧版本。如果在此过程中出现问题,可以将流量切换回旧版本。

这里我们亿 productcatalog-service服务为例来说明如何进行金丝雀发布。首先为现有的 productcatalog服务添加一个 version=v1的标签,表示该服务的版本为 v1

yaml
apiVersion:apps/v1kind:Deploymentmetadata:name:productcatalogservicenamespace:product-catalogspec:selector:matchLabels:app:productcatalogserviceversion:v1template:metadata:labels:app:productcatalogserviceversion:v1# ......

然后我们可以使用 DestinationRule来为该服务配置一个 v1版本的子集:

yaml
# destination-v1.yamlapiVersion:networking.istio.io/v1beta1kind:DestinationRulemetadata:name:productcatalogservicenamespace:product-catalogspec:host:productcatalogservice.product-catalog.svc.cluster.localsubsets:- labels:version:v1name:v1

然后通过 VirtualService流量路由到该子集上面:

yaml
# virtualservice-v1.yamlapiVersion:networking.istio.io/v1beta1kind:VirtualServicemetadata:name:productcatalogservicenamespace:frontendspec:hosts:- productcatalogservice.product-catalog.svc.cluster.localhttp:- route:- destination:host:productcatalogservice.product-catalog.svc.cluster.localsubset:v1

需要注意的是这里的 VirtualService对象需要在 frontend命名空间下面,因为是该命名空间下面的服务来访问 productcatalog的服务。直接应用上面的资源对象到集群中:

bash
kubectlapply-fdestination-v1.yamlkubectlapply-fvirtualservice-v1.yaml

现在我们再浏览器中去访问 Online Boutique项目依然可以正常访问。

由于 productcatalog的数据是从 JSON文件中读取的,所以我们可以修改 productcatalogJSON文件,然后将这个 JSON 文件以 volumes 形式挂载到容器中去,重新部署该服务,将其作为 v2版本的服务。

原始的 products.json数据如下所示:

json
{"products":[{"id":"OLJCESPC7Z","name":"Sunglasses","description":"Add a modern touch to your outfits with these sleek aviator sunglasses.","picture":"/static/img/products/sunglasses.jpg","priceUsd":{"currencyCode":"USD","units":19,"nanos":990000000},"categories":["accessories"]},{"id":"66VCHSJNUP","name":"Tank Top","description":"Perfectly cropped cotton tank,with a scooped neckline.","picture":"/static/img/products/tank-top.jpg","priceUsd":{"currencyCode":"USD","units":18,"nanos":990000000},"categories":["clothing","tops"]},{"id":"1YMWWN1N4O","name":"Watch","description":"This gold-tone stainless steel watch will work with most of your outfits.","picture":"/static/img/products/watch.jpg","priceUsd":{"currencyCode":"USD","units":109,"nanos":990000000},"categories":["accessories"]},{"id":"L9ECAV7KIM","name":"Loafers","description":"A neat addition to your summer wardrobe.","picture":"/static/img/products/loafers.jpg","priceUsd":{"currencyCode":"USD","units":89,"nanos":990000000},"categories":["footwear"]},{"id":"2ZYFJ3GM2N","name":"Hairdryer","description":"This lightweight hairdryer has 3 heat and speed settings. It's perfect for travel.","picture":"/static/img/products/hairdryer.jpg","priceUsd":{"currencyCode":"USD","units":24,"nanos":990000000},"categories":["hair","beauty"]},{"id":"0PUK6V6EV0","name":"Candle Holder","description":"This small but intricate candle holder is an excellent gift.","picture":"/static/img/products/candle-holder.jpg","priceUsd":{"currencyCode":"USD","units":18,"nanos":990000000},"categories":["decor","home"]},{"id":"LS4PSXUNUM","name":"Salt &Pepper Shakers","description":"Add some flavor to your kitchen.","picture":"/static/img/products/salt-and-pepper-shakers.jpg","priceUsd":{"currencyCode":"USD","units":18,"nanos":490000000},"categories":["kitchen"]},{"id":"9SIQT8TOJO","name":"Bamboo Glass Jar","description":"This bamboo glass jar can hold 57 oz (1.7 l) and is perfect for any kitchen.","picture":"/static/img/products/bamboo-glass-jar.jpg","priceUsd":{"currencyCode":"USD","units":5,"nanos":490000000},"categories":["kitchen"]},{"id":"6E92ZMYYFZ","name":"Mug","description":"A simple mug with a mustard interior.","picture":"/static/img/products/mug.jpg","priceUsd":{"currencyCode":"USD","units":8,"nanos":990000000},"categories":["kitchen"]}]}

我们修改 products.json文件,将 idOLJCESPC7Z的商品的 name修改为 Sunglasses V2,图片地址我们也可以替换下,然后将其作为 v2版本的数据。使用下面的命令将其关联到 ConfigMap 中去:

bash
kubectlcreateconfigmapproducts-v2--from-file=products.json-nproduct-catalog

然后将其挂载到容器中去,并为新版本的服务添加一个 version=v2的标签:

yaml
# productcatalog-v2.yamlapiVersion:apps/v1kind:Deploymentmetadata:name:productcatalogservice-v2namespace:product-catalogspec:selector:matchLabels:app:productcatalogservicetemplate:metadata:labels:app:productcatalogserviceversion:v2spec:volumes:- name:productsconfigMap:name:products-v2containers:- env:- name:PORTvalue:"3550"- name:DISABLE_PROFILERvalue:"1"image:gcr.dockerproxy.com/google-samples/microservices-demo/productcatalogservice:v0.9.0imagePullPolicy:IfNotPresentlivenessProbe:grpc:port:3550name:serverports:- containerPort:3550protocol:TCPvolumeMounts:- name:productssubPath:products.jsonmountPath:/src/products.jsonreadinessProbe:grpc:port:3550resources:limits:cpu:200mmemory:128Mirequests:cpu:100mmemory:64MisecurityContext:allowPrivilegeEscalation:falsecapabilities:drop:- ALLprivileged:falsereadOnlyRootFilesystem:truesecurityContext:fsGroup:1000runAsGroup:1000runAsNonRoot:truerunAsUser:1000serviceAccountName:product-catalogterminationGracePeriodSeconds:5

接下来我们将 v2版本的服务部署到集群中去:

bash
kubectlapply-fproductcatalog-v2.yaml-nproduct-catalog

现在我们就包含了两个版本的 productcatalog服务了:

bash
$kubectlgetpods-nproduct-catalog-lapp=productcatalogserviceNAMEREADYSTATUSRESTARTSAGEproductcatalogservice-5b74bdd4d6-wblqn2/2Running033mproductcatalogservice-v2-8678ff9fcc-fzvtj2/2Running03m5s

但是现在我们的服务还是只有一个 v1版本的,因为我们只定义了 v1的子集,要将流量路由到 v2版本的服务上面去,首先我们还是得为 v2版本的服务添加一个 v2的子集,更新上面的 DestinationRule对象,添加一个 v2的子集:

yaml
# destination-v2.yamlapiVersion:networking.istio.io/v1beta1kind:DestinationRulemetadata:name:productcatalogservicenamespace:product-catalogspec:host:productcatalogservice.product-catalog.svc.cluster.localsubsets:- labels:version:v1name:v1- labels:version:v2name:v2

现在我们就拥有了 v1v2两个版本的子集了。接下来我们可以先通过 VirtualService将一小部分流量定向到 v2版去验证下。

yaml
# vs-split.yamlapiVersion:networking.istio.io/v1beta1kind:VirtualServicemetadata:name:productcatalogservicenamespace:frontendspec:hosts:- productcatalogservice.product-catalog.svc.cluster.localhttp:- route:- destination:host:productcatalogservice.product-catalog.svc.cluster.localsubset:v1weight:75- destination:host:productcatalogservice.product-catalog.svc.cluster.localsubset:v2weight:25

这里我们将 v2版本的服务的流量权重设置为 25,然后将其应用到集群中去:

bash
kubectlapply-fvs-split.yaml-nproduct-catalog

创建后我们就可以去访问 Online Boutique项目了,可以多刷新几次页面。

img

但结果却是并没有出现期望中的 V2版本,那我们应该如何去排查问题呢?

还记得之前我们学习过的 istioctl proxy-config命令吗?我们可以使用该命令来查看 envoy 配置中的一些规则是否符合预期。

bash
$kubectlgetpods-nfrontendNAMEREADYSTATUSRESTARTSAGEfrontend-55df6b66cf-5tndm2/2Running046s

首先使用 istioctl proxy-config routes命令来查看 frontend服务的路由规则:

bash
$istioctlproxy-configroutesfrontend-55df6b66cf-5tndm-nfrontendNAMEVHOSTNAMEDOMAINSMATCHVIRTUALSERVICE80frontend-external.frontend.svc.cluster.local:80frontend-external,frontend-external.frontend+1more.../*80frontend.frontend.svc.cluster.local:80frontend,frontend.frontend+1more.../*80productcatalogservice.product-catalog.svc.cluster.local:80productcatalogservice.product-catalog.svc.cluster.local/*productcatalogservice.frontendinbound|8080||inbound|http|80*/*backend*/stats/prometheus*InboundPassthroughClusterIpv4inbound|http|0*/*inbound|8080||inbound|http|80*/*InboundPassthroughClusterIpv4inbound|http|0*/*backend*/healthz/ready*

从上面的输出可以看出包含一个 productcatalogservice.product-catalog.svc.cluster.local:80的虚拟服务,看到这里我们就知道我们的 VirtualService已经生效了。然后再次查看下 endpoint 下面是否包含我们定义的两个子集:

bash
$istioctlproxy-configendpointfrontend-55df6b66cf-5tndm-nfrontendENDPOINTSTATUSOUTLIERCHECKCLUSTER10.100.10.116:5050HEALTHYOKPassthroughCluster10.103.97.248:7070HEALTHYOKPassthroughCluster10.104.11.53:7000HEALTHYOKPassthroughCluster10.108.221.130:8080HEALTHYOKPassthroughCluster10.109.51.243:3550HEALTHYOKPassthroughCluster10.110.175.145:9555HEALTHYOKPassthroughCluster10.111.7.61:50051HEALTHYOKPassthroughCluster10.244.1.126:3550HEALTHYOKoutbound|3550|v1|productcatalogservice.product-catalog.svc.cluster.local10.244.1.126:3550HEALTHYOKoutbound|3550||productcatalogservice.product-catalog.svc.cluster.local10.244.1.128:8080HEALTHYOKinbound|8080||10.244.1.128:8080HEALTHYOKoutbound|80||frontend-external.frontend.svc.cluster.local10.244.1.128:8080HEALTHYOKoutbound|80||frontend.frontend.svc.cluster.local10.244.2.62:3550HEALTHYOKoutbound|3550|v2|productcatalogservice.product-catalog.svc.cluster.local10.244.2.62:3550HEALTHYOKoutbound|3550||productcatalogservice.product-catalog.svc.cluster.local10.98.118.194:9411HEALTHYOKzipkin127.0.0.1:15000HEALTHYOKprometheus_stats127.0.0.1:15020HEALTHYOKagentunix:unix:kind:Servicemetadata:name:productcatalogservicenamespace:product-catalogspec:type:ClusterIPselector:app:productcatalogserviceports:- name:grpcport:80targetPort:3550

这样就可以匹配到了,当然还要记得修改 frontend中访问 productcatalogservice服务的端口为 80

yaml
env:- name:PORTvalue:"8080"- name:PRODUCT_CATALOG_SERVICE_ADDRvalue:"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对象:

yaml
# currency-policy-deny.yamlapiVersion:security.istio.io/v1kind:AuthorizationPolicymetadata:name:currency-policynamespace:currencyspec:selector:matchLabels:app:currencyserviceaction:DENYrules:- from:- source:notRequestPrincipals:["*"] # 不存在任何请求身份(Principal)的 requests

需要注意上面对象的 selector标签选择器,rules里面我们这里没有指定任何身份,表示对任何请求都拒绝,这里我们将该对象应用到 currencyservice服务上面去,然后我们将其应用到集群中去:

bash
kubectlapply-fcurrency-policy-deny.yaml

应用后当我们再次访问 Online Boutique项目时,页面上就会出现类似 rpc error:code =PermissionDenied desc =RBAC:access denied could not retrieve currencies这样的错误:

img

从提示信息可以看出我们的请求被拒绝了。我们还可以查看日志来确定,首先我们将 currencyservice服务的 Envoy 代理日志级别设置为 trace

bash
$exportCURRENCY_POD=$(kubectlgetpod-ncurrency|grepcurrency|awk'{print $1}')$kubectlexec-it$CURRENCY_POD -ncurrency-cistio-proxy--curl-XPOST"http:curl-s-Ihttp:2024-01-16T02:24:06.535248Zdebugenvoyrbacexternal/envoy/source/extensions/filters/http/rbac/rbac_filter.cc:158enforceddenied,matchedpolicyns[currency]-policy[currency-policy]-rule[0]thread=232024-01-16T02:24:06.535256Zdebugenvoyhttpexternal/envoy/source/common/http/filter_manager.cc:946[Tags:"ConnectionId":"652","StreamId":"10429042324993795331"]Preparinglocalreplywithdetailsrbac_access_denied_matched_policy[ns[currency]-policy[currency-policy]-rule[0]]thread=232024-01-16T02:24:06.535365Ztraceenvoyhttpexternal/envoy/source/common/http/filter_manager.cc:530[Tags:"ConnectionId":"652","StreamId":"10429042324993795331"]decodeHeadersfilteriterationabortedduetolocalreply:filter=envoy.filters.http.rbacthread=232024-01-16T02:24:06.535366Ztraceenvoyhttpexternal/envoy/source/common/http/filter_manager.cc:539[Tags:"ConnectionId":"652","StreamId":"10429042324993795331"]decodeheaderscalled:filter=envoy.filters.http.rbacstatus=1thread=23

我们可以在上面的日志中看到一条 enforced denied的消息,表示 currencyservice已设置为屏蔽入站请求了。

当然我们也可以设置允许访问部分工作负载,而不是设置 DENYALL策略,在有的微服务架构中,我们可能会希望确保只有已获授权的服务才能相互通信。

这里我们可以配置访问 currency这个 gRPC 服务的 /hipstershop.CurrencyService/Convert/hipstershop.CurrencyService/GetSupportedCurrencies两个端点的请求即可放行:

yaml
# currency-policy-allow.yamlapiVersion:security.istio.io/v1beta1kind:AuthorizationPolicymetadata:name:currency-policynamespace:currencyspec:selector:matchLabels:app:currencyserviceaction:ALLOWrules:- to:- operation:paths:- /hipstershop.CurrencyService/Convert- /hipstershop.CurrencyService/GetSupportedCurrenciesmethods:- POSTports:- "7000"# rules:# - from:# - source:# principals:["cluster.local/ns/frontend/sa/frontend"]# - from:# - source:# principals:["cluster.local/ns/checkout/sa/checkout"]

正常配置 from.source.principals属性即可,但是我测试后发现不生效。可以通过命令 istioctl proxy-config all <pod>-n currency -o yaml来查看生成的 Envoy 配置。

然后我们将其应用到集群中去:

bash
kubectlapply-fcurrency-policy-allow.yaml

应用后我们再次访问 Online Boutique项目,页面就可以正常显示了。

JWT 认证

Istio 还提供安全访问的身份验证机制,这里我们将使用 JWT Token 来启用身份验证。我们首先需要创建并应用一个强制 JWT 身份验证的策略。

在前面的课程中我们学习过可以使用 jwx命令行工具来生成 JWK(Istio 使用 JWK描述验证 JWT签名所需要的信息)。

使用下面的命令来安装 jwx命令行工具:

bash
$exportGOPROXY="https:$gitclonehttps:$cdjwx$makejwxgo:downloadinggithub.com/lestrrat-go/jwx/v2v2.0.11go:downloadinggithub.com/urfave/cli/v2v2.24.4# ......go:downloadinggithub.com/russross/blackfriday/v2v2.1.0go:downloadinggolang.org/x/sysv0.8.0Installedjwxin/root/go/bin/jwx

下面我们使用 jwx命令行工具生成一个 JWK,通过模板指定 kidyoudianzhishi-key:

bash
$jwxjwkgenerate--keysize4096--typeRSA--template'{"kid":"youdianzhishi-key"}'-orsa.jwk$catrsa.jwk{"d":"AxxxwBw6Jok","dp":"j3xxxuvQ","dq":"zzxxxqQ","e":"AQAB","kid":"youdianzhishi-key","kty":"RSA","n":"5sxxxwV8","p":"-yxxxQ","q":"6zkC_xxxxKw","qi":"LExxxTw"}

然后从 rsa.jwk中提取 JWK公钥:

bash
$jwxjwkfmt--public-key-orsa-public.jwkrsa.jwk$catrsa-public.jwk{"e":"AQAB","kid":"youdianzhishi-key","kty":"RSA","n":"5sxxxV8"}

上面生成的 JWK其实就是 RSA 公钥私钥换了一种存储格式而已,我们可以使用下面的命令将它们转换成 PEM 格式的公钥和私钥:

bash
$jwxjwkfmt-Ijson-Opemrsa.jwk-----BEGINPRIVATEKEY-----MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCym3O0Ik5QGZ8i......-----ENDPRIVATEKEY-----$jwxjwkfmt-Ijson-Opemrsa-public.jwk-----BEGINPUBLICKEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsptztCJOUBmfIqSE8LR5......-----ENDPUBLICKEY-----

接下来我们就可以使用 jwx命令行签发 JWT Token并验证其有效性了:

bash
jwxjwssign--keyrsa.jwk--algRS256--header'{"typ":"JWT"}'-otoken.txt-<<EOF{"iss":"admin@youdianzhishi.com","sub":"cnych001","iat":1700648397,"exp":1708066614,"name":"Yang Ming"}EOF

然后查看生成的 Token 文件内容:

bash
$cattoken.txteyJhbGciOiJSUzI1NiIsImtpZCI6InlvdWRpYW56aGlzaGkta2V5In0......

上面生成 JWT Token 实际上是由下面的算法生成的:

bash
base64url_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,具体的签名算法如下:

bash
RSASHA256(base64UrlEncode(header) + "."+base64UrlEncode(payload),<rsa-public-key>,<rsa-private-key>

最后可以使用 RSA Public Key 验证 JWT Token 的有效性:

bash
$jwxjwsverify--algRS256--keyrsa-public.jwktoken.txt{"iss":"admin@youdianzhishi.com","sub":"cnych001","iat":1700648397,"exp":1700656042,"name":"Yang Ming"}

接下来我们就可以添加一个请求认证策略对象,该策略要求 Ingress 网关指定终端用户的 JWT。

yaml
# jwt-policy.yamlapiVersion:"security.istio.io/v1"kind:RequestAuthenticationmetadata:name:jwt-demonamespace:istio-systemspec:selector:matchLabels:istio:ingressgatewayjwtRules:- issuer:"admin@youdianzhishi.com"# 签发者,需要和 JWT payload 中的 iss 属性完全一致。# forwardOriginalToken:truejwks:|# jwk 公钥数据{"keys":[{"e":"AQAB","kid":"youdianzhishi-key","kty":"RSA","n":"nu3nRXyHjSX6lWI1oY8AGc7GpxXPrjHpWcAeZP8gFkA7gg8f81G8_RVJzCwcRBL71j13mc9Eftk4vJk4yjBgU_QhCyeiVcBXmkJyV0ciTLRttWIouHLw3vaLTaMBZ9r23PdA1r5WmtcjeYaVD8hk7vIDNpLMm1fv0PW6HrbDB4tJNa5C-CZax_qmlcL6XofctVijiPfDV6hnQqHVH0TuESSbTztgVocdC819IsTC7P080veqr2AWLMU-lLUlrfCOrBAs0AR7d8oLuplvdyCEhvTruqwChi6dT72F1vFH5FJl7P3bBpWnQHo1kMJirEwm5oy12NkXE1gSQ-YWw9hQ5A7QayMdbgPl-_DVeWoQXWqqejEITZB0SX1ORJOwBEjF178A3B15YedtHtbM43kHa04gv6LKV7sYXvvW7i6csj2JwUEwrM1AaxaOa94zWL2vyIv09aEmCrsn-E_7vp-mzfUSSkCmbwlXgTaTAyrRRDzVZx5asoY2bXEoRPrhfV1pt8MpAN9GCoWOVwuvEGYGQp3AIioHJsy37UQyN1yzb8byvlOgbadS_mRBe4RZF_Uj_GsOkpm0hf_TS0ZDeHLfqqiM0Cmm6dJl_lKWXVD1zXvIC6qA_NRU5PA8WycC6JbsXBS5aJ7OlI1Uu8B__ERUHljCiwv5XprFVH7SwSyhaws"}]}

直接应用上面的对象到集群中去:

bash
kubectlapply-fjwt-policy.yaml

现在我们去直接访问 Online Boutique项目,可以看到现在依然可以正常访问,但是如果我们请求的时候带上一个无效的 JWT Token,则会返回 401错误:

bash
$curl--header"Authorization:Bearer abcd""http:401

要想正常访问,我们需要使用上面生成的 JWT Token 来进行访问:

bash
$TOKEN=$(cattoken.txt)$curl--header"Authorization:Bearer $TOKEN""http:200

可以看到就可以正常访问了。

当然我们也可以在服务之间进行 JWT 认证,我们还可以在使用 AuthorizationPolicy对象的时候来配置服务之间的 JWT 认证等。

关于我

我的博客主旨:

  • 排版美观,语言精炼;
  • 文档即手册,步骤明细,拒绝埋坑,提供源码;
  • 本人实战文档都是亲测成功的,各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人帮您解决问题,让我们一起进步!

🍀 微信二维码

x2675263825 (舍得), qq:2675263825。

image-20230107215114763

🍀 微信公众号

《云原生架构师实战》

image-20230107215126971

🍀 个人博客站点

https:

版权:此文章版权归 One 所有,如有转载,请注明出处!

链接:可点击右上角分享此页面复制文章链接

上次更新时间:

最近更新