Docker與Golang的巧妙結合

llitfkitfk發表於2016-10-27

譯文轉載: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/...

注意:不需要設定GOOSGOARCH,正因為,想要一個執行在 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 debianFROM 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 駭客日提出的。

我要感謝所有的校對材料、提出建議和意見讓它更好的人,包括但不侷限於:

所有的錯誤和拼寫錯誤都是我自己的;所有的好東西都是他們的!

原文連結:Docker + Golang = <3(翻譯:陳晏娥 審校:田浩浩

更多原創文章乾貨分享,請關注公眾號
  • Docker與Golang的巧妙結合
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章