docker-composeを使ってapiサーバコンテナとDBコンテナを立てた際に、
起動時のDB接続でコケたので解決した経緯を書きます。
## やりたい事
・docker-composeでdbコンテナとapiコンテナを同時に起動する
・apiコンテナ起動時に、db接続を行う
今回のdocker-compose, Dockerfileは以下になります。
version: '3'
services:
db:
image: mysql:latest
container_name: mysql_host
hostname: db_host
ports:
- "3306:3306"
# 初期設定
volumes:
- "./docker/mysql/my.conf:/etc/mysql/my.conf"
# 環境変数
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
# ネットワーク
networks:
- test-network
restart: always
api:
build:
context: ./
dockerfile: ./docker/api/Dockerfile
container_name: blog_api
tty: true
volumes:
- .:/go/src/app
ports:
- "50051:50051"
command: go run main.go
# 環境変数
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
DB_NAME: ${DB_NAME}
DB_ADRESS: db:3306
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
# ネットワーク
networks:
- test-network
restart: always
networks:
test-network:
external: true
apiでは以下のDockerfileをビルドしています。
FROM golang:1.12
ENV PKG_PATH /go/src/app
ENV GO111MODULE on
WORKDIR /go/src/app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . $PKG_PATH
ここまでですが、今回のサービスはapi,dbの2つから構成されます。
dbにはmysqlを使っています。
apiではgoを使っています。
docker-compose.yamlで指定している環境変数についてですが、
ルートディレクトリに配置した.envファイルに指定した値を読んでいます。
MYSQL_ROOT_PASSWORD=root
DB_NAME=name
DB_USER=user
DB_PASSWORD=password
apiコンテナは、起動時に go run main.goを実行します。
api:
# (中略)
command: go run main.go
goのmainメソッドでは、以下の処理を呼び出し、DBと接続します。
func initDB() {
var err error
DBMS := "mysql"
DB_ADRESS := os.Getenv("DB_ADRESS")
DB_NAME := os.Getenv("DB_NAME")
DB_PASSWORD := os.Getenv("DB_PASSWORD")
DB_USER := os.Getenv("DB_USER")
PROTOCOL := fmt.Sprintf("tcp(%s)", DB_ADRESS)
DB_OPTION := "?charset=utf8mb4&parseTime=True&loc=Local"
CONNECTION := fmt.Sprintf("%s:%s@%s/%s%s", DB_USER, DB_PASSWORD, PROTOCOL, DB_NAME, DB_OPTION)
DB, err = gorm.Open(DBMS, CONNECTION)
if err != nil {
panic(err)
}
}
この状態で
$ docker network create test-network
$ docker-compose build
$ docker-compose up
を順に実行すると、以下のログを吐いてコケました。
api | panic: dial tcp 172.25.0.2:3306: connect: connection refused
api |
api | goroutine 1 [running]:
api | github.com/yzmw1213/GoMicroApp/db.initDB()
api | /go/src/app/db/db.go:35 +0x3d2
api | github.com/yzmw1213/GoMicroApp/db.Init()
api | /go/src/app/db/db.go:40 +0x20
api | main.start()
api | /go/src/app/main.go:13 +0x22
api | main.main()
api | /go/src/app/main.go:9 +0x20
api | exit status 2
##原因解決のために試したこと
- CONNECTIONをログ出力し、環境変数が正しく読めてるか試した
- docker networkコマンドでtest-networkをinspectした
- dbコンテナの中に入って、データベースが起動できてるか確認した
どれもいまいち不具合らしき物は見つからず...
原因: DBコンテナが起動する前にapiコンテナが起動していた
ここで、
「あ、そういえばまだログをちゃんと見てなかった」
という事で、dockerのログを見てみました。
$ docker-compose logs
すると、、どうやらapiコンテナが先に起動していたみたいです。
api | panic: dial tcp 172.25.0.2:3306: connect: connection refused
api |
api | goroutine 1 [running]:
api | github.com/yzmw1213/GoMicroApp/db.initDB()
api | /go/src/app/db/db.go:35 +0x3d2
api | github.com/yzmw1213/GoMicroApp/db.Init()
api | /go/src/app/db/db.go:40 +0x20
api | main.start()
api | /go/src/app/main.go:13 +0x22
api | main.main()
api | /go/src/app/main.go:9 +0x20
api | exit status 2
api | panic: dial tcp 172.25.0.2:3306: connect: connection refused
api |
api | goroutine 1 [running]:
api | github.com/yzmw1213/GoMicroApp/db.initDB()
api | /go/src/app/db/db.go:35 +0x3d2
api | github.com/yzmw1213/GoMicroApp/db.Init()
api | /go/src/app/db/db.go:40 +0x20
api | main.start()
api | /go/src/app/main.go:13 +0x22
api | main.main()
api | /go/src/app/main.go:9 +0x20
api | exit status 2
mysql_host | 2020-06-06 17:05:21+09:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.20-1debian10 started.
mysql_host | 2020-06-06 17:05:21+09:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
mysql_host | 2020-06-06 17:05:21+09:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.20-1debian10 started.
(以後略)
1行目でapiはdbへの接続を試みていますが、失敗しpanicをおこしています。
この時点でdbが先に起動を完了している必要があります。
解決策: Dockerizeを使う
「コンテナの起動順を制御しなければならない...」
という事で、調べてみたところ以下の記事を見つけました。
Dockerizeを使ってDocker Composeのコンテナの起動順を制御する
dockerizeを使うと、プロトコルで指定したサービスが動き出すまで、サービス起動を制御することができます。
dockerizeのインストールですが、apiのDockerfileに記述します。
FROM golang:1.12
ENV PKG_PATH /go/src/app
ENV GO111MODULE on
ENV DOCKERIZE_VERSION v0.6.1
WORKDIR /go/src/app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . $PKG_PATH
RUN apt-get update && apt-get install -y wget
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
また、docker-composeではapiのentrypointを追加しました。
api:
# (中略)
entrypoint:
- dockerize
- -timeout
- 60s
- -wait
- tcp://db:3306
ここでは、
「apiの起動はdbコンテナに接続できるようになるまで待つ」
という事を指示しています。
ただし、db起動が失敗する場合なども有り得るため、timeoutで待機する制限時間を指定しています。
この状態で、もう一度 docker-compose up まで行うと、DB接続まで成功し、無事にapiコンテナが立ち上がりました。
ちなみに、この時apiコンテナのログは
api | 2020/06/06 09:27:12 Waiting for: tcp://db:3306
api | 2020/06/06 09:27:12 Problem with dial: dial tcp 172.25.0.2:3306: connect: connection refused. Sleeping 1s
api | 2020/06/06 09:27:13 Connected to tcp://db:3306
api | 2020/06/06 09:27:21 main grpc server has started
のように、確かにdb接続ができるまで起動を待ってくれてました。
##結論
まずはログをちゃんと読もう!!
これに尽きるかなと思います。
バグってからすぐにあれこれ試すのもいいですが、
まずはログを読む。
そうすると、実際に起こっている事を客観的に判断できますし、
原因を明らかにしておいた方が調査しやすいです。
P.S
先ほど紹介した記事では、Dockerizeを使って起動するコンテナの順番を制御する方法が書かれていましたが、併せて以下のような説明がありました。
docker-composeにはdepends_onやlinksといったオプションがあるが、depends_onはコンテナの作成順序を、linksはdepends_onの機能に加えてコンテナ間の名前解決を行うもので、どちらも起動の順番はまでは制御しない。
サービスの起動順には、depends_on,links などと言った方法があるみたいですが、いずれもそれほど効果的ではないようです。
ちなみに、docker composeの公式ドキュメントによると、
depends_on では...(中略) 待てるのはコンテナを開始するまでです。サービスの準備が整うまで待たせる必要がある場合は、 起動順番の制御 に関するドキュメントで、問題への対処法や方針をご確認ください。
という注意書きがあり、要は
depends_onではコンテナを作る順番は制御できるが、起動してからは制御できない
という事ですね。
linksが非推奨である事や、dockerizeについての言及もありましたので、ご興味のある方はぜひ参照してみてください。
以上、docker-compose起動時にdbとapiコンテナの接続を行うまで、についてでした。