# Istio Helm Chart 详解 - Gateways

> 这是《Istio Helm Chart 详解》系列的第四篇，对 Gateways Chart 进行一些介绍，并讲解一下使用 Helm 创建 Istio Gateway 的方法。

## 前言

前面提到过，Istio 的 Helm Chart，除去用于安装之外，还有部分对 Istio 部署进行调整的能力。Gateways 一节内容，就包含了定制 Istio Ingress/Egress Gateway 的能力。

这个 Chart 的文件结构和其他组件类似，不同的在于内容，它通过对 `values.yaml` 中定义的 Gateways 相关内容的循环遍历，生成不同的 Gateway 单元，下面将会进行讲解和试验。

## `values.yaml` 中的变量定义

~~~yaml
gateways:
  enabled: true

  istio-ingressgateway:
    enabled: true
    labels:
      app: istio-ingressgateway
      istio: ingressgateway
    replicaCount: 1
    autoscaleMin: 1
    autoscaleMax: 5
    resources: {}
      # limits:
      #  cpu: 100m
      #  memory: 128Mi
      #requests:
      #  cpu: 1800m
      #  memory: 256Mi
    cpu:
      targetAverageUtilization: 80
    loadBalancerIP: ""
    serviceAnnotations: {}
    type: LoadBalancer #change to NodePort, ClusterIP or LoadBalancer if need be

    ports:
      ## You can add custom gateway ports
    - port: 80
      targetPort: 80
      name: http2
      nodePort: 31380
    - port: 443
...
~~~

其中包含了一个层次结构：Gateways 的下级除了用于 `requirements.yaml` 使用的 `enabled` 字段之外，还包含一个数组，数组的每个元素定义了一个网关。

`range $key, $spec := .Values`：对 `gateways` 一节的**局部变量**进行遍历，第一层遍历的值用 `$key` 和 `$spec` 两个变量来表示键值对，根据每个键值对的定义，逐个创建资源，下面会提到的 `$spec` 引用就是相当于每个网关控制器的定义变量，`$key` 就是每个网关控制器的名称。

## Chart.yaml

元数据文件，无需赘述。

## autoscale.yaml

首先讲解一下头部的渲染条件：

~~~yaml
{{- range $key, $spec := .Values }}
{{- if and (ne $key "global") (ne $key "enabled") }}
{{- if and $spec.enabled $spec.autoscaleMin }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
    name: {{ $key }}
    namespace: {{ $spec.namespace | default $.Release.Namespace }}
spec:
    maxReplicas: {{ $spec.autoscaleMax }}
    minReplicas: {{ $spec.autoscaleMin }}
    scaleTargetRef:
      apiVersion: apps/v1beta1
      kind: Deployment
      name: {{ $key }}
    metrics:
    - type: Resource
      resource:
        name: cpu
        targetAverageUtilization: {{ $spec.cpu.targetAverageUtilization }}
---
{{- end }}
{{- end }}
{{- end }}
~~~

顾名思义，这个文件是用来创建 [HPA](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) 的，但是整个文件的外层由一个 [`range`](https://docs.helm.sh/chart_template_guide/#looping-with-the-range-action) 语句所包围：

- `{{- if and (ne $key "global") (ne $key "enabled") }}` 将会跳过 `global` 和 `enabled` 键。
- `{{- if and $spec.enabled $spec.autoscaleMin }}`：如果前面读到的 `$spec` 变量中的 `enabled` 和 `autoscaleMin` 都是 `true`，才会进行处理。这里条件跟其它几个文件不同：只有设置了 `autoscaleMin` 的情况下才会渲染 HPA 对象。

引用变量：

- `$key`：也就是网关的名称，例如前面的 `istio-ingressgateway`。
- `$spec.namespace`：可以为这一 Gateway 定义命名空间，如果没有定义，则沿用 Istio 的命名空间，也就是 `Release.Namespace`。
- `$spec.autoscaleMax` 和 `$spec.autoscaleMin`，Gateway 伸缩的上下限。
- `cpu.targetAverageUtilization`：伸缩指标，目标平均 CPU 占用率。

## RBAC 资源

同样的，这里定义了每个网关所对应的 `ServiceAccount`、`ClusterRole` 以及 `ClusterRoleBinding`，用于 RBAC。

Gateway 角色需要对 `thirdpartyresources`、`virtualservices`、`destinationrules` 以及 `gateways` 几种资源进行读写。

看 `meta.name` 的定义可以看出，每个网关都会有自己的 RBAC 资源，命名规则为 `[网关名称]-[Istio 所在的命名空间]`。

另外 ServiceAccount 中引用了全局变量 `imagePullSecrets`。

## `deployment.yaml`

这个模板用于 `Deployment` 的创建过程。这个部署运行的主要服务进程和 Ingress Chart 一样，是 [pilot-agent](https://istio.io/docs/reference/commands/pilot-agent/)。

### 全局变量

- priorityClassName
- hub
- tag
- istioNamespace
- proxy.envoyStatsd
- controlPlaneSecurityEnabled
- defaultResources
- nodeaffinity

### 变量使用细节

- `$spec.labels`：这里可以看出，我们可以使用在 `values.yaml` 中定义标签 `labels` 的方式，为新的 Deployment 指定标签。**标签将同时出现在 Deployment 和下面的 Pod 中。**，从而定义 `Gateway` 资源时，可以用标签来指定对应的控制器。
- `$spec.replicaCount`：可以指定初始副本数量。
- `$spec.ports`：在 `ports` 中定义的各种端口，会在容器中进行发布。
- Gateway 名称在这里还作为 `--serviceCluster` 的值，这一参数在 Sidecar 中一般取值为 `istio-proxy`。
- 如果定义了 `global.istioNamespace`，会使用 `[服务名].[命名空间]` 的方式定义 `zipkin`、`istio-pilot` 的服务地址。
- 根据 `global.proxy.envoyStatsd` 设置 `statsd` 地址。
- 如果 Gateway 定义中包含了资源限制的内容，则会在这里进行包含，否则只会使用缺省资源限制。
- `$spec.additionalContainers` 中还可以定义该 Pod 中额外的容器。
- 如果有加载额外 tls secret 的需求，可以定义在 `$spec.secretVolume` 中。
- 如果有加载额外 Configmap 的需求，可以定义在 `$spec.configVolumes` 中。

## service.yaml

- 和其他元素一样，Service 也是使用循环的方式逐个建立的。
- 服务名称同样也是直接使用 `$key`。
- 端口、命名空间、标签和 Deployment 模板一致。
- `$spec.serviceAnnotations` 用于生成服务注解。
- `selector` 的定义**也和标签定义一致**。
- 如果定义了 `$spec.loadBalancerIP`，这里会给服务的 `loadBalancerIP` 赋值。
- 如果定义了 `.type`，则将服务类型进行修改。

## 测试一下

在 `values.yaml` 的 `gateways` 一节加入这样一段：

~~~yaml
  istio-myingress:
    enabled: true
    namespace: default
    labels:
      app: istio-ingressgateway
      istio: myingress
    replicaCount: 1
    autoscaleMax: 5
    resources: {}
    cpu:
      targetAverageUtilization: 80
    loadBalancerIP: ""
    serviceAnnotations: {}
    type: NodePort
    ports:
    - port: 80
      targetPort: 80
      name: http-myingress
~~~

然后用命令行生成对应的安装文件：

~~~bash
$ helm template istio --name istio -f \
    values-new-gateway.yaml --namespace \
    istio-system > istio-myingress.yaml
~~~

生成的 yaml，用编辑器打开，使用正则表达式 `source:.*?gateways` 进行搜索，会看到生成的内容符合之前的描述，在 `Default` 命名空间中出现了新的 ServiceAccount、ClusterRole、ClusterRoleBinding 资源，因为删除了 `autoscaleMin`，所以不会产生 HPA 对象，同时服务类型也改成了 NodePort。

仔细看看会发现其中有一些问题：

- Pilot、Statsd 等依赖服务的地址还在本命名空间，没有引用 `istio-system` 中的服务。
- ClusterRoleBinding 引用的 ServiceAccount 还是指向了 `istio-system` 中的 ServiceAccount，但是很明显，这是不存在的。

这两点结合起来，足够给这个 Gateway 控制器判了死刑，是不可能正常工作的。如果用这一个文件安装 Istio，这个 Gateway 对应的 Pod 日志一定会出现错误。要修正错误，有三个方式：

- 不再定义 `namespace`。
- 修正 Chart。
- 修改渲染后的 YAML 文件。

所以这里妥协一下，删掉 `namespace` 一行，使用缺省设置，重新渲染安装。

安装完成以后，按照[控制 Ingress 流量](https://preliminary.istio.io/zh/docs/tasks/traffic-management/ingress/)一文的介绍，安装 `httpbin` 服务，并为其设置 Gateway 和 VirtualService（注意替换其中的域名），其中的 Gateway Selector 使用我们新建的网关 `myingress`：

~~~yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: myingress
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "httpbin.example.rocks"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - "httpbin.example.rocks"
  gateways:
  - httpbin-gateway
  http:
    route:
    - destination:
        port:
          number: 8000
        host: httpbin
~~~

创建这两个资源之后，使用 curl 访问 Ingress 服务，会看到正确的结果返回，例如：

~~~bash
$ curl httpbin.example.rocks/ip
{
  "origin": "10.232.0.37"
}
~~~

## 再来一次

前面的测试我们模拟了从头部署 Istio 的方式，如果是一个现存的 Istio 部署，又应该怎样新建网关？

根据前面的分析，可以得出引用的所有全局变量：

- priorityClassName
- hub
- tag
- istioNamespace
- proxy
- controlPlaneSecurityEnabled
- defaultResources
- nodeaffinity
- imagePullSecrets

如果保证这些变量的完整性，并且和正在运行的 Istio 一致；同时关掉其它的不必要的组件渲染，应该就可以达到效果。这样写一个 `values.yaml`：

~~~yaml
global:
  hub: docker.io/istio
  tag: 1.0.2
  proxy:
    envoyStatsd:
      enabled: true
      host: istio-statsd-prom-bridge
      port: 9125
  imagePullPolicy: IfNotPresent
  controlPlaneSecurityEnabled: false
  imagePullSecrets:
    # - private-registry-key
  defaultResources:
    requests:
      cpu: 10m
  priorityClassName: ""
sidecarInjectorWebhook:
  enabled: false
security:
  enabled: false
ingress:
  enabled: false
mixer:
  enabled: false
pilot:
  enabled: false
grafana:
  enabled: false
prometheus:
  enabled: false
servicegraph:
  enabled: false
tracing:
  enabled: false
galley:
  enabled: false
kiali:
  enabled: false
certmanager:
  enabled: false

gateways:
  enabled: true
  istio-ingressgateway:
    enabled: false
  istio-egressgateway:
    enabled: false    
  istio-newingress:
    enabled: true
    labels:
      app: istio-ingressgateway
      istio: newingress
    replicaCount: 1
    # autoscaleMin: 1
    autoscaleMax: 5
    resources: {}
    cpu:
      targetAverageUtilization: 80
    loadBalancerIP: ""
    serviceAnnotations: {}
    type: LoadBalancer
    ports:
    - port: 80
      targetPort: 80
      name: http-newingress
~~~

重新渲染并执行。

修改 httpbin Gateway 定义，将 Selector 变更为 `istio: newingress`，提交后使用 CURL 进行验证，会发现新的 Gateway 已经生效。

## 小结

目前的 Gateway 管理还无法让人满意，多命名空间或者按需调度方面的功能还有很大缺憾；但是借用 Helm Chart 进行大块功能管理的方式还是一个有趣而且可能有效的尝试。
