基于docker multi-stage分离Golang编译与运行时镜像
在没有docker技术之前,我们利用Jenkins实现CI/CD的时候,代码是在Jenkins宿主机上完成编译的。
这样不同的Jenkins构建任务之间就缺乏良好的隔离性,可能A项目需要Golang1.12版本,而B项目需要Golang1.13版本,这样宿主机就很不方便同时满足两者。
docker带来的改变
在docker技术出现之后,给隔离带来了便捷性。
我们完全可以为每一个构建任务启动一个专属容器,根据环境需要使用不同版本的Golang镜像完成代码编译。
我们可以直接把编译go程序的容器打包成镜像直接用于发布,但是会面临一个问题:
编译程序的容器内残留了golang编译的依赖,当然也包含了golang语言本身。而实际golang编译产生的二进制是不需要golang环境的,可以直接独立运行的。
借助multi-stage实现优化
出于镜像体积的考虑,如果我们可以把编译好的go二进制copy到另外一个纯净的镜像里发布,那么PAAS集群下载镜像的体积就比较小,分发速度就可以得到保证。
上述思路需要基于docker提供的multi-stage功能实现,它允许在1个Dockerfile中定义连续构建多个镜像,并且允许在镜像之间进行文件Copy。
因此,golang程序的Docker打包过程就变成了这样:
- 构建第1个镜像,在内部完成依赖下载与程序编译,产生二进制。
- 构建第2个镜像,从第1个镜像仅copy二进制到第2个镜像内,最终发布第2个镜像提供服务。
演示项目地址:https://github.com/owenliang/docker-multi-stage
下面简单讲解一下项目。
写一个Go程序
采用go module管理依赖,程序只有1个文件:https://github.com/owenliang/docker-multi-stage/blob/master/cmd/main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package main import ( "fmt" "github.com/robfig/cron/v3" ) func main() { cron := cron.New() cron.AddFunc("* * * * *", func() { fmt.Println("running...") }) cron.Run() } |
引用了第三方的库实现定时任务。
Dockerfile的stage1
接下来编写Dockerfile来实现上述程序的编译与打包。
完整Dockerfile见:https://github.com/owenliang/docker-multi-stage/blob/master/Dockerfile
这里使用了docker的multi-stage功能,因此第1个stage就是为了编译go程序:
1 2 3 4 5 6 7 8 9 |
ARG GOLANG_VER # stage1 - 编译代码生成二进制 FROM golang:${GOLANG_VER} as BINARY WORKDIR /root COPY cmd ./project/cmd/ COPY go.sum go.mod ./project/ ENV GOPROXY=goproxy.io RUN cd ./project/cmd && go build -o main |
忽略一些细节的话,大家应该可以理解这个镜像的构造过程:
- FROM:基于dockerhub上的官方golang镜像作为base image,版本是通过ARG传参进来的。
- COPY:把cmd目录、go.sum、go.mod等项目文件统统copy到容器内。
- ENV:配置golang走代理下载依赖。
- RUN:在容器内进入到.go文件目录,执行go build自动完成依赖下载和程序编译,产生二进制main。
比较陌生的细节有2个:
- ARG:执行docker build的时候,可以传参给Dockerfile,以便实现灵活控制;这里就是支持命令行传参GOLANG_VER,用来指定要使用哪个版本的golang镜像用于程序编译。
- as BINARY:这相当于给stage起了一个名,叫做BINARY;因为后一个stage中我们要从该stage构建的镜像中把main二进制copy过去,因此需要给第1个stage起一个名字。
实际命令行会这样执行:
1 |
docker build --build-arg GOLANG_VER=1.13 --build-arg APP_LOCATION=/go/myapp -t test . |
至于另外一个APP_LOCATION则是用于我们第2个stage使用的参数,稍后会看到。
Dockerfile的stage2
stage2的目的是把stage1的main二进制拷贝过来,输出的镜像就是用于发布用途的运行时镜像了。
golang的二进制可以直接运行,不依赖于go环境,所以我这里采用centos作为基础镜像。
1 2 3 4 5 6 7 8 |
# stage2 - 构造运行时镜像 FROM centos:7 ARG APP_LOCATION ENV APP_LOCATION ${APP_LOCATION} WORKDIR /root COPY --from=BINARY /root/project/cmd/main ${APP_LOCATION} RUN chmod a+rwx ${APP_LOCATION} CMD ${APP_LOCATION} |
这里需要注意的包含2点:
- ENV与CMD:APP_LOCATION参数是为了控制将main二进制移动到什么路径下;但是CMD部分并不能访问到ARG APP_LOCATION,因为CMD命令是容器拉起时才会解释执行的,所以必须通过ENV保存APP_LOCATION,这样CMD在执行时才会取到值。
- COPY:这里用了–from=BINARY指定了从哪个stage的镜像中进行文件copy。
根据之前docker build命令可知,stage2构建的镜像最终命名为test,接下来做一个测试。
测试image
先启动容器到后台,容器的入口CMD命令就是执行了go的二进制程序:
1 2 |
docker run -d test f9d511032da47653f984de04d438ecadaad57b7eb353202ddb7c4836dc12641b |
然后通过bash进入容器,确认go二进制正常运行:
1 2 3 4 5 6 |
docker exec -it f9d511032da47653f984de04d438ecadaad57b7eb353202ddb7c4836dc12641b bash [root@f9d511032da4 ~]# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.2 0.0 102964 1292 ? Ssl 02:55 0:00 /go/myapp root 9 1.5 0.1 11828 2880 pts/0 Ss 02:56 0:00 bash root 23 1.0 0.1 51748 3372 pts/0 R+ 02:56 0:00 ps aux |
可以看到/go/myapp已经拉起运行。
最后
其实无论是什么语言,基于docker multi-stage均可以实现编译环境与运行环境的隔离,实现灵活的CI/CD控制。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
