安心してRailsアップグレードを行うための工夫

こんにちは。技術部の国分 (@k0kubun) です。

3/28にクラウドワークスさんで行なわれたRails Upgrade Casual Talksで、Railsアップグレードの際にクックパッドが行なっている工夫について紹介しました。

影響範囲の予測が難しいRailsのアップグレードを安全に行なうための動作確認のやり方について参考になればということで、本記事でも改めて紹介いたします。

CookpadのRailsアップグレードの流れ

Rails 4.1から4.2にアップグレードした際の例を紹介します。

CIにRails 4.2用ジョブを用意

まずはRails 4.2にアップグレードするためのrails42ブランチでテストを通します。リリースするまでこのブランチはmasterからrebaseし続けるので、リリースまでテストを通る状態を保つため、CIにrails42ブランチ用のジョブを用意します。このジョブはCIサーバーのリソースが空いている早朝に実行します。

cherry-pick

テストが通るブランチができたら、デプロイ後の問題の切り分けを容易にしたり、レビューの負担軽減のためPull Requestを分割します。ここでは、例えばRails 4.1, 4.2両方で動く修正やgemのアップグレードを先に行います。

非互換の一時的抑制

Rails 4.2にはMasked Authenticity Tokenという、セッションに後方互換性のない変更を加える機能があります。一度デプロイしてしまうと安易にロールバックできなくなるため、最初のRails 4.2アップグレード時には以下のようなモンキーパッチを行なってリリースしました。

Masked Authenticity Tokenの変更を抑制するパッチ

ActiveSupport.on_load(:action_controller) do
  module ActionController
    module RequestForgeryProtectionExtension
      def form_authenticity_token
        session[:_csrf_token] ||= SecureRandom.base64(32)
      end
    end
    Base.prepend RequestForgeryProtectionExtension
  end
end

このパッチはRails 4.2のリリース後落ちついてから外しました。@minamijoyoさんの発表にあったbreach-mitigation-railsを使うのも良いかもしれません。

動作確認

複数の部署が関わるアプリケーションであり、私一人では全ての影響範囲の確認が困難なため、関係部署が動作確認できる期間を2週間設けています。後述しますが、これと並行して本番に近い環境でエラーが出るかの確認も行います。

cookpad.com のリリース

直前に動かなくなるリスクを抑えるため前日夜からコードフリーズし、デプロイの影響を小さくするため、午前7時に出社して8時にデプロイを行います。なるべくトラフィックが低い時間帯にデプロイしたいものの、関係者全員に深夜の出社を要求するほどではないためこの時間になっています。

複数ブランチ運用

cookpad.com と同じリポジトリに、一つのMountable Engineを共有する8つのアプリが同居しているため、 cookpad.com をデプロイして終わりではありません。間を開けずにリリースすると問題が起きた時の対応が大変になるので、約1週間かけて段階的にデプロイします。しかし、その間はRails 4.1用のrails41ブランチを作りmasterの変更をバックポートし続けるため、あまり移行期間を長くするとその作業が大変になってしまいます。

デプロイ後の監視

デプロイ後はSentryを使ってエラーの確認を行い、自社製のモニタリングツールを使って各サーバーのレスポンスタイムを監視します。

アップグレードフローのまとめ

  1. テストを通しCIにジョブを用意する
  2. Pull Requestの分割や非互換の抑制により変更の粒度を小さくする
  3. 2週間動作確認
  4. 複数のアプリを1週間かけて段階的にリリースする
  5. デプロイ後、レスポンスタイムとエラーを監視

リリース前の4段階の動作確認

Railsのアップグレードをリリースするまでに以下の4段階の動作確認を行なっています。

1. 開発用の検証環境での確認

開発用のDBを参照する検証環境にRails 4.2のブランチをデプロイします。Railsアップグレード関係なく、もともと任意のブランチをhttps://rails42.staging.~/のような任意のサブドメインにデプロイできるようになっていて、この環境を使って各部署に動作確認を依頼します。

2. 本番環境での手動確認

本番環境のサーバーの一つにRails 4.2のブランチをデプロイし、リバースプロキシの設定を変更して動作確認を行なう人だけがそのサーバーにリクエストを飛ばせるようにして、手動で動作確認を行います。

Webの場合

特別なクッキーを持つ場合のみRails 4.2の環境にプロキシされるようにし、適当にクッキーをセットした上で手動で動作確認を行ないます。

f:id:k0kubun:20160329033708p:plain

モバイルアプリ用APIの場合

特別なリクエストヘッダーを持つ場合のみRails 4.2の環境にプロキシされるようにし、mitmproxyでアプリの全てのリクエストにそのリクエストヘッダーを付加し、手動で動作確認を行ないます。

f:id:k0kubun:20160329033718p:plain

3. Kage

github.com

Kageを使い、あるサーバーに来たリクエストをRails 4.2をデプロイしたものにも流し、そこではDBなどへの書き込みや外部サービスへのリクエストを抑止します。実際の様々なリクエストが流せるため、手動では見つかりにくいエラーを発見したり、パフォーマンスの変化を確認したりすることができます。

f:id:k0kubun:20160329033633p:plain

4. Production Test

Railsアップグレード関係なく、クックパッドでCIが通ったサービスは「Production Test」と呼ばれるほぼ本番の環境に自動的にデプロイされます。リリース直前で再度本番と同じ環境で動作確認をすることで、より安心してデプロイを行なうことができます。

Cookpadが遭遇したRails 4.2のバグ

Rails 4.1 → 4.2のアップグレードではリリース後にエラーが大量に出るようなことはなく、これまでリリース後に発見されたRails自体のバグは以下の2つだけでした。自社のアプリの保守性のためにモンキーパッチを防ぐという意味だけでなく、コミュニティに貢献するという意味でもなるべくOSSのバグは本家にPull Requestを投げて修正するようにしています。

Encoding::UndefinedConversionError

  • マルチバイト文字列かつ半角の"%"を含むファイル名のファイルをアップロードした際に出るレアなエラー
    • Rails 4.1 → 4.2 アップグレード時に唯一出たエラー
  • 本家に @eagletmt がPull Requestを投げ、4.2.4に取り込まれました

undefined method 'unpack' for nil:NilClass

  • Rails 4.2.4とRuby 2.0.0の組み合わせでのみ発生するエラー
    • Ruby 2.0.0だとERB::Util.url_encode内のgsubのブロックで$&が参照されますが、ActiveSupport::SafeBufferだとこれが動きません
  • 本家に @k0kubun がPull Requestを投げ、4.2.5に取り込まれました

まとめ

クックパッドがRailsアップグレードの際に行なっている工夫について紹介しましたがいかがだったでしょうか。このようにしてクックパッドは常に最新のRailsを使い続けており、今も http://cookpad.com はRails 4.2.6で動いています。より開発しやすく、バグや脆弱性の少ない最新のRailsを使い続けることで、より良いサービスを届けられるようにしたいですね。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/