首先我们来看一下Istio大概是个什么东西呢?

我们经常听到的,和它绑定的词汇就是service mesh,但是如果又要追溯service mesh,然后看的越来越多,搞得自己都迷糊了。所以,我们就看Istio。

Istio是架构与Kubernetes之上的一个服务治理架构,我们可以看一下它在官网上的架构图。

k8s和istio k8s和istio的关系介绍_字段

Istio在逻辑上分为数据平面控制平面。可以在图上看到控制平面,它负责了路由,策略配置,收集信息,身份认证等多方面的功能;而数据平面就是下方的,pod集合,每个pod中除了我们部署的服务容器外,还包含一个proxy容器,它是由Envoy实现的,控制层的那些功能的实现,都要依托于它。

接下来,我们不扯那些概念,理念,开始的时候,看的越多越懵。我们从实操开始,一点一点讲起。在进行实操之前,首先你需要有一定的Kubernetes的知识储备,不然后面的内容仍然是云里雾里。这里只讲Istio,所以如果没有Kubernetes的相关知识,自己去补。


项目部署

首先,我们需要做什么呢?当然是启动服务啊,连服务都启动不了,要它何用?
所以我们就从一个简单服务启动开始。我们需要一个YAML文件,用于定义k8s中的资源。
我们先写一个最简单的,部署单一服务。

apiVersion: v1
kind: Service
metadata:
  name: route-guide
  labels:
    app: route-guide
spec:
  ports:
  - name: http
    port: 8848
  selector:
    app: route-guide
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: route-guide-v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: route-guide
        version: v1
    spec:
      containers:
      - image: yubotao/route-guide:v1
        imagePullPolicy: IfNotPresent
        name: route-guide
        ports:
        - containerPort: 8848
---

上面便是一个简单的部署模板,其中的Deployment是k8s中的概念,用于控制pod;Service也是k8s的概念,用于代表后端的pod,是一个抽象,方便服务间的调用。需要注意的是,Deployment中的containerPort和Service中的port需要一致。Deployment中的replicas就是用于控制pod的数量的。

有了部署模板后,我们需要使用k8s命令及istio命令进行手动部署,如下:

kubectl apply -f <(istioctl kube-inject -f <filename>) [-n <namespace>]

上面的命令含义如下,<>中的内容是替换内容,[]中的内容是可选内容,第一个 -f 后的 < 是文件传输的命令,-f 参数表示使用的是文件;-n 参数表示命名空间;kubectl 为k8s的控制命令,istioctl 为istio的控制命令;kube-inject 参数是istio对我们的k8s部署模板文件加入了istio需要的相当部分配置,可以通过 istioctl kube-inject -f <filename> > <anotherfile> 命令,来对比<filename><anothername>中的内容,来观察istio添加了什么。

新建完成后,我们需要看一下pod的运行状态,保证它们是Running,正常运作。
使用命令 kubectl get pod [-n <namespace>] 来查看。

到此,我们就部署完pod和服务了,服务之间的互相调用,在同一命名空间下,使用serviceName:port的形式即可访问,不同命名空间下,通过serviceName.namespace:port来访问服务。

可以说,到这里,我们就完成了最基本的功能实现了,因为istio自带的服务注册和发现功能让我们可以实现上面的功能。


流量管理

首先是流量管理,这也是service mesh,或者说istio最值得称道的地方了。
首先需要介绍的就是外部如何访问istio内部的服务。因为istio的内部与外部是隔离的,所以说我们需要一个门,让流量进来,这个门就是gateway。

在讨论gateway之前,我们需要说一下,istio有关路由管理的四种类型模板:VirtualServiceDestinationRuleGatewayServiceEntry
这里我分别简单的介绍一下。
VirtualService:定义了在istio服务网格中控制如何路由到一个服务的请求的规则。
DestinationRule:配置了适用于VirtualService路由发生后的请求策略,由服务所有者撰写。
Gateway:描述了一个运行在网格边缘的负载均衡器,用于接受传入或传出的HTTP/TCP连接。
ServiceEntry:允许向Istio的内部服务注册表中添加其他条目,以便网格中自动发现的服务可以访问/路由到这些手动指定的服务。

可能你看了上面的说明还是看不懂,我们还是先来看例子,边看边说。
首先我们回到上文,看一个gateway的模板定义。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: route-gateway
spec:
  selector:
    istio: ingressgateway # use Istio default gateway implementation
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: route-virtualservice
spec:
  hosts:
  - "*"
  gateways:
  - route-gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        port:
          number: 8848
        host: route-guide
---

我们看到这里gateway和VirtualService搭配使用。首先,在VirtualService中的gateways对应了Gateway的name;其次两者的hosts也对应(目前来看,直接写”*”是最简单方便地);最后,VirtualService中地http定义了匹配前缀,及路由到的后端服务。这个匹配前缀需要多提一嘴,就是改前缀必须要在后端服务中,比如prefix值为/has/something/,那么,后端服务中就必须要有相应的url何其匹配,且该gateway只能路由到这个路径上;另外,gateway目前只能存在一个,即使不同命名空间,也只能在存在一个,如果出现多个,则最早声明的生效,其余不生效,不知道后续是否会修复这个bug。

讲到这里,应该对VirtualService有个直观的认识了,就是之前的那句话,它控制如何路由到一个服务。

现在我们看到外部访问集群内部,那内部互相访问怎么搞呢?
这个我不说,你可以翻到前面去看看。

接下来我们看看如何从集群内部访问外部。比如我们的服务启动的时候都要连接外部的rds,如果不能从内部访问外部服务,我们的服务就无法启动。

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: rds-entry
spec:
  hosts:
  - rds地址
  ports:
  - number: 3306
    name: rds
    protocol: TCP

之前我们提到,使用ServiceEntry可以访问外部服务,正如我们上面定义的。其中需要注意的地方是hosts填写rds的外部访问地址,域名或者ip(ip没有测试,猜测可以),number对应port端口,其中的protocol需要注意,因为mysql传输时TCP连接,所以协议用的时tcp,如果连接的外部服务用的时HTTP,需要注意修改。

接下来,我们看一下如何路由流量,以及关于服务请求的一些管理。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: istio-route
spec:
  hosts:
    - istio-route
  http:
  - route:
    - destination:
        host: istio-route
        subset: v1
      weight: 50
    - destination:
        host: istio-route
        subset: v2
      weight: 50
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: istio-route
spec:
  host: istio-route
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
---

我们可以看到这是一个版本管理的路由选择,可以将请求平均分配给两个版本服务。

这里我们可以看到DestinationRule的作用,正如它的名字,表示目的地。也就是说VirtualService中的
destination:subset 和 DestinationRule 中 subsets 关联,而DestinationRule 中 subsets的version,就和Deployment建立联系了。

此外,我们可以模拟网络延迟

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: istio-route
spec:
  hosts:
  - istio-route
  http:
  - fault:
      delay:
        percent: 100
        fixedDelay: 2s
    route:
    - destination:
        host: istio-route
        subset: v1

这表示,对于istio-route的请求,100%延迟2s,可以用于模拟网络波动。

当然我们还可以设置熔断,不过在这里就不展开了。


安全策略

讲到安全,我们需要说的内容就有点多了。首先你需要了解RBAC,然后还要知道k8s中的相关概念。这里还是去繁就简。

我们只看两个方面,一个是服务间开启互相TLS认证,另一个就是RBAC策略。

首先说TLSTransport Layer Security(安全传输层协议)
当我们开启相互TLS认证的时候,服务到服务之间的通信都经过了加密。而对应的密钥/证书是放在Envoy构成的Istio proxy中,由架构中的Citadel完成加解密等。

我们在安装istio的时候,可以以两种方式进行安装,一种是默认没有TLS认证的;另一种是默认带有TLS认证的。

首先我们看一下,如果使用没有TLS认证的安装方式,我们需要开启服务间互相TLS访问应该怎么做:

  • 我们首先需要一个策略;
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "example-1"
  namespace: "foo"
spec:
  peers:
  - mtls:
  • 接下来,需要一个开启互相TLS的目标规则;
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "example-1"
  namespace: "foo"
spec:
  host: "*.foo.svc.cluster.local"
  trafficPolicy:
    tls:
      mode: ISTIO_MUTUAL

我们稍微解释一下,上面的策略表示的是,我们在foo命名空间定义了一个命名空间级的策略,但是这个策略是干什么用的,就需要我们定义一个相应的目标规则(注意该命名空间下如果由其他目标规则,则需要进行相应的调整,尽量保证只有一个);下面的目标规则表示,我们在命名空间foo中的所有服务,都开启了互相TLS认证机制,也即其他命名空间的服务向foo中的服务发送请求时,需要进行TLS认证。

那如果我们想要取消互相TLS认证时(针对默认为开启TLS的安装模式,否则直接删除对应规则和策略即可;另一种情况是,使用服务级的策略覆盖命名空间级的策略),该怎么做呢?

  • 同样是先定义一个策略;
apiVersion: "authentication.istio.io/v1alpha1"
kind: "Policy"
metadata:
  name: "example-3"
spec:
  targets:
  - name: two

这次我们可以看到,我们是直接针对某一服务进行策略的配置。

  • 然后我们定义相应的目标规则:
apiVersion: "networking.istio.io/v1alpha3"
kind: "DestinationRule"
metadata:
  name: "example-3"
spec:
  host: two.test-istio.svc.cluster.local
  trafficPolicy:
    tls:
      mode: DISABLE

上述策略很明显,针对的是test-istio命名空间中的two服务,取消了它的互相TLS认证。

那么我们把TLS就简单的介绍到这里吧。

接下来介绍RBAC
RBAC:基于角色的权限访问控制(Role-Based Access Control)。这个概念我们应该听烂了,有很多框架就是用来做这个的,比如shiro,spring security都可以做。
但是!istio中的RBAC是针对服务及服务管理者而言的,并不是我们应用层面的,但是功能性是一致的。

我们通过定义相应的服务角色(类似于服务的管理者),指定它的访问权限,然后为每个服务绑定相应的服务角色,最后达到通过不同的服务角色来限制对不同服务的访问。

当然,这里面也同样包含了相当多的概念,这里我简单的阐述一下。

  • 首先我们要保证我们开启了互相TLS认证,然后我们需要新建一些服务账户,并在这些账户下部署服务;
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test-one
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test-two
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: one
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: one
        version: v1
    spec:
      serviceAccountName: test-one
      containers:
      - image: yubotao/istio-one:test
        imagePullPolicy: IfNotPresent
        name: one
        ports:
        - containerPort: 8180
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: two
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: two
        version: v1
    spec:
      serviceAccountName: test-two
      containers:
      - image: yubotao/istio-two:test
        imagePullPolicy: IfNotPresent
        name: two
        ports:
        - containerPort: 8080

如上分别定义了两个ServiceAccounts,并分别部署了服务。

  • 接下来,我们需要开启RBAC,我们看一下是如何开启RBAC的;
apiVersion: "config.istio.io/v1alpha2"
kind: authorization
metadata:
  name: requestcontext
  namespace: istio-system
spec:
  subject:
    user: source.user | ""
    groups: ""
    properties:
      app: source.labels["app"] | ""
      version: source.labels["version"] | ""
      namespace: source.namespace | ""
  action:
    namespace: destination.namespace | ""
    service: destination.service | ""
    method: request.method | ""
    path: request.path | ""
    properties:
      app: destination.labels["app"] | ""
      version: destination.labels["version"] | ""
---
apiVersion: "config.istio.io/v1alpha2"
kind: rbac
metadata:
  name: handler
  namespace: istio-system
spec:
  config_store_url: "k8s://"
---
apiVersion: "config.istio.io/v1alpha2"
kind: rule
metadata:
  name: rbaccheck
  namespace: istio-system
spec:
  match: destination.namespace == "test-istio"
  actions:
  - handler: handler.rbac
    instances:
    - requestcontext.authorization

我们可以看到,上述的定义都是针对命名空间istio-system的,而需要修改的部分就在rule中的match部分,我们需要在哪个命名空间开启RBAC,就在这里的destination.namespace字段填上相应内容。

此时,由于存在RBAC策略,我们可以看到服务之间的访问是不通的,会出现错误。

开启RBAC后,我们看一下它的效果,首先我们测试命名空间级的RBAC控制访问策略

apiVersion: "config.istio.io/v1alpha2"
kind: ServiceRole
metadata: 
  name: service-viewer
  namespace: test-istio
spec:
  rules:
  - services: ["*"]
    methods: ["*"]
    constraints:
    - key: "app"
      values: ["one","two"]
---
apiVersion: "config.istio.io/v1alpha2"
kind: ServiceRoleBinding
metadata:
  name: bind-service-viewer
  namespace: test-istio
spec:
  subjects:
  - properties:
      namespace: "istio-system"
  - properties:
      namespace: "test-istio"
  roleRef:
    kind: ServiceRole
    name: "service-viewer"

上述ServiceRole定义了一个service-viewer服务角色,它可以对两个应用one/two(在constraints字段体现)的所有服务(rules中对应字段)有所有的访问权限方法,方法包含GET,POST,HEAD等,不明确的话,就推荐使用“*”。

而下面的ServiceRoleBinding就是将刚才定义的ServiceRole绑定到test-istio命名空间上,这样就可以保证该命名空间的服务是互通的。

我们看到,上面出现三个名词,ServiceAccount、ServiceRole、ServiceRoleBinding;那么它们之间是什么关系呢?
首先Istio RBAC定义了ServiceRole和ServiceRoleBinding。ServiceRole中的constraints的字段和“authorization”模板中的action部分的properties一一对应。也就是说action中properties有什么,constraints中才能写什么。而ServiceRoleBinding和ServiceRole一一绑定,在roleRef中可以体现出来。而其subjects中的属性必须和“authorization”模板中的subject部分的各属性匹配(“user” or “groups” or one of the “properties”)。所以说,ServiceRole和ServiceRoleBinding离不开启动RBAC的“authorization”模板。

ServiceAccount是k8s中的身份与权限控制,它为pod中的进程提供身份信息,当pod中的进程与apiserver联系时,就带有特定的ServiceAccount。换句话说,如果pod和ServiceAccount做了绑定,那么只有在该ServiceAccount下,才能够访问pod中的资源,否则无法访问。这也就是为什么开启RBAC后,会出现权限问题,拒绝访问服务;而没开启RBAC时,就不存在这些影响。

接下来,我们看一下服务级的访问控制。服务级的访问控制就需要针对每个服务做控制了,以我们上面的例子,需要对one和two分别做访问控制,如果有其他服务在调用链路上,还需要新加策略。

  • 首先我们看one服务的策略,保证访问one是畅通的;
apiVersion: "config.istio.io/v1alpha2"
kind: ServiceRole
metadata:
  name: one-viewer
  namespace: test-istio
spec:
  rules:
  - services: ["one.test-istio.svc.cluster.local"]
    methods: ["GET"]
---
apiVersion: "config.istio.io/v1alpha2"
kind: ServiceRoleBinding
metadata:
  name: bind-one-viewer
  namespace: test-istio
spec:
  subjects:
  - user: "*"
  roleRef:
    kind: ServiceRole
    name: "one-viewer"

这里和命名空间级策略有所不同,ServiceRole没有constraints,ServiceRoleBinding使用的是user,代表每个服务(和服务对应的pod绑定的ServiceAccount)。这里是保证gateway能够访问到one服务。

  • 接下来就是需要保证one服务能够访问到two服务了。
apiVersion: "config.istio.io/v1alpha2"
kind: ServiceRole
metadata:
  name: two-viewer
  namespace: test-istio
spec:
  rules:
  - services: ["two.test-istio.svc.cluster.local"]
    methods: ["GET"]
---
apiVersion: "config.istio.io/v1alpha2"
kind: ServiceRoleBinding
metadata:
  name: bind-two-viewer
  namespace: test-istio
spec:
  subjects:
  - user: "cluster.local/ns/test-istio/sa/test-one"
  roleRef:
    kind: ServiceRole
    name: "two-viewer"

和上面类似,不多说了。不过,这里在ServiceRoleBinding处做了更精细的设置,在subject中的user里,我们指定了特定的ServiceAccount(它和对应服务的pod进行了绑定。)。在Istio中的服务账户形式为: “spiffe://<domain>/ns/<namespace>/sa/<serviceaccount>” ;domain是当前的cluster.local

到此,我们就基本把istio的常用安全策略等内容讲完了。


追踪监控

这一部分是比较简单的,不需要在应用代码中插入对接代码了,直接可以通过istio完成对应中间件的部署,并且都是现成模板,唯一一点就是,你需要会用相应的软件。
目前提供的有Jaeger、Zipkin(追踪);Prometheus(metric);Grafana(istio监控的图形界面,这个挺不错的);Service Graph(服务间调用关系)等。其中绝大部分中间件都是以Prometheus为基的,所以目前0.8版本默认就安装好Prometheus了。

相关的安装之类的可以看翻译的文档,没什么好说的。



终于,目前关于Istio所暴露出来的常用功能就介绍完了,还有相当的待开发功能,能力有限,暂时没有相关资料,有一点是我比较在意的,就是Istio的Mixer这个概念以及它所提供的功能,这个应该是一个功能比较强大的可插拔组件的功能模块,如果有余力可以研究研究这个。