Docker与Golang的巧妙结合
docker与golang的巧妙结合
【编者的话】这是一个展示在使用go语言时如何让docker更有用的提示与技巧的简辑。例如,如何使用不同版本的go工具链来编译go代码,如何交叉编译到不同的平台(并且测试结果!),或者如何制作真正小的容器镜像。
下面的文章假定你已经安装了docker。不必是最新版本(这篇文章不会使用docker任何花哨的功能)。
没有go的go
...意思是:“不用安装go就能使用go”
如果你写go代码,或者你对go语言有一点点兴趣,你肯定要安装了go编译器和go工具链,所以你可能想知道:“重点是什么?”;但有些情况下,你想不安装go就来编译go。
- 机器上依旧有老版本go 1.2(你不能或不想更新),不得不使用这个代码库,需要一个高版本的工具链。
- 想使用go1.5的交叉编译功能(例如,确保能从一个linux系统创建操作系统x的二进制文件)。
- 想拥有多版本的go,但不想完全弄乱系统。
- 想100%确定项目和它所有的依赖,下载,建立和运行在一个纯净的系统上。
如果遇到上述情况,找docker来解决!
在容器里编译一个程序
当你安装了go,你可以执行go get -v github.com/user/repo来下载,创建和安装一个库。(-v只是信息显示,如果你喜欢工具链快速和静默地运行,可以将它移除!)
你也可以执行go get github.com/user/repo/...来下载,创建和安装那个repo(包括库和二进制文件)里面所有的东西。
我们可以在一个容器里面这样做!
试试这个:
docker run golang go get -v github.com/golang/example/hello/...
这将拉取golang镜像(除非你已经有了,那它会马上启动),并且创建一个基于它的容器。在那个容器里,go会下载一个“hello world”的例子,创建它,安装它。但它会把它安装到这个容器里……我们现在怎么运行那个程序呢?
在容器里运行程序
一个办法是提交我们刚刚创建的容器,即,打包它到一个新的镜像:
docker commit $(docker ps -lq) awesomeness
注意:docker ps –lq输出最后一个执行的容器的id(只有id!)。如果你是机器的唯一用户,并且你从上一个命令开始没有创建另一个容器,那这个容器就是你刚刚创建的“hello world”的例子。
现在,可以用刚刚构建的镜像创建容器来运行程序:
docker run awesomeness hello
输出会是hello, go examples!。
闪光点
当用docker commit构建镜像时,可以用--change标识指定任意dockerfile命令。例如,可以使用一个cmd或者entrypoint命令以便docker run awesomeness自动执行hello。
在一次性容器上运行
如果不想创建额外的镜像只想运行这个go程序呢?
使用:
docker run --rm golang sh -c \
"go get github.com/golang/example/hello/... && exec hello"
等等,那些花哨的东西是什么?
- --rm 告诉docker cli一旦容器退出,就自动发起一个docker rm命令。那样,不会留下任何东西。
- 使用shell逻辑运算符&&把创建步骤(go get)和执行步骤(exec hello)联接在一起。如果不喜欢shell,&&意思是“与”。它允许第一部分go get...,并且如果(而且仅仅是如果!)那部分运行成功,它将执行第二部分(exec hello)。如果你想知道为什么这样:它像一个懒惰的and计算器,只有当左边的值是true才计算右边的。
- 传递命令到sh –c,因为如果是简单的做docker run golang "go get ... && hello",docker将试着执行名为go space get space etc的程序。并且那不会起作用。因此,我们启动一个shell,并让shell执行命令序列。
- 使用exec hello而不是hello:这将使用hello程序替代当前的进程(我们刚才启动的shell)。这确保hello在容器里是pid 1。而不是shell的是pid 1而hello作为一个子进程。这对这个微小的例子毫无用处,但是当运行更有用的程序,这将允许它们正确地接收外部信号,因为外部信号是发送给容器里的pid 1。你可能会想,什么信号啊?好的例子是docker stop,发送sigterm给容器的pid 1。
使用不同版本的go
当使用golang镜像,docker扩展为golang:latest,将(像你所猜的)映射到docker hub上的最新可用版本。
如果想用一个特定的go版本,很容易:在镜像名字后面用那个版本做标签指定它。
例如,想用go 1.5,修改上面的例子,用golang:1.5替换golang:
docker run --rm golang:1.5 sh -c \
"go get github.com/golang/example/hello/... && exec hello"
你能在docker hub的golang镜像页面上看到所有可用的版本(和变量)。
在系统上安装
好了,如果想在系统上运行编译好的程序,而不是一个容器呢?我们将复制这个编译了的二进制文件到容器外面。注意,仅当容器架构和主机架构匹配的时候,才会起作用;换言之,如果在linux上运行docker。(我排除的可能是运行windows容器的人!)
最容易在容器外获得二进制文件的方法是映射$gopath/bin目录到一个本地目录,在golang容器里,$gopath是/go.所以我们可以如下操作:
docker run -v /tmp/bin:/go/bin \
golang go get github.com/golang/example/hello/...
/tmp/bin/hello
如果在linux上,将看到hello, go examples!消息。但如果是,例如在mac上,可能会看到:
-bash:
/tmp/test/hello: cannot execute binary file
我们又能做什么呢?
交叉编译
go 1.5具备优秀的开箱即用交叉编译能力,所以如果你的容器操作系统和/或架构和你的系统不匹配,根本不是问题!
开启交叉编译,需要设置goos和/或goarch。
例如,假设在64位的mac上:
docker run -e goos=darwin -e goarch=amd64 -v /tmp/crosstest:/go/bin \
golang go get github.com/golang/example/hello/...
交叉编译的输出不是直接在$gopath/bin,而是在$gopath/bin/$goos_$goarch.。换言之,想运行程序,得执行/tmp/crosstest/darwin_amd64/hello.。
直接安装到$path
如果在linux上,甚至可以直接安装到系统bin目录:
docker run -v /usr/local/bin:/go/bin \
golang get github.com/golang/example/hello/...
然而,在mac上,尝试用/usr作为一个卷将不能挂载mac的文件系统到容器。会挂载moby vm(小linux vm藏在工具栏docker图标的后面)的/usr目录。(译注:目前docker for mac版本可以自定义设置挂载路径)
但可以使用/tmp或者在你的home目录下的什么其它目录,然后从这里复制。
创建依赖镜像
我们用这种技术产生的go二进制文件是静态链接的。这意味着所有需要运行的代码包括所有依赖都被嵌入了。动态链接的程序与之相反,不包含一些基本的库(像“libc”)并且使用系统范围的复制,是在运行时确定的。
这意味着可以在容器里放弃go编译好的程序,没有别的,并且它会运行。
我们试试!
scratch镜像
docker生态系统有一个特殊的镜像:scratch.这是一个空镜像。它不需要被创建或者下载,因为定义的就是空的。
给新的go依赖镜像创建一个新的空目录。
在这个新目录,创建下面的dockerfile:
from scratch
copy ./hello /hello
entrypoint ["/hello"]
这意味着:从scratch开始(一个空镜像),增加hello文件到镜像的根目录,*定义hello程序为启动这个容器后默认运行的程序。
然后,产生hello二进制文件如下:
docker run -v $(pwd):/go/bin --rm \
golang go get github.com/golang/example/hello/...
注意:不需要设置goos和goarch,正因为,想要一个运行在docker容器里的二进制文件,不是在主机上。所以不用设置这些变量!
然后,创建镜像:
docker build -t hello .
测试它:
docker run hello
(将显示“hello, go examples!”)
最后但不重要,检查镜像的大小:
docker images hello
如果一切做得正确,这个镜像大约2m。相当好!
构建东西而不推送到github
当然,如果不得不推送到github,每次编译都会浪费很多时间。
想在一个代码段上工作并在容器中创建它时,可以在golang容器里挂载一个本地目录到/go。所以$gopath是持久调用:docker run -v $home/go:/go golang ....
但也可以挂载本地目录到特定的路径上,来“重载”一些包(那些在本地编辑的)。这是一个完整的例子:
# adapt the two following environment variables if you are not running on a mac export goos=darwin goarch=amd64 mkdir go-and-docker-is-love cd go-and-docker-is-love git clone git://github.com/golang/example cat example/hello/hello.go sed -i .bak s/olleh/eyb/ example/hello/hello.go docker run --rm \ -v $(pwd)/example:/go/src/github.com/golang/example \ -v $(pwd):/go/bin/${goos}_${goarch} \ -e goos -e goarch \ golang go get github.com/golang/example/hello/... ./hello # should display "bye, go examples!"
网络包和cgo的特殊情况
进入真实的go代码世界前,必须承认的是:在二进制文件上有一点点偏差。如果在使用cgo,或如果在使用net包,go链接器将生成一个动态库。这种情况下,net包(里面确实有许多有用的go程序!),罪魁祸首是dns解析。大多数系统都有一个花哨的,模块化的名称解析系统(像名称服务切换),它依赖于插件,技术上,是动态库。默认地,go将尝试使用它;这样,它将产生动态库。
我们怎么解决?
重用另一个版本的libc
一个解决方法是用一个基础镜像,有那些程序功能所必需的库。几乎任何“正规”基于gnu libc的linux发行版都能做到。所以,例如,使用from debian或from fedora,替代from scratch。现在结果镜像会比原来大一些;但至少,大出来的这一点将和系统里其它镜像共享。
注意:这种情况不能使用alpine,因为alpine是使用musl库而不是gnu libc。
使用自己的libc
另一个解决方案是像做手术般地提取需要的文件,用copy替换容器里的。结果容器会小。然而,这个提取过程困难又繁琐,太多更深的细节要处理。
如果想自己看,看看前面提到的ldd和名称服务切换插件。
用netgo生成静态二进制文件
我们也可以指示go不用系统的libc,用本地dns解析代替go的netgo。
要使用它,只需在go get选项加入-tags netgo -installsuffix netgo。
-tags netgo指示工具链使用netgo。
-installsuffix netgo确保结果库(任何)被一个不同的,非默认的目录所替代。如果做多重go get(或go build)调用,这将避免
代码创建和用不用netgo之间的冲突。如果像目前我们讲到的这样,在容器里创建,是完全没有必要的。因为这个容器里面永远没有其他go代码要编译。但它是个好主意,习惯它,或至少知道这个标识存在。
ssl证书的特殊情况
还有一件事,你会担心,你的代码必须验证ssl证书;例如,通过https联接外部api。这种情况,需要将根证书也放入容器里,因为go不会捆绑它们到二进制文件里。
安装ssl证书
再次,有很多可用的选择,但最简单的是使用一个已经存在的发布里面的包。
alpine是一个好的选择,因为它非常小。下面的dockerfile将给你一个小的基础镜像,但捆绑了一个过期的跟证书:
from alpine:3.4
run apk add --no-cache ca-certificates apache2-utils
来看看吧,结果镜像只有6mb!
注意:--no-cache选项告诉apk(alpine包管理器)从alpine的镜像发布上获取可用包的列表,不保存在磁盘上。你可能会看到dockerfiles做这样的事apt-get update && apt-get install ... && rm -rf /var/cache/apt/*;这实现了(即在最终镜像中不保留包缓存)与一个单一标志相当的东西。
一个附加的回报:把你的应用程序放入基于alpine镜像的容器,让你获得了一堆有用的工具。如果需要,现在你可以吧shell放入容器并在它运行时做点什么。
打包
我们看到docker如何帮助我们在干净独立的环境里编译go代码;如何使用不同版本的go工具链;以及如何在不同的操作系统和平台之间交叉编译。
我们还看到go如何帮我们给docker创建小的,容器依赖镜像,并且描述了一些静态库和网络依赖相关的微妙联系(没别的意思)。
除了go是真的适合docker项目这个事实,我们希望展示给你的是,go和docker如何相互借鉴并且一起工作得很好!
致谢
这最初是在2016年gophercon骇客日提出的。
我要感谢所有的校对材料、提出建议和意见让它更好的人,包括但不局限于:
aaron lehmann
stephen day
aj bowen
所有的错误和拼写错误都是我自己的;所有的好东西都是他们的!
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!