M1 Mac において Go のリモートデバッグができない問題を解決し原因を考察した

本記事は HRBrain Advent Calendar 2022 の25日目の記事です。

https://qiita.com/advent-calendar/2022/hrbrain

# はじめに

こんにちは。今年の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](https://tilt.dev/) を使って、マイクロサービスをローカルの開発環境に構築しています。
開発環境や Tilt の詳細については、弊社のテックブログを見ていただけると幸いです。

https://times.hrbrain.co.jp/entry/2021/12/01/tilt

# リモートデバッグできない問題の解決と考察

では、リモートデバッグできなかった状態と解決方法を記載します。

## 問題

VS Code からリモートデバッグ接続することはできていたのですが、ブレークポイントが止まってくれない状態でした。
エラーは見られなかったため、手探りで `Dockerfile` や `docker-compose.yaml` を見て解決方法を探していました。

## 解決方法

Intel Mac では正常にデバッグできていたことから、M1 Mac のみに不都合な設定がされていると予想し `Dockerfile` を見てみると、`amd64` の一行を発見しました。
AMD64 は AMD によって開発された64ビットプロセッサアーキテクチャで、Intel CPU で採用されていますが、M1 には採用されていないため M1 Mac のみに問題が起こると予想できます。

```Dockerfile:Dockerfile
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 のビルドターゲットがそれぞれ以下を指定された状態でビルドされることが、ローカル環境の適切な状態と言えます。

```zsh
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` 修正前と修正後で比較してみました。

```zsh:修正前
❯ docker exec <container id> go env | egrep -i 'os=|arch='
GOARCH="amd64"
GOHOSTARCH="arm64"
GOHOSTOS="linux"
GOOS="linux"
```

```zsh:修正後
❯ 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` に下記の記述が見つかりました。

```Dockerfile:docker-library/golang/1.18/alpine3.16/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 ;; \
```

https://github.com/docker-library/golang/blob/b9c8662f4c9726d8a13d83a4ca394e1ddbcf2a5c/1.18/alpine3.16/Dockerfile#L28-L52

上記の `Dockerfile` から、`golang:1.18.4-alpine3.16` では、環境変数の `GOARCH` と `GOOS` はデフォルトで決められていることわかります。

`"$(apk --print-arch)"` の部分は Docker を動かしている物理マシンの CPU で変わります。

```zsh:Intel Mac
❯ docker exec <container-id> apk --print-arch
x86_64
```

```zsh:M1 Mac
❯ docker exec <container-id> apk --print-arch
aarch64
```

つまり、Intel Mac と M1 Mac とで変わることがわかりました。
そのため環境変数 `GOARCH` の値も、物理マシンの CPU で下記のように変わります。

```zsh
Intel Mac: GOARCH='amd64' GOOS='linux'
M1 Mac   : GOARCH='arm64' GOOS='linux'
```

したがって、開発環境の `Dockerfile` で明記しない限り、物理マシンの CPU によりデフォルトで環境変数 `GOARCH` が決められることがわかりました。

### 考察2: Delve インストール時とビルド実行時の `GOARCH` のズレが原因の可能性

もう少し深ぼって開発環境の `Dockerfile` を見ていきたいと思います。

```Dockerfile:修正前の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 言語で書かれたプログラムをビルドしています。

したがって、リモートデバッグ時にブレイクポイントで止まらなかった根本原因は、
```Dockerfile
RUN go install github.com/go-delve/delve/cmd/dlv@latest
```
を実行時の `GOARCH` と
```Dockerfile
RUN go build -mod=mod -o /app -gcflags="all=-N -l"
```
実行時の `GOARCH` のズレにあった可能性が考えられます。

# おわりに

本記事では、特定のプロダクトの開発環境で起こっていた、M1 Mac において Go のリモートデバッグができない問題を解決し原因を考察しました。

入社して間も無く、本記事の問題に先輩エンジニアの方と取り組めて大変勉強になりました。記事を書く中でもわからないことを調べたり、なるべくわかりやすく書くようにしたことで、頭の中が整理できました。
コードを書いていると、たった一行が問題で正常に動かないこともあり、今回改めてコード理解や仮説検証することの大切さを学びました。

HRBrain では一緒に働いてくれる仲間を募集しています。興味がありましたら、ぜひご応募ください。

https://www.hrbrain.co.jp/recruit

# 参考

- https://go.dev/doc/install/source#environment
- https://github.com/docker-library/golang
- https://github.com/go-delve/delve