Build in Docker

开发协作中,源码可以通过Git共享协作,但是开发环境一致性,是大家往往容易忽略的问题。项目一开始就实现开发环境统一,可以规避很多团队成员重复搭建开发环境问题,以及帮助新同学快速参与到项目开发中。

虽然Python、Java等语言本身和平台无关,但是项目中难免出现异构多语言、脚本工具等情况。随着项目迭代,项目除核心代码之外,开发构建会出现各种各样的周边依赖,比如bash、make、gcc、curl等等,复杂的项目编译还会依赖特定的库。

Docker镜像是管理这些依赖的很好载体,团队可以共享同一个构建镜像,编译项目时候使用该镜像启动一个容器,将代码放到这个容器中构建,保证每次构建的环境一致性。

Rancher的dapper工具,帮助我们管理构建镜像和一键启动构建容器,在Docker Cli和Dockerfile之上,添加其他辅助功能,帮助我们实现Build in Docker。

Dapperfile

Dapper基于Dockerfile定义了Dockerfile.dapper,用户通过Dockerfile.dapper管理构建镜像依赖,dapper使用该文件一键执行: docker build、 docker run、docker cp 等等命令。

  • docker build 完成在本地构建镜像的生成。
  • docker run 启动一个构建容器,可以依照参数向容器中注入代码,环境变量,构建命令等等。
  • docker cp 可以实现从构建容器中将产物从容器中拷贝到本地。

可以理解为dapper是docker cli的一个封装,一键快速实现多个动作。

Dapper复用了Dockerfile的ENV指令,扩展了特定ENV key,实现docker run 和docker cp的参数注入。

  • DAPPER_RUN_ARGS 执行docker run 时候额外的参数。
  • DAPPER_ENV 获取环境中变量注入到容器中。
  • DAPPER_SOURCE 容器中代码路径。
  • DAPPER_OUTPUT 构建产物目录注入到docker cp中,从容器中复制产物到本地。

另外dapper复用了Dockerfile的ARG指令,扩展ARG从环境中获取ARG key相同的环境变量,注入到docker build –build-args中,动态影响构建镜像的生成。

Dapper示例

以Kine项目的Dockerfile.dapper为例:

FROM golang:1.19-alpine3.16 AS dapper

ARG ARCH=amd64

RUN apk -U add bash coreutils git gcc musl-dev docker-cli vim less file curl wget ca-certificates
RUN GOPROXY=direct go install golang.org/x/tools/cmd/goimports@gopls/v0.9.5
RUN rm -rf /go/src /go/pkg

RUN if [ "${ARCH}" == "amd64" ]; then \
    curl -sL https://raw.githubusercontent.com/golangci/golangci-lint/v1.50.0/install.sh | sh -s;  \
    fi

ENV DAPPER_RUN_ARGS --privileged -v kine-cache:/go/src/github.com/k3s-io/kine/.cache
ENV DAPPER_ENV ARCH REPO TAG DRONE_TAG IMAGE_NAME CROSS SKIP_VALIDATE
ENV DAPPER_SOURCE /go/src/github.com/k3s-io/kine/
ENV DAPPER_OUTPUT ./bin ./dist
ENV DAPPER_DOCKER_SOCKET true
ENV HOME ${DAPPER_SOURCE}
WORKDIR ${DAPPER_SOURCE}

ENTRYPOINT ["./scripts/entry"]
CMD ["ci"]
  • ARG ARCH=amd64 默认指定生成构建镜像时为amd64的环境架构,后面判断ARCH非amd64则不安装golangci相关工具。
  • DAPPER_SOURCE 指定将当前目录代码文件,放到容器中的/go/src/github.com/k3s-io/kine/目录。
  • DAPPER_OUTPUT 声明产物在./bin ./dist目录,dapper工作目录是相对当前目录,所以cp产物出来时候,就到当前目录的./bin ./dist目录。
  • DAPPER_DOCKER_SOCKET 挂载本地的docker socket到构建容器中,可以实现在容器中执行docker命令,比如我们在容器中执行docker build 将产物打包成镜像。

Dapper源码解析

# https://github.com/rancher/dapper/blob/5e204736a984c5ccae817af5618c3c310283f0e4/file/file.go#L112

func (d *Dapperfile) Run(commandArgs []string) error {
	// 执行docker build 生成构建镜像
	// 执行之前解析Dockerfile,获取所有ARG环境变量并注入到docker build --build-arg命令中
	tag, err := d.build(nil, true)
	if err != nil {
		return err
	}


	//生成docker run命令
	//通过docker inspect image 获取所有构建镜像的env信息,解析所有DAPPER_*相关的env信息
	//使用DAPPER_*相关的env信息,生成docker run 命令
	logrus.Debugf("Running build in %s", tag)
	name, args := d.runArgs(tag, "", commandArgs)
	defer func() {
		if d.Keep {
			logrus.Infof("Keeping build container %s", name)
		} else {
			logrus.Debugf("Deleting temp container %s", name)
			if _, err := d.execWithOutput("rm", "-fv", name); err != nil {
				logrus.Debugf("Error deleting temp container: %s", err)
			}
		}
	}()

	//执行docker run 命令
	if err := d.run(args...); err != nil {
		return err
	}

	//通过DAPPER_*相关的env信息,生成docker cp命令并执行
	source := d.env.Source()
	output := d.env.Output()
	if !d.IsBind() && !d.NoOut {
		for _, i := range output {
			p := i
			if !strings.HasPrefix(p, "/") {
				p = path.Join(source, i)
			}
			targetDir := path.Dir(i)
			if err := os.MkdirAll(targetDir, 0755); err != nil {
				return err
			}
			logrus.Infof("docker cp %s %s", p, targetDir)
			if err := d.exec("cp", name+":"+p, targetDir); err != nil {
				logrus.Debugf("Error copying back '%s': %s", i, err)
			}
		}
	}
	return nil
}