docker-compose でDBコンテナが起動してからAPIコンテナを起動したい。

docker-composeを使ってapiサーバコンテナとDBコンテナを立てた際に、
起動時のDB接続でコケたので解決した経緯を書きます。

##　やりたい事
・docker-composeでdbコンテナとapiコンテナを同時に起動する
・apiコンテナ起動時に、db接続を行う
## 


今回のdocker-compose, Dockerfileは以下になります。

```yaml:docker-compose.yaml
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をビルドしています。

```Dockerfile: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ファイルに指定した値を読んでいます。


```:.env
MYSQL_ROOT_PASSWORD=root
DB_NAME=name
DB_USER=user
DB_PASSWORD=password
```


apiコンテナは、起動時に go run main.goを実行します。

```yaml:docker-compose.yaml
  api:
    # (中略)
    command: go run main.go
```


goのmainメソッドでは、以下の処理を呼び出し、DBと接続します。

```go:db.go

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)
	}
}
```


この状態で 

```terminal
$ docker network create test-network
$ docker-compose build
$ docker-compose up 
```
を順に実行すると、以下のログを吐いてコケました。


```console

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のログを見てみました。

```console
$ docker-compose logs
```
すると、、どうやらapiコンテナが先に起動していたみたいです。

```console
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のコンテナの起動順を制御する](https://bmf-tech.com/posts/Dockerize%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6Docker%20Compose%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AE%E8%B5%B7%E5%8B%95%E9%A0%86%E3%82%92%E5%88%B6%E5%BE%A1%E3%81%99%E3%82%8B)

dockerizeを使うと、プロトコルで指定したサービスが動き出すまで、サービス起動を制御することができます。

dockerizeのインストールですが、apiのDockerfileに記述します。

```: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を追加しました。

```yaml:docker-compose.yaml
  api:
    # (中略)
    entrypoint:
      - dockerize
      - -timeout
      - 60s
      - -wait
      - tcp://db:3306
```

ここでは、

 「apiの起動はdbコンテナに接続できるようになるまで待つ」

という事を指示しています。
ただし、db起動が失敗する場合なども有り得るため、timeoutで待機する制限時間を指定しています。

この状態で、もう一度 docker-compose up まで行うと、DB接続まで成功し、無事にapiコンテナが立ち上がりました。

ちなみに、この時apiコンテナのログは

```console
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 

[先ほど紹介した記事](https://bmf-tech.com/posts/Dockerize%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6Docker%20Compose%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AE%E8%B5%B7%E5%8B%95%E9%A0%86%E3%82%92%E5%88%B6%E5%BE%A1%E3%81%99%E3%82%8B)では、Dockerizeを使って起動するコンテナの順番を制御する方法が書かれていましたが、併せて以下のような説明がありました。

>docker-composeにはdepends_onやlinksといったオプションがあるが、depends_onはコンテナの作成順序を、linksはdepends_onの機能に加えてコンテナ間の名前解決を行うもので、どちらも起動の順番はまでは制御しない。

サービスの起動順には、depends_on,links などと言った方法があるみたいですが、いずれもそれほど効果的ではないようです。

ちなみに、docker composeの[公式ドキュメント](http://docs.docker.jp/compose/compose-file.html#compose-file-depends-on)によると、

> depends_on では...(中略) 待てるのはコンテナを開始するまでです。サービスの準備が整うまで待たせる必要がある場合は、 起動順番の制御 に関するドキュメントで、問題への対処法や方針をご確認ください。

という注意書きがあり、要は
**depends_onではコンテナを作る順番は制御できるが、起動してからは制御できない**
という事ですね。
linksが非推奨である事や、dockerizeについての言及もありましたので、ご興味のある方はぜひ参照してみてください。

以上、docker-compose起動時にdbとapiコンテナの接続を行うまで、についてでした。