再公開に失敗しても、動いているバックエンドは消えない
9月25日、再公開に失敗したことで、それまで2時間半ほど正常に稼働していたアプリのバックエンドが削除されました。公開の失敗を取り消すコードが、その公開が触れたサーバーアプリをすべて削除していたためです。そこには、公開の開始前からすでに稼働していたものも含まれていました。現在は、公開によって作成されたアプリだけが削除の対象となり、既存のアプリは公開前に動いていたバージョンへ戻されます。同じ日に行った2つの追加修正により、ホスティング基盤自身のデプロイツールが失敗した場合にも取り消しが確実に実行されるようになり、スリープ中だったというだけで正常なアプリが失敗扱いされることもなくなりました。
バックエンドの公開の流れ
Mythexのアプリにバックエンド(APIやサーバー。静的ファイルだけのものとは異なります)がある場合、公開時にはDockerfile(アプリをイメージにパッケージ化する手順書)からビルドし、そのイメージをホスティング基盤のマシンに展開します。その後、ヘルスチェックが新しいバージョンの起動と安定稼働を確認してから、公開が完了とマークされます。
途中で何かが失敗した場合は、ロールバックが実行されて変更を取り消します。ロールバックは、すでに問題が起きているときに動くため、正しく動作することが公開機能の中でもとりわけ重要です。
問題1:復元すべきものを削除していた
ロールバックは、公開が展開したサーバーアプリの一覧を保持しており、失敗すると一覧にあるものをすべて削除していました。初回の公開であれば、これは正しい動きです。アプリはついさっき作られたばかりで、何にも依存されておらず、削除すれば元の状態に戻るからです。
しかし再公開の場合は、まったく誤った動きになります。アプリはすでに存在しており、実際の訪問者にサービスを提供していました。9月25日には、約2時間半稼働していたアプリのAPIが、失敗した再公開によって削除されました。
同じコードには、もう一つの問題もありました。削除が完了しなかった場合、壊れた新しいイメージがマシン上に残ったままになる一方で、オーナーには現在のサイトがまだ稼働中だと伝えていたのです。ヘルスチェックが失敗を検出する時点では、新しいイメージはすでにすべてのマシンに展開されています。何もしないことは、取り消すことと同じではありません。
修正:自分が作ったものだけを削除し、それ以外は復元する
現在、公開のたびに、触れたサーバーアプリごとに次の情報を記録します。
- この公開で作成したかどうか
- 作成されていない場合は、公開直前にどのイメージで動いていたか
失敗時には、各アプリに対して次の3つのいずれかの処理を行います。
- この公開で作成したもの: 従来どおり削除します。
- 既存で、以前のイメージが分かっているもの: 同じ設定のまま以前のイメージを再展開します。ビルドは行わず、イメージを元に戻すだけです。
- 既存で、以前のイメージが不明なもの: 何もせずそのままにします。既存のアプリは、復元できない場合でも削除しません。
これらのルールは、テストを備えた小さな純粋関数として実装しています。そのため、判断が「すべて削除」に戻ってしまえば、テストが失敗して気づけます。
問題2:取り消し方を記録する前に、デプロイツールが失敗しうる
この修正の直後、別の抜け穴が見つかりました。ホスティング基盤のデプロイツールは独自のスモークチェックを実行し、新しいマシンがクラッシュした場合にはエラーで終了します。ただし、それは新しいイメージがマシンに展開された後に行われます。
しかし当時のコードは、ツールが正常に完了した後でしか、アプリをロールバックの対象として登録していませんでした。そのため、ツール自体がデプロイを失敗させた場合、アプリはまだ一覧に入っておらず、何も復元されませんでした。ステージング環境では、壊れた再公開の後に壊れたイメージがそのまま応答を返し続けていました。
修正:開始前に取り消し方を登録する
現在は、アプリを準備した時点で、作成済みかどうかと実行中だったイメージを含めてロールバックの対象に登録します。これは、何かがマシンに展開される前に行われます。その後に何が失敗しても、ロールバックは何を戻すべきかを把握しています。ローカルでも検証しました。壊れた再公開がデプロイツールのスモークチェックで失敗すると、以前のバージョンが引き続き応答を返しました。
問題3:スリープ中のアプリが「起動しなかった」と判定されていた
Mythexで公開されたアプリは、クレジットを節約するためにアイドル時にスリープします。再公開すると、スリープ中のマシンは停止したままになるため、ヘルスチェックがマシンを起動させる必要があります。当時のコードは、マシンの起動を基盤に1回だけ依頼し、その応答を無視していました。
9月25日には、その1回の起動依頼が空振りに終わりました。正常なAPIが停止したままヘルスチェックの5分間を過ごし、「マシン1台が起動しませんでした(最後の状態:停止中)」として失敗扱いされたのです。
ホスティング基盤が起動を拒否することは実際にあります。この問題を再現している途中にも、基盤は「lease currently held(リースが保持されています)」という競合エラーを返しました。さらに、マシンが更新中の間は起動を拒否します。
修正:繰り返し依頼し、ドアをノックする
ヘルスチェックは次のように動作するようになりました。
- 停止中のマシンの起動を、待機中は20秒ごとに繰り返し依頼します。
- 基盤が拒否した場合は、その応答を記録します。
- アプリにも通常のリクエストを1回送ります。訪問者がアクセスするのと同じ要領です。基盤のプロキシは、リクエストが届くとスリープ中のマシンを起こします。
実際のホスティング基盤で、最初の起動が拒否されるよう仕組んだ使い捨てのアプリを使って再現しました。修正前は91秒後に公開が失敗しました。修正後は20秒で公開が完了しました。起動時に本当にクラッシュするアプリは、これまでどおり失敗します。
得られた教訓
- ロールバックは、新規と既存の違いを区別しなければなりません。 作成したものに対する「取り消し」は削除であり、もともと存在していたものに対する「取り消し」は復元です。両者を同じ扱いにすると、安全のための仕組みが動いているアプリを削除してしまいます。
- 行動する前に、取り消し方を書き留めておきましょう。 取り消し方が、ある処理が成功した後にしか記録されない場合、途中で処理が失敗すると取り消すべきものが記録されていない状態になります。
- 頼りにしているリクエストへの応答は、決して無視してはいけません。 無視された1回の「いいえ」が、正常なアプリを失敗した公開に変えてしまいました。
同じ日に行った関連する修正として、公開直後にクラッシュするアプリを検出するもあります。公開の失敗がお使いの環境側で起きた場合の対処については、公開のトラブルシューティング(ドキュメント)をご覧ください。