0
点赞
收藏
分享

微信扫一扫

Docker与Golang的巧妙结合


没有go的Go
在容器里编译一个程序
docker run golang go get -v github.com/golang/example/hello/...在容器里运行程序
docker commit $(docker ps -lq) awesomenessdocker 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如何相互借鉴并且一起工作得很好!

​​

​​


举报

相关推荐

0 条评论