# 在 Knative 中进行应用程序的构建和部署

原文：[Building and deploying applications to Knative](https://starkandwayne.com/blog/building-and-deploying-applications-to-knative/)

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

Knative 有三个高级子系统：Serving 用来协调服务 Pod 的自动伸缩以及路由；Build 提供了将代码转换为镜像的工具链；Eventing 则会使用事件的发布订阅来触发松耦合服务。

前一篇文章中我们将一个构件好的容器镜像发布到了 Knative Serving 中。

本篇文章将使用 Knative Build 把我们的应用通过 Dockerfile 以及 Cloud Foundry buildpack 在发布过程中转化为容器镜像。我们还会尝试从本地文件系统以及远程 Git 仓库中进行部署的方式。

前面的操作都很简单，看得出在 Kubernetes 环境中安装 Knative、运行现有容器镜像、使用 curl 与应用进行交互都不难。总结一下就是：

```bash
knctl install [--node-ports] [--exclude-monitoring]
knctl deploy --service <service-name> --image <image-name>
knctl curl --service <service-name>
```

后面的两步 - 把镜像部署为 Kubernetes 并运行，为每个部署进行版本化处理，使用 HTTP 路由进行交互，都是 [Serving 子系统](https://github.com/knative/serving/blob/master/docs/spec/overview.md)的功劳。

Knative 还支持用于构建容器镜像的弹性子系统，构建生成的镜像将会以 Kubernetes Pod 的形式运行。

[Knative Build](https://github.com/knative/docs/tree/master/build) 子系统非常有弹性。我们会探讨集中用例：

* 上传一个本地目录，使用 Dockerfile 进行构建。
    
* 上传一个本地目录，使用 Buildpack 进行构建。
    
* 用上面两种方式来构建来自于远程 Git 仓库的代码。
    

## 命名空间

前一篇文章中我们在每个 `knctl` 命令中使用了 `--namespace helloworld` 参数来显式的指定命名空间。对我来说这样显得更清晰。可能有别的用户希望设置一个缺省命名空间，从而能够缩短命令。

可以给 `knctl` 配置一个当前命名空间：

```bash
kubectl create ns my-simple-app
export KNCTL_NAMESPACE=my-simple-app
```

所有的 `knctl` 命令都会使用这一命名空间。

```bash
$ knctl service list
Services in namespace 'my-simple-app'

Name  Domain  Annotations  Age

0 services
```

可以在 `kubectl` 命令中复用 `$KNCTL_NAMESPACE`：

`kubectl get pods -n $KNCTL_NAMESPACE`

## 上传一个带有 Dockerfile 的本地目录

在所有 Knative Build 的示例中，都会产生一个副产品——容器镜像。这些镜像必须放在什么地方，例如 [Docker Hub](https://hub.docker.com/)、[GCP 容器库](https://cloud.google.com/container-registry/)、[Azure 容器库](https://azure.microsoft.com/en-au/services/container-registry/)，或者借助 [Harbor](https://goharbor.io/) 之类的软件自建的私库。

我们需要在每个应用所在的 Kubernetes 命名空间中给 Knative 配置一个镜像库，然后 `knctl basic-auth-secret create` 给 Knative 配置一个 Secret。

以 Docker Hub 为例，使用 `--docker-hub`：

```bash
knctl basic-auth-secret create -s registry --docker-hub -u <username> -p <password>
```

GCP 容器仓库可以使用 `--gcr` 选项：

```bash
knctl basic-auth-secret create -s registry --gcr -u <username> -p <password>
```

其它私库可以使用 `--type` 和 `--url`：

```bash
knctl basic-auth-secret create -s registry --type docker --url https://registry.domain.com/ -u <username> -p <password>
```

下一步把镜像库 Secret 映射到 [Kubernetes Service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)，它会在 Knative Build 的 Pod 中提供上面的登录信息。

```bash
knctl service-account create --service-account build -s registry
```

这个操作会体现在 Kubernetes 的 Service account 中：

```shell
$ kubectl get serviceaccount -n $KNCTL_NAMESPACE
NAME      SECRETS   AGE
build     2         37s
default   1         3h
```

这样我们就准备好使用 Knative Build 创建新容器镜像所需的认证凭据了。

Clone 一个 Go 应用作为样例，并从它的本地目录推送到 Docker hub：

```shell
git clone https://github.com/cppforlife/simple-app
cd simple-app

DOCKER_IMAGE=index.docker.io/<your hub.docker.com org or user>/knative-simple-app

knctl deploy \
    --service simple-app \
    --directory=$PWD \
    --service-account build \
    --image ${DOCKER_IMAGE:?required} \
    --env SIMPLE_MSG="Built from local directory using Dockerfile"
```

> 容器的显式命名事实上只是从 Build 到 Serve 的工作过程中的一个中间步骤的副产品，但是必须提供。

`knctl deploy` 的输出大概是这样的：

```bash
Name  simple-app

Waiting for new revision to be created...

Tagging new revision 'simple-app-00001' as 'latest'

Tagging new revision 'simple-app-00001' as 'previous'

[2018-10-15T13:18:31+10:00] Uploading source code...

[2018-10-15T13:19:59+10:00] Finished uploading source code...

Watching build logs...

build-step-build-and-push | INFO[0000] Downloading base image golang:1.10.1
build-step-build-and-push | ERROR: logging before flag.Parse: E1015 03:20:01.547607       1 metadata.go:142] while reading 'google-dockercfg' metadata: http status code: 404 while fetching url http://metadata.google.internal./computeMetadata/v1/instance/attributes/google-dockercfg
build-step-build-and-push | ERROR: logging before flag.Parse: E1015 03:20:01.550268       1 metadata.go:159] while reading 'google-dockercfg-url' metadata: http status code: 404 while fetching url http://metadata.google.internal./computeMetadata/v1/instance/attributes/google-dockercfg-url
build-step-build-and-push | INFO[0001] Executing 0 build triggers
build-step-build-and-push | INFO[0001] Extracting layer 0
build-step-build-and-push | INFO[0003] Extracting layer 1
build-step-build-and-push | INFO[0004] Extracting layer 2
build-step-build-and-push | INFO[0004] Extracting layer 3
build-step-build-and-push | INFO[0007] Extracting layer 4
build-step-build-and-push | INFO[0010] Extracting layer 5
build-step-build-and-push | INFO[0015] Extracting layer 6
build-step-build-and-push | INFO[0015] Taking snapshot of full filesystem...
build-step-build-and-push | INFO[0027] WORKDIR /go/src/github.com/mchmarny/simple-app/
build-step-build-and-push | INFO[0027] cmd: workdir
build-step-build-and-push | INFO[0027] Changed working directory to /go/src/github.com/mchmarny/simple-app/
build-step-build-and-push | INFO[0027] Creating directory /go/src/github.com/mchmarny/simple-app/
build-step-build-and-push | INFO[0027] COPY . .
build-step-build-and-push | INFO[0027] RUN CGO_ENABLED=0 GOOS=linux go build -v -o app
build-step-build-and-push | INFO[0027] cmd: /bin/sh
build-step-build-and-push | INFO[0027] args: [-c CGO_ENABLED=0 GOOS=linux go build -v -o app]
build-step-build-and-push | net
build-step-build-and-push | vendor/golang_org/x/net/lex/httplex
build-step-build-and-push | vendor/golang_org/x/net/proxy
build-step-build-and-push | net/textproto
build-step-build-and-push | crypto/x509
build-step-build-and-push | crypto/tls
build-step-build-and-push | net/http/httptrace
build-step-build-and-push | net/http
build-step-build-and-push | github.com/mchmarny/simple-app
build-step-build-and-push | INFO[0030] Taking snapshot of full filesystem...
build-step-build-and-push | INFO[0034] Storing source image from stage 0 at path /kaniko/stages/0
build-step-build-and-push | INFO[0038] trying to extract to /kaniko/0
build-step-build-and-push | INFO[0038] Extracting layer 0
build-step-build-and-push | INFO[0040] Extracting layer 1
build-step-build-and-push | INFO[0041] Extracting layer 2
build-step-build-and-push | INFO[0041] Extracting layer 3
build-step-build-and-push | INFO[0043] Extracting layer 4
build-step-build-and-push | INFO[0046] Extracting layer 5
build-step-build-and-push | INFO[0051] Extracting layer 6
build-step-build-and-push | INFO[0051] Extracting layer 7
build-step-build-and-push | INFO[0051] Deleting filesystem...
build-step-build-and-push | INFO[0053] No base image, nothing to extract
build-step-build-and-push | INFO[0053] Taking snapshot of full filesystem...
build-step-build-and-push | INFO[0062] COPY --from=0 /go/src/github.com/mchmarny/simple-app/app .
build-step-build-and-push | INFO[0063] Taking snapshot of files...
build-step-build-and-push | INFO[0063] EXPOSE 8080
build-step-build-and-push | INFO[0063] cmd: EXPOSE
build-step-build-and-push | INFO[0063] Adding exposed port: 8080/tcp
build-step-build-and-push | INFO[0063] ENTRYPOINT ["/app"]
build-step-build-and-push | ERROR: logging before flag.Parse: E1015 03:21:04.751338       1 metadata.go:142] while reading 'google-dockercfg' metadata: http status code: 404 while fetching url http://metadata.google.internal./computeMetadata/v1/instance/attributes/google-dockercfg
build-step-build-and-push | ERROR: logging before flag.Parse: E1015 03:21:04.753927       1 metadata.go:159] while reading 'google-dockercfg-url' metadata: http status code: 404 while fetching url http://metadata.google.internal./computeMetadata/v1/instance/attributes/google-dockercfg-url
build-step-build-and-push | 2018/10/15 03:21:06 pushed blob sha256:72a682eea3309941d5e8e6f993a07ae4d33a413b8b7fa2762f8e969310b5996a
build-step-build-and-push | 2018/10/15 03:21:07 pushed blob sha256:9c24aa788ba416c5e1e631d8af3e3115519ad7ca0f659ac10f40682524c6d9cd
build-step-build-and-push | 2018/10/15 03:21:07 index.docker.io/drnic/knative-simple-app:latest: digest: sha256:b5823ead77d9544998b5bc844f049d1a7dfb0aefe7461b74b3e4f67fb5481fa1 size: 428
nop | Nothing to push

Succeeded
```

## Knative Build 的调试

目前 `knctl deploy` 没有显示任何来自 Knative Build 系统的内部错误或者警告。只需要看着 `Waiting for new revision to be created...` 坐享其成就可以了。

一个调试方法就是使用 [kail](https://github.com/boz/kail) 工具处理来自 Knative Build 子系统的消息：

```bash
kail -n knative-build
```

这样就会看到大量的日志，可以再其中查找错误信息，例如 `"msg":"Failed the resource specific validation{error 25 0 serviceaccounts \"build\" not found}"`。

## 使用 Buildpack 进行构建

我本人很喜欢 Cloud Foundry 和 Heroku 的镜像构建方式，幸运的是，Knative Build 通过[自定义构建模板](https://github.com/knative/build-templates/tree/master/buildpack)的方式提供了这种支持。

首先用 `buildpack` 这个名字在活动命名空间中注册一个[构建模板](https://github.com/knative/build-templates/blob/master/buildpack/buildpack.yaml)：

```bash
kubectl -n $KNCTL_NAMESPACE apply -f \
https://raw.githubusercontent.com/knative/build-templates/master/buildpack/buildpack.yaml
```

加入 `--template buildpack` 就可以使用这一自定义模板了。构建模板所需的附加环境变量都可以用 `--template-env NAME=value` 的方式进行植入。

例如 Cloud Foundry Go Buildpack 需要 `$GOPACKNAME`（[参考文档](https://docs.cloudfoundry.org/buildpacks/go/index.html#pushing_Apps_with_native_Go_vendoring)）：

```bash
knctl deploy \
    --service simple-app \
    --directory=$PWD \
    --service-account build \
    --image ${DOCKER_IMAGE:?required} \
    --env SIMPLE_MSG="Built from local directory using Buildpack template" \
    --template buildpack \
    --template-env GOPACKAGENAME=main
```

输出内容和 Cloud Foundry buildpack 是一致的：

```bash
Name  simple-app

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

Tagging new revision 'simple-app-00002' as 'latest'

Tagging older revision 'simple-app-00001' as 'previous'

[2018-10-15T13:40:41+10:00] Uploading source code...

[2018-10-15T13:42:08+10:00] Finished uploading source code...

Watching build logs...

build-step-build | -----> Go Buildpack version 1.8.26
build-step-build | -----> Installing godep 80
build-step-build |        Download [https://buildpacks.cloudfoundry.org/dependencies/godep/godep-v80-linux-x64-cflinuxfs2-06cdb761.tgz]
build-step-build | -----> Installing glide 0.13.1
build-step-build |        Download [https://buildpacks.cloudfoundry.org/dependencies/glide/glide-v0.13.1-linux-x64-cflinuxfs2-aab48c6b.tgz]
build-step-build | -----> Installing dep 0.5.0
build-step-build |        Download [https://buildpacks.cloudfoundry.org/dependencies/dep/dep-v0.5.0-linux-x64-cflinuxfs2-52c14116.tgz]
build-step-build | -----> Installing go 1.8.7
build-step-build |        Download [https://buildpacks.cloudfoundry.org/dependencies/go/go1.8.7.linux-amd64-cflinuxfs2-fff10274.tar.gz]
build-step-build |        **WARNING** Installing package '.' (default)
build-step-build | -----> Running: go install -tags cloudfoundry -buildmode pie .
build-step-export | 2018/10/15 03:47:58 mounted blob: sha256:1124eb40dd68654b8ca8f5d9ec7e439988a4be752a58c8f4e06d60ab1589abdb
build-step-export | 2018/10/15 03:47:58 mounted blob: sha256:6be38da025345ffb57d1ddfcdc5a2bc052be5b9491825f648b49913d51e41acb
build-step-export | 2018/10/15 03:47:58 mounted blob: sha256:a5733e6358eec8957e81b1eb93d48ef94d649d65c69a6b1ac49f616a34a74ac1
build-step-export | 2018/10/15 03:47:58 mounted blob: sha256:21324a9f04e76c93078f3a782e3198d2dded46e4ec77958ddd64f701aecb69c0
build-step-export | 2018/10/15 03:47:59 pushed blob sha256:efa2d34b82bc07588a1a8fd4526322257408109547ee089a792b3f51c383f8e6
build-step-export | 2018/10/15 03:47:59 pushed blob sha256:d495696b33936c79216ec8178726b9fbe915fafbffdd0911a7fdabce4297d9a4
build-step-export | 2018/10/15 03:48:00 index.docker.io/drnic/knative-simple-app:latest: digest: sha256:e5ef1d4d255b4bcbb38d4b43bb6302423c33e6eeabd0e20d5fda4e5ce4c46668 size: 1082
nop | Nothing to push
```

现在就能看到应用已经部署成功：

```shell
$ knctl curl -s simple-app
<h1>Built from local directory using Buildpack template</h1>
```

## 私有 Git Secret

前面两节我们从本地上传了源码然后构建了 Docker 镜像（使用 Dockerfile 或 Cloud Foundry buildpack），最后运行应用。

Knative 还能从 Git 仓库获取源码（正式的说法是，Knative Build 只支持从 Git 仓库获取源码，本地代码的支持是 `knctl` 提供的）。

让 Knative Build 获取 Git 仓库中的代码，需要用 `--git-url` 和 `--git-revision` 来取代 `--directory=$PWD`。

如果你的 Git 仓库是私有的，那就还需要在 Service account（在上面的例子中的 `build`）里包含 Git ssh 凭据。`knctl ssh-auth-secret create` 能够协助用户创建一个 kubernetes.io/ssh-auth secret。

```shell
$ knctl ssh-auth-secret create --secret git --github --private-key "$(cat ~/.ssh/id_rsa)"
Name  git
Type  kubernetes.io/ssh-auth

$ kubectl get secrets -n $KNCTL_NAMESPACE
NAME                  TYPE                                  DATA   AGE
...
git                   kubernetes.io/ssh-auth                1      5m
registry              kubernetes.io/basic-auth              2      3h
```

现在需要把 `git` secret 加入到我们的 `build` Service account 之中了。

在成文之时，`knctl` 还没有提供 `knctl serviceaccounts update` 这样的命令，所以需要删除重新创建：

```bash
kubectl delete serviceaccounts -n $KNCTL_NAMESPACE build
knctl service-account create --service-account build -s registry -s git
```

## 从 Git 部署

用 `--git-url` 和 `--git-revision` 替代 `--directory` 来进行 Git 部署：

```bash
knctl deploy \
    --service simple-app \
    --git-url git@github.com:cppforlife/simple-app.git \
    --git-revision master \
    --service-account build \
    --image ${DOCKER_IMAGE:?required} \
    --env SIMPLE_MSG="Built from Git repo using Buildpack template" \
    --template buildpack \
    --template-env GOPACKAGENAME=main
```

## 总结

`knctl deploy` 命令在 Knative 的基础上提供了创建新镜像的良好体验，可以从本地目录或者 Git 仓库开始，使用 Dockerfile 或 Cloud Foundry buildpack 进行构建，并支持不同的镜像仓库。
