DB変更があるデプロイ起因の障害をGithub Actionsで対策する
TL;DR
- DBのマイグレーションとソースコードのデプロイは実行順番に気を付けないと障害が起きる
- Github Actionsを使ってCI/CDワークフローに障害を発生しにくくする仕組みを組み込んだ
はじめに
レバテック開発部の基盤システムグループの南です。
運用しているマイクロサービスで発生した障害が、やってしまいがちなものだと思ったので原因や再発防止策を共有したいと思います!
発生した事象
- リリースの直後から、以下のようなエラーが大量に発生した
'database_name.table_name' doesn't exist
- 切り戻そうか考えていると数分で落ち着きエラーが発生しなくなった
発生原因
今回の障害はデータベースのマイグレーションとソースコードのデプロイが同一ワークフローにあり
ソースコードのデプロイより先にデータベースのマイグレーションを実行してしまったのが原因でした。
- データベースのマイグレーションが実行される(テーブルが削除される)
- 存在しないテーブルにアクセスしようとする(障害発生)
- ソースコードがデプロイされる(復旧)
今回のようにデータベースとソースコードに修正を加える場合、リリース順番に気を付けないと障害が発生してしまいます。
再発防止策
再発防止策としてデータベースに変更がある場合のリリース手順を明文化しました。
- テーブル、カラムを削除する場合
1. ソースコードをデプロイする
2. データベースをマイグレーションする- テーブル、カラムを追加する場合
1. データベースをマイグレーションする
2. ソースコードをデプロイする
この手順に加えて各リリースの前にデータベースを利用した網羅的なテストを実施することで、今回のようなデプロイ起因の障害を防ぐことができます。
またこのルールの前提としてリリース時にデータベースの変更とソースコードの変更が同居してはいけません。
よってCI/CDワークフローに以下のルールを組み込むことで運用時にミスが発生しにくくなるようにしました。
- Pull Requestに、マイグレーションファイルとそれ以外のソースコードの修正が同居しないようにする
- リリースに、マイグレーションファイルとそれ以外のソースコードの修正が同居しないようにする
実装例
Pull Request作成時のワークフロー
Pull Requestのブランチとそのマージ先のブランチを比較することで、不正なPull Requestを検知するようにします。
name: 'ci'
on:
pull_request:
branches:
- master
- staging
- feature/*
~~~
jobs:
fail_if_change_db_and_app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Check for DB migration and application code changes
run: |
# DBマイグレーションファイルの変更を検知
db_changes=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- 'マイグレーションファイルのパス')
# アプリケーションコードの変更を検知
app_changes=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- ./ ':(exclude)マイグレーションファイルのパス')
if [[ -n "$db_changes" && -n "$app_changes" ]]; then
echo "Error: PRにDBマイグレーションとアプリケーションコードの変更が含まれています。"
exit 1
fi
setup:
needs: fail_if_change_db_and_app # PRにDBマイグレーションとアプリケーションコードの変更が含まれていないかチェック
runs-on: ubuntu-latest
リリース時のワークフロー
リリースはタグの変更をトリガーにしているため、最新のタグとその一つ前のタグのソースコードを比較することで、不正なリリースを検知するようにしています。
fail_if_change_db_and_app
でリリースに含まれる変更ファイルを検証し、その結果によって実行するジョブを変更しています。
name: 'deploy-or-migration-production'
on:
release:
types:
- published
~~~
jobs:
fail_if_change_db_and_app:
runs-on: ubuntu-latest
outputs:
app_changes: ${{ steps.check.outputs.app_changes }}
db_changes: ${{ steps.check.outputs.db_changes }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- id: check
name: Check for DB migration and application code changes
run: |
current_tag=$(git describe --tags --abbrev=0)
previous_tag=$(git describe --tags --abbrev=0 --always "$current_tag^")
app_changes=$(git diff --name-only $previous_tag..$current_tag -- ./ ':(exclude)マイグレーションファイルのパス')
db_changes=$(git diff --name-only $previous_tag..$current_tag -- 'マイグレーションファイルのパス')
# マイグレーションファイルとソースコードの両方に変更があったらエラー
if [[ -n "$db_changes" && -n "$app_changes" ]]; then
echo "Error: PRにDBマイグレーションとアプリケーションコードの変更が含まれています。"
exit 1
fi
# ソースコードのみ変更があったらapp_changesに値が入る
if [[ -n "$app_changes" ]]; then
echo "app_changes=true" >> "$GITHUB_OUTPUT"
else
echo "app_changes=false" >> "$GITHUB_OUTPUT"
fi
# マイグレーションファイルのみ変更があったらdb_changesに値が入る
if [[ -n "$db_changes" ]]; then
echo "db_changes=true" >> "$GITHUB_OUTPUT"
else
echo "db_changes=false" >> "$GITHUB_OUTPUT"
fi
deploy-to-production:
needs: fail_if_change_db_and_app
if: ${{ needs.fail_if_change_db_and_app.outputs.app_changes == 'true' }}
~~~
migrate-production:
needs: fail_if_change_db_and_app
if: ${{ needs.fail_if_change_db_and_app.outputs.db_changes == 'true' }}
最後に
今回の障害を通じて、データベースとアプリケーションの変更を同時に行うリスクを再認識しました!
CI/CDワークフローにチェック機能を組み込むことで、人為的なミスを防ぎ、安全なリリースができるようになりました!
今回のCI/CDワークフローを導入することにより以下のような弊害や懸念点があると考えられます。
- リリースの複雑化
- 開発の柔軟性の低下
- 開発者の学習コストの増加
しかしデータベースとソースコードの変更を分離することで、リリースの安全性や品質が向上するため、長期的なメリットは大きいと考えています。
参考にしていただけたら幸いです。
Discussion