Knative:精简代码之道

点击查看目录

本文为翻译文章,点击查看原文

[编者案]

Knative 作为 Google 发起开源的 serverless 项目,给我们提供了一套简单易用的 serverless 开源解决方案。本文作者直观地向我们展示了如何使用 Knative 来一步一步逐渐精简我们的代码,来更加简单容易的开发云原生应用。作者使用实例来逐步向我们讲述如何使用 Knative 提供的 Build、Serving 和 Eventing 三大组件来发挥威力,逐渐精简代码。如果您对 Knative 有兴趣,本文对于你通过 Knative 实践 serverless 一定会有所启发,希望您能喜欢。

对我来说,2018 年最好的开源项目是Knative,这是一个建立在 Kubernetes 之上的 serverless 平台。不仅是为了 serverless 平台本身,也是为了它所倡导的整个开发范式。事件驱动开发并不新鲜,但 Knative 为围绕事件构建生态系统奠定了基础。

如果您不熟悉 Knative,那么您读到的任何文档都将它分为三大组件:

  • 构建 (Build) —— 我如何构建代码和打包代码?
  • 服务 (Serving) —— 我的代码如何为请求提供服务?它是如何伸缩的?
  • 事件 (Eventing) —— 我的代码如何被各种事件触发?

事实上这并不是一篇“Knative 入门”的文章 (在不久的将来会有更多关于这方面的内容),但是我最近一直在思考的是,随着开发人员越来越多地利用 Knative 提供的功能,他们如何减少自己需要编写的代码。这是最近 Twitter 上一个特别热门的话题,尤其是在 KubeCon 时代。我注意到的一个常见问题是,“如果您正在编写 Dockerfile,它真的是一个 serverless 的平台吗?”但是,其他人认为将代码打包为容器可能是最合理的解决方案,因为它是可移植的、全面的,并且具有所有依赖项。不乏强烈持有这种观点的人们,他们非常渴望争辩。

与其火上浇油,不如让我们看看 Knative 给开发人员提供了哪些选项来逐渐减少我们编写的代码量。我们将从最冗长的示例开始—我们自己构建的预构建容器(prebuilt container)。从这里开始,我们将逐渐减少基准代码量,减少构建自己的容器的需要,减少编写自己的 Dockerfile 的需要,最后减少编写自己的配置的需要。最重要的是,我们将看到 Pivotal Function Service (PFS) 的强大功能,以及它如何允许开发人员关注代码而不是配置。

我们将看到的所有代码都包含在两个 git repos 中:knative-hello-worldpfs-hello-world

预构建(Prebuilt)的 Docker 容器

我们将看到的第一个场景是为 Knative 提供一个预构建的容器镜像,该镜像已经上传到我们选择的容器镜像库。您将在 Knative 中看到的大多数Hello World示例都采用直接构建和管理容器的方式。这是有意义的,因为它很容易理解,而且没有引入很多新概念,这是一个很好的起点。这个概念非常简单直接:传给 Knative 一个暴露端口的容器,它将处理剩余的所有事情。它不关心你的代码是用GoRuby还是Java写的;它会接收传入的请求并将它们发送到你的应用程序。

让我们从一个使用Express web框架基于 node.js 实现的 hello world 应用程序开始。

const express = require("express");
const bodyParser = require('body-parser')

const app = express();
app.use(bodyParser.text({type: "*/*"}));

app.post("/", function(req, res) {
 res.send("Hello, " + req.body + "!");
});

const port = process.env.PORT || 8080;
app.listen(port, function() {
 console.log("Server started on port", port);
});

是不是漂亮且简洁。这段代码将启动一个 web 服务器,监听端口 8080(除非端口已被占用),并通过返回 Hello 来响应 HTTP POST 请求。当然,还需要 package.json 文件,它定义了一些东西 (如何启动应用程序,依赖关系等),但这有点超出了我们所看到的范围。另一部分是 Dockerfile,它描述如何将所有内容打包到一个容器中。

FROM node:10.15.1-stretch-slim

WORKDIR /usr/src/app
COPY . .
RUN npm install

ENV PORT 8080
EXPOSE $PORT

ENTRYPOINT ["npm", "start"]

这里也没什么好惊讶的。我们将我们的镜像建立在官方 node.js 镜像的基础之上,将我们的代码复制到容器中并安装依赖项,然后告诉它如何运行我们的应用程序。剩下的就是将其上传到 Docker Hub。

$ docker build . -t brianmmcclain/knative-hello-world:prebuilt

$ docker push brianmmcclain/knative-hello-world:prebuilt

如果您曾经在类似于 Kubernetes 的应用程序上运行过,那么所有这些看起来应该非常熟悉。将代码放入容器中,让调度器处理以确保它处于正常状态。我们可以告诉 Knative 关于这个容器的信息,再加上一点 metadata,它会处理所有的东西。随着请求数量的增长,它将扩展实例的数量,缩减到 0,路由请求,连接事件——竭尽所能。我们真正需要告诉 Knative 的是调用我们的应用程序,运行它的名称空间,以及容器镜像的位置。

apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
 name: knative-hello-world-prebuilt
 namespace: default
spec:
 runLatest:
   configuration:
     revisionTemplate:
       spec:
         container:
           image: docker.io/brianmmcclain/knative-hello-world:prebuilt

$ kubectl apply -f 01-prebuilt.yaml

几分钟后,我们将看到一个新的 pod 运行起来,准备为请求服务,如果在一段时间内没有收到任何流量,Pod 数量将会缩减为 0。我们可以 POST 一些数据,看看我们收到的响应。首先,让我们获取 Kubernetes 集群的 Ingress IP,并将其分配给$SERVICE_IP 变量:

$ export SERVICE_IP=`kubectl get svc istio-ingressgateway -n istio-system -o jsonpath="{.status.loadBalancer.ingress[*].ip}"`

然后使用 IP 向我们的服务发送请求,在我们的请求中设置 HOST header:

$ curl -XPOST http://$SERVICE_IP -H "Host: knative-hello-world-prebuilt.default.example.com" -d "Prebuilt"

Hello, Prebuilt!

Kaniko 容器构建器

上面介绍的一切可以很好的工作,但是我们甚至还没有开始接触 Knative 的“Build”部分。实际上,我们没有碰它,我们自己构建了这个容器。您可以在 Knative 文档中阅读所有关于构建以及它们如何工作的信息。总的来说,knative 有一个名为“Build Templates”的概念,我喜欢这样描述他们:他们是关于如何从代码到容器的可共享逻辑。这些构建模板中的大多数模板都能够完成我们构建容器和上传镜像的需要。这些模板中最基本的可能是Kaniko Build Templates

顾名思义,它基于谷歌的Kaniko, Kaniko 是在容器中构建容器镜像的工具,不依赖于正在运行的 Docker 守护进程。向 Kaniko 容器镜像提供 Dockerfile 和一个上传结果的位置,它就可以据此构建镜像。我们无需拉取代码、在本地构建镜像、上传到 Docker Hub,然后从 Knative 拉取镜像,我们可以让 Knative 为我们做这些,只需要多做一点配置。

但是,在执行此操作之前,我们需要告诉 Knative 如何根据容器注册中心进行身份验证。为此,我们首先需要在 Kubernetes 中创建一个 Secret,这样我们就可以对 Docker Hub 进行身份验证,然后创建一个服务帐户来使用该 Secret 并运行构建。让我们从创造 Secret 开始:

apiVersion: v1
kind: Secret
metadata:
 name: dockerhub-account
 annotations:
   build.knative.dev/docker-0: https://index.docker.io/v1/
type: kubernetes.io/basic-auth
data:
 # 'echo -n "username" | base64'
 username: dXNlcm5hbWUK
 # 'echo -n "password" | base64'
 password: cGFzc3dvcmQK

uesrname 和 password 作为 base64 编码的字符串发送给 Kubernetes。(对于有安全意识的读者来说,这是一种传输机制,而不是安全机制。有关 Kubernetes 如何存储 Secret 的更多信息,请在有空时查看关于on encrypting secret data at rest)。提交之后,我们将创建一个名为 build-bot 的服务帐户,并告诉它在推送到 Docker Hub 时使用这个 Secret:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: build-bot
secrets:
- name: dockerhub-account

有关身份验证的更多信息,请确保查看 knative 文档中的how-authentication-work -in- knative文档。

构建模板 (Build Templates) 的好处是任何人都可以创建并与社区共享它们。我们可以告诉 Knative 通过传递一些 YAML 来安装这个构建模板:

$ kubectl apply -f https://raw.githubusercontent.com/knative/build-templates/master/kaniko/kaniko.yaml

然后我们需要在我们的应用 YAML 中添加更多:

apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
 name: knative-hello-world-kaniko
 namespace: default
spec:
 runLatest:
   configuration:
     build:
       serviceAccountName: build-bot
       source:
         git:
           url: https://github.com/BrianMMcClain/knative-hello-world.git
           revision: master
       template:
         name: kaniko
         arguments:
         - name: IMAGE
           value: docker.io/brianmmcclain/knative-hello-world:kaniko
     revisionTemplate:
       spec:
         container:
           image: docker.io/brianmmcclain/knative-hello-world:kaniko

虽然直接比较有点困难,但是我们实际上只向 YAML 中添加了一个部分—“Build”部分。我们添加的内容可能看起来很多,但如果你花时间逐条查看的话,它实际上并不坏:

  • serviceAccountName:在 Knative auth 文档中,它遍历了设置服务帐户的过程。所有这些都是通过设置一个 Kubernetes Secret 来验证我们的容器镜像库,然后将其封装到一个服务帐户中。
  • source:代码所在的位置。例如,git repository。
  • template:要使用哪个 Build Template。在本例中,我们将使用 Kaniko Build Template。

让我们向应用程序的新版本发送一个请求,以确保一切正常:

$ curl -XPOST http://$SERVICE_IP -H "Host: knative-hello-world-kaniko.default.example.com" -d "Kaniko"

Hello, Kaniko!

尽管这可能是一种更预先的配置,但权衡的结果是,现在我们不必每次更新代码时都构建或推送我们自己的容器镜像。相反,Knative 将为我们处理这些步骤!

Buildpack Build Template

所以,这个博客的重点是我们如何编写更少的代码。虽然我们已经使用 Kaniko Build Template 删除了部署的一个操作组件,但是我们仍然在代码之上维护一个 Dockerfile 和一个配置文件。但是如果我们可以抛弃 Dockerfile 呢?

如果您具有使用 PaaS 的习惯,那么您可能已经习惯了简单地向上推代码,然后发生了一些神奇的事情,然后您就有了一个正常工作的应用程序。你不在乎这是怎么做到的。我们所知道的是,您不必编写 Dockerfile 来将其放入容器中,而且它可以正常工作。在Cloud Foundry,这是通过名为buildpacks的框架实现的,该框架为应用程序提供运行时和依赖项。

实际上给我们带来两大好处。不仅有一个使用 buildpacks 的 Build Template,还有一个用于 Node.js 的 buildpacks。就像 Kaniko Build Template 一样,我们将在 Knative 中安装 buildpack Build Template:

kubectl apply -f https://raw.githubusercontent.com/knative/build-templates/master/buildpack/buildpack.yaml

现在,让我们看看使用 Buildpack Build Template 的 YAML 是什么样子的:

apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
 name: knative-hello-world-buildpack
 namespace: default
spec:
 runLatest:
   configuration:
     build:
       serviceAccountName: build-bot
       source:
         git:
           url: https://github.com/BrianMMcClain/knative-hello-world.git
           revision: master
       template:
         name: buildpack
         arguments:
         - name: IMAGE
           value: docker.io/brianmmcclain/knative-hello-world:buildpack
     revisionTemplate:
       spec:
         container:
           image: docker.io/brianmmcclain/knative-hello-world:buildpack

这与我们使用 Kaniko Build Template 时非常相似。实际上,我们来做个比较:

<   name: knative-hello-world-kaniko
>   name: knative-hello-world-buildpack
---
<           name: kaniko
>           name: buildpack
---
<             value: docker.io/brianmmcclain/knative-hello-world:kaniko
>             value: docker.io/brianmmcclain/knative-hello-world:buildpack
---
<             image: docker.io/brianmmcclain/knative-hello-world:kaniko
>             image: docker.io/brianmmcclain/knative-hello-world:buildpack

那么区别是什么呢?首先,我们可以完全抛弃 Dockerfile。Buildpack Build Template 将分析我们的代码,确定它是一个 Node.js 应用程序,并通过下载 Node.js 运行时和依赖项为我们构建一个容器。虽然 Kaniko Build Template 将我们从 Docker 容器生命周期的管理工作中解放出来,但 Buildpack Build Template 更进一步,完全不需要管理 Dockerfile 了。

$ kubectl apply -f 03-buildpack.yaml
service.serving.knative.dev "knative-hello-world-buildpack" configured

$ curl -XPOST http://$SERVICE_IP -H "Host: knative-hello-world-buildpack.default.example.com" -d "Buildpacks"
Hello, Buildpacks!

Pivotal Function Service

让我们检查一下代码库的剩余部分。我们有响应 POST 请求的 Node.js 代码,使用 Express 框架设置 web 服务器。package.json 文件定义了我们的依赖项。虽然这不是真正的代码,但我们也在维护定义 Knative 服务的 YAML。不过,我们可以继续削减。

进入Pivotal Function Service (PFS),这是构建在 Knative 之上的 Pivotal 的商业 serverless 产品。PFS 旨在消除管理代码以外的任何东西的需要。这包括我们在代码库中管理自己的 web 服务器。使用 PFS,我们的代码如下:

module.exports = x => "Hello, " + x + "!";

就是这样,没有 Dockerfile,没有 YAML。只要一行代码。当然,像所有优秀的节点开发人员一样,我们仍然需要有自己的 package.json 文件,尽管它不依赖于 Express。一旦部署完毕,riff 将使用这一行代码并将其封装在自己的托管容器镜像中。它将把它与调用代码所需的逻辑打包在一起,并像运行在 Knative 上的任何其他函数一样提供服务。

PFS CLI 使得部署我们的函数变得非常容易。我们将给函数命名为 pfs-hello-world,为它提供到代码所在的 GitHub 存储库的链接,并告诉它将生成的容器映像上传到我们的私有容器镜像库中。

pfs function create pfs-hello-world --git-repo https://github.com/BrianMMcClain/pfs-hello-world.git --image $REGISTRY/$REGISTRY_USER/pfs-hello-world --verbose

几分钟后,我们将看到我们的函数进入运行状态,我们可以像任何其他 Knative 函数一样,向其发送请求:

$ curl -XPOST http://$SERVICE_IP -H "Host: pfs-hello-world.default.example.com" -H "Content-Type: text/plain" -d "PFS"

Hello, PFS!

或者,更简单的是,使用 riff CLI 来调用我们的函数:

$ pfs service invoke pfs-hello-world --text -- -d "PFS CLI"

Hello, PFS CLI!

我们终于实现了目标!由 23 行 YAML、14 行代码和一个 10 行 Dockerfile 组成的简化代码行。

是不是一下子对 PFS 感兴趣了呢?要申请提前访问,只需填写这张快速表格!

接下来工作?

越来越多的构建模板。这是 Knative 最令人兴奋的特性之一,因为它有很大的潜力为各种场景打开一个自定义构建模板的社区。现在,您可以为JibBuildKit等工具使用模板。已经有一个pull request来更新 Buildpack 构建模板,以支持Cloud Native Buildpacks

2018 年是一个激动人心的开始,但是更让我兴奋的是看到 Knative 社区在 2019 年的增长。我们当然可以期望从社区获得更多的构建模板和更多的事件源。不仅如此,我们还可以期望与现有技术更好地集成,例如 Spring,它已经对此功能提供了强大的支持

如果您希望开始使用 Knative 进行开发,那么 Bryan Friedman 和我将在 2 月 21 日主持一个很棒的网络研讨会,讨论developing serverless applications on Kubernetes with Knative。我们将深入研究 Knative 的三个组件,它们是如何工作的,以及作为开发人员如何利用它们来编写更好的代码。

如果你 4 月 2-4 日在费城,请加入我们的CF Summit! Bryan 和我将讨论the way to build serverless on Knative,或者如果您看到我们,就说声 hi!

编辑本页