Go 開発者環境のコンテナー化 – パート 2

これは、Docker を使用してコードで Go 開発環境を定義する方法を示す一連の投稿の第 2 部です。 この目的は、あなた、あなたのチーム、CIがすべて同じ環境を使用していることを確認することです。 第 1 回では、ローカルの Go 開発用にコンテナ化された開発環境を開始する方法、さまざまなプラットフォーム用のサンプル CLI ツールを構築する方法、ビルド コンテキストを縮小してビルドを高速化する方法について説明しました。 次に、さらに一歩進んで、依存関係を追加してプロジェクトをより現実的にする方法、キャッシュを追加してビルドを高速化する方法、および単体テストを学習します。

依存関係の追加

パート1のGoプログラムは非常に単純で、依存関係はありませんGo依存関係。 一般的に使用される github.com/pkg/errors 単純な依存関係を追加しましょう パッケージ:

package main

import (
   "fmt"
   "os"
   "strings"
   "github.com/pkg/errors"

)

func echo(args []string) error {
   if len(args) < 2 {
       return errors.New("no message to echo")
   }
   _, err := fmt.Println(strings.Join(args[1:], " "))
   return err
}

func main() {
   if err := echo(os.Args); err != nil {
       fmt.Fprintf(os.Stderr, "%+v\n", err)
       os.Exit(1)
   }
}

サンプルプログラムは、ユーザーが入力した引数または「エコーするメッセージなし」と、何も指定されていない場合はスタックトレースを書き出す単純なエコープログラムになりました。

Goモジュールを使用して、この依存関係を処理します。 次のコマンドを実行すると、 go.modgo.sum ファイルが作成されます。

$ go mod init
$ go mod tidy

これで、ビルドを実行すると、ビルドするたびに依存関係がダウンロードされることがわかります

$ make
[+] Building 8.2s (7/9)
 => [internal] load build definition from Dockerfile
...
0.0s
 => [build 3/4] COPY . . 
0.1s
 => [build 4/4] RUN GOOS=darwin GOARCH=amd64 go build -o /out/example .
7.9s
 => => # go: downloading github.com/pkg/errors v0.9.1

これは明らかに非効率的であり、物事を遅くします。 これは、Dockerfileの別のステップとして依存関係をダウンロードすることで修正できます。

FROM --platform=${BUILDPLATFORM} golang:1.14.3-alpine AS build
WORKDIR /src
ENV CGO_ENABLED=0
COPY go.* .
RUN go mod download
COPY . .
ARG TARGETOS
ARG TARGETARCH
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/example .


FROM scratch AS bin-unix
COPY --from=build /out/example /
...

goが追加されたことに注意してください。 ファイルを作成し、残りのソースを追加する前にモジュールをダウンロードします。 これにより、Dockerは、これらの手順が実行された場合にのみ再実行されるため、モジュールをキャッシュできます。 ファイルが変更されます。

キャッシング

依存関係のダウンロードをビルドから分離することは大きな改善ですが、ビルドを実行するたびに、コンパイルを最初から開始します。 小さなプロジェクトの場合、これは問題ではないかもしれませんが、プロジェクトが大きくなるにつれて、Goのコンパイラキャッシュを活用したいと思うでしょう。

これを行うには、BuildKit の Dockerfile フロントエンド (https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md) を使用する必要があります。 更新された Dockerfile は次のとおりです。

# syntax = docker/dockerfile:1-experimental

FROM --platform=${BUILDPLATFORM} golang:1.14.3-alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
ENV CGO_ENABLED=0
COPY go.* .
RUN go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/example .


FROM scratch AS bin-unix
COPY --from=build /out/example /
...

Dockerfile の上部にある # 構文は、実験的な Dockerfile フロントエンドを選択し、run コマンドにアタッチされている –mount オプションであることに注意してください。 このマウントオプションは、go buildコマンドが実行されるたびに、コンテナのキャッシュがGoのコンパイラキャッシュフォルダーにマウントされることを意味します。

2017 MacBook Pro 13 "のサンプルバイナリでこの変更をベンチマークすると、小さなコード変更はキャッシュなしでビルドするのに11秒、キャッシュなしで2秒未満かかることがわかります。 これは大きな改善です!

単体テストの追加

すべてのプロジェクトにはテストが必要です! echo 関数の簡単なテストをファイルに追加します main_test.go

package main

import (
    "testing"
    "github.com/stretchr/testify/require"

)

func TestEcho(t *testing.T) {
    // Test happy path
    err := echo([]string{"bin-name", "hello", "world!"})
    require.NoError(t, err)
}

func TestEchoErrorNoArgs(t *testing.T) {
    // Test empty arguments
    err := echo([]string{})
    require.Error(t, err)
}

このテストでは、echo 関数に空の引数リストが渡された場合にエラーが発生することを確認します。

ここで、Dockerfile の別のビルドターゲットを使用して、テストを実行し、バイナリを個別にビルドできるようにします。 これには、基本ステージにリファクタリングしてから、単体テストとビルドステージが必要です。

# syntax = docker/dockerfile:1-experimental

FROM --platform=${BUILDPLATFORM} golang:1.14.3-alpine AS base
WORKDIR /src
ENV CGO_ENABLED=0
COPY go.* .
RUN go mod download
COPY . .


FROM base AS build
ARG TARGETOS
ARG TARGETARCH
RUN --mount=type=cache,target=/root/.cache/go-build \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /out/example .


FROM base AS unit-test
RUN --mount=type=cache,target=/root/.cache/go-build \
go test -v .


FROM scratch AS bin-unix
COPY --from=build /out/example /
...

Go testはビルドと同じキャッシュを使用するため、このステージのキャッシュもマウントすることに注意してください。 これにより、Goはコード変更があった場合にのみテストを実行でき、テストの実行が速くなります。

Makefile を更新してテストターゲットを追加することもできます。

all: bin/example
test: unit-test

PLATFORM=local

.PHONY: bin/example
bin/example:
    @docker build . --target bin \
    --output bin/ \
    --platform ${PLATFORM}

.PHONY: unit-test
unit-test:
    @docker build . --target unit-test

次は何ですか?

この投稿では、Goの依存関係を効率的に追加する方法、ビルドを高速化するためのキャッシュ方法、コンテナ化されたGo開発環境への単体テストの方法を見てきました。 シリーズの次の最終回となる記事では、リンターの追加方法、GitHub Actions CIの設定方法、および追加のビルド最適化について学習します。

この例の完成したソースは、私のGitHubで見つけることができます: https://github.com/chris-crone/containerized-go-dev

実験的なDockerfile構文の詳細については、こちらをご覧ください。 https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md

Docker でのビルドに興味がある場合は、Buildx リポジトリをご覧ください。 https://github.com/docker/buildx