使用容器進行 Go 開發

先決條件

逐步完成 將您的映像檔作為容器執行 模組的步驟,以瞭解如何管理容器的生命週期。

簡介

在本模組中,您將瞭解如何在容器中執行資料庫引擎,並將其連接到範例應用程式的擴展版本。您將看到一些用於保存持久性資料和連接容器以彼此通訊的選項。最後,您將學習如何使用 Docker Compose 有效地管理此類多容器本機開發環境。

本地資料庫和容器

您將使用的資料庫引擎稱為 CockroachDB。它是一個現代化的、雲原生的、分散式 SQL 資料庫。

您將使用 CockroachDB 的 Docker 映像檔pgxpqGORMupper/db 也適用於 CockroachDB。

如需 Go 與 CockroachDB 之間關係的更多資訊,請參閱 CockroachDB 文件,雖然這對於繼續閱讀本指南並非必要。

儲存

資料庫的重點是擁有永續的資料儲存空間。磁碟區 (Volumes) 是用於保存 Docker 容器產生和使用的資料的首選機制。因此,在您啟動 CockroachDB 之前,請先為它建立磁碟區。

要建立受管理的磁碟區,請執行

$ docker volume create roach
roach

您可以使用以下指令檢視 Docker 執行個體中所有受管理磁碟區的清單

$ docker volume list
DRIVER    VOLUME NAME
local     roach

網路

範例應用程式和資料庫引擎將透過網路互相通訊。有不同種類的網路設定方式,而您將使用所謂的使用者定義橋接網路。它將為您提供 DNS 查閱服務,以便您可以透過主機名稱來參考您的資料庫引擎容器。

以下指令會建立一個名為 `mynet` 的新橋接網路

$ docker network create -d bridge mynet
51344edd6430b5acd121822cacc99f8bc39be63dd125a3b3cd517b6485ab7709

與受管理磁碟區的情況一樣,有一個指令可以列出 Docker 執行個體中設定的所有網路

$ docker network list
NETWORK ID     NAME          DRIVER    SCOPE
0ac2b1819fa4   bridge        bridge    local
51344edd6430   mynet         bridge    local
daed20bbecce   host          host      local
6aee44f40a39   none          null      local

您的橋接網路 `mynet` 已成功建立。其他三個名為 `bridge`、`host` 和 `none` 的網路是預設網路,它們是由 Docker 本身建立的。雖然這與本指南無關,但您可以在 網路概觀 章節中了解更多關於 Docker 網路的資訊。

為磁碟區和網路選擇好的名稱

俗話說,電腦科學中只有兩件難事:快取失效和命名事物。以及差一錯誤 (off-by-one errors)。

在為網路或受管理磁碟區選擇名稱時,最好選擇一個能表明預期用途的名稱。本指南旨在簡潔,因此使用了簡短、通用的名稱。

啟動資料庫引擎

現在,例行工作已完成,您可以在容器中執行 CockroachDB,並將其連接到您剛建立的磁碟區和網路。當您執行以下指令時,Docker 將從 Docker Hub 拉取映像檔並在本地端為您執行

$ docker run -d \
  --name roach \
  --hostname db \
  --network mynet \
  -p 26257:26257 \
  -p 8080:8080 \
  -v roach:/cockroach/cockroach-data \
  cockroachdb/cockroach:latest-v20.1 start-single-node \
  --insecure

# ... output omitted ...

請注意巧妙地使用標籤 `latest-v20.1` 來確保您拉取的是 20.1 的最新修補程式版本。可用標籤的多樣性取決於映像檔維護者。在這裡,您的目的是擁有 CockroachDB 的最新修補程式版本,同時隨著時間的推移不會偏離已知的工作版本太遠。要查看 CockroachDB 映像檔可用的標籤,您可以前往 Docker Hub 上的 CockroachDB 頁面設定資料庫引擎

現在資料庫引擎已啟動,在您的應用程式開始使用它之前,還有一些設定要做。幸運的是,這並不多。您必須

  1. 建立一個空的資料庫。
  2. 在資料庫引擎中註冊一個新的使用者帳戶。
  3. 授予該新使用者對資料庫的存取權限。

您可以藉助 CockroachDB 內建的 SQL shell 來完成此操作。要在資料庫引擎執行的同一個容器中啟動 SQL shell,請輸入

  1. 在 SQL shell 中,建立範例應用程式將使用的資料庫

  2. 使用使用者名稱 `totoro` 在資料庫引擎中註冊一個新的 SQL 使用者帳戶。

  3. 授予新使用者必要的權限

  4. 輸入 `quit` 退出 shell。

以下是與 SQL shell 互動的範例。

認識範例應用程式

現在您已啟動並設定資料庫引擎,您可以將注意力轉移到應用程式。

此模組的範例應用程式是您先前模組中使用的 docker-gs-ping 應用程式的擴充版本。您有兩個選項

  • 您可以更新您的 docker-gs-ping 本機副本,以符合本章中介紹的新擴充版本;或
  • 您可以複製 docker/docker-gs-ping-dev 儲存庫。建議使用後一種方法。

要取出範例應用程式,請執行

$ git clone https://github.com/docker/docker-gs-ping-dev.git
# ... output omitted ...

應用程式的 main.go 現在包含資料庫初始化程式碼,以及實作新業務需求的程式碼

  • 發送到 /send 的 HTTP POST 請求,其中包含 { "value" : string } JSON 必須將值儲存到資料庫中。

您還有一個針對另一個業務需求的更新。該需求是

  • 應用程式會以包含心形符號 ("<3") 的文字訊息回應對 / 的請求。

現在它將會是

  • 應用程式會以包含儲存在資料庫中的訊息計數的字串回應,並以括號括起來。

    範例輸出:Hello, Docker! (7)

以下是 main.go 的完整原始程式碼清單。

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/cenkalti/backoff/v4"
	"github.com/cockroachdb/cockroach-go/v2/crdb"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {

	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	db, err := initStore()
	if err != nil {
		log.Fatalf("failed to initialize the store: %s", err)
	}
	defer db.Close()

	e.GET("/", func(c echo.Context) error {
		return rootHandler(db, c)
	})

	e.GET("/ping", func(c echo.Context) error {
		return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
	})

	e.POST("/send", func(c echo.Context) error {
		return sendHandler(db, c)
	})

	httpPort := os.Getenv("HTTP_PORT")
	if httpPort == "" {
		httpPort = "8080"
	}

	e.Logger.Fatal(e.Start(":" + httpPort))
}

type Message struct {
	Value string `json:"value"`
}

func initStore() (*sql.DB, error) {

	pgConnString := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable",
		os.Getenv("PGHOST"),
		os.Getenv("PGPORT"),
		os.Getenv("PGDATABASE"),
		os.Getenv("PGUSER"),
		os.Getenv("PGPASSWORD"),
	)

	var (
		db  *sql.DB
		err error
	)
	openDB := func() error {
		db, err = sql.Open("postgres", pgConnString)
		return err
	}

	err = backoff.Retry(openDB, backoff.NewExponentialBackOff())
	if err != nil {
		return nil, err
	}

	if _, err := db.Exec(
		"CREATE TABLE IF NOT EXISTS message (value TEXT PRIMARY KEY)"); err != nil {
		return nil, err
	}

	return db, nil
}

func rootHandler(db *sql.DB, c echo.Context) error {
	r, err := countRecords(db)
	if err != nil {
		return c.HTML(http.StatusInternalServerError, err.Error())
	}
	return c.HTML(http.StatusOK, fmt.Sprintf("Hello, Docker! (%d)\n", r))
}

func sendHandler(db *sql.DB, c echo.Context) error {

	m := &Message{}

	if err := c.Bind(m); err != nil {
		return c.JSON(http.StatusInternalServerError, err)
	}

	err := crdb.ExecuteTx(context.Background(), db, nil,
		func(tx *sql.Tx) error {
			_, err := tx.Exec(
				"INSERT INTO message (value) VALUES ($1) ON CONFLICT (value) DO UPDATE SET value = excluded.value",
				m.Value,
			)
			if err != nil {
				return c.JSON(http.StatusInternalServerError, err)
			}
			return nil
		})

	if err != nil {
		return c.JSON(http.StatusInternalServerError, err)
	}

	return c.JSON(http.StatusOK, m)
}

func countRecords(db *sql.DB) (int, error) {

	rows, err := db.Query("SELECT COUNT(*) FROM message")
	if err != nil {
		return 0, err
	}
	defer rows.Close()

	count := 0
	for rows.Next() {
		if err := rows.Scan(&count); err != nil {
			return 0, err
		}
		rows.Close()
	}

	return count, nil
}

儲存庫還包含 Dockerfile,它幾乎與先前模組中介紹的多階段 Dockerfile 完全相同。它使用官方 Docker Go 映像檔來建置應用程式,然後透過將編譯的二進位檔放入更精簡的無發行版映像檔來建置最終映像檔。

無論您是更新了舊的範例應用程式,還是取出了新的範例應用程式,都必須建置這個新的 Docker 映像檔,以反映應用程式原始程式碼的變更。

建構應用程式

您可以使用熟悉的 build 命令建置映像檔

$ docker build --tag docker-gs-ping-roach .

執行應用程式

現在,執行您的容器。這次您需要設定一些環境變數,以便您的應用程式知道如何存取資料庫。目前,您將直接在 docker run 命令中執行此操作。稍後,您將看到使用 Docker Compose 的更方便方法。

注意

由於您是在不安全模式下執行 CockroachDB 叢集,因此密碼的值可以是任何值。

在正式環境中,請勿在不安全模式下執行。

$ docker run -it --rm -d \
  --network mynet \
  --name rest-server \
  -p 80:8080 \
  -e PGUSER=totoro \
  -e PGPASSWORD=myfriend \
  -e PGHOST=db \
  -e PGPORT=26257 \
  -e PGDATABASE=mydb \
  docker-gs-ping-roach

關於此命令,有幾點需要注意。

  • 這次您將容器埠 8080 映射到主機埠 80。因此,對於 GET 請求,您可以直接使用 curl localhost

    $ curl localhost
    Hello, Docker! (0)
    

    或者,如果您願意,也可以使用正確的 URL

    $ curl http://localhost/
    Hello, Docker! (0)
    
  • 目前儲存的訊息總數為 0。這沒問題,因為您尚未向您的應用程式發布任何內容。

  • 您透過其主機名稱 db 來參考資料庫容器。這就是為什麼您在啟動資料庫容器時使用 --hostname db 的原因。

  • 實際密碼無關緊要,但必須將其設定為某個值,以免混淆範例應用程式。

  • 您剛執行的容器名為 rest-server。這些名稱對於管理容器生命週期很有用

    # Don't do this just yet, it's only an example:
    $ docker container rm --force rest-server
    

測試應用程式

在上一節中,您已經測試了使用 GET 查詢您的應用程式,並且它為儲存的訊息計數器傳回了零。現在,向它發布一些訊息

$ curl --request POST \
  --url http://localhost/send \
  --header 'content-type: application/json' \
  --data '{"value": "Hello, Docker!"}'

應用程式會以訊息的內容回應,這表示它已儲存在資料庫中

{ "value": "Hello, Docker!" }

傳送另一則訊息

$ curl --request POST \
  --url http://localhost/send \
  --header 'content-type: application/json' \
  --data '{"value": "Hello, Oliver!"}'

同樣地,您會收到訊息的值

{ "value": "Hello, Oliver!" }

執行 curl 並查看訊息計數器顯示的內容

$ curl localhost
Hello, Docker! (2)

在此範例中,您傳送了兩則訊息,並且資料庫保留了它們。還是它有保留?停止並移除所有容器,但不要移除磁碟區,然後再試一次。

首先,停止容器

$ docker container stop rest-server roach
rest-server
roach

然後,移除它們

$ docker container rm rest-server roach
rest-server
roach

確認它們已消失

$ docker container list --all
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

然後再次啟動它們,先啟動資料庫

$ docker run -d \
  --name roach \
  --hostname db \
  --network mynet \
  -p 26257:26257 \
  -p 8080:8080 \
  -v roach:/cockroach/cockroach-data \
  cockroachdb/cockroach:latest-v20.1 start-single-node \
  --insecure

接著是服務

$ docker run -it --rm -d \
  --network mynet \
  --name rest-server \
  -p 80:8080 \
  -e PGUSER=totoro \
  -e PGPASSWORD=myfriend \
  -e PGHOST=db \
  -e PGPORT=26257 \
  -e PGDATABASE=mydb \
  docker-gs-ping-roach

最後,查詢您的服務

$ curl localhost
Hello, Docker! (2)

太好了!來自資料庫的記錄計數是正確的,儘管您不僅停止了容器,而且還在啟動新的執行個體之前移除了它們。差異在於 CockroachDB 的受管理磁碟區,您重複使用了它。新的 CockroachDB 容器已從磁碟讀取資料庫檔案,就像它在容器外執行時一樣。

結束所有操作

請記住,您是在不安全模式下執行 CockroachDB。現在您已經建置並測試了您的應用程式,在繼續之前,是時候結束所有工作了。您可以使用 list 命令列出您正在執行的容器

$ docker container list

現在您知道容器 ID 了,您可以使用 docker container stopdocker container rm,如先前模組中所示。

在繼續之前,請停止 CockroachDB 和 docker-gs-ping-roach 容器。

使用 Docker Compose 提高生產力

此時,您可能想知道是否有辦法避免處理 docker 命令的長參數清單。您在本系列中使用的玩具範例需要五個環境變數來定義與資料庫的連線。一個真正的應用程式可能需要更多更多。然後還有相依性的問題。理想情況下,您希望確保在執行應用程式之前啟動資料庫。而啟動資料庫執行個體可能需要另一個具有許多選項的 Docker 命令。但是,有一種更好的方法可以協調這些部署以用於本機開發目的。

在本節中,您將建立一個 Docker Compose 檔案,以使用單個命令啟動您的 docker-gs-ping-roach 應用程式和 CockroachDB 資料庫引擎。

設定 Docker Compose

在您的應用程式目錄中,建立一個名為 docker-compose.yml 的新文字檔案,其中包含以下內容。

version: "3.8"

services:
  docker-gs-ping-roach:
    depends_on:
      - roach
    build:
      context: .
    container_name: rest-server
    hostname: rest-server
    networks:
      - mynet
    ports:
      - 80:8080
    environment:
      - PGUSER=${PGUSER:-totoro}
      - PGPASSWORD=${PGPASSWORD:?database password not set}
      - PGHOST=${PGHOST:-db}
      - PGPORT=${PGPORT:-26257}
      - PGDATABASE=${PGDATABASE:-mydb}
    deploy:
      restart_policy:
        condition: on-failure
  roach:
    image: cockroachdb/cockroach:latest-v20.1
    container_name: roach
    hostname: db
    networks:
      - mynet
    ports:
      - 26257:26257
      - 8080:8080
    volumes:
      - roach:/cockroach/cockroach-data
    command: start-single-node --insecure

volumes:
  roach:

networks:
  mynet:
    driver: bridge

這個 Docker Compose 設定非常方便,因為您不必輸入要傳遞給 docker run 命令的所有參數。您可以在 Docker Compose 檔案中以宣告方式執行此操作。Docker Compose 文件頁面 內容相當廣泛,並且包含 Docker Compose 檔案格式的完整參考。

.env 檔案

如果可用,Docker Compose 將自動從 .env 檔案讀取環境變數。由於您的 Compose 檔案需要設定 PGPASSWORD,請將以下內容新增到 .env 檔案

PGPASSWORD=whatever

確切值對於此範例並不重要,因為您是在不安全模式下執行 CockroachDB。請確保將變數設定為某個值,以避免發生錯誤。

合併 Compose 檔案

檔案名稱 docker-compose.ymldocker compose 命令在未提供 -f 旗標時預設辨識的檔案名稱。這表示如果您的環境有此需求,您可以有多個 Docker Compose 檔案。此外,Docker Compose 檔案是…可組合的(一語雙關),因此可以在命令列中指定多個檔案,將配置的各個部分合併在一起。以下列出一些此功能非常實用的情境範例:

  • 在本地開發時使用繫結掛載原始碼,但在執行 CI 測試時則不使用;
  • 在某些 API 應用程式中,切換使用預先建置的映像檔作為前端,或是建立原始碼的繫結掛載;
  • 新增額外的服務以進行整合測試;
  • 以及更多其他情境…

這裡不會涵蓋任何這些進階使用案例。

Docker Compose 中的變數替換

Docker Compose 真正酷炫的功能之一是變數替換。您可以在 Compose 檔案的 environment 區段中看到一些範例。舉例說明:

  • PGUSER=${PGUSER:-totoro} 表示在容器內,環境變數 PGUSER 應設定為與執行 Docker Compose 的主機上的值相同。如果主機上沒有此名稱的環境變數,則容器內的變數將取得預設值 totoro
  • PGPASSWORD=${PGPASSWORD:?database password not set} 表示如果主機上未設定環境變數 PGPASSWORD,Docker Compose 將顯示錯誤。這是可以接受的,因為您不希望將密碼的預設值寫死在程式碼中。您可以在 .env 檔案中設定密碼值,該檔案是您機器上的本地檔案。將 .env 新增到 .gitignore 中,以防止密碼被提交到版本控制系統,始終是一個好主意。

Docker 文件的變數替換區段中,記載了其他處理未定義或空值的 方法。

驗證 Docker Compose 設定

在套用對 Compose 設定檔所做的變更之前,您可以使用以下命令驗證設定檔的內容:

$ docker compose config

執行此命令時,Docker Compose 會讀取 docker-compose.yml 檔案,將其解析為記憶體中的資料結構,盡可能進行驗證,並從其內部表示重新建構該設定檔並印出。如果由於錯誤而無法執行此操作,Docker 會改為印出錯誤訊息。

使用 Docker Compose 建構和執行應用程式

啟動您的應用程式並確認它正在執行。

$ docker compose up --build

您傳遞了 --build 旗標,因此 Docker 將編譯您的映像檔,然後啟動它。

注意

Docker Compose 是一個很有用的工具,但它也有其自身的特殊之處。例如,除非提供 --build 旗標,否則在更新原始碼時不會觸發重建。編輯原始碼後,在執行 docker compose up 時忘記使用 --build 旗標,是一個非常常見的陷阱。

由於您的設定現在由 Docker Compose 執行,它已為其分配了一個專案名稱,因此您的 CockroachDB 執行個體會獲得一個新的磁碟區。這表示您的應用程式將無法連線到資料庫,因為此新磁碟區中不存在該資料庫。終端機將顯示資料庫的驗證錯誤。

# ... omitted output ...
rest-server             | 2021/05/10 00:54:25 failed to initialise the store: pq: password authentication failed for user totoro
roach                   | *
roach                   | * INFO: Replication was disabled for this cluster.
roach                   | * When/if adding nodes in the future, update zone configurations to increase the replication factor.
roach                   | *
roach                   | CockroachDB node starting at 2021-05-10 00:54:26.398177 +0000 UTC (took 3.0s)
roach                   | build:               CCL v20.1.15 @ 2021/04/26 16:11:58 (go1.13.9)
roach                   | webui:               http://db:8080
roach                   | sql:                 postgresql://root@db:26257?sslmode=disable
roach                   | RPC client flags:    /cockroach/cockroach <client cmd> --host=db:26257 --insecure
roach                   | logs:                /cockroach/cockroach-data/logs
roach                   | temp dir:            /cockroach/cockroach-data/cockroach-temp349434348
roach                   | external I/O path:   /cockroach/cockroach-data/extern
roach                   | store[0]:            path=/cockroach/cockroach-data
roach                   | storage engine:      rocksdb
roach                   | status:              initialized new cluster
roach                   | clusterID:           b7b1cb93-558f-4058-b77e-8a4ddb329a88
roach                   | nodeID:              1
rest-server exited with code 0
rest-server             | 2021/05/10 00:54:25 failed to initialise the store: pq: password authentication failed for user totoro
rest-server             | 2021/05/10 00:54:26 failed to initialise the store: pq: password authentication failed for user totoro
rest-server             | 2021/05/10 00:54:29 failed to initialise the store: pq: password authentication failed for user totoro
rest-server             | 2021/05/10 00:54:25 failed to initialise the store: pq: password authentication failed for user totoro
rest-server             | 2021/05/10 00:54:26 failed to initialise the store: pq: password authentication failed for user totoro
rest-server             | 2021/05/10 00:54:29 failed to initialise the store: pq: password authentication failed for user totoro
rest-server exited with code 1
# ... omitted output ...

由於您使用 restart_policy 設定部署的方式,因此失敗的容器每 20 秒就會重新啟動一次。因此,為了修復問題,您需要登入到資料庫引擎並建立使用者。您之前在設定資料庫引擎中已執行過此操作。

這不是什麼大問題。您只需連線到 CockroachDB 執行個體,並執行三個 SQL 命令來建立資料庫和使用者,如設定資料庫引擎中所述。

因此,請從另一個終端機登入到資料庫引擎:

$ docker exec -it roach ./cockroach sql --insecure

並執行與之前相同的命令來建立資料庫 mydb、使用者 totoro,並授予該使用者必要的權限。完成此操作後(範例應用程式容器會自動重新啟動),rest-service 將停止失敗和重新啟動,終端機將會靜默。

原本可以連線您先前使用的磁碟區,但就本範例而言,這樣做弊大於利,而且它也提供了一個機會,展示如何透過 restart_policy Compose 檔案功能,將彈性引入您的部署中。

測試應用程式

現在,測試您的 API 端點。在新終端機中,執行以下命令:

$ curl http://localhost/

您應該會收到以下回應:

Hello, Docker! (0)

關閉

要停止 Docker Compose 啟動的容器,請在執行 docker compose up 的終端機中按下 ctrl+c。要在容器停止後移除它們,請執行 docker compose down

分離模式

您可以像使用 docker 命令一樣,使用 -d 旗標,以分離模式執行由 docker compose 命令啟動的容器。

要以分離模式啟動由 Compose 檔案定義的堆疊,請執行:

$ docker compose up --build -d

然後,您可以使用 docker compose stop 停止容器,並使用 docker compose down 移除它們。

進一步探索

您可以執行 docker compose 來查看其他可用的命令。

總結

本章中特意未涵蓋一些相關但有趣的主題。對於更具冒險精神的讀者,本節提供了一些進一步研究的指標。

永久儲存

受管磁碟區並不是為您的容器提供持久性儲存的唯一方法。強烈建議您熟悉可用的儲存選項及其使用案例,這些內容在在 Docker 中管理資料中有說明。

CockroachDB 叢集

您執行了一個 CockroachDB 執行個體,這足以滿足本範例的需求。但是,可以執行一個 CockroachDB 叢集,它由多個 CockroachDB 執行個體組成,每個執行個體都在自己的容器中執行。由於 CockroachDB 引擎在設計上是分散式的,因此您只需對程序進行極少的更改,即可執行具有多個節點的叢集。

這種分散式設定提供了有趣的可能性,例如應用混沌工程技術來模擬叢集部分故障,並評估您的應用程式應對此類故障的能力。

如果您有興趣試用 CockroachDB 叢集,請查看:

其他資料庫

由於您沒有執行 CockroachDB 執行個體叢集,您可能會想知道是否可以使用非分散式資料庫引擎。答案是「可以」,如果您要選擇更傳統的 SQL 資料庫,例如 PostgreSQL後續步驟

在此模組中,您設定了一個容器化的開發環境,您的應用程式和資料庫引擎在不同的容器中運行。您還編寫了一個 Docker Compose 檔案,它將兩個容器連結在一起,並提供簡便的開發環境啟動和關閉方式。

在下一模組中,您將了解在 Docker 中運行功能測試的一種可能方法。