跳到主要内容

实战项目

更新于: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 仓库中。

  • 我们可以通过下面命令来部署该项目:
$ git clone https://github.com/cnych/istio-demo && cd istio-demo
$ kubectl apply -f kubernetes-manifests/namespaces/

正常会输出下面的信息:

namespace/ad created
namespace/cart created
namespace/checkout created
namespace/currency created
namespace/email created
namespace/frontend created
namespace/loadgenerator created
namespace/payment created
namespace/redis created
namespace/product-catalog created
namespace/recommendation created
namespace/shipping created
  • 然后接下来可以部署 ServiceAccount 和 Deployment:
kubectl apply -f kubernetes-manifests/deployments/

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

预期会输出如下结果:

serviceaccount/ad created
deployment.apps/adservice created
serviceaccount/cart created
deployment.apps/cartservice created
serviceaccount/checkout created
deployment.apps/checkoutservice created
serviceaccount/currency created
deployment.apps/currencyservice created
serviceaccount/email created
deployment.apps/emailservice created
serviceaccount/frontend created
deployment.apps/frontend created
serviceaccount/loadgenerator created
deployment.apps/loadgenerator created
serviceaccount/payment created
deployment.apps/paymentservice created
serviceaccount/product-catalog created
deployment.apps/productcatalogservice created
serviceaccount/recommendation created
deployment.apps/recommendationservice created
serviceaccount/redis created
deployment.apps/redis-cart created
serviceaccount/shipping created
deployment.apps/shippingservice created
  • 然后创建 Service 服务:
kubectl apply -f kubernetes-manifests/services/

预期会输出如下结果:

service/adservice created
service/cartservice created
service/checkoutservice created
service/currencyservice created
service/emailservice created
service/frontend created
service/frontend-external created
service/paymentservice created
service/productcatalogservice created
service/recommendationservice created
service/redis-cart created
service/shippingservice created

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

  • 部署完成后我们可以查看该项目的所有 Pod
$ for ns in ad cart checkout currency email frontend loadgenerator \
payment product-catalog recommendation shipping redis; do
kubectl get pods -n $ns
done;
NAME READY STATUS RESTARTS AGE
adservice-599557d587-k78zz 1/1 Running 0 8m14s
NAME READY STATUS RESTARTS AGE
cartservice-d965d797d-x6zm8 1/1 Running 0 7m34s
NAME READY STATUS RESTARTS AGE
checkoutservice-558cb79cd9-f8dv5 1/1 Running 0 2m19s
NAME READY STATUS RESTARTS AGE
currencyservice-7bccdbb75c-fhjjk 1/1 Running 0 8m13s
NAME READY STATUS RESTARTS AGE
emailservice-b77685c45-2l7p5 1/1 Running 0 8m13s
NAME READY STATUS RESTARTS AGE
frontend-8495d678c6-k2l45 1/1 Running 0 52s
NAME READY STATUS RESTARTS AGE
loadgenerator-7dcc798c94-l7lll 1/1 Running 0 8m13s
NAME READY STATUS RESTARTS AGE
paymentservice-5d488686d9-s7qz5 1/1 Running 0 2m19s
NAME READY STATUS RESTARTS AGE
productcatalogservice-784876db87-tpxwx 1/1 Running 0 2m19s
NAME READY STATUS RESTARTS AGE
recommendationservice-67ddb5f6dc-thgrd 1/1 Running 0 8m13s
NAME READY STATUS RESTARTS AGE
shippingservice-576794b87c-94fph 1/1 Running 0 8m12s
NAME READY STATUS RESTARTS AGE
redis-cart-69bcdbcc59-8vjc8 1/1 Running 0 8m13s

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

apiVersion: v1
kind: Service
metadata:
name: frontend-external
namespace: frontend
spec:
type: LoadBalancer
selector:
app: frontend
ports:
- name: http
port: 80
targetPort: 8080

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

kubectl get service frontend-external -n frontend | awk '{print $4}'

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

$ kubectl get svc frontend-external -n frontend
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend-external LoadBalancer 10.103.160.160 <pending> 80:30877/TCP 7m17s

这样我们就可以通过 http://<NodeIP>:30877 来访问该服务了。

  • 哦,shift,我这里报错了

老师的没报错,我的又报错了。。。

就这样吧,这里不再折腾了,仅记录文档。

当然更好的方式是通过 Ingress 来访问该服务,我们可以通过下面命令创建一个 Ingress

$ kubectl -n demo apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend
namespace: frontend
spec:
ingressClassName: nginx
rules:
- host: ob.k8s.local
http:
paths:
- backend:
service:
name: frontend
port:
name: web
path: /
pathType: Prefix
EOF

然后通过 http://ob.k8s.local 来访问该服务(记得解析)。

到这里我们只是将该项目部署到了集群中,但是并没有使用 Istio,接下来我们将使用 Istio 来管理该项目。

  • 首先将上面的命名空间标记为 istio-injection=enabled,这样在该命名空间中的 Pod 就会自动注入 Istiosidecar
for ns in ad cart checkout currency email frontend loadgenerator \
payment product-catalog recommendation shipping redis; do
kubectl label namespace $ns istio-injection=enabled --overwrite
done;
  • 然后我们重启所有的 Pod,让他们重新注入 Istiosidecar
for ns in ad cart checkout currency email frontend loadgenerator \
payment product-catalog recommendation shipping redis; do
kubectl rollout restart deployment -n ${ns}
done;
  • 再次查看这些 Pod 的时候,我们会发现每个 Pod 都变成了两个容器了,其中一个就是新增的 istio-proxy 的容器:
$ for ns in ad cart checkout currency email frontend loadgenerator \
payment product-catalog recommendation shipping redis; do
kubectl get pods -n $ns
done;
NAME READY STATUS RESTARTS AGE
adservice-864dc4fc56-cvp24 2/2 Running 0 83s
NAME READY STATUS RESTARTS AGE
cartservice-b68fbffff-8trmd 2/2 Running 0 83s
NAME READY STATUS RESTARTS AGE
checkoutservice-67f54f5f76-j5cf6 2/2 Running 0 83s
NAME READY STATUS RESTARTS AGE
currencyservice-679fb5dd5d-pbkzz 2/2 Running 0 83s
NAME READY STATUS RESTARTS AGE
emailservice-68889c47d7-4dhg4 2/2 Running 0 82s
NAME READY STATUS RESTARTS AGE
frontend-6fc8b5c99c-6vjc8 2/2 Running 0 82s
NAME READY STATUS RESTARTS AGE
loadgenerator-84df465667-5pvx6 2/2 Running 0 83s
NAME READY STATUS RESTARTS AGE
paymentservice-8668445687-s7fvd 2/2 Running 0 83s
NAME READY STATUS RESTARTS AGE
productcatalogservice-7f8db6d6cc-xjrkh 2/2 Running 0 82s
NAME READY STATUS RESTARTS AGE
recommendationservice-84ddccb8f4-x9lbg 2/2 Running 0 82s
NAME READY STATUS RESTARTS AGE
shippingservice-df977cc6d-h866h 2/2 Running 0 82s
NAME READY STATUS RESTARTS AGE
redis-cart-cdd7d87dd-4w769 2/2 Running 0 82s

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

  • 首先我们我们创建一个用来暴露 frontend 服务的 Gateway
# frontend-gateway.yaml
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: frontend-gateway
namespace: frontend
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"

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

$ kubectl get pods -n istio-system -l app=istio-ingressgateway
NAME READY STATUS RESTARTS AGE
istio-ingressgateway-9c8b9b586-p2w67 1/1 Running 0 36d
$ kubectl get svc -n istio-system -l app=istio-ingressgateway
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
istio-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 服务暴露出去:
# frontend-virtualservice.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: frontend-ingress
namespace: frontend
spec:
hosts:
- "*"
gateways:
- frontend-gateway
http:
- route:
- destination:
host: frontend
port:
number: 80
  • 这两个资源对象非常简单,直接应用到集群中即可:
kubectl apply -f frontend-gateway.yaml
kubectl apply -f frontend-virtualservice.yaml
  • 部署完成后我们就可以通过 istio ingressgateway 的地址 http://<NODE-IP>:31896 来访问该服务了。

img

  • 我们是通过 Istio 的入口网关来访问该服务的,我们可以查看 Istio 的入口网关的日志:
$ kubectl logs -f istio-ingressgateway-9c8b9b586-p2w67 -n istio-system
[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 - -
[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 - -
# ......

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

$ kubectl logs -f frontend-6fc8b5c99c-6vjc8 -c istio-proxy -n frontend
[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
[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
[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
# ......

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

服务治理

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

金丝雀发布

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

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

apiVersion: apps/v1
kind: Deployment
metadata:
name: productcatalogservice
namespace: product-catalog
spec:
selector:
matchLabels:
app: productcatalogservice
version: v1
template:
metadata:
labels:
app: productcatalogservice
version: v1
# ......

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

# destination-v1.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: productcatalogservice
namespace: product-catalog
spec:
host: productcatalogservice.product-catalog.svc.cluster.local
subsets:
- labels:
version: v1
name: v1

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

# virtualservice-v1.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: productcatalogservice
namespace: frontend
spec:
hosts:
- productcatalogservice.product-catalog.svc.cluster.local
http:
- route:
- destination:
host: productcatalogservice.product-catalog.svc.cluster.local
subset: v1

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

kubectl apply -f destination-v1.yaml
kubectl apply -f virtualservice-v1.yaml

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

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

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

// products.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 中去:

kubectl create configmap products-v2 --from-file=products.json -n product-catalog

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

# productcatalog-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: productcatalogservice-v2
namespace: product-catalog
spec:
selector:
matchLabels:
app: productcatalogservice
template:
metadata:
labels:
app: productcatalogservice
version: v2
spec:
volumes:
- name: products
configMap:
name: products-v2
containers:
- env:
- name: PORT
value: "3550"
- name: DISABLE_PROFILER
value: "1"
image: gcr.dockerproxy.com/google-samples/microservices-demo/productcatalogservice:v0.9.0
imagePullPolicy: IfNotPresent
livenessProbe:
grpc:
port: 3550
name: server
ports:
- containerPort: 3550
protocol: TCP
volumeMounts:
- name: products
subPath: products.json
mountPath: /src/products.json
readinessProbe:
grpc:
port: 3550
resources:
limits:
cpu: 200m
memory: 128Mi
requests:
cpu: 100m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
readOnlyRootFilesystem: true
securityContext:
fsGroup: 1000
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 1000
serviceAccountName: product-catalog
terminationGracePeriodSeconds: 5

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

kubectl apply -f productcatalog-v2.yaml -n product-catalog

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

$ kubectl get pods -n product-catalog -l app=productcatalogservice
NAME READY STATUS RESTARTS AGE
productcatalogservice-5b74bdd4d6-wblqn 2/2 Running 0 33m
productcatalogservice-v2-8678ff9fcc-fzvtj 2/2 Running 0 3m5s

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

# destination-v2.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: productcatalogservice
namespace: product-catalog
spec:
host: productcatalogservice.product-catalog.svc.cluster.local
subsets:
- labels:
version: v1
name: v1
- labels:
version: v2
name: v2

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

# vs-split.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: productcatalogservice
namespace: frontend
spec:
hosts:
- productcatalogservice.product-catalog.svc.cluster.local
http:
- route:
- destination:
host: productcatalogservice.product-catalog.svc.cluster.local
subset: v1
weight: 75
- destination:
host: productcatalogservice.product-catalog.svc.cluster.local
subset: v2
weight: 25

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

kubectl apply -f vs-split.yaml -n product-catalog

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

img

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

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

$ kubectl get pods -n frontend
NAME READY STATUS RESTARTS AGE
frontend-55df6b66cf-5tndm 2/2 Running 0 46s

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

$ istioctl proxy-config routes frontend-55df6b66cf-5tndm -n frontend
NAME VHOST NAME DOMAINS MATCH VIRTUAL SERVICE
80 frontend-external.frontend.svc.cluster.local:80 frontend-external, frontend-external.frontend + 1 more... /*
80 frontend.frontend.svc.cluster.local:80 frontend, frontend.frontend + 1 more... /*
80 productcatalogservice.product-catalog.svc.cluster.local:80 productcatalogservice.product-catalog.svc.cluster.local /* productcatalogservice.frontend
inbound|8080|| inbound|http|80 * /*
backend * /stats/prometheus*
InboundPassthroughClusterIpv4 inbound|http|0 * /*
inbound|8080|| inbound|http|80 * /*
InboundPassthroughClusterIpv4 inbound|http|0 * /*
backend * /healthz/ready*

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

$ istioctl proxy-config endpoint frontend-55df6b66cf-5tndm -n frontend
ENDPOINT STATUS OUTLIER CHECK CLUSTER
10.100.10.116:5050 HEALTHY OK PassthroughCluster
10.103.97.248:7070 HEALTHY OK PassthroughCluster
10.104.11.53:7000 HEALTHY OK PassthroughCluster
10.108.221.130:8080 HEALTHY OK PassthroughCluster
10.109.51.243:3550 HEALTHY OK PassthroughCluster
10.110.175.145:9555 HEALTHY OK PassthroughCluster
10.111.7.61:50051 HEALTHY OK PassthroughCluster
10.244.1.126:3550 HEALTHY OK outbound|3550|v1|productcatalogservice.product-catalog.svc.cluster.local
10.244.1.126:3550 HEALTHY OK outbound|3550||productcatalogservice.product-catalog.svc.cluster.local
10.244.1.128:8080 HEALTHY OK inbound|8080||
10.244.1.128:8080 HEALTHY OK outbound|80||frontend-external.frontend.svc.cluster.local
10.244.1.128:8080 HEALTHY OK outbound|80||frontend.frontend.svc.cluster.local
10.244.2.62:3550 HEALTHY OK outbound|3550|v2|productcatalogservice.product-catalog.svc.cluster.local
10.244.2.62:3550 HEALTHY OK outbound|3550||productcatalogservice.product-catalog.svc.cluster.local
10.98.118.194:9411 HEALTHY OK zipkin
127.0.0.1:15000 HEALTHY OK prometheus_stats
127.0.0.1:15020 HEALTHY OK agent
unix://./etc/istio/proxy/XDS HEALTHY OK xds-grpc
unix://./var/run/secrets/workload-spiffe-uds/socket HEALTHY OK sds-grpc

从输出结果可以看到我们的 productcatalogservice 服务下面包含了两个子集,这说明我们的 DestinationRule 已经生效了。但是我们仔细观察就可以看到问题出在什么地方了,我们的 productcatalogservice 服务下面的两个子集的端口都是 3550,但是我们的 VirtualService 中的 productcatalogservice 服务的端口使用的是默认的 80 端口,没有匹配到,所以带权重的路由规则并没有生效。

我们可以去修改 productcatalogservice 的 Service 服务端口为 80

apiVersion: v1
kind: Service
metadata:
name: productcatalogservice
namespace: product-catalog
spec:
type: ClusterIP
selector:
app: productcatalogservice
ports:
- name: grpc
port: 80
targetPort: 3550

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

env:
- name: PORT
value: "8080"
- name: PRODUCT_CATALOG_SERVICE_ADDR
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 对象:

# currency-policy-deny.yaml
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: currency-policy
namespace: currency
spec:
selector:
matchLabels:
app: currencyservice
action: DENY
rules:
- from:
- source:
notRequestPrincipals: ["*"] # 不存在任何请求身份(Principal)的 requests

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

kubectl apply -f currency-policy-deny.yaml

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

img

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

$ export CURRENCY_POD=$(kubectl get pod -n currency |grep currency|awk '{print $1}')
$ kubectl exec -it $CURRENCY_POD -n currency -c istio-proxy -- curl -X POST "http://localhost:15000/logging?level=trace"

默认情况下,日志中不会记录被阻止的授权调用。然后我们可以使用 curl 命令将流量发送到应用,以生成日志:

for i in {0..10}; do
curl -s -I http://192.168.0.100:31896 ; done

然后我们可以查看 istio-proxy 中基于角色的访问权限控制 (RBAC) 相关的日志:

kubectl logs -n currency $CURRENCY_POD -c istio-proxy | grep -m5 rbac

正常会输出如下所示内容:

2024-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'
2024-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
2024-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
2024-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
2024-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 两个端点的请求即可放行:

# currency-policy-allow.yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: currency-policy
namespace: currency
spec:
selector:
matchLabels:
app: currencyservice
action: ALLOW
rules:
- to:
- operation:
paths:
- /hipstershop.CurrencyService/Convert
- /hipstershop.CurrencyService/GetSupportedCurrencies
methods:
- POST
ports:
- "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 配置。

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

kubectl apply -f currency-policy-allow.yaml

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

JWT 认证

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

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

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

$ export GOPROXY="https://goproxy.io"
$ git clone https://github.com/lestrrat-go/jwx.git
$ cd jwx
$ make jwx
go: downloading github.com/lestrrat-go/jwx/v2 v2.0.11
go: downloading github.com/urfave/cli/v2 v2.24.4
# ......
go: downloading github.com/russross/blackfriday/v2 v2.1.0
go: downloading golang.org/x/sys v0.8.0
Installed jwx in /root/go/bin/jwx

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

$ jwx jwk generate --keysize 4096 --type RSA  --template '{"kid":"youdianzhishi-key"}' -o rsa.jwk
$ cat rsa.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 公钥:

$ jwx jwk fmt --public-key -o rsa-public.jwk rsa.jwk
$ cat rsa-public.jwk
{
"e": "AQAB",
"kid": "youdianzhishi-key",
"kty": "RSA",
"n": "5sxxxV8"
}

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

$ jwx jwk fmt -I json -O pem rsa.jwk
-----BEGIN PRIVATE KEY-----
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCym3O0Ik5QGZ8i
......
-----END PRIVATE KEY-----

$ jwx jwk fmt -I json -O pem rsa-public.jwk
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsptztCJOUBmfIqSE8LR5
......
-----END PUBLIC KEY-----

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

jwx jws sign --key rsa.jwk --alg RS256 --header '{"typ":"JWT"}' -o token.txt - <<EOF
{
"iss": "admin@youdianzhishi.com",
"sub": "cnych001",
"iat": 1700648397,
"exp": 1708066614,
"name": "Yang Ming"
}
EOF

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

$ cat token.txt
eyJhbGciOiJSUzI1NiIsImtpZCI6InlvdWRpYW56aGlzaGkta2V5In0......

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

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

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

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

$ jwx jws verify --alg RS256 --key rsa-public.jwk token.txt
{
"iss": "admin@youdianzhishi.com",
"sub": "cnych001",
"iat": 1700648397,
"exp": 1700656042,
"name": "Yang Ming"
}

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

# jwt-policy.yaml
apiVersion: "security.istio.io/v1"
kind: RequestAuthentication
metadata:
name: jwt-demo
namespace: istio-system
spec:
selector:
matchLabels:
istio: ingressgateway
jwtRules:
- issuer: "admin@youdianzhishi.com" # 签发者,需要和 JWT payload 中的 iss 属性完全一致。
# forwardOriginalToken: true
jwks: | # 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"
}
]
}

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

kubectl apply -f jwt-policy.yaml

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

$ curl --header "Authorization: Bearer abcd" "http://192.168.0.100:31896/" -s -o /dev/null -w "%{http_code}\n"
401

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

$ TOKEN=$(cat token.txt)
$ curl --header "Authorization: Bearer $TOKEN" "http://192.168.0.100:31896/" -s -o /dev/null -w "%{http_code}\n"
200

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

当然我们也可以在服务之间进行 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

最后

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