golang 開発環境のベストプラクティスが分からん
はじめに
golang 開発環境を Docker で仮想化する際のベストプラクティスが分からん。ChatGPT に聞いても Claude に聞いても「マルチステージビルドでイメージを小さくすることができます」とか見当違いなことを答えてくる。golang で開発してる人はどうやって開発環境を仮想化してるんだ。
未だにベストプラクティスは分からないが、許容範囲の暫定的な解としてはこんな感じだろうか、と自分で思える方法を書いておく。
どうであればよいのか
要件としてはいくつか考えられる。
- golang のコンパイラなどは仮想環境に隔離すること
- 仮想環境は複数のサーバーで簡単に持ち回せること
- プロジェクトのソースコードは VSCode から編集できること
これを Docker でやってみようとすれば分かるが、1, 2, 3 を同時に満たすのが絶妙に面倒くさい。
絶妙に面倒くさい
少しだけその面倒くささについて説明しておこう。まずプロジェクトのソースコードは Docker の外に永続化されていないといけないので、ボリュームをバインドマウントすることになる。
しかしホストが Linux の場合、バインドマウントしたファイルを自由にいじろうとすると、ホストとコンテナ内でユーザーIDとグループIDを揃えねばならない。さもなくばどこかのタイミングで chmod
や chown
を走らせてファイルの権限を調整するという面倒な「一手間」が発生する。
一方でコンテナ内にホストの Linux アカウントと同様のユーザーIDとグループIDを持ったユーザーを作成できるタイミングは限られる。特に工夫しなければ Dockerfile
の中でしか作成することはできない。頑張れば ENTRYPOINT
の中で指定した Linux ユーザーを作成することもできるのだが、これはこれでけっこう頑張らねばならないし、コンテナの起動時にやたら気を遣うので正直面倒である。
そして最大の問題は、頑張って作ってしまうと壊すのがもったいなくなってしまうことである。新しい環境に対応したいときもサクッと手軽にとはいかなくなる。
とりあえず Docker でなんとかしてみる
今回考えた構成は以下である。今回はとにかくシンプルに行く。
- プロジェクトのソースコードはローカルでもサーバー上でもよい。
- プロジェクトのソースコードをサーバー上におく場合は VSCode の Remote Development で編集する。
- ソースコードが置いてあるマシンには Docker をインストールしておく。
- 開発環境のイメージをビルドするときにホストのアカウントと同じ ID を持つユーザーを自動で作成する。
- 使用するコマンドはすべて
Makefile
にまとめておく。
プロジェクトの構成は以下である。
golang/
├── Dockerfile
├── Makefile
└── work
└── myapp
└── server.go
# sudo で実行するかどうか
SUDO ?= yes
# CMD_PREFIX を設定
ifeq ($(SUDO), yes)
CMD_PREFIX := sudo
else
CMD_PREFIX :=
endif
# ホストの Linux アカウント情報を取得する
DEV_USER := $(shell id -un)
DEV_UID := $(shell id -u)
DEV_GROUP := $(shell id -gn)
DEV_GID := $(shell id -g)
# 開発用イメージにつける名前
TAG ?= golang-dev-${DEV_USER}:latest
# golang サーバー起動時のポートバインディング
PORT ?= 1323:1323
# コンパイル対象
SOURCE ?= server.go
# アウトプット
TARGET ?= server
# 開発用イメージのビルド
.PHONY: image
image:
${CMD_PREFIX} docker build \
--build-arg DEV_USER=${DEV_USER} \
--build-arg DEV_UID=${DEV_UID} \
--build-arg DEV_GROUP=${DEV_GROUP} \
--build-arg DEV_GID=${DEV_GID} \
-t ${TAG} .
# 開発用コンテナのシェルを起動
.PHONY: shell
shell:
${CMD_PREFIX} docker container run -it --rm \
-u ${DEV_UID} \
-v ./work:/work \
${TAG} bash
# ソースをビルド
# make build/myapp のような感じで使う
.PHONY: build/%
build/%:
${CMD_PREFIX} docker container run --rm \
-u ${DEV_UID} \
-v ./work:/work \
-w "/work/$(notdir $@)" \
${TAG} go build -o ${TARGET} ${SOURCE}
# ビルドしたバイナリをフォアグラウンドで走らせる
# make run/myapp のような感じで使う
.PHONY: run/%
run/%:
${CMD_PREFIX} docker container run --rm \
-p ${PORT} \
-u ${DEV_UID} \
-v ./work:/work \
-w "/work/$(notdir $@)" \
${TAG} ./${TARGET}
# ビルドしたバイナリをバックグラウンドで走らせる
# make run/myapp のような感じで使う
# 出力は docker container logs コンテナ名 で確認できる
.PHONY: rund/%
rund/%:
${CMD_PREFIX} docker container run -d --rm \
-p ${PORT} \
-u ${DEV_UID} \
-v ./work:/work \
-w "/work/$(notdir $@)" \
${TAG} ./${TARGET}
FROM golang:1.22-bookworm
# ホスト側のアカウントの情報を受け取る
ARG DEV_USER
ARG DEV_UID
ARG DEV_GROUP
ARG DEV_GID
# ユーザー作成のためいったん root になる
USER root
# 必要なグループとユーザーを作成する
RUN groupadd --gid ${DEV_GID} ${DEV_GROUP} \
&& useradd --no-log-init -m --home "/home/${DEV_USER}" \
--shell /bin/bash --uid "${DEV_UID}" --gid "${DEV_GID}" "${DEV_USER}"
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update --yes \
&& apt-get upgrade --yes \
&& apt-get install --yes --no-install-recommends \
vim iputils-ping net-tools \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# ワーキングディレクトリを /work にしておく
WORKDIR /work
// from https://echo.labstack.com/docs/quick-start
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
e.Logger.Fatal(e.Start(":1323"))
}
使い方
使い方は make
コマンドを経由すればとても簡単である。
0. sudo の切り替え
Makefile
の中身について少し補足しておく。
Docker を使用する場合はユーザーに sudo
権限を渡すか、ユーザーを docker
グループに追加するかのどちらかである。前者であれば docker
コマンドを実行するとき手前に sudo
をつける必要があり、後者であればつける必要がない。
Makefile
の中にあるコマンドは上記のように状況に応じて sudo
を書くか書かないか編集するのが面倒なので、SUDO
という make 変数で sudo
実行を制御できるようにしてある。Makefile
の冒頭にある SUDO ?= yes
の部分をそのままにしておけば sudo
実行され、SUDO ?= no
のように書き変えれば sudo
なしで実行される。
以下のようにして単発での切り替えも可能である。
# sudo ありで実行する
SUDO=yes make image
# sudo なしで実行する
SUDO=no make image
1. 開発用コンテナイメージのビルド
まずは開発用コンテナのイメージをビルドする。このコマンドで作成されるイメージの名前には golang-dev-{username}
のようにホスト側のユーザー名が刻まれるので、複数のアカウントで使用したときも作成されるイメージが被ることはない。
逆に言えばアカウントの分だけイメージが作成されてしまうので、大人数で開発する場合にはこの運用は向かない。起動時にユーザーを切り替える方法などを検討する。
# イメージをビルドする
cd golang
make image
# ビルド結果を確認する
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
golang-dev-josh latest 824d9b0fee39 3 days ago 822MB
2. プロジェクトの初期化
開発用コンテナでシェルを起動してプロジェクトを初期化する。今回は echo
を使ったサーバーであり、以下のクイックスタートガイドにあるソースコードをそのまま使用しているので、初期化手順もこのクイックスタートに倣う。
# 開発用コンテナのシェルを起動する
cd golang
make shell
# 以下は開発用コンテナの中で行う
# 初期化の手順はプロジェクトごとに好きなようにやる
cd myapp
go mod init myapp
go get github.com/labstack/echo/v4
この手順が完了するとホスト側からも以下のように go.mod
と go.sum
が見えるようになるはずである。
golang
├── Dockerfile
├── Makefile
└── work
└── myapp
├── go.mod
├── go.sum
└── server.go
3. ソースコードのビルド
上記の手順が完了していれば以下のようにソースコードをビルドすることができる。
cd golang
make build/myapp
build/{subdir}
の subdir
には golang/work
以下のビルド対象のサブディレクトリを指定できるように Makefile
を書いてある。これにより複数のプロジェクトをこの開発用コンテナで管理することができる。
上記を実行すると server
というバイナリが生成される。
golang
├── Dockerfile
├── Makefile
└── work
└── myapp
├── go.mod
├── go.sum
├── server ← これ
└── server.go
コンパイルしたいソースコードと、ターゲットのバイナリの名前は変数にしてあるので以下のように指定することもできる。
SOURCE=main.go TARGET=a.out make build/yourapp
この辺りは Makefile
を好きなように書き換えて自分の環境に合わせるとよい。書き方が分からなければ ChatGPT なり Claude なりに質問すればよい。
4. ビルドしたバイナリを実行する
ビルドしたバイナリにはフォアグラウンド実行の run
コマンドとバックグラウンド実行の rund
コマンドを用意した。これもサブディレクトリを指定できる。
# フォアグラウンド実行
cd golang
make run/myapp
# バックグラウンド実行
cd golang
make rund/myapp
# ポートバインディングは PORT で指定できる
# ターゲットとなるバイナリは TARGET で指定できる
PORT=3000:1323 TARGET=a.out make run/yourapp
josh@mynotepc:~/Projects/golang$ make rund/myapp
sudo docker container run -d --rm \
-p 1323:1323 \
-u 1000 \
-v ./work:/work \
-w "/work/myapp" \
golang-dev-josh:latest ./server
336a11b31b7645db7cde5c86821e8a0219b6688fc3dea2b1b24bf85bf5d0259e
josh@mynotepc:~/Projects/golang$ sudo docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
336a11b31b76 golang-dev-josh:latest "./server" 10 seconds ago Up 9 seconds 0.0.0.0:1323->1323/tcp, :::1323->1323/tcp inspiring_chatterjee
josh@mynotepc:~/Projects/golang$ sudo docker container logs inspiring_chatterjee
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.12.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
⇨ http server started on [::]:1323
josh@mynotepc:~/Projects/golang$ curl http://localhost:1323
Hello, World!
5. 他のプロジェクトも追加する
Makefile
は複数プロジェクトに対応できるように書いたので、以下のようにサブディレクトリを切れば複数のプロジェクトも管理できる。あなたのプロジェクトに合わなければ好きなように Makefile
を改造してほしい。
golang/
├── Dockerfile
├── Makefile
└── work
├── myapp
│ └── server.go
└── yourapp
└── server.go
おしまい
golang の開発環境を Docker で構築できないかいろいろ試してみたのだが、いまのところ今回紹介する方法が構築の手間と汎用性のバランスが取れていてよいと思っている。
もっといい方法があればぜひ教えてください。
Discussion