ステージング環境のマイグレーションでDB定義が競合しにくい仕組みにしてみた - その1

オズビジョンのSREチームリーダの卜部です。

社内ではステージング環境は、開発途中のもののデモとして使ったり、手動での動作テストに使ったり、外部のステージング環境から接続してもらう場合の環境として用意されています。
複数環境があるわけではないので、デプロイでソースを切り替えています。

![ステージングイメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218682/33a52028-aa28-f73b-93d5-210c86810881.png)

# 問題になること

もう想像できる方もいるかもしれません。
この場合、ブランチの切り替え方によっては、マイグレーションの処理がぶつかって、デプロイに失敗するか、謎の残骸が残ることになります。

下記の図の ○ はブランチのツリーの中のコミット履歴を表現しており、それぞれの ○ の中はマイグレーションのバージョンを表現しています。


ここでいくと、ブランチA でデプロイされていたソースをブランチBに切り替えると、未反映のマイグレーションスクリプトが単純に実行されるだけの挙動となりますので、 ver Z のマイグレーションが反映されたまま、ver A, ver B が適用されることになります。ブランチBの最新では、想定しないマイグレーションスクリプト（ver Z）の実行が混入された状態となっているわけです。

![ブランチイメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218682/fa1c80ba-3922-a995-065f-0618438522eb.png)

歴史を幹までマイグレーションを巻き戻して、最新のブランチを反映していきたいところですが、このままではステージング環境のテーブル定義がカオスになっていく未来しか見えません。

# 課題解決方法

ステージング環境ですから、ぶっちゃけデプロイごとにデータベースをまっさらにして、ゼロからマイグレーションを行うというアプローチもあるかとは思いますし、そのようにされている会社は多いと思います。

しかし、現状関係者がエンジニアだけでなく、他チーム、社外の連携まで絡んできているので、そこまでしなくてもよい方法を検討しました。

## 課題解決方法のイメージ

下記図のように ① ② ③ と、一度歴史を戻して、ブランチB の最先端に切り替えれば、カオスが生まれなくなります。これをやります。

![ブランチイメージ-案1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218682/4205ac73-d35a-ff5b-6024-072a2a3e2342.png)

# codebuild のビルドスクリプト修正

先述したように codebuild でマイグレーションを行っております。[buildspec.yml](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/getting-started-create-build-spec-console.html) ファイルには以下のように設定を記述しています。
DoctrineMigrationsBundle を使ってマイグレーションを行っています。何の変哲もない定義ファイルです。


```yaml
version: 0.2
env:
  shell: bash
phases:
  pre_build:
    commands:
      - mkdir -p var/{logs,sessions,cache}
      - chmod -R 777 var 
      - chmod +x ./bin/console
      - php /usr/local/bin/composer install --no-progress --no-interaction --optimize-autoloader --no-dev
  build:
    commands:
      - php ./bin/console doctrine:migrations:migrate --em=migration --no-interaction
```

このままでは、新しいマイグレーションスクリプトをひたすら実行していくだけの状態となっております。ここに一つスクリプトを追加します。

```yaml
version: 0.2
env:
  shell: bash
phases:
  pre_build:
    commands:
      - mkdir -p var/{logs,sessions,cache}
      - chmod -R 777 var 
      - chmod +x ./bin/console
      - php /usr/local/bin/composer install --no-progress --no-interaction --optimize-autoloader --no-dev
  build:
    commands:
      - chmod +x ./codebuild/rollback_for_staging.sh && ./codebuild/rollback_for_staging.sh
      - php ./bin/console doctrine:migrations:migrate --em=migration --no-interaction
```

ということで、新たなシェルスクリプト (./codebuild/rollback_for_staging.sh) を追加しました。この中身を見ていきましょう。

```bash
#!/bin/bash

# 環境変数が staging の場合にのみ実行 (codebuild の環境変数 ENVIRONMENT を使っている)
if [ "$ENVIRONMENT" == "staging" ]; then
    readonly S3CURRENT_PATH=s3://php-migration/current/$ENVIRONMENT
    readonly S3BACKUP_PATH=s3://php-migration/backup/$ENVIRONMENT

    readonly TARGET_PATH=./app/DoctrineMigrations
    readonly TMP_PATH=~/tmp

    # マイグレーションアップ予定のマイグレーションスクリプトを待避
    echo "Running command 'mv $TARGET_PATH $TMP_PATH'"
    mv $TARGET_PATH $TMP_PATH

    # S3 に保存された前回実行したマイグレーションスクリプトをコンテナ内にコピー
    echo "Running command 'aws s3 cp --recursive --quiet $S3CURRENT_PATH $TARGET_PATH'"
    aws s3 cp --recursive --quiet $S3CURRENT_PATH $TARGET_PATH

    # DB のバックアップ実行が必要かのフラグ、ループ内で1度だけ実行
    NEEDS_BUCKUP=true

    # ./app/DoctrineMigrations 配下に配置している migration スクリプトのファイルを降順で検索 （新しいバージョンから順番にちぇっくしていき、ロールバックするため）
    for file in $(ls $TARGET_PATH/*.php|sort -rn); do
        base_filename=$(basename $file);

        # 反映予定のマイグレーションスクリプトと前回反映したマイグレーションスクリプトを比較
        diff $TARGET_PATH/$base_filename $TMP_PATH/$base_filename > /dev/null 2>&1

        # 差分あり、片方ファイルなし の場合ロールバック対象
        if [ $? -ne 0 ]; then
            if [ $NEEDS_BUCKUP ]; then
                # S3 の保存先を指定して、DB バックアップを行うコマンド
                ./codebuild/backup_command.sh $S3BACKUP_PATH
                # 1度実行したら次回は実行しない
                NEEDS_BUCKUP=false
            fi

            # マイグレーションスクリプトファイル名からバージョン番号を取得
            version=$(echo $base_filename | perl -pe 's/^Version([0-9]{14})\.php$/\1/g')

            # バージョンを指定して、マイグレーションダウンを行う
            echo "Running command './bin/console doctrine:migration:execute -vvv --em=migration --down --no-interaction $version'"
            php ./bin/console doctrine:migration:execute -vvv --em=migration --down --no-interaction $version
        fi  
    done

    # 一時的に反映した前回反映のマイグレーションスクリプトを削除
    echo "Running command 'mv $TARGET_PATH /tmp/'"
    mv $TARGET_PATH /tmp/

    # 新しいソースのマイグレーションスクリプトを戻す
    echo "Running command 'mv $TMP_PATH $TARGET_PATH'"
    mv $TMP_PATH $TARGET_PATH

    # S3 のカレントマイグレーションスクリプトを削除
    echo "Running command 'aws s3 rm --recursive --quiet $S3CURRENT_PATH'"
    aws s3 rm --recursive --quiet $S3CURRENT_PATH

    # 今回実行する予定のマイグレーションスクリプトを S3 のカレントマイグレーションスクリプトとして配置
    echo "Running command 'aws s3 cp --recursive --quiet $TARGET_PATH $S3CURRENT_PATH'"
    aws s3 cp --recursive --quiet $TARGET_PATH $S3CURRENT_PATH
else
    echo Production environment does not roll back
fi
```

少し強引ではありますが、上記手法でマイグレーションのロールバックが行い （DB は念のため S3 にバックアップ）、最新のマイグレーションスクリプトを実行するという流れができあがりました。

とはいえ、エンジニア側で マイグレーションのアップとダウンを正しく書いてくれているか。テストをしてくれているかという人依存の課題が残っています。ここの部分の解決アプローチを次回お伝えします。