dockerでgoの開発環境を構築する
本記事は以下の記事からのリンクとして作成されました。
以下の記事も是非ご覧ください!!
本記事について
本記事は、Go言語の開発環境をdockerで構築するための方法を記した記事です。
ホストOSからコンテナ上で動作する実行ファイルをデバッグできることが目標です。
完成したリポジトリは以下です。
https://github.com/fumiya-uehara/go_dev
環境
- macOS Catalina ver 10.15
- go 1.13
- Docker version 18.09.2, build 6247962
- docker-compose version 1.24.0, build 0aa5906
事前準備
Goのインストール
ホストOSにgoがインストールされていることを確認してください。
❯ go version
go version go1.13 darwin/amd64
もしもインストールされていない場合は Homebrew
などのパッケージマネージャを利用してインストールしてください。
❯ brew install go
GOPATHを設定する
Go言語ではGOPATHという環境変数を設定する必要があります。
インストールされたライブラリをソースコード内でimportする際に利用されたり、 go get
で外部ライブラリをインストールした際に保存先として指定されたりと様々なケースで使われます。
GOPATHを設定していないとimportで指定したライブラリを読み込めないので注意が必要です。
また、Goの実行環境がDocker上にある場合、Docker上ではインストールされているライブラリがローカルには存在しないため、IDEによる定義ジャンプができないなど不便な面もあるのでDocker上でインストールされたライブラリに関してはローカルにもインストールするなどの考慮が必要です。(なんかいい方法ないのだろうか・・・)
以下が設定方法です。(筆者はshellにzshを利用しているのでzshの設定方法です)
❯ cat << EOL >> ~/.zprofile
then cmdand> export GOPATH="$HOME/work/go/dev"
then cmdand> export PATH="$HOME/work/go/dev/bin:$PATH"
then cmdand> EOL
❯ echo $GOPATH
/Users/sumio/work/go/dev
GOPATHに指定するディレクトリは自由ですので悩まずに適当なところに置けばよいです。
もちろん後から変更もできますが、変更するとこれまでにインストールしたライブラリへのパスが通らないのでimportで呼び出せないことを頭の片隅に入れといてください。(環境がdocker上にある場合はあまり関係ないですが)
realizeのインストール
今回realizeというタスクランナーツールを利用し、ホットリロードを行います。
このツールのみローカル環境へのインストールが必要となるため事前にインストールしておきます。
❯ go get -u github.com/oxequa/realize
❯ which realize
/Users/sumio/work/go/dev/bin/realize
インストール後、realizeへのパスが通っていればOKです。
構築手順
勉強不足で誤った理解で記述している箇所があるかもしれません。その際はご指摘ください。
GoModulesの導入
GoModulesはバージョン管理システムです。
Projectのディレクトリを作成する際にも少し触れましたが、これまでGo言語でProjectを作成するとなると ${GOPATH}/src
配下に置くことが暗黙的なルールとしてあり、このルールに従って開発を進めるとプロジェクト毎にライブラリのバージョンを変えることができないなどの問題がありました。
これまではこの問題に対して各Projectディレクトリ配下にvendorというディレクトリを切り、そこにProjectで利用する固有バージョンのライブラリを置くことで対処してきました。(GoはvendorというディレクトリがProjectディレクトリ配下にある時優先的にそこにあるライブラリを読み込みます。)
しかし、GoModulesの導入によってvendorは廃止となり、容易にライブラリを管理することができるようになりました。仕組みとしては、go.mod
というファイルでProjectで利用するライブラリのバージョンを管理し、実際のライブラリは${GOPATH}/pkg/mod
配下に置かれます。
もしも、必要としているライブラリがインストールされていなければ${GOPATH}/pkg/mod
配下に追加しますし、前述したPATHにライブラリが存在しているのであれば、それを利用するといった感じで動作します。(すみません。検証していないので確証はないですがドキュメントなど読むとそうなのかなって思ってます。)
前置きが非常に長くなってしまいました。
以下を実行し、module-aware mode
で動作させ、go.mod
ファイルを作りましょう。
※go mod init github.com/fumiya-uehara/go_dev
の github.com/fumiya-uehara/go_dev
は対象プロジェクトのリモートリポジトリを指定することをお勧めします。
❯ cd ${PROJECT_ROO}
❯ export GO111MODULE=on
❯ go mod init github.com/fumiya-uehara/go_dev
export GO111MODULE=on
にて module-aware mode
に変更しています。デフォルトは auto
です。
Go1.13からは常に module-aware mode
で動作すると言われていましたが、最終的には auto
で落ち着いたようです。
ただ、 auto
の場合でも go.mod
ファイルがカレントディレクトリもしくは親ディレクトリに存在する場合は、 on
になります。
さらに詳しく知りたい方は以下のリリースノートをチェックしてください。
realizeの設定
タスクランナーツールとしてrealizeを導入しています。
開発時にコードを変更する度に go run
や go build
を行うのはとても不便です。
そのためにコードの変更を検知し自動でbuildしてくれるホットリロードの仕組みが必要となります。
本プロジェクトではホットリロードを実現するためにrealizeを導入しています。
cd ${PROJECT_ROOT}
realize start
上記コマンド実行後、.realize.yaml
が作成されればOKです。
realizeがフォアグラウンドで動作し続けるので Ctrl + c
を押下してKillしましょう。
settings:
legacy:
force: false
interval: 0s
schema:
- name: go_dev
path: .
commands:
install:
status: true
method: go build -o /app/go_dev
run:
status: true
method: /go/bin/dlv
args:
- exec
- /app/go_dev
- --headless=true
- --listen=:3000
- --api-version=2
- --accept-multiclient
watcher:
extensions:
- go
paths:
- /
ignore:
paths:
- .git
- .realize
- vendor
大事な部分のみピックアップして説明します。
commands:
install:
status: true
method: go build -o /app/go_dev
run:
status: true
method: /go/bin/dlv
args:
- exec
- /app/go_dev
- --headless=true
- --listen=:3000
- --api-version=2
- --accept-multiclient
変更を検知すると対象のファイルをbuildし、最新の状態になります。
ただ、リモートデバッグを行っている最中にコードを変更するとbuildは走りますが、一度コンテナとのコネクションを切断し、新たに繋ぎ直さないと新しいbinaryファイルを読み込みません。
当然の動作のように見えますし、そこまで手間ではないですが、良い方法があれば知りたいものです。
Dockerfile(Go実行コンテナ)
multi-stage build
を採用していますが、# Final Stage
は利用されていません。
なので、実際にこちらを見ながら構築する方は消しちゃって問題ありません。
# Variable
ARG GO_VERSION=1.13
ARG ALPINE_VERSION=3.10
# Build Stage
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-stage
ENV CGO_ENABLED 0
WORKDIR /go/go_dev
COPY . .
RUN set -ex && \
apk update && \
apk add --no-cache git && \
go get gopkg.in/urfave/cli.v2@master && \
go get github.com/oxequa/realize && \
go get github.com/go-delve/delve/cmd/dlv && \
go build -o /go/bin/dlv github.com/go-delve/delve/cmd/dlv && \
go build -o /app/go_dev
# Final Stage
FROM alpine:${ALPINE_VERSION}
WORKDIR /app
COPY --from=build-stage /app/go_dev .
EXPOSE 9000
ENTRYPOINT ["/app/go_dev"]
一つだけ説明します。
go get gopkg.in/urfave/cli.v2@master && \
go get github.com/oxequa/realize && \
realizeのインストールに失敗する問題があり、対策で gopkg.in/urfave/cli.v2@master
を前段でインストールしています。
realizeの依存関係の問題のようです。以下のissueです。
https://github.com/oxequa/realize/issues/253
Dockerfile(mysql実行コンテナ)
以下を実行し、ディレクトリとファイルを作成します。
PATHについては適宜変更してください。
$ cd ~/work/go/go_dev && \
mkdir -p ./docker/db && \
cd ./docker/db && touch {Dockerfile,my.cnf}
FilePath: ${PROJECT_ROOT}/docker/db/Dockerfile
FROM mysql:8.0.17
COPY my.cnf /etc/my.cnf
FilePath: ${PROJECT_ROOT}/docker/db/my.cnf
[mysqld]
port=3306
socket=/tmp/mysql.sock
key_buffer_size=16M
max_allowed_packet=8M
default_authentication_plugin=mysql_native_password
[mysqld_safe]
pid-file=/var/run/mysqld/mysqld.pid
ログの設定を行いたい場合はご自由にmy.cnfへ追記してください。
コンテナ内のログ出力先とホストOSの任意のディレクトリをマッピングするとコンテナに入らずともログを確認できると思うので、docker-composeを書くときに意識するといいかもしれません。
docker-compose
version: '3.7'
services:
gin:
build:
context: .
target: build-stage
depends_on:
- db
volumes:
- ./:/go/go_dev
command: realize start --run
ports:
- "19001:9000"
- "3000:3000"
security_opt:
- seccomp:unconfined
cap_add:
- SYS_PTRACE
db:
build: ./docker/db
ports:
- "13306:3306"
volumes:
- ./docker/db/mysql:/docker-entrypoint-initdb.d
environment:
MYSQL_ROOT_PASSWORD: "example"
MYSQL_DATABASE: "test"
全てを説明していては時間が足りないので、大事なところだけ抜粋します。
build:
context: .
target: build-stage
Dockerfile内ではmulti-stage build構成を採用していますが、ローカルの開発環境ではFinal-Stageの利用はありません。
そのため、targetにはbuild-stageが指定されています。
command: realize start --run
上記コマンドを実行することで変更を自動検知しbuildを行い、buildされたbinaryファイルを対象にデバッガを起動します。
security_opt:
- seccomp:unconfined
cap_add:
- SYS_PTRACE
Docker上で動作するプロセスをホストOSにアタッチするための設定です。
リモートデバッグを行うのに利用します。
main.go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/structJSON_test", func(context *gin.Context) {
var msg struct {
Name string `json:"user"`
Message string
Number int
}
msg.Name = "foo"
msg.Message = "これは構造体をJSONで返すためのテストです。"
msg.Number = 1111
context.JSON(http.StatusOK, msg)
})
_ = r.Run(":9000")
}
ginを利用しており、エンドポイントとして/structJSON_test
を用意しました。
これは構造体をJSON形式で返すAPIです。
用途としてはテスト用です。
動作確認
以下を実行します。
❯ cd ${PROJECT_ROOT}
❯ docker-compose build
❯ docker-compose up -d
もしも、起動に失敗している場合は最初に貼ったリポジトリと見比べて誤りがないか確認してください。
それでも解決することが難しい場合はコメントください。(筆者がこれまで記述した手順に誤りがある可能性も十分にあるので・・・)
問題なく起動に成功したらデバッグの設定を行います。
筆者はIDEにGoLandを利用していますのでそちらの設定を紹介します。
以下の Edit Configurations... をクリックします。
左上の『+』マークをクリックし、「Go Remote」をクリックします。
Portを「3000」に設定し、右下のAPPLYをクリックします。
以下のコマンドを実行してください
❯ curl localhost:19001/structJSON_test
最後に以下のような結果が返ってくれば完了です。
{"user":"foo","Message":"これは構造体をJSONで返すためのテストです。","Number":1111}
最後に
拙い文章で読み辛い箇所がたくさんあったと思いますが、ここまで読んでいただきありがとうございます。
Goの環境を構築する上で大変だったことは、$GOPATH
やGoModules
などの独自概念を理解することでした。色々な記事を読み、実際に動かして検証してやっと今の理解まで至りました。(まだまだ初歩ですが)
こればっかやってた2ヶ月でコードはほとんど書いてなかったりします。笑
やっと「書ける!」って思っていたのですが、次はディレクトリ構成で悩んでおり、コードを書ける日はまだ遠そうです。
参考
realize & delve
multi stage build
- https://qiita.com/minamijoyo/items/711704e85b45ff5d6405
- https://medium.com/travis-on-docker/multi-stage-docker-builds-for-creating-tiny-go-images-e0e1867efe5a
GoModules