重学容器05: 使用nerdctl + buildkitd构建容器镜像
2021-05-07
1.容器核心技术和Docker的创新:镜像技术 #
容器技术主要包括Cgroup
和Namesapce
这两个内核级别的特性。
Cgroup是control group的缩写即控制组,主要用来做资源控制,可以将一组进程放到一个控制组里,通过给这个控制组分配可用资源,达到控制这组进程可用资源的目的,对不同资源的具体管理是由各个子系统分工完成的,例如cpu控制cpu使用率,memory控制内存的使用上限等等。
Namespace即命名空间,主要用来做访问隔离,针对一类资源进行抽象封装给一个容器使用,对于这类资源,每个容器都有自己的抽象,它们之间彼此不可见实现了访问隔离,例如PID namespace用来隔离进程号,User namespace用来隔离用户和用户组,linux共有6种命名空间。
另外,容器还需要rootfs
实现文件系统隔离。rootfs是一个容器启动时其内部进程可见的文件系统,在容器内用户视角下修改文件时,一般会采用Copy-On-Write(COW)机制。镜像的本质就是一个rootfs。
从某种程度上来说容器 = Cgroup + Namespace + rootfs + 容器引擎
,但Cgroup、Namespace这些特性都是linux中早就存在的技术,因此有人说Docker是"新瓶装旧酒",在Docker之前也有其他使用这些技术实现类似容器功能的实践,例如当年号称第一个开源PaaS平台的Cloud Foundry在将其固定格式应用宝部署到虚拟机后,也使用了Linux提供的Namespace和Cgroup技术对不同应用进行隔离和资源限制。而Docker当年却是凭着Docker镜像
这个创新脱颖而出,可以说镜像技术是Docker成功的最重要之一。
前面我们已经使用nerdctl+containerd实现了在容器管理方面像直接使用docker一样的体验,本节我们更近一般学习nerdctl+buildkitd这对组合,实现镜像构建和镜像管理的功能。
2.buildkit简介 #
buildkit项目也是Docker公司的人开源出来的一个构建工具包,支持OCI标准的镜像构建。它主要包含以下部分:
- 服务端buildkitd,当前支持runc和containerd作为worker,默认是runc,我们这里使用containerd
- 客户端buildctl,负责解析Dockerfile,并向服务端buildkitd发出构建请求
可以看出buildkit是典型的C/S架构,client和server可以不在一台服务器上。而nerdctl在构建镜像方面也可以作为buildkitd的客户端,我们这里使用nerdctl。
3.部署buildkitd #
下面在测试服务器上部署buildkitd。下载完成后,仍然将buildctl和buildkitd安装到/usr/local/containerd目录中:
1wget https://github.com/moby/buildkit/releases/download/v0.8.3/buildkit-v0.8.3.linux-amd64.tar.gz
2tar -zxvf buildkit-v0.8.3.linux-amd64.tar.gz -C /usr/local/containerd/
3
4tree /usr/local/containerd/bin/ | grep build
5├── buildctl
6├── buildkitd
7├── buildkit-qemu-aarch64
8├── buildkit-qemu-arm
9├── buildkit-qemu-i386
10├── buildkit-qemu-ppc64le
11├── buildkit-qemu-riscv64
12├── buildkit-qemu-s390x
13├── buildkit-runc
14
15ln -s /usr/local/containerd/bin/buildkitd /usr/local/bin/buildkitd
16ln -s /usr/local/containerd/bin/buildctl /usr/local/bin/buildctl
创建systemd服务相关文件/etc/systemd/system/buildkit.socket:
1[Unit]
2Description=BuildKit
3Documentation=https://github.com/moby/buildkit
4
5[Socket]
6ListenStream=%t/buildkit/buildkitd.sock
7
8[Install]
9WantedBy=sockets.target
/etc/systemd/system/buildkit.service:
1[Unit]
2Description=BuildKit
3Requires=buildkit.socket
4After=buildkit.socketDocumentation=https://github.com/moby/buildkit
5
6[Service]
7ExecStart=/usr/local/bin/buildkitd --oci-worker=false --containerd-worker=true
8
9[Install]
10WantedBy=multi-user.target
启动buildkitd:
1systemctl daemon-reload
2systemctl enable buildkit
3systemctl start buildkit
4.构建alpine和busybox基础镜像 #
alpine linux的核心特点是体积小、简单和安全,因此很多人选择alpine镜像作为基础镜像。 我们编写一个Dockerfile在docker官方alpine镜像的基础上,安装glibc,同时配置中国时区:
1FROM alpine:3.13.5
2
3# Here we install GNU libc (aka glibc) and set C.UTF-8 locale as default.
4ENV LANG=C.UTF-8
5RUN echo "**** install packages ****" && \
6 sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories &&\
7 ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \
8 ALPINE_GLIBC_PACKAGE_VERSION="2.33-r0" && \
9 ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
10 ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
11 ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
12 apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \
13 echo \
14 "-----BEGIN PUBLIC KEY-----\
15 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\
16 y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\
17 tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\
18 m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\
19 KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\
20 Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\
21 1QIDAQAB\
22 -----END PUBLIC KEY-----" | sed 's/ */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \
23 wget \
24 "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
25 "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
26 "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
27 apk add --no-cache \
28 "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
29 "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
30 "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
31 \
32 rm "/etc/apk/keys/sgerrand.rsa.pub" && \
33 /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true && \
34 echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \
35 \
36 apk del glibc-i18n && \
37 \
38 rm "/root/.wget-hsts" && \
39 apk del .build-dependencies && \
40 rm \
41 "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
42 "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
43 "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME"
44
45
46RUN apk update --no-cache && apk add ca-certificates --no-cache && \
47 apk add tzdata --no-cache && \
48 ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
49 echo "Asia/Shanghai" > /etc/timezone
构建alpine基础镜像:
1nerdctl build -t base-alpine .
2
3nerdctl images
4WARN[0000] unparsable image name "overlayfs@sha256:8628f76ed129c628eccb2259cf61a3138adac73247565f04fb00ea79fab6ae28"
5REPOSITORY TAG IMAGE ID CREATED SIZE
6base-alpine latest 8628f76ed129 3 minutes ago 8.8 MiB
7 8628f76ed129 15 hours ago 8.8 MiB
构建镜像成功后,查看镜像时,报了一个警告WARN[0000] unparsable image name
,nerdctl images
除了列出我们构建的base-alpine:latest
镜像外,还有一个tag为空的重复镜像,
nerdctl的github上有人提了issues 177,官方还没有回应是buildkit的问题还是nerdctl的问题,但这并不影响我们对base-alpine
这个基础镜像的使用。
接下来构建busybox基础镜像,busybox是一个集成了一百多个最常用的Linux命令和工具的软件工具箱,它在单一的可执行文件中提供了精简的工具集,有人把busybox作为linux系统的瑞士军刀, 这里编写一个Dockerfile在docker官方busybox镜像的基础上:
1FROM busybox:1.33.1-glibc
2
3COPY zoneinfo /usr/share/zoneinfo/
4RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
构建busybox基础镜像:
1cp -R /usr/share/zoneinfo .
2nerdctl build -t base-busybox .
将前面构建的两个基础镜像推送到镜像仓库:
1nerdctl image tag base-alpine:latest harbor.mycompany.com/base/base-alpine:latest
2nerdctl image tag base-busybox:latest harbor.mycompany.com/base/base-busybox:latest
3
4nerdctl login -u username -p password harbor.mycompany.com
5
6nerdctl push harbor.mycompany.com/base/base-alpine:latest
7nerdctl push harbor.mycompany.com/base/base-busybox:latest
5.编写Web应用并完成镜像构建 #
下面我们用go写一个简单hello world的web应用main.go:
1package main
2
3import (
4 "io"
5 "log"
6 "net/http"
7)
8
9func main() {
10 handler := func(w http.ResponseWriter, req *http.Request) {
11 io.WriteString(w, "Hello, world!\n")
12 }
13 http.HandleFunc("/hello", handler)
14 err := http.ListenAndServe(":8080", nil)
15 if err != nil {
16 log.Fatal(err)
17 }
18}
编写构建这个程序的Dockerfile:
1FROM golang:1.16.4-alpine3.13 as builder
2WORKDIR /go/src/hello
3COPY main.go .
4ENV GO111MODULE=off
5RUN go build .
6
7FROM harbor.mycompany.com/base/base-alpine:latest
8WORKDIR /app/
9COPY --from=0 /go/src/hello/hello .
10USER nobody
11CMD ["./hello"]
12EXPOSE 8080
这里使用多阶段构建,golang:1.16.4-alpine3.13
只在构建阶段会用到,go build出的二进制文件在构建的第二阶段被拷贝到base-alpine
中。
使用多阶段构建可以确保构建出的应用服务镜像体积尽可能的小。
构建这个程序的镜像:
1nerdctl build -t go-web-hello .
启动容器并测试:
1nerdctl run -p 8080:8080 -d go-web-hello
2
3curl localhost:8080/hello
4Hello, world!
6.总结 #
通过本节的学习,使用nerdctl + buildkitd也可以轻松的完成容器镜像的构建。 使用containerd及nerdctl,buildkit等工具已经可以完全替代docker在镜像构建、单机启动容器和管理容器的功能了。