無停止で安全にデプロイするためのチェック観点

現在ではAWSやkubernetesを使用することで簡単に無停止デプロイの仕組みを使うことが可能ですが、ユーザーに影響を与えずにデプロイできるかは、アプリケーションやDBの定義を互換性をどう保つかにん依存しているのかなと思います。今回は私自身が無停止で安全にデプロイするために気をつけていることをまとめてようと思います。

本記事はAWS上にwebアプリケーションをLB + Rails + sidekiq + Auto scalingで構築し、AWS codedeployを使用して、Blue/Greenデプロイをしていることを前提にチェック観点をまとめています。DBは主にMySQLを使用しています。

## DBのロールバックにはデータ削除がセットになる

Railsにはデータベーススキーマのマイグレーションを管理する[Active Record マイグレーション](https://railsguides.jp/active_record_migrations.html)という機能を持っています。
この中に[ロールバック](https://railsguides.jp/active_record_migrations.html#%E3%83%AD%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF)という機能があるので、カラム追加やテーブル追加を行なった後にロールバックをして、以前のDBスキーマの状態に戻すことが可能です。
この機能は開発時には非常に強力ですが、本番環境にデプロイする時にはあまり役に立つ場面は少ないです。当たり前ですがDBスキーマのロールバックには基本的には追加したカラムやテーブル内のデータの削除が伴うからです。
追加したカラムにデータが入っていない状態などではロールバックなどをすることが可能かもしれませんが、基本的にはDBスキーマはロールバックができないことを前提にロールバック戦略を立てる方が賢明だと思います。

## オンラインDDL

MySQLのようなRDBMSを使用するアプリケーションのデプロイ時の多くの場合にはデータベーススキーマの変更が伴います。MySQLでは一部のDDLを除いてスキーマの変更をするテーブルに対してロックを取得せずにスキーマの変更ができるオンラインDDLがサポートされているが、ロックの取得を伴うものも存在するので注意が必要です。

オンラインDDLがサポートされていないDDL([MySQL5.6の場合](https://dev.mysql.com/doc/refman/5.6/ja/innodb-create-index-overview.html))

- カラムのデータ型を変更
- 主キーを削除する
- 文字セットを変換する
- 文字セットを指定する

注意すべきは一番初めの `カラムのデータ型を変更` の場合でしょうか。例えば文字列型で最大文字列を増やすためにやむなく`TEXT`型などに変更する場合には、該当のテーブルに対してDDLが完了するまでにロックが発生します。実施をする場合にはDDLの実行時間などを計算して、ユーザーに影響がないものなのかなどの考慮をする必要があります。

## アプリケーションの機能追加 / 削除

本記事は筆者がよく使用しているBlue/Greenデプロイでのデプロイを前提に記事を書いているが、Blue/Greenやインプレース、ローリングアップデートなどでも同様の考えを適用できると思っています。

無停止デプロイの場合で難しいのは、LBでラウンドロビンを採用している場合は過去バージョンと新バージョンが混在する時間が必ず存在することです。以下のような問題が起こると想定できます。

- 過去バージョンのアプリが新しいDBスキーマを正しく参照できない
- 新バージョンでレンダリングされたHTMLで過去バージョンのAPIにアクセスした場合にそのAPIが存在しない

これらの問題に対応するための方法を以下に記載していきます。

(ALBなどの[スティッキーセッション](https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/sticky-sessions.html)を使用して同一のCookieの場合には同じサーバーにアクセスをさせることで過去バージョンと新バージョンを混在させない、という戦略もあるがここでは割愛)

### 機能追加を行う場合

次にアプリケーションの機能の追加を行う場合についてです。HTMLを使用したwebアプリケーションの場合の追加は下記のように段階的にデプロイを行うことで互換性を保つことが可能です。

1. DBのカラム追加またはテーブルの追加
    - RailsのActive Recordでは問題ないが、過去バージョンのORMがカラムが追加されたテーブルに問題なくアクセスできるかを注意する
1. APIの追加
    - HTMLにAPIのリンクを追加する前にAPIだけ実装。この時にユーザーには追加したAPIが使用されない想定
1. フロント部分の追加
    - HTMLにAPIのリンクを追加する。上記でAPIを既に実装しているので新バージョンのHTMLから過去バージョンのAPIにリクエストをした場合にエラーにならない


逆に上記の機能追加を一気にデプロイした場合には新バージョンのHTMLから過去バージョンのAPIにリクエストを行った場合に404エラーが発生することになります。実際には段階的にデプロイを行うのもコストがかかりますので、機能追加時の影響範囲を考えて一時的に404を許容して一気にデプロイをする、というのも問題ないかなと思います。

### 機能削除を行う場合

次に逆に機能削除を行う場合です。機能追加とは逆にフロント -> DBの順で削除を行なっていき、ユーザーからのアクセスが行われる同線を削除してからAPIやDBのカラムを削除することで、安全にデプロイすることが可能です。

1. フロント部分の追加
    - HTMLにAPIのリンクを削除する。このデプロイでユーザーからのアクセス同線を消す。
1. APIの削除
    - アプリのコードからDBにアクセスされる部分を削除する。
1. DBのカラム削除またはテーブルの削除
    - アプリのコードからDBにアクセスされることがなくなってから削除。
    - データの削除が伴うので、削除して良いのかを確認すべき。

上記のように段階的にデプロイを行えば、機能の削除も無停止で安全に削除が可能です。多くのwebサービスを運用している企業では積極的にリファクタを実施しているところも多いと思いますので、段階的にデプロイをすることで安全にリファクタも可能なことを知っておくのは良いことかなと思います。
(ただし3のDBの削除は切り戻しができないのでデータ削除して良いのかを要確認する必要があります)

## 非同期処理

Railsでsidekiqを採用している場合はRailsとsidekiqを一緒にデプロイしている場合が一般的に多いと思います。
この場合以下の問題が起こることが考えられます。

- 新バージョンで追加したジョブが過去バージョンで存在せずにエラーになる

この問題に対応するために以下のことが考えられます。

- ジョブの処理だけ先にデプロイし、ジョブのエンキュー処理を後からデプロイする
- 無停止を諦めて過去バージョンのsidekiqを止める

`ジョブの処理だけ先にデプロイし、ジョブのエンキュー処理を後からデプロイする` の場合は上記のアプリのデプロイと同様に段階的にデプロイすることで互換性を保つアプローチになります。
`無停止を諦めて過去バージョンのsidekiqを止める` の場合は、そもそも非同期処理の場合は5~10分程度の停止は許容される、という前提のもとで過去バージョンのsidekiqを止めることで安全にデプロイを行うアプローチになります。
前者はユーザー影響が少ないがデプロイ時のコストがかかる、後者はユーザーに多少の影響があるがデプロイ時のコストが少ない、というそれぞれにメリットデメリットがあるので、時と場合に応じて選択するのが良いのかなと思います。


## websocketの挙動

websocketはリアルタイムアプリケーションを実装するのに有効な方法ですが、特定のサーバに対して接続を維持することを前提にしたプロトコルなので、Blue/Greenなどのサーバーをイミュータブルに変更をするデプロイ時には再接続時に問題が発生しないかの確認が必要になります。
Railsの場合では [ApplicationCable](https://railsguides.jp/action_cable_overview.html)を使用していれば接続情報をRedisなどに保管を行うことが可能なため、Blue/Greenのデプロイ時に過去バージョンのサーバーが破棄された時に再接続処理が行われても問題なく処理を再開することが可能です。
この辺りは自分で実装すると問題が発生しやすい部分ですので、有名なF/Wを使用するのが良いのかなと思います。

## 過去バージョンのリクエストの登録解除

最後にBlue/Greenのデプロイ時に過去バージョンに対してリクエストを登録解除する時の処理についてです。
例えばAWSのターゲットグループでは[デフォルトで300秒の登録解除の遅延時間を設けています](https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-target-groups.html#deregistration-delay)。
これは処理時間が長いリクエストがあった場合を考慮して、接続を解除するまでの時間を伸ばすものになります。基本的にAPI等の処理で300秒以上かかるものは少ないと思いますが、300秒以上かかる処理がある場合は`登録解除の遅延`を300秒以上に設定することが必要です。
(逆に処理時間が300秒かからないのであれば、この時間を短くすることでデプロイ時間を短縮することが可能です。)


## まとめ

AWSなどでDevOpsの選択肢は増えましたが、それらを安全にデプロイし切るのは考慮すべきことが実際にはかなり多いです。しかしそれらを乗り切ることがDevOpsの役割なのかなと思います。正解がない分野ですがその時々のベストを尽くしていきましょう💪