# 在 Knative 上部署 12 要素应用程序

原文：[Deploying 12 Factor Apps to Knative](https://starkandwayne.com/blog/introduction-to-knative/)

作者：[Dr Nic Williams](https://twitter.com/starkandwayne)

> 我尝试按照本文内容在 Azure 上使用 ACS-Engine 部署的 Kubernetes 上进行了测试，版本部分的案例得到了相反结果，不过还是按照原文发了出来，作为一个获取感性认识的入门还是不错的。

Google Next18 活动中，Google 宣称将会把 GKE 上的无服务器插件以 [Knative](https://github.com/knative/) 的名称进行开源。当时它被描述为无服务器平台的构建块，由此推断，Knative 可能需要 Google、Pivotal 或者 RedHat 的协助才能使用。这可能是开源的古怪时机。从我最初的摸索来看，Knative 能工作；当我把 Heroku/Cloud Foundry buildpacks 加入进来之后，整个系统变得越来越像 Heroku/Cloud Foundry，相对于原始 Kubernetes，我更加了解和喜爱这一系统。

本文中我们会将 Knative 安装到你自己的 Kubernetes 集群中（`knctl install`），然后用 Knative 做些有趣的事情（`knctl deploy`）。

Knative 能够为在 Kubernetes 集群上运行无状态应用的运维人员带来很多惊喜。对我来说，最引人入胜的一点就是伸缩性：在高负载时候进行扩容，没人喜欢你的应用了，就会一直缩容到 0。

[下载和安装](https://github.com/cppforlife/knctl/releases) Knative 客户端工具 `knctl`，然后就可以在你的 Kubernetes 上部署 Knative，然后发布你的应用了。

在 MacOS 中，可以利用我们的 Homebrew tap 进行安装：

`brew install starkandwayne/kubernetes/knctl`

这里我假设你再使用 Minikube。在 Minikube 中，可以使用 Node Port 代替 Load balancer：

```bash
minikube start --memory=8192 --cpus=3 \
  --kubernetes-version=v1.11.3 \
  --vm-driver=hyperkit \
  --bootstrapper=kubeadm

knctl install --node-ports --exclude-monitoring
```

可以参考 [Knative 文档](https://github.com/knative/docs/tree/master/install)来获取在其它类型 Kubernetes 集群上进行部署的要点。

`knctl install` 命令可能要花上几分钟，甚至是十分钟或者更多。这个过程中需要下载大概一打镜像。不管是互联网带宽还是镜像尺寸都可能有变化，所以坐下放松一会，或者出去走走也好。

如果 `knctl install` 失败了，可能是你的 Internet 比较慢，Docker 镜像在命令超时之前还没能完成下载。运行 `kubectl get pods --all-namespaces` 直到所有 Pod 都在运行，然后再次运行 `knctl install` 命令继续完成安装过程。

现在你的 Kubernetes 就是无服务器架构了，不错吧。

可以运行 `kubectl get pods --all-namespaces` 看看原始的 Knative 的 Pod 们。

```bash
$ kubectl get pods --all-namespaces
NAMESPACE         NAME                                        READY   STATUS      RESTARTS   AGE
istio-system      istio-citadel-7d8f9748c5-zgm9x              1/1     Running     0          21m
istio-system      istio-cleanup-secrets-j4pkx                 0/1     Completed   0          21m
istio-system      istio-egressgateway-676c8546c5-dnwsd        1/1     Running     0          21m
istio-system      istio-galley-5669f7c9b-g774b                1/1     Running     0          21m
istio-system      istio-ingressgateway-5475685bbb-q5f2x       1/1     Running     0          21m
istio-system      istio-pilot-5795d6d695-9klrz                2/2     Running     0          21m
istio-system      istio-policy-7f945bf487-2wh88               2/2     Running     0          21m
istio-system      istio-sidecar-injector-d96cd9459-7knkm      1/1     Running     0          21m
istio-system      istio-statsd-prom-bridge-549d687fd9-lcmb7   1/1     Running     0          21m
istio-system      istio-telemetry-6c587bdbc4-t4jql            2/2     Running     0          21m
istio-system      knative-ingressgateway-7f4477dd99-n9wz2     1/1     Running     0          4m
knative-build     build-controller-7dcc4b7544-rkgwb           1/1     Running     0          4m
knative-build     build-webhook-fb6484576-sr4fk               1/1     Running     0          4m
knative-serving   activator-77d46b585d-b6g8n                  2/2     Running     0          4m
knative-serving   controller-85768cfd45-t8ktc                 1/1     Running     0          4m
knative-serving   webhook-56dd548f8-hjw44                     1/1     Running     0          4m
...
```

## 部署预构建的应用

接下来我们试试用一个现有的 Docker 镜像来作为自动伸缩的无状态应用运行到 Knative 上，在当前 Kubernetes 命令空间中：

```bash
kubectl create ns helloworld

knctl deploy \
      --namespace helloworld \
      --service hello \
      --image gcr.io/knative-samples/helloworld-go \
      --env TARGET=Rev1
```

命令执行后会输出一些信息，表明 `hello` 服务已经创建，它的第一个版本 `hello-00001` 已经创建，并且被标记为 `latest` 和 `previous`（第一个版本）。

```bash
Name  hello

Waiting for new revision to be created...

Tagging new revision 'hello-00001' as 'latest'

Tagging new revision 'hello-00001' as 'previous'

Succeeded
```

我们可以用一个 curl 请求，发送到 Knative 的路由层，来调用我们的 `hello` 服务：

```shell
$ knctl curl --namespace helloworld --service hello
Running: curl '-H' 'Host: hello.helloworld.example.com' 'http://192.168.64.8:32380'

Hello World: Rev1!

Succeeded
```

如果没有马上显示 `Hello World: Rev1!`，可能是因为你的系统还在下载应用镜像、可以稍后重试。

我使用的是 Minikube 中的 NodePort Ingress，这意味着我不能设置漂亮的 DNS 路由。以后我会讨论一下公共负载均衡、DNS、Knative 路由以及 `https://github.com/cppforlife/kwt`。

`knctl deploy` 之后，我们的 Kubernetes 用单 Pod 的形式运行这一服务：

```shell
$ kubectl get pods --namespace helloworld
NAME                                      READY   STATUS    RESTARTS   AGE
hello-00001-deployment-5864685cbc-v8r7n   3/3     Running   0          15s
```

Knative 中，服务的版本是一个重要特性。我们可以看到我们的唯一版本的服务正在处理 100% 的流量：

```shell
$ knctl revisions list --namespace helloworld --service hello
Revisions for service 'hello'

Name         Tags      Allocated Traffic %  Serving State  Annotations  Age
hello-00001  latest    100%                 Active         -            3m
             previous

1 revisions
```

下一步我们使用 `knctl deploy` 创建一个新的版本，然后把所有流量分配到新版本：

```shell
$ knctl deploy \
      --namespace helloworld \
      --service hello \
      --image gcr.io/knative-samples/helloworld-go \
      --env TARGET=Rev2

Name  hello

Waiting for new revision (after revision 'hello-00001') to be created...

Tagging new revision 'hello-00002' as 'latest'

Tagging older revision 'hello-00001' as 'previous'

Succeeded
```

现在请求被发送到了我们的新版本：

```shell
$ knctl revisions list --namespace helloworld --service hello
Revisions for service 'hello'

Name         Tags      Allocated Traffic %  Serving State  Annotations  Age
hello-00002  latest    100%                 Active         -            1m
hello-00001  previous  0%                   Active         -            10m

$ knctl curl --namespace helloworld --service hello
Running: curl '-H' 'Host: hello.helloworld.example.com' 'http://192.168.64.8:32380'

Hello World: Rev2!
```

部署第二个版本之后，起初两个版本的 Pod 都会持续运行：

```shell
$ kubectl get pods --namespace helloworld
NAME                                      READY   STATUS    RESTARTS   AGE
hello-00001-deployment-5864685cbc-v8r7n   3/3     Running   0          1m
hello-00002-deployment-7874bf89b8-4b4k5   3/3     Running   0          29s
```

我们会看到 Knative 自动将没有路由指向的版本缩容到 0。

## 路由

下图展示了路由到服务某版本的过程中所涉及到的 [Knative Serving](https://github.com/knative/serving/blob/master/docs/spec/overview.md) 模块：

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1746318418514/0071cb84-11cb-4ffe-8bc5-cb19655fb3f4.png align="center")

版本是代码、依赖和配置的的只读快照。没有被路由引用的版本会被放弃，其中的 Kubernetes 资源会被删除。

我们可以看到当前的路由：

```shell
$ knctl routes list --namespace helloworld
Routes in namespace 'helloworld'

Name   Traffic         All Traffic Assigned  Ready  Domain                        Age
hello  100% -> hello:  true                  true   hello.helloworld.example.com  2h
```

## 缩容至 0

如果离开五分钟再回来，你会发现 `hello-00002` Pod 正在被终结或者已经不见了：

```shell
$ kubectl get pods --namespace helloworld
NAME                                      READY   STATUS        RESTARTS   AGE
hello-00001-deployment-5864685cbc-v8r7n   3/3     Running       0          6m
hello-00002-deployment-7874bf89b8-4b4k5   2/3     Terminating   0          5m
```

下一次 `knctl curl`，Knative 会动态的启动一个 Pod 来满足这一请求。

```shell
$ knctl curl --namespace helloworld --service hello
$ kubectl get pods --namespace helloworld
NAME                                      READY   STATUS    RESTARTS   AGE
hello-00001-deployment-5864685cbc-v8r7n   3/3     Running   0          8m
hello-00002-deployment-7874bf89b8-kfbm2   3/3     Running   0          10s
```

我还不太清楚为什么 `hello-00001-deployment-...` Pod 没有被缩容和终结。

## 未完待续

后续文章中将会尝试：

* Knative Build 组件：使用 `Dockerfile` 或者 Cloud Foundry buildpack 自动从定制代码构建容器镜像（代码可以保存本地或者 Git 仓库之中）。
    
* 为 Kubernetes 的负载均衡设置 DNS，从而为 Knative 路由和服务提供公共 URL。
    
* 在不同版本之间进行流量分割（例如 10% 到最新版本，90% 到前一版本；然后将 100% 分配给新版本，老版本流量降低到 0%）。
    
* Knative Eventing：在应用中进行 [CloudEvents](https://github.com/cloudevents/spec)的绑定和分发。让你的服务更加“无服务器”。
    

## 社区

Knative 核心团队有自己的 [Knative Slack](https://knative.slack.com/)，可以在 [https://slack.knative.dev](https://slack.knative.dev) 申请加入。

[knative-dev Group](https://groups.google.com/forum/#!forum/knative-dev) 中包含了总结和提议。

## 鸣谢

感谢 Google 的 [Mark Chmarny](https://m.chmarny.com/)，在 2018 Spring One 上首先回应了我的问题。

感谢 Pivotal 的 [Dmitriy Kalinin](https://github.com/cppforlife) 花时间帮助我将 Knative 运行起来，并给我展示了他的 [knctl Knative CLI](https://github.com/cppforlife/knctl)，以及 [kwt Kubernetes Workstation Tools](https://github.com/cppforlife/kwt)。相对于 YAML + `kubectl` 组合来说，一个好用的客户端工具更能够帮助 Knative 走向实用。
