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 的优势
目前,实现 CRI 和 OCI 接口的运行时组件越来越多,例如 cri-o、containerd。这些运行时管理工具,并没有类似 /var/run/docker.sock
的文件用于挂载服务。daemon-less 的构建方式能够对接这些工具。
这些 daemon-less 工具,通常运行在 userspace ,而不是以 root 特权运行,提供更加安全的运行时。
通过挂载 /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
|
参数说明:
- 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 页面可以查看到推送的镜像:
执行命令:
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=[email protected]
|
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 中运行类似,这里就不重复贴出。
1
| kubectl -n kaniko-demo run kaniko --image=shaowenchen/kaniko-demo:v2
|
1
| kubectl -n kaniko-demo expose deploy/kaniko --type=NodePort --port=80 --target-port=80
|
1
2
3
| 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 的 Kaniko 执行器: https://github.com/GoogleContainerTools/kaniko/releases/tag/v1.3.0
官方提供的一种方式是直接将秘钥和分支参数拼接在 Git 仓库的 URL 中
需要在 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. 参考