再见 Docker,是时候拥抱下一代容器镜像构建工具 Kaniko 了

Posted by Mike on 2020-12-15

1. daemon-less 镜像构建工具

1.1 什么是 daemon-less 镜像构建工具

在 CICD 流程中,经常会涉及镜像构建,常规的做法是使用 Docker in Docker 或者 Docker out of Docker 进行构建。详情可以参考文档:如何在 Docker 中使用 Docker

实际上,为了避免垄断,促进行业发展,基于 Docker 的镜像格式,早就指定了统一的 OCI 镜像格式规范。也就是说,只需要通过 Dockerfile 得到一个符合 OCI 规范的镜像即可,并没有强制要求谁来做这件事。

daemon-less 的构建工具,可以不依赖于 Docker Daemon 进行构建镜像。这在 CICD 场景下,具有重要的意义。同时,Kubernetes 正在摆脱 Docker 的影响,构建更加开放的架构生态,CICD 需要与 Docker Daemon 解耦。

1.2 daemon-less 的优势

  • 免挂载 sock 文件

目前,实现 CRI 和 OCI 接口的运行时组件越来越多,例如 cri-o、containerd。这些运行时管理工具,并没有类似 /var/run/docker.sock 的文件用于挂载服务。daemon-less 的构建方式能够对接这些工具。

  • 更加安全

这些 daemon-less 工具,通常运行在 userspace ,而不是以 root 特权运行,提供更加安全的运行时。

  • 更适合 Kubernetes

通过挂载 /var/run/docker.sock 的方式,使用 Docker Daemon 构建镜像,是一种集中的构建方式。镜像的构建主要交给节点上的 Docker Daemon 来完成。而如果能直接将 Dockerfile 转换成镜像,更加适应 Kubernetes、Serverless 基础设施,构建规模和效率将得到提高。

1.3 相关的开源项目

  • Kaniko, Google 主导
  • Buildah, Red Hat 主导
  • Img, Jessie Frazelle 发起

从 Star 量上看,Buildah、Img 目前是 3K+,而 Kaniko 达到 7K+,下面以 Kaniko 为例行文。

2. Kaniko 的工作原理

如上图是 Kaniko 的工作原理图。Kaniko 执行器从 Dockerfile 构建镜像,并将其推送到镜像仓库。主要分为以下几步:

  • 根据 Dockerfile 中 FROM 描述,解压基础镜像的文件系统
  • 执行 Dockerfile 中的每条命令,在用户空间建立文件快照
  • 将变更的文件层添加到基础镜像中,更新镜像的元数据
  • 推送镜像

已知的问题

  • 不支持构建 Windows 镜像
  • kaniko 命令只能在官方镜像中运行,不支持其他 Docker 镜像
  • 不支持 Registry V1 接口

3. Kaniko Demo

3.1 测试 Demo 选择

这里选择的是 https://github.com/traefik/whoami 项目。访问该服务之后,接口会返回访问者的相关信息。

对项目的要求是,通过 Dockerfile 能够直接编译得到镜像。一共有两个验证点:

  • 能够构建镜像
  • 构建的镜像能够运行

3.2 在 Docker 上运行 Kaniko

  • 生成推送镜像的凭证
1
2
3
4
5
6
7
8
9
10
11
$ export AUTH=$(echo -n YOUR_USERNAME:YOUR_PASSWORD | base64 )

$ cat > config.json <<-EOF
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "${AUTH}"
}
}
}
EOF
  • 构建镜像
1
2
3
4
5
$ docker run \
--interactive -v `pwd`/config.json:/kaniko/.docker/config.json gcr.io/kaniko-project/executor:latest \
--context git://github.com/traefik/whoami \
--dockerfile Dockerfile \
--destination=shaowenchen/kaniko-demo:v1

参数说明:

1
2
3
- context, 构建需要的上下文。支持多种格式,S3、本地目录、标准输入、Git 仓库等
- dockerfile, Dockerfile 路径
- destination, 构建后推送的镜像地址

在生产环境,可以配置缓存,用于加速镜像构建。

  • 查看日志

执行上一步的命令之后,可以看到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
Enumerating objects: 17, done.
Counting objects: 100% (17/17), done.
Compressing objects: 100% (13/13), done.
Total 157 (delta 3), reused 12 (delta 3), pack-reused 140
INFO[0004] Resolved base name golang:1-alpine to builder
INFO[0004] Retrieving image manifest golang:1-alpine
INFO[0004] Retrieving image golang:1-alpine
INFO[0007] Retrieving image manifest golang:1-alpine
INFO[0007] Retrieving image golang:1-alpine
INFO[0010] No base image, nothing to extract
INFO[0010] Built cross stage deps: map[0:[/usr/share/zoneinfo /etc/ssl/certs/ca-certificates.crt /go/whoami/whoami]]
INFO[0010] Retrieving image manifest golang:1-alpine
INFO[0010] Retrieving image golang:1-alpine
INFO[0012] Retrieving image manifest golang:1-alpine
INFO[0012] Retrieving image golang:1-alpine
INFO[0015] Executing 0 build triggers
INFO[0015] Unpacking rootfs as cmd RUN apk --no-cache --no-progress add git ca-certificates tzdata make && update-ca-certificates && rm -rf /var/cache/apk/* requires it.
INFO[0035] RUN apk --no-cache --no-progress add git ca-certificates tzdata make && update-ca-certificates && rm -rf /var/cache/apk/*
INFO[0035] Taking snapshot of full filesystem...
INFO[0041] cmd: /bin/sh
INFO[0041] args: [-c apk --no-cache --no-progress add git ca-certificates tzdata make && update-ca-certificates && rm -rf /var/cache/apk/*]
INFO[0041] Running: [/bin/sh -c apk --no-cache --no-progress add git ca-certificates tzdata make && update-ca-certificates && rm -rf /var/cache/apk/*]
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz
(1/7) Installing nghttp2-libs (1.41.0-r0)
(2/7) Installing libcurl (7.69.1-r2)
(3/7) Installing expat (2.2.9-r1)
(4/7) Installing pcre2 (10.35-r0)
(5/7) Installing git (2.26.2-r0)
(6/7) Installing make (4.3-r0)
(7/7) Installing tzdata (2020c-r1)
Executing busybox-1.31.1-r19.trigger
OK: 25 MiB in 22 packages
WARNING: ca-certificates.crt does not contain exactly one certificate or CRL: skipping
INFO[0042] Taking snapshot of full filesystem...
INFO[0045] WORKDIR /go/whoami
INFO[0045] cmd: workdir
INFO[0045] Changed working directory to /go/whoami
INFO[0045] Creating directory /go/whoami
INFO[0045] Taking snapshot of files...
INFO[0045] COPY go.mod .
INFO[0045] Taking snapshot of files...
INFO[0045] COPY go.sum .
INFO[0045] Taking snapshot of files...
INFO[0045] RUN GO111MODULE=on GOPROXY=https://proxy.golang.org go mod download
INFO[0045] cmd: /bin/sh
INFO[0045] args: [-c GO111MODULE=on GOPROXY=https://proxy.golang.org go mod download]
INFO[0045] Running: [/bin/sh -c GO111MODULE=on GOPROXY=https://proxy.golang.org go mod download]
INFO[0045] Taking snapshot of full filesystem...
INFO[0047] COPY . .
INFO[0047] Taking snapshot of files...
INFO[0047] RUN make build
INFO[0047] cmd: /bin/sh
INFO[0047] args: [-c make build]
INFO[0047] Running: [/bin/sh -c make build]
CGO_ENABLED=0 go build -a --trimpath --installsuffix cgo --ldflags="-s" -o whoami
INFO[0058] Taking snapshot of full filesystem...
INFO[0062] Saving file usr/share/zoneinfo for later use
INFO[0062] Saving file etc/ssl/certs/ca-certificates.crt for later use
INFO[0062] Saving file go/whoami/whoami for later use
INFO[0062] Deleting filesystem...
INFO[0063] No base image, nothing to extract
INFO[0063] Executing 0 build triggers
INFO[0063] Unpacking rootfs as cmd COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo requires it.
INFO[0063] COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
INFO[0063] Taking snapshot of files...
INFO[0063] COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
INFO[0063] Taking snapshot of files...
INFO[0063] COPY --from=builder /go/whoami/whoami .
INFO[0063] Taking snapshot of files...
INFO[0063] ENTRYPOINT ["/whoami"]
INFO[0063] EXPOSE 80
INFO[0063] cmd: EXPOSE
INFO[0063] Adding exposed port: 80/tcp

最后正常退出,构建并推送镜像成功。

  • 查看构建镜像是否落盘本地
1
$ docker images|grep kaniko-demo

执行完命令,没有任何输出,符合预期。构建之后的镜像,直接被推送到了远程 Registry。

  • DockerHub 查看镜像

在 DockerHub 页面可以查看到推送的镜像:

  • 使用镜像,创建容器

执行命令:

1
$ docker run -d -p 8011:80 shaowenchen/kaniko-demo:v1

验证服务是否正常:

1
2
3
4
5
6
7
8
9
10
$ curl localhost:8011

Hostname: 6dd22f1e4100
IP: 127.0.0.1
IP: 172.17.0.2
RemoteAddr: 172.17.0.1:40940
GET / HTTP/1.1
Host: localhost:8011
User-Agent: curl/7.29.0
Accept: */*

3.3 在 Kubernetes 上运行 Kaniko

  • 查看 Kubernetes 版本 - v1.17.9

对于不同版本的 Kubernetes,下面有些命令会有所差异,需要自行适配。

1
2
3
4
$ kubectl version

Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.9", GitCommit:"4fb7ed12476d57b8437ada90b4f93b17ffaeed99", GitTreeState:"clean", BuildDate:"2020-07-15T16:18:16Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.9", GitCommit:"4fb7ed12476d57b8437ada90b4f93b17ffaeed99", GitTreeState:"clean", BuildDate:"2020-07-15T16:10:45Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
  • 创建命名空间
1
$ kubectl create ns kaniko-demo
  • 创建镜像推送秘钥
1
2
3
4
5
$ kubectl -n kaniko-demo create secret docker-registry kaniko-secret \
--docker-server=https://index.docker.io/v1/ \
--docker-username=YOUR_USERNAME \
--docker-password=YOUR_PASSWORD \
--docker-email=mail@chenshaowen.com
  • 创建 kaniko Pod 构建镜像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ cat > kaniko-builder.yaml <<-EOF
apiVersion: v1
kind: Pod
metadata:
name: kaniko
namespace: kaniko-demo
spec:
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/traefik/whoami"
- "--destination=shaowenchen/kaniko-demo:v2"
volumeMounts:
- name: kaniko-secret
mountPath: /kaniko/.docker/
restartPolicy: Never
volumes:
- name: kaniko-secret
secret:
secretName: kaniko-secret
items:
- key: .dockerconfigjson
path: config.json
EOF
  • 创建 Pod
1
$ kubectl apply -f kaniko-builder.yaml
  • 查看日志
1
$ kubectl -n kaniko-demo logs kaniko

日志内容与直接在 Docker 中运行类似,这里就不重复贴出。

  • 查看 DockerHub 页面的镜像

  • 使用构建的镜像创建负载
1
$ kubectl -n kaniko-demo run kaniko --image=shaowenchen/kaniko-demo:v2
  • 暴露服务并查看服务端口
1
2
3
4
$ kubectl -n  kaniko-demo expose deploy/kaniko --type=NodePort --port=80 --target-port=80
$ kubectl -n kaniko-demo get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kaniko NodePort 10.233.27.225 <none> 80:30772/TCP 29s
  • 验证构建的镜像能正常使用
1
2
3
4
5
6
7
8
9
10
$ curl 192.168.13.3:30772

Hostname: kaniko-5ddbf597b6-5h8k8
IP: 127.0.0.1
IP: 10.233.90.187
RemoteAddr: 192.168.13.3:11443
GET / HTTP/1.1
Host: 192.168.13.3:30772
User-Agent: curl/7.29.0
Accept: */*

4. 其他关注的问题

  • 支持 ARM 镜像构建

在最新的版本中,已经可以看到 ARM 的 Kaniko 执行器: https://github.com/GoogleContainerTools/kaniko/releases/tag/v1.3.0

  • 无法给 Git 仓库单独配置秘钥和分支参数

官方提供的一种方式是直接将秘钥和分支参数拼接在 Git 仓库的 URL 中

  • 没有提供构建之前的 prehook 命令

需要在 Dockerfile 中完整描述从源码到镜像的构建过程。最好借助于分阶段构建,改造有些项目中直接 Copy 构建产物的的 Dockerfile 。类似下面的 Dockerfile 不能直接使用:

1
2
3
4
5
6
7
FROM java:openjdk-8-jre-alpine

WORKDIR /home

COPY target/*.jar /home

ENTRYPOINT java -jar *.jar

5. 参考

  1. https://github.com/GoogleContainerTools/kaniko
  2. https://github.com/genuinetools/img
  3. https://github.com/containers/buildah

本文转载自:「陈少文的博客」,原文:https://tinyurl.com/yyrnttya,版权归原作者所有。欢迎投稿,投稿邮箱: editor@hi-linux.com