本記事は HRBrain Advent Calendar 2022 の25日目の記事です。
はじめに
こんにちは。今年の11月から HRBrain のエンジニアとして働いている清水です。
本記事では、特定のプロダクトの開発環境で起こっていた、M1 Mac において Go のリモートデバッグができない問題を解決し原因を考察しました。
対象読者
- M1 Mac において Go のリモートデバッグができない方
- Go のデバッグや Tilt について理解を深めたい方
上記のような方に読んでいただけると嬉しいです!
開発環境
- macOS 12.6
- チップ Apple M1
- Go 1.18.4
- Tilt 0.30.10
- Docker 4.13.1
HRBrain では Tilt を使って、マイクロサービスをローカルの開発環境に構築しています。
開発環境や Tilt の詳細については、弊社のテックブログを見ていただけると幸いです。
リモートデバッグできない問題の解決と考察
では、リモートデバッグできなかった状態と解決方法を記載します。
問題
VS Code からリモートデバッグ接続することはできていたのですが、ブレークポイントが止まってくれない状態でした。
エラーは見られなかったため、手探りで Dockerfile
や docker-compose.yaml
を見て解決方法を探していました。
解決方法
Intel Mac では正常にデバッグできていたことから、M1 Mac のみに不都合な設定がされていると予想し Dockerfile
を見てみると、amd64
の一行を発見しました。
AMD64 は AMD によって開発された64ビットプロセッサアーキテクチャで、Intel CPU で採用されていますが、M1 には採用されていないため M1 Mac のみに問題が起こると予想できます。
ENV GOARCH=amd64
上記一行を消してデバッグしてみると無事に、ブレイクポイントが止まりデバッグに成功しました!
ここでは解決方法のみ記載しましたが、トライ&エラーを繰り返して、やっと解決できました。
原因の考察
ここでは、ローカル環境の適切な状態と、考えられる2つの原因の考察を記載します。
ローカル環境の適切な状態
原因を考察する前に、ローカル環境がどのような状態であるべきだったのか考えます。
まず、HRBrain の開発環境(Tilt環境)ではビルド環境と実行環境が同一で golang:1.18.4-alpine3.16
が使われています。そのため、ビルドする Go の実行バイナリは、golang:1.18.4-alpine3.16
で動くようにターゲットである GOARCH
と GOOS
を指定する必要があります。
GOARCH
と GOOS
の有効な組み合わせは Go の公式サイトに記載されています。
(参照: https://go.dev/doc/install/source#environment の $GOOS and $GOARCH
)
GOARCH
と GOOS
はそれぞれ Go のプログラムをビルドする際にターゲットとする CPU、OS の種類を表す環境変数であるため、Intel Mac、M1 Mac などの環境に応じて適切に設定する必要があります。
したがって、GOARCH
と GOOS
が CPU や OS に応じて適切に設定され、有効な組み合わせの状態でビルドされることが、ローカル環境の適切な状態と考えられます。
つまり、Go のビルドターゲットがそれぞれ以下を指定された状態でビルドされることが、ローカル環境の適切な状態と言えます。
Intel Mac: GOARCH='amd64' GOOS='linux'
M1 Mac : GOARCH='arm64' GOOS='linux'
考察1: 環境変数が Intel CPU 用になっていたことが原因の可能性
では、以上のローカル環境の適切な状態を踏まえて原因を考察していきます。
ENV
コマンドは、Docker イメージ内で使用される環境変数を設定するものです。そのため、今回問題が起きていた Dockerfile
の ENV GOARCH=amd64
という行が記述されていたことにより、Docker イメージ内で環境変数 GOARCH
の値が amd64
に設定されていたことになります。
GOARCH
環境変数によって、ビルドされるプログラムがどのようなアーキテクチャの CPU で動作するかが決まるため、この変数の指定が原因で M1 Mac でのみリモートデバッグができなかった可能性が考えられます。
実際に、Tilt を起動した状態で Docker コンテナ内の Go に関する環境変数を Dockerfile
修正前と修正後で比較してみました。
❯ docker exec <container id> go env | egrep -i 'os=|arch='
GOARCH="amd64"
GOHOSTARCH="arm64"
GOHOSTOS="linux"
GOOS="linux"
❯ docker exec <container id> go env | egrep -i 'os=|arch='
GOARCH="arm64"
GOHOSTARCH="arm64"
GOHOSTOS="linux"
GOOS="linux"
修正前は GOARCH
の値が amd64
に設定されていますが、Dockerfile
の ENV GOARCH=amd64
という行を削除したことにより(修正後)、GOARCH
の値が arm64
に設定されています。
したがって、Docker コンテナで環境変数 GOARCH
の値が arm64
ではなく amd64
に設定されていたことが原因で、リモートデバッグが正常にできなかった可能性が考えられます。
なぜ ENV GOARCH=amd64
の一行を削除しただけで、GOARCH="arm64"
に設定されているか
ここで、なぜ ENV GOARCH=amd64
の一行を削除しただけで、GOARCH="arm64"
に設定されているか考察しました。
現状、開発環境(Tilt環境)では、ビルド環境と実行環境が同一で golang:1.18.4-alpine3.16
が使われています。そこで、実際に docker-library のソースコードを見てみると、Dockerfile
に下記の記述が見つかりました。
arch="$(apk --print-arch)"; \
url=; \
case "$arch" in \
'x86_64') \
export GOAMD64='v1' GOARCH='amd64' GOOS='linux'; \
;; \
'armhf') \
export GOARCH='arm' GOARM='6' GOOS='linux'; \
;; \
'armv7') \
export GOARCH='arm' GOARM='7' GOOS='linux'; \
;; \
'aarch64') \
export GOARCH='arm64' GOOS='linux'; \
;; \
'x86') \
export GO386='softfloat' GOARCH='386' GOOS='linux'; \
;; \
'ppc64le') \
export GOARCH='ppc64le' GOOS='linux'; \
;; \
's390x') \
export GOARCH='s390x' GOOS='linux'; \
;; \
*) echo >&2 "error: unsupported architecture '$arch' (likely packaging update needed)"; exit 1 ;; \
上記の Dockerfile
から、golang:1.18.4-alpine3.16
では、環境変数の GOARCH
と GOOS
はデフォルトで決められていることわかります。
"$(apk --print-arch)"
の部分は Docker を動かしている物理マシンの CPU で変わります。
❯ docker exec <container-id> apk --print-arch
x86_64
❯ docker exec <container-id> apk --print-arch
aarch64
つまり、Intel Mac と M1 Mac とで変わることがわかりました。
そのため環境変数 GOARCH
の値も、物理マシンの CPU で下記のように変わります。
Intel Mac: GOARCH='amd64' GOOS='linux'
M1 Mac : GOARCH='arm64' GOOS='linux'
したがって、開発環境の Dockerfile
で明記しない限り、物理マシンの CPU によりデフォルトで環境変数 GOARCH
が決められることがわかりました。
考察2: Delve インストール時とビルド実行時の GOARCH
のズレが原因の可能性
もう少し深ぼって開発環境の Dockerfile
を見ていきたいと思います。
FROM golang:1.18.4-alpine3.16 as deps
~~~省略~~~
# ① ここでは、GOARCH=arm64
RUN go install github.com/go-delve/delve/cmd/dlv@latest
~~~省略~~~
ENV GOARCH=amd64
~~~省略~~~
# ② ここでは、GOARCH=amd64
RUN go build -mod=mod -o /app -gcflags="all=-N -l"
golang:1.18.4-alpine3.16
インストール時の GOARCH
がデフォルトで決定されることがわかったため、GOARCH
の状態は上記①②のようになっています。
①では、Go 言語用のデバッグツールである Delve をインストールしています。
②では、Go 言語で書かれたプログラムをビルドしています。
したがって、リモートデバッグ時にブレイクポイントで止まらなかった根本原因は、
RUN go install github.com/go-delve/delve/cmd/dlv@latest
を実行時の GOARCH
と
RUN go build -mod=mod -o /app -gcflags="all=-N -l"
実行時の GOARCH
のズレにあった可能性が考えられます。
おわりに
本記事では、特定のプロダクトの開発環境で起こっていた、M1 Mac において Go のリモートデバッグができない問題を解決し原因を考察しました。
入社して間も無く、本記事の問題に先輩エンジニアの方と取り組めて大変勉強になりました。記事を書く中でもわからないことを調べたり、なるべくわかりやすく書くようにしたことで、頭の中が整理できました。
コードを書いていると、たった一行が問題で正常に動かないこともあり、今回改めてコード理解や仮説検証することの大切さを学びました。
HRBrain では一緒に働いてくれる仲間を募集しています。興味がありましたら、ぜひご応募ください。