# Istio 大入门 - Ingress

> 本文涉及细节较多，需读者对 Docker、Kubernetes 以及 Istio 有一定了解和实践经验，否则可能难于操作。

Istio 从 v1alpha3 开始，用 Ingress Gateway 组件替代了符合 Kubernetes 规范的 Ingress Controller，因此对入站流量具有了更大的控制能力，但是用法也有了较大不同。

安装：在使用 Helm 进行 Istio 部署的时候，需要使用下面的设置来启用 Ingress Gateway：

~~~yaml
gateways:
  enabled: true
  istio-ingressgateway:
    enabled: true
~~~

## 基本概念

`VirtualService` 是流量控制的核心组件，起着承上（`Gateway`）启下（`DestinationRule`）的作用。这三种对象在本文中涉及的主要职责：

- `Gateway` 对象：
    - 选择 Gateway Ingress Controller。
    - 设置端点开放方式：域名、端口、入站协议等。
- `DestinationRule`：
    - 定义负载均衡行为。
    - 定义服务版本子集。
- `VirtualService`：
    - 根据 Gateway 选择服务版本。

根据 [VirtualService 对象文档](https://istio.io/docs/reference/config/istio.networking.v1alpha3/#VirtualService)中的说明，`VirtaulService` 对象中的 `gateways` 字段的类型为 `string[]`，包括一或多个字符串值。如果省略该字段内容，会隐式的赋值为 `["mesh"]`，`mesh` 网关指代所有的网格内通信。如果要对外提供服务，就需要定义 `Gateway` 对象，并在 `gateways` 字段中进行赋值。

> 注意，一旦在 `gateways` 中填写了非缺省的对象名称，要继续对内部通信进行流量控制，必须显式的将内置的 `mesh` 对象名称也加入到列表之中。

## 准备工作

### 环境准备

1. 安装和部署 Kubernetes 以及 Istio，并启用 Ingress Gateway 支持。
2. 将备用的两个域名（假设为 `flask.example2.com` 以及 `flask.example1.com`）指向 `istio-ingressgateway` 服务的外部 IP。
3. 为测试域名生成 HTTPS 证书。

### 域名准备

### 工作负载

1. 首先编写一个基于 Flask 的 Python 脚本，并打入镜像，脚本功能很简单，显示环境变量中的 "VERSION"。
1. 接下来编写 Dockerfile，安装 Python3、Flask，并进行构建和推送。
1. 编写 Kubernetes 工作负载文件，常见的 Deployment + Service 模式。
1. 在 Kubernetes 上运行工作负载。

以上涉及代码可以在页面尾部获取。

### 目标规则定义

~~~yaml
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: flask
spec:
  host: flask
  trafficPolicy:
    loadBalancer:
      simple: RANDOM
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
~~~

这里根据工作负载中的标签，将 `flask` 服务拆分为 `v1` 和 `v2` 两个版本。

## 在外网开放服务

这里我们设计，将所有外网请求经过 `flask.example1.com` 路由到 `v2` 版本上。

首先要编写一个 `Gateway` 定义：

~~~yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: flask-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - flask.example1.com
        - flask.example2.com
~~~

这里定义了一个 `Gateway`，其中要求 80 端口响应两个域名的请求。接下来定义一个 `VirtualService`：

~~~yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
    name: flask
spec:
    hosts:
    - "flask.example1.com"
    gateways:
    - flask-gateway
    http:
    - route:
        - destination:
            host: flask
            subset: v2
~~~

然后使用 curl 进行访问：`curl http://flask.example1.com` 会发现返回了字符串 "v2"，证明我们的定义生效了，该域名指向了 `v2` 服务。

再用 curl 访问第二个域名 `curl http://flask.example2.com`，会看到返回了 404，这是因为我们的 `Gateway` 虽然接收了请求，但是并没有服务路由完成这一请求。那么可以再定义一个 `VirtualService`，让 `flask.example2.com` 域名指向 `v1`：

~~~yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
    name: flask1
spec:
    hosts:
    - "flask.example2.com"
    gateways:
    - flask-gateway
    http:
    - route:
        - destination:
            host: flask
            subset: v1
~~~

再次使用 `curl http://flask.example2.com`，访问，会看到返回结果变成了 `v1`，证明我们的服务定义生效了。

## 将一个域名服务升级为 tls

随意修改一个需求，要求开放 `flask.example2.com` 为 http，而 `flask.example1.com` 升级为 https，既然涉及 https，就要用到证书，[官方文档（中文版哦）](https://istio.io/zh/docs/tasks/traffic-management/secure-ingress/)陈述如下：

> 具体说来就是使用 kubectl 命令在命名空间 istio-system 中创建一个 secret 对象，命名为 istio-ingressgateway-certs。Istio 网关会自动载入这个 secret。
> 这里的 secret 必须 在 istio-system 命名空间中，并且命名为 istio-ingressgateway-certs，否则就不会被正确载入，也就无法 Istio gateway 中使用了。

接着是使用命令为 `flask.example1.com` 的证书创建 `Secret`：

~~~shell
$ kubectl create -n istio-system secret tls \
    istio-ingressgateway-certs \
    --key key.pem --cert cert.pem
~~~

最后修改我们的 `Gateway` 定义：

~~~yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: flask-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - flask.example2.com
    - port:
        number: 443
        name: https-rocks
        protocol: HTTPS
      tls:
        mode: SIMPLE
        serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
        privateKey: /etc/istio/ingressgateway-certs/tls.key
      hosts:
        - flask.example1.com
~~~

然后再次使用 curl 分别访问 `https://flask.example1.com/version` 以及 `http://flask.example2.com/version`，会发现返回了预期结果，而一旦错用 `http` 和 `https`，就会出现异常，这是因为我们的 `Gateway` 限制了端口。

这里如此操作的原因可以在他的部署中找到：

~~~shell
$ kubectl get deployment istio-ingressgateway \
    -o yaml -n istio-system
~~~

返回的 YAML 中会有这么一段

~~~yaml
...
- mountPath: /etc/istio/ingressgateway-ca-certs
    name: ingressgateway-ca-certs
    readOnly: true
...
volumes:
- name: ingressgateway-certs
secret:
    defaultMode: 420
    optional: true
    secretName: istio-ingressgateway-certs
...
~~~

上方代码可以看到，Ingress Gateway 中用可选方式加载了一个名称为 `istio-ingressgateway-certs` 的 `Secret`，并 Mount 到了 `/etc/istio/ingressgateway-ca-certs` 目录之中，这就是上文中要求我们固定 Secret 名称和命名空间的原因。众所周知，`tls` 类型的 `Secret` 加载后会成为 `tls.key` 和 `tls.crt` 两个文件，因此在我们的 `Gateway` 定义中就使用了 `/etc/istio/ingressgateway-certs/tls.crt` 这样的文件名。

## 所有域名都升级为 tls

根据上一节的描写，不难发现按照官方文档，一个 `Gateway` 是无法处理两个域名的 https 的：

1. tls secret 只能包含一个证书对。
2. 泛域名证书可以完成这一任务，但因为 Envoy 的限制，这里无法同时使用两个泛域名。

讨论到这里就很明显了，关键在于如何加载多个证书对，可以修改前面所说的加载指令为加载多个 `Secret`，或者干脆换成 `Configmap`，当然这样会引起服务中断，`Configmap` 用于存放证书也略显粗糙——好在我们还可以换用 `Generic` 类型的证书，这里我们可以删除原有 Secret 重新创建 ：

~~~shell
kubectl delete secret istio-ingressgateway-certs \
    -n istio-system

kubectl create secret generic  \
    istio-ingressgateway-certs \
    -n istio-system \
    --from-file=rocks-crt.pem \
    --from-file=rocks-key.pem \
    --from-file=xyz-crt.pem \
    --from-file=xyz-key.pem
~~~

接下来改造我们的 `Gateway` 定义：

~~~yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: flask-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 443
      name: https-rocks
      protocol: HTTPS
    tls:
      mode: SIMPLE
      serverCertificate: /etc/istio/ingressgateway-certs/rocks-crt.pem
      privateKey: /etc/istio/ingressgateway-certs/rocks-key.pem
    hosts:
    - "flask.example1.com"
  - port:
      number: 443
      name: https-xyz
      protocol: HTTPS
    tls:
      mode: SIMPLE
      serverCertificate: /etc/istio/ingressgateway-certs/xyz-crt.pem
      privateKey: /etc/istio/ingressgateway-certs/xyz-key.pem
    hosts:
    - "flask.example2.com"
~~~

这样一来，我们开放了两个 HTTPS 端口，各自使用不同的证书，分别都可以通过 `curl` 命令获得正确结果了。

## 补充

`tls.mode` 实际还支持双向和透传两种方式，都可以在流量控制参考中找到相关内容。

## 涉及链接

1. Istio 流量控制参考：`https://istio.io/docs/reference/config/istio.networking.v1alpha3/`
1. 用 HTTPS 加密 Gateway：`https://istio.io/zh/docs/tasks/traffic-management/secure-ingress/`

## 代码

### Python 脚本

~~~python
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

from flask import Flask
import os

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

@app.route("/version")
def getversion():
    return os.getenv("VERSION")
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80, debug=True)
~~~

### Dockerfile

~~~dockerfile
FROM alpine
RUN  apk add --update --no-cache python3 && \
  mkdir /web && \
  pip3 install Flask
COPY server.py /web
CMD "/web/server.py"
~~~

### Kubernetes 工作负载

~~~yaml
apiVersion: v1
kind: Service
metadata:
  name: flask
  labels:
    app: flask
spec:
  ports:
    - name: http
      port: 80
  selector:
    app: flask
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flask-v1
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: flask
        version: v1
    spec:
      containers:
        - image: 'abc:25000/python-flask-server:v2'
          imagePullPolicy: IfNotPresent
          name: flask
          env:
            - name: VERSION
              value: v1
          ports:
            - containerPort: 80
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: flask-v2
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: flask
        version: v2
    spec:
      containers:
        - image: 'abc:25000/python-flask-server:v2'
          imagePullPolicy: IfNotPresent
          name: flask
          env:
            - name: VERSION
              value: v2
          ports:
            - containerPort: 80
~~~

