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

開発チームに参加すると、生産性が高まるまでに時間がかかります。 これは通常、コードベースの学習と環境のセットアップの組み合わせです。 多くの場合、環境をセットアップするための何らかのオンボーディングドキュメントがありますが、私の経験では、これは決して最新ではなく、必要なツールについて常に誰かに助けを求める必要があります。

この問題は、チームでより多くの時間を費やすにつれて続きます。 使用しているツールのバージョンが、チームの誰か、またはさらに悪いことにCIが使用しているバージョンと異なるため、問題が見つかります。 私は複数のチームに所属しており、Slackで「自分のマシンで動作します」と叫んだり、すべて大文字で書かれたりしており、CIで物事をデバッグするのに多くの時間を費やしましたが、これは非常に苦痛です。

多くのユーザーは、ローカルでの開発中や運用アプリケーションのコンテナー化中に、データベースなどのアプリケーションの依存関係を実行する方法として Docker を使用します。 Docker は、開発環境をコードで定義して、チーム メンバーと CI がすべて同じツール セットを使用していることを確認するための優れたツールでもあります。

私たちはDockerで多くのGo開発を行っています。 Goツールチェーンは素晴らしく、コンパイル時間の短縮、組み込みの依存関係管理、簡単なクロスコンパイル、コードのフォーマットなどに関する強い意見を提供します。 このツールチェーンを使用しても、Goのバージョンの不一致、依存関係の欠落、構成のわずかな違いなどの問題が発生することがよくあります。 この良い例は、多くのプロジェクトにgRPCを使用しているため、コードベースで動作する特定のバージョンのprotocが必要なことです。

これは、Go開発にDockerを使用する方法を示す一連のブログ投稿の最初のものです。 ビルド、テスト、CI、およびビルドを高速化するための最適化について説明します。

シンプルに始める

簡単なGoプログラムから始めましょう。

package main

import "fmt"

func main() {
    fmt.Println("Hello world!")
}

次のコマンドを使用して、これをバイナリに簡単にビルドできます。
$ go build -o bin/example .

次の Dockerfile を使用しても同じことができます。

FROM golang:1.14.3-alpine AS build
WORKDIR /src
COPY . .
RUN go build -o /out/example .
FROM scratch AS bin
COPY --from=build /out/example /

この Dockerfile は、AS キーワードで識別される 2 つのステージに分かれています。 最初の段階であるビルドは、Go Alpine イメージから始まります。 Alpine は musl C ライブラリを使用しており、通常の Debian ベースの Golang イメージの最小限の代替手段です。 使用するGoのバージョンを定義できることに注意してください。 次に、コンテナ内の作業ディレクトリを設定し、ホストからコンテナにソースをコピーし、前からgo buildコマンドを実行します。 2 番目のステージである bin は、スクラッチ (つまり、空の) 基本イメージを使用します。 次に、結果のバイナリを最初のステージからファイルシステムにコピーするだけです。 バイナリがCA証明書などの他のリソースを必要とする場合は、これらも最終的なイメージに含める必要があることに注意してください。

このブログ投稿ではBuildKitを利用しているため、Docker 19.03以降を使用し、環境で設定 DOCKER_BUILDKIT=1 して有効にする必要があります。 Linux、macOS、または WSL 2 を使用している場合は、次のコマンドを使用してこれを行うことができます。
$ export DOCKER_BUILDKIT=1

Windows for PowerShell では、次を使用できます。
$env:DOCKER_BUILDKIT=1

またはコマンドプロンプトの場合:
set DOCKER_BUILDKIT=1

ビルドを実行するには、docker build コマンドを output オプションと共に使用して、結果をホストのファイルシステムに書き込むように指示します。
$ docker build --target bin --output bin/ .

次に、binディレクトリ内にサンプルバイナリがあることがわかります。

$ ls bin
example

単純なクロスコンパイル

Docker はさまざまなプラットフォーム用のイメージのビルドをサポートしているため、build コマンドにはプラットフォーム フラグがあります。 これにより、ターゲットOS(LinuxまたはWindowsなど)とアーキテクチャ(amd64、arm64など)を設定できます。 他のプラットフォーム用にネイティブにビルドするには、ビルダーをこれらのプラットフォーム用に設定する必要があります。 Golangのツールチェーンにはクロスコンパイルが含まれているため、ビルダーのセットアップを行う必要はありませんが、プラットフォームフラグを活用できます。

ホストオペレーティングシステムのバイナリをクロスコンパイルするには、Dockerfileに引数を追加し、dockerビルドコマンドのプラットフォームフラグから引数を入力します。 更新された Dockerfile は次のとおりです。

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

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

これで、BUILDPLATFORM 変数が基本イメージのプラットフォームとして設定されていることに注意してください。 これにより、ビルダーが実行されているプラットフォームにイメージがピン留めされます。 コンパイルステップでは、ビルドプラットフォームフラグで埋められたTARGETOS変数とTARGETARCH変数を使用して、ビルドするプラットフォームをGoに指示します。 物事を単純化するために、そしてこれは単純なアプリケーションであるため、 を設定して CGO_ENABLED=0バイナリを静的にコンパイルします。 これは、結果のバイナリがどのCライブラリにもリンクされないことを意味します。 アプリケーションがシステムライブラリ(システムの暗号化ライブラリなど)を使用している場合、このようにバイナリを静的にコンパイルすることはできません。

ホスト オペレーティング システム用にビルドするには、ローカル プラットフォームを指定します。
$ docker build --target bin --output bin/ --platform local .

docker buildコマンドが非常に長くなっているので、Makefile(または選択したスクリプト言語)に入れることができます。

all: bin/example
.PHONY: bin/example
bin/example:
   @docker build . --target bin \
   --output bin/ \
   --platform local

これにより、次のようにビルドを実行できます。

$ make bin/example
$ make

さらに一歩進んで、クロスコンパイルターゲットをDockerfileに追加できます。

FROM --platform=${BUILDPLATFORM} golang:1.14.3-alpine AS build
WORKDIR /src
ENV CGO_ENABLED=0
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 /

FROM bin-unix AS bin-linux
FROM bin-unix AS bin-darwin

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

FROM bin-${TARGETOS} AS bin

上記では、UnixライクなOS(bin-unix)とWindows(bin-windows)の2つの異なる段階があります。 次に、Linux (bin-linux) と macOS (bin-darwin) のエイリアスを追加します。 これにより、変数に依存し TARGETOS 、dockerビルドプラットフォームフラグによって自動的に設定される動的ターゲット(bin)を作成できます。

これにより、特定のプラットフォーム用にビルドできます。

$ docker build --target bin --platform windows/amd64 .
$ file bin/
bin/example.exe: PE32+ executable (console) x86-64 (stripped to external
PDB), for MS Windows

更新された Makefile には、設定可能な PLATFORM 変数があります。

all: bin/example

PLATFORM=local

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

つまり、PLATFORM を設定することで、特定のプラットフォーム用にビルドできます。
$ make PLATFORM=windows/amd64

有効なオペレーティング システムとアーキテクチャの組み合わせの一覧については、 https://golang.org/doc/install/source#environment を参照してください。

ビルドコンテキストの縮小

デフォルトでは、docker build コマンドは、渡されたパス内のすべてを取得し、ビルダーに送信します。 私たちの場合、これにはビルドで使用しないbin/ディレクトリの内容が含まれます。 ビルダーにこれを行わないように指示するには、 .dockerignore ファイル:

bin/*

には bin/ directory 数メガバイトのデータが含まれているため、 .dockerignore file は、ビルドにかかる時間を少し短縮します。

同様に、コード管理に Git を使用しているが、ビルド プロセスの一部として git コマンドを使用しない場合は、 .git/ ディレクトリも。

次は何ですか?

この記事では、ローカルの Go 開発用のコンテナ化された開発環境を開始する方法、さまざまなプラットフォーム用のサンプル CLI ツールをビルドする方法、およびビルド コンテキストを縮小してビルドを高速化する方法について説明しました。 このシリーズの次回の記事では、サンプルプロジェクトをより現実的にするための依存関係の追加、ビルドを高速化するためのキャッシュ、単体テストの追加について説明します。

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

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