初回起動時にコケる!? Docker Composeで解決するrails db:migrateの自動化

# はじめに
Docker Compose非常に便利ですよね。最近ようやく本腰を入れて勉強してみたのですが、アプリケーションの開発・デプロイを簡単かつスピーディーに行えるようになりました。

しかし、そんなある日、railsとmysqlを使用して、いざrailsにアクセスしてみるとエラー画面が。。。
調べてみると、初回起動時にrails db:migrateが実行されていないため、データベースエラーが発生していました。
自分で使う分にはいいけど、複数人で同様の開発を始めたらいちいちコンテナに入ってdb:create,migrateの操作をしなければいけないのはちょっと面倒。。。

このエラーを解決するため、wait-for-itを使用した自動化の方法をご紹介します。初心者の方でも理解しやすいよう、詳細な手順を解説していきますので、ぜひ最後までお付き合いください。


# Docker Composeの設定で起こる初回起動時のエラー
改めて問題を整理します。
Docker Composeを使ってRailsアプリケーションを動かす場合、初回起動時にデータベースのマイグレーションを行わないと、エラーが発生してrailsが使用できませんでした。
自身のローカルであれば、create,migrateコマンドを叩けば良いでしょう。
```sh:bash
$ rails db:create
$ rails db:migrate
```
しかし、今回はDokcerのRailsコンテナ内での問題です。
コンテナに入って直接create,migrateすれば良いのですが、毎回新しい人に説明するのも面倒だし手間です。

ではどうすればいいのでしょうか？
今回Dokcerfile内に上記のコードを書ければ解決ですね。


。。。と甘いお話ではありませんでした。
確かにRails側のコンテナはそのコマンドで問題ありません。
しかし、railsのコンテナが立ち上がると同時にMysql側がlistenになっている保証がないのです。また、Mysqlは初回起動時の動作のなかに、「再起動」が入るため、一度listenになったのち、通信が断絶され、再度listenになります。なんと凶悪な。

# wait-for-itを使用した自動化
上記の問題を解決するためにwait-for-itというツールを使用します。
このwait-for-itは対象のコンテナが正常稼働するまで、一定期間waitとリクエストを繰り返してくれる便利なスクリプトです。
公式にも障害に対する回復力がそこまで必要とない場合はwait-for-itを使用して良いと[記載](https://docs.docker.jp/compose/startup-order.html)されています。

## wait-for-itのDL
では実際に解決していきましょう。

https://github.com/vishnubob/wait-for-it

上記のサイトからコードを引っ張ってきてください。
今回使用したのはwait-for-it.shのみですので、cloneするなりcopyするなりしてください。

## Dockerfile周辺（既に設定済みの場合は省略）
Dokcerfileにentrypointの定義がない場合は、付け加えてください。
今回は./entrypoint.shをENTRYPOINTとしました。

```dockerfile:docker-compose.yml
version: "3"

services:
  db:
    platform: linux/x86_64
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
    command: --default-authentication-plugin=mysql_native_password
    ports:
      - 3306:3306

  api:
    build:
      context: ./backend/
      dockerfile: Dockerfile.dev
    image: rails:dev
    volumes:
      - ./backend:/sample
    environment:
      TZ: Asia/Tokyo
      RAILS_ENV: development
      GITHUB_ACTIONS: ${GITHUB_ACTIONS:-false}
    ports:
      - 3000:3000
    entrypoint: ./entrypoint.sh
    depends_on:
      - db
```

## entrypoint.shの設定

```shell:entrypoint.sh
set -e

./entrypoint-script/wait-for-it.sh db:3306
```
entrypoint.shを設定します。コンテナ起動時にこのシェルスクリプトがまず読み込まれることになります。
今回は事前にアプリケーションのルートにentrypoint-scriptフォルダを自分で作成して、その中に必要な処理を書くことにしました。

したがって、entrypoint.shはentrypoint-scriptフォルダで、最初に動かしたいスクリプトファイルを起動させる役割しか持たせません。
もちろん最初にwait-for-it.shを起動させるのですが、この時起動を待つデータベースコンテナの公開ポートを引数に入れておく必要があります。


## wait-for-it.shの設定
```sh:wait-for-it.sh
(省略)

wait_for_wrapper()
{
    ...

    WAITFORIT_RESULT=$?
    if [[ $WAITFORIT_RESULT -ne 0 ]]; then
        echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
    fi

    （ここに正常稼働後に実行したいスクリプトを記述）
    
    return $WAITFORIT_RESULT
}

(省略)
```

wait-for-it.shを自作したentrypoint-scriptフォルダに配置します。
大概の部分はそのままコピペでokです。

これだけで、設定したMysqlの3306ポートを見に行って正常稼働しているか確認してくれます。まだ稼働していないようなら、数秒待ってを繰り返します。（素晴らしい。）

さて、今回はMysqlの正常起動後にrails db:create,migrateを行いたいという話でした。
記述はwait-for-itの中には書きたくないので、別ファイルに分けることにします。
（ここに正常稼働後に実行したいスクリプトを記述）という記述の部分に以下を記述します。

```sh:wait-for-it.sh
    fi

    ./entrypoint-script/after-db-config.sh
    
    return $WAITFORIT_RESULT

```
after-db-config.shというスクリプトファイルを起動させます。

## after-db-config.shの作成

```sh:after-db-config.sh
rm -f tmp/pids/server.pid

while true; do
  if rails db:version >/dev/null 2>&1; then
    break
  else
    echo "Creating database..."
    rails db:create
    echo "Start Migration..."
    rails db:migrate
  fi

  if rails db:version >/dev/null 2>&1; then
    break
  else
    echo "Failed to db initializetion, waiting for 15 seconds..."
    sleep 15
  fi
done

rails s -p 3000 -b '0.0.0.0'
```

実際にwait-for-itでMysqlの起動を待ってから実行させるスクリプトを記述します。
今回は、railsの初回起動時に発生するcreate,migrateを自動化するのが目的でした。

したがって、rails db:versionで値が返ってくるか確認し、存在しないようであればcreate,migrateを行うといった処理を記述しています。


# まとめ
本記事で説明した手順に従って、Docker Composeでrails db:migrateを自動化し、初回起動時のエラーを解決できました！
スクリプトの位置や、より良い方法が他にもある気がしますが、もしコメントがあれば遠慮なくお願いします！